Refresh ARCHITECTURE.md Part 2 for the post-refactor design

Part 2 was written as a design sketch before the port was implemented and
refactored, so much of it described the planned code rather than the final
code. Updated the RogueGame/Stats/Object/Flags/Level sketches to the current
names and types (typed ObjectKind, DiceSpec, split o_arm fields, step-1 flag
names, TrapCount, Level list methods); rewrote §4.7 to say the static tables
now live on the per-game gameData struct (no package globals); noted the
daemon and effect handler tables (step 7), the MessageLine extraction (step 6),
the Terminal interface, the flat gob SaveState, and the New(Params) +
os.Exit-on-game-over design (step 8). Added §7.1, a C-name → Go-name rename
table, and a README note about the make targets. Docs only.
This commit is contained in:
2026-07-23 08:40:15 +07:00
parent bcdfaf4ab4
commit cb1e302102
2 changed files with 230 additions and 129 deletions

View File

@@ -1086,7 +1086,7 @@ game/ package game — the port, one .go file per .c fil
init.go per-game randomization (init.c)
command.go command dispatch (command.c)
io.go message/status line (io.c)
screen.go Screen: tcell wrapper + Window (curses + mdport display shims)
screen.go Terminal iface, Screen, Window (curses + mdport display shims)
newlevel.go level orchestration (new_level.c)
rooms.go room/maze generation (rooms.c)
passages.go corridors (passages.c)
@@ -1099,12 +1099,13 @@ game/ package game — the port, one .go file per .c fil
potions.go, scrolls.go, rings.go, sticks.go, weapons.go, armor.go
misc.go look(), eat, direction input (misc.c)
daemon.go scheduler (daemon.c)
daemons.go the 11 callbacks (daemons.c)
daemons.go daemon/fuse callbacks (daemons.c)
options.go options screen + ROGUEOPTS (options.c)
wizard.go wizard commands, identify (wizard.c)
rip.go tombstone, victory, scores (rip.c)
save.go save/restore via gob (save.c + state.c)
score.go scoreboard (score.h + rip.c/save.c score I/O)
term/tcell.go package term — tcell-backed Terminal (curses + mdport input/display)
```
`mach_dep.c`, `mdport.c`, `xcrypt.c`, `state.c`, `list.c`, `vers.c` have no
@@ -1138,7 +1139,7 @@ type RogueGame struct {
Level Level // map, rooms, passages, level objects, monsters
Depth int // C `level`
MaxDepth int // C `max_level`
Amulet bool
HasAmulet bool
SeenStairs bool
// --- turn/command engine ---
@@ -1155,7 +1156,7 @@ type RogueGame struct {
ToDeath, Kamikaze, HasHit bool
MaxHit int
Take byte // glyph of thing picked up this move
Delta Coord // last direction from GetDir
Delta Coord // last direction from promptDirection
DirCh byte
LastComm, LastDir byte // command repeat state ('a' command)
LLastComm, LLastDir byte
@@ -1166,9 +1167,9 @@ type RogueGame struct {
Daemons DaemonList // d_list[20] + `between` counter
// --- messages / UI ---
scr *Screen // tcell-backed terminal (stdscr + hw scratch window)
Msgs MsgLine // msgbuf/newpos/mpos/huh/save state
Options Options // terse, jump, seefloor, passgo, flush, tombstone, inv_type...
scr *Screen // tcell-backed terminal (stdscr + hw scratch window)
Msgs MessageLine // msgbuf/newpos/mpos/huh/save state
Options Options // terse, jump, seefloor, passgo, flush, tombstone, inv_type...
StatMsg bool
InvDescribe bool
Oldpos Coord // hero position before last look()
@@ -1182,13 +1183,16 @@ type RogueGame struct {
LastScore int
AllScore bool
ScorePath string
// --- static data tables (extern.c/init.c ROM) ---
data *gameData // this game's copy of the immutable C tables (§4.7)
}
```
Player-centric C globals move onto `Player`, world-centric onto `Level` (see
§4/§5); the remainder sit directly on `RogueGame` with the original semantics.
The command.c/io.c/things.c file-statics become lowercase fields of `RogueGame`,
`MsgLine`, and the inventory pagination helper respectively.
`MessageLine`, and the inventory pagination helper respectively.
### Entrypoints
@@ -1211,13 +1215,13 @@ exit code.
type Coord struct{ X, Y int } // value type; C `ce(a,b)` == `a == b`
type Stats struct {
Str int // str_t; 3..31
Exp int
Lvl int
Arm int // armor class, lower is better
HP int // s_hpt
Dmg string // damage dice "1x4/1x2" — kept as string, parsed by rollEm
MaxHP int
Str int // str_t; 3..31
Exp int
Lvl int
ArmorClass int // armor class, lower is better (was s_arm)
HP int // s_hpt
Dmg DiceSpec // damage dice, parsed once by ParseDice (§4.3)
MaxHP int
}
```
@@ -1268,29 +1272,31 @@ both the AI and the save format rely on.
```go
type Object struct {
Type byte // POTION '!', SCROLL '?', WEAPON ')', ...
Pos Coord
Text string // scroll gibberish
Launch int // launcher weapon index, -1 none
PackCh byte // inventory letter
Damage string // wield dice
HurlDmg string // thrown dice
Count int // stack size
Which int // subtype (P_HEALING, MACE, R_PROTECT, ...)
Kind ObjectKind // item category (was o_type); Glyph() gives the map char
Pos Coord
Text string // scroll gibberish
Launch WeaponKind // launcher weapon (noWeapon if none)
PackCh byte // inventory letter
Damage DiceSpec // wield dice, parsed once
HurlDmg DiceSpec // thrown dice
Count int // stack size
Which int // subtype index (P_HEALING, MACE, R_PROTECT, ...)
HPlus, DPlus int
Arm int // OVERLOADED, as in C: armor class | charges | gold value
Flags ObjFlags
Group int // stack group id
Label string // player-applied name
ArmorClass int // armor only (was part of the overloaded o_arm)
Charges int // wands/staffs
GoldValue int // gold piles
Bonus int // rings (protection, add strength, ...)
Flags ObjFlags
Group int // stack group id
Label string // player-applied name
}
func (o *Object) Charges() int { return o.Arm } // sticks
func (o *Object) SetCharges(n int) { o.Arm = n }
func (o *Object) GoldVal() int { return o.Arm } // gold piles
```
The overload stays (one field, named accessors) so ported arithmetic like "rust:
`o.Arm++`" keeps its original shape.
The single C `o_arm` field was overloaded (armor class | charges | gold value |
ring bonus). The port splits it into four honest fields (step 3); `o_type`
became a typed `ObjectKind` distinct from the display glyph (step 2). Damage
strings are parsed once into a `DiceSpec` (`[]DiceRoll`) at table-definition
time rather than re-parsed on every swing.
### 4.4 Flags
@@ -1301,11 +1307,11 @@ type RoomFlags uint16
type PlaceFlags uint8
const (
CanHuh CreatureFlags = 1 << iota // creature can confuse
CanSee; IsBlind; IsCanc; IsFound; IsGreed; IsHaste; IsTarget
IsHeld; IsHuh; IsInvis; IsMean; IsRegen; IsRun; IsFly; IsSlow
CanConfuse CreatureFlags = 1 << iota // ISHUH: creature can confuse
CanSeeInvisible; Blind; Cancelled; Found; Greedy; Hasted; Targeted
Held; Confused; Invisible; Mean; Regenerates; Awake; Flying; Slowed
// hero aliases sharing C bit values are separate named constants:
IsLevit = IsCanc; IsHalu = IsMean; SeeMonst = IsFly
Levitating = Cancelled; Hallucinating = Mean; SenseMonsters = Flying
)
func (f CreatureFlags) Has(b CreatureFlags) bool
@@ -1313,10 +1319,11 @@ func (f *CreatureFlags) Set(b CreatureFlags)
func (f *CreatureFlags) Clear(b CreatureFlags)
```
C's `on(player, ISHALU)` ports to `g.Player.Flags.Has(IsHalu)`. The C bit
collisions (ISCANC/ISLEVIT, ISMEAN/ISHALU, ISFLY/SEEMONST) are preserved as
equal values — they are disjoint by owner (monster vs hero), and the save format
keeps working.
The C `IS*`/`CAN*` macro names became descriptive Go constants in step 1
(`ISHUH``Confused`, `SEEMONST``SenseMonsters`, `ISRUN``Awake`, ...). C's
`on(player, ISHALU)` ports to `g.Player.On(Hallucinating)`. The C bit collisions
(ISCANC/ISLEVIT, ISMEAN/ISHALU, ISFLY/SEEMONST) are preserved as equal values —
they are disjoint by owner (monster vs hero), and the save format keeps working.
### 4.5 Level and Place
@@ -1336,7 +1343,7 @@ type Level struct {
Objects []*Object // lvl_obj
Monsters []*Monster // mlist
Stairs Coord
NTraps int
TrapCount int
}
func (l *Level) At(y, x int) *Place // INDEX
@@ -1345,8 +1352,16 @@ func (l *Level) SetChar(y, x int, ch byte)
func (l *Level) FlagsAt(y, x int) *PlaceFlags // flat
func (l *Level) MonsterAt(y, x int) *Monster // moat
func (l *Level) VisibleChar(y, x int) byte // winat: disguise if monster, else Ch
func (l *Level) ObjectAt(y, x int) *Object // find_obj
func (l *Level) AddObject(o *Object) // attach to lvl_obj
func (l *Level) RemoveObject(o *Object)
func (l *Level) AddMonster(m *Monster) // attach to mlist
func (l *Level) RemoveMonster(m *Monster)
```
Object and monster list management (attach/detach on `lvl_obj`/`mlist`) and the
`find_obj` lookup are `Level` methods (step 6), not free functions on `g`.
### 4.6 Room
```go
@@ -1362,14 +1377,18 @@ type Room struct {
### 4.7 Static tables
`tables.go` holds the immutable data as package-level **constants and `var`
tables that are never written after init** (the only permitted package-level
data — they are ROM, not state): `monsterTable [26]MonsterKind` (name, carry %,
flags, stats), `initDam` weapon table, base `objInfo` tables (copied into
`RogueGame.Items` at New so per-game mutation — probability resumming,
`oi_know`, `oi_guess` — stays instance-local), `eLevels`, `rainbow`, `stones`,
`woods`, `metals`, `sylls`, trap names, help text, `lvlMons`/`wandMons` spawn
strings, `hNames`/`mNames` combat messages.
`tables.go` holds the immutable data from extern.c/init.c on a `gameData`
struct, built by `newGameData()` and hung on each game as `g.data`. Every game
carries its own copy, so the package keeps **no** non-constant package-level
state (the linter's `gochecknoglobals` is clean): `monsterTable [26]MonsterKind`
(name, carry %, flags, stats), the `initWeaps` weapon table, base `ObjInfo`
tables (copied into `RogueGame.Items` at New so per-game mutation — probability
resumming, `Know`, `Guess` — stays instance-local), `eLevels`, `rainbow`,
`stoneTable`, `woods`, `metals`, `sylls`, trap names, help text,
`lvlMons`/`wandMons` spawn strings, `hNames`/`mNames` combat messages, and the
effect-dispatch handler tables (§5, step 7). Nothing on `gameData` is written
during play, except that the game's own `Monsters` copy (not the template) takes
the venus-flytrap damage hack.
```go
type ObjInfo struct {
@@ -1408,9 +1427,9 @@ func (r *Rng) next() int { // the RN macro
r.Seed = r.Seed*11109 + 13849 // 32-bit wraparound, as C int
return int(r.Seed>>16) & 0xffff
}
func (g *RogueGame) Rnd(rng int) int // rnd(): 0 if range==0 else abs(RN)%range
func (g *RogueGame) Roll(n, sides int) int
func (g *RogueGame) Spread(n int) int // misc.c: n ± 10%
func (g *RogueGame) rnd(rng int) int // rnd(): 0 if range==0 else abs(RN)%range
func (g *RogueGame) roll(n, sides int) int
func (g *RogueGame) spread(n int) int // misc.c: n ± 10%
```
Same seed → same dungeon, byte for byte. This is the port's master correctness
@@ -1424,10 +1443,12 @@ small integers. The port makes that mapping primary:
```go
type DaemonID int
const (
DRollwand DaemonID = iota + 1 // matches state.c save ids
DNone DaemonID = iota // empty slot
DRollwand // matches state.c save ids
DDoctor; DStomach; DRunners; DSwander
DNohaste; DUnconfuse; DUnsee; DSight
DVisuals; DComeDown; DLand // extra fuses beyond the C save map
DVisuals; DComeDown; DLand
DTurnSee // potions.c casts turn_see to a fuse callback
)
type delayedAction struct {
@@ -1447,23 +1468,32 @@ func (g *RogueGame) Fuse(f DaemonID, arg, time, typ int)
func (g *RogueGame) Lengthen(f DaemonID, xtime int)
func (g *RogueGame) Extinguish(f DaemonID)
func (g *RogueGame) KillDaemon(f DaemonID)
func (g *RogueGame) DoDaemons(flag int) // dispatches via one switch on DaemonID
func (g *RogueGame) DoDaemons(flag int) // fires the ready daemons/fuses
func (g *RogueGame) DoFuses(flag int)
```
The 11 callbacks in daemons.go become methods (`g.doctor()`, `g.stomach()`, ...)
invoked from the dispatch switch. This is more robust than function values
because it is comparable and serializable — the two properties the C design
needed pointers for.
The callbacks in daemons.go become methods (`g.doctor()`, `g.stomach()`, ...).
`runDaemon` looks the callback up in a `daemonHandlers` table on `gameData` (the
C `d_func` function pointers restored as method expressions, step 7). A
`DaemonID` is comparable and serializable — the two properties the C design
needed function pointers for — so it is also what the save format stores.
### 5.3 Screen — curses → tcell
```go
// Screen wraps tcell and exposes the curses vocabulary the ported code speaks.
// Terminal is the physical device, behind an interface so tests run headless.
// The real game uses term.Tcell; tests use a scripted testTerm.
type Terminal interface {
Render(w *Window) // blit a window to the device
ReadChar() byte // event loop → C char codes (arrows→hjkl, ^C→quit)
Fini() // restore the device (curses endwin) on exit
}
// Screen exposes the curses vocabulary the ported code speaks, over a Terminal.
type Screen struct {
T tcell.Screen
Std *Window // stdscr equivalent (the game map is drawn here)
Hw *Window // the scratch window for inventory/help overlays
term Terminal
Std *Window // stdscr equivalent (the game map is drawn here)
Hw *Window // the scratch window for inventory/help overlays
}
// Window is an in-memory cell buffer with a cursor, standout attribute, and
@@ -1471,10 +1501,11 @@ type Screen struct {
type Window struct{ ... }
func (w *Window) Move(y, x int); func (w *Window) AddCh(ch byte)
func (w *Window) MvAddCh(y, x int, ch byte); func (w *Window) MvAddStr(y, x int, s string)
func (w *Window) Printw(format string, a ...any); func (w *Window) Inch(y, x int) byte
func (w *Window) Printwf(format string, a ...any); func (w *Window) Inch(y, x int) byte
func (w *Window) Clear(); func (w *Window) Clrtoeol(); func (w *Window) Standout(on bool)
func (s *Screen) Refresh(w *Window) // blit buffer to tcell
func (s *Screen) ReadChar() byte // event loop → C char codes (arrows→hjkl, ^C→quit)
func (s *Screen) Refresh() // blit stdscr to the device
func (s *Screen) RefreshWin(w *Window) // blit any window
func (s *Screen) Fini() // tear the device down
```
Ported drawing code keeps its structure: `mvaddch(y, x, ch)`
@@ -1488,20 +1519,24 @@ SIGHUP/SIGTERM → `autoSave` via `os/signal`.
### 5.4 Messaging
```go
type MsgLine struct {
// MessageLine owns the io.c message machinery and the top screen line, wired
// to its screen/redraw/input needs via attach() (step 6).
type MessageLine struct {
buf strings.Builder // msgbuf/newpos
Mpos int // cursor col on the message line
Huh string // last message (Ctrl-R repeat)
SaveMsg bool; LowerMsg bool; MsgEsc bool
scr *Screen; look func(bool); readChar func() byte
}
func (g *RogueGame) Msg(format string, a ...any) bool // msg(); returns ESC-pressed
func (g *RogueGame) AddMsg(format string, a ...any)
func (g *RogueGame) EndMsg() bool
func (m *MessageLine) Msg(format string, a ...any) int // msg(); ^Escape / Escape
func (m *MessageLine) Addf(format string, a ...any)
func (m *MessageLine) End() int
```
C's `%s`+`vowelstr` message assembly ports directly on `fmt`. The io.c
status-line shadow statics become fields of an unexported `statusCache` struct
on `RogueGame`.
`RogueGame` keeps one-line `g.msg` / `g.addmsgf` / `g.endmsg` shorthands over
`g.Msgs`, so the ~400 ported call sites are unchanged. C's `%s`+`vowelstr`
message assembly ports directly on `fmt`. The io.c status-line shadow statics
become fields of an unexported `statusCache` struct on `RogueGame`.
### 5.5 Options
@@ -1519,29 +1554,34 @@ holding closures.
### 5.6 Save/restore — gob replaces state.c
The 2,134-line hand-rolled serializer exists because C cannot reflect. Go can:
the save file is `encoding/gob` of a `SaveState` snapshot struct
(version-tagged, gzip-compressed). C-format saves are **not** readable — the
formats were never cross-version compatible anyway (the file embeds the exact
version string).
the save file is `encoding/gob` of a `SaveState` snapshot struct (version-tagged
with `Release + "-go3"`; no compression). C-format saves are **not** readable —
the formats were never cross-version compatible anyway (the file embeds the
exact version string).
What still needs explicit code is exactly what state.c's `rs_fix_thing` did:
pointers that alias other structures are encoded as indices —
`SaveState` is a flat, pointer-free mirror of the game rather than an embedded
`RogueGame` — gob silently drops embedded fields of unexported types, and the
pointer aliasing has to be flattened anyway. What needs explicit code is exactly
what state.c's `rs_fix_thing` did: pointers that alias other structures are
encoded as indices, then re-aliased on load —
```go
type SaveState struct {
Version string
Game RogueGame // gob-friendly: exported fields
// pointer fixups:
CurArmorIdx, CurWeaponIdx int // index into Player.Pack, -1 = nil
CurRingIdx [2]int
MonstDest []DestRef // per monster: {Kind: hero|monster|object|gold, Index int}
PlaceMonst []int // per map cell: index into Level.Monsters
Version string
// ... the scalar game state, then value-copied worlds:
Player savedPlayer // Body savedCreature + equipment as pack indices
Places []savedPlace // map cells without the monster pointer
Objects []Object
Monsters []savedCreature
Dests []destRef // per monster: {Kind: hero|monster|object|gold, Idx}
// rooms, passages, daemons, items, bestiary, screen contents, ...
}
```
Save-file hygiene keeps the C behavior: the file is deleted on restore, and
`Run` exits after a successful save. The XOR encryption and symlink checks are
dropped (they were 1980s shared-machine anti-cheat).
Save-file hygiene keeps the C behavior: the file is deleted on restore, and the
save command exits the process after a successful write (via `myExit`, §6). The
XOR encryption and symlink checks are dropped (they were 1980s shared-machine
anti-cheat).
### 5.7 Scoreboard
@@ -1552,46 +1592,93 @@ Tombstone/victory screens port verbatim from rip.c.
## 6. C construct → Go construct map
| C construct | Go translation |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
| global variable | `RogueGame` field (or Player/Level/subsystem field) |
| file-scope static | unexported field on `RogueGame` or subsystem struct |
| `THING` union | `Creature` / `Object` structs |
| linked lists (`l_next/l_prev`, attach/detach) | slices + `attach(&s, x)`-style helpers (prepend, remove by identity) |
| `coord*` aliasing (t_dest) | `*Coord` pointers into live structs (kept) |
| function pointers (daemons, options, nameit) | enum IDs + dispatch switch (daemons, save-relevant); closures (options, nameit) |
| `when`/`otherwise`/`until` macros | `case`/`default`/`for !cond` |
| `goto` (retry loops in pack.c, move.c, passages.c) | labeled loops / restructured `for` |
| char-indexed tables (`monsters[type-'A']`) | same, `monsterTable[t.Type-'A']` |
| damage strings `"3x4/1x2"` | kept as strings; parsed by `rollEm` (`strings`/`strconv`) |
| curses stdscr/hw windows | `Window` cell buffers over tcell |
| `mvinch` screen reads | `Window.Inch` from the buffer |
| signal handlers (SIGHUP autosave, SIGTSTP, SIGINT) | `os/signal` goroutine → channel checked in ReadChar; tcell handles TSTP/resize |
| `setjmp`-free exits (`my_exit`, `exit()` everywhere) | `myExit` restores the terminal and calls `os.Exit(0)`; one game run is one process |
| `vsprintf` message building | `fmt.Sprintf` |
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
| DES wizard password | `ROGUE_WIZARD=1` env check |
| `md_*` platform shims | stdlib (`os`, `os/user`, `time`) or deleted |
| C construct | Go translation |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| global variable | `RogueGame` field (or Player/Level/subsystem field) |
| file-scope static | unexported field on `RogueGame` or subsystem struct |
| `THING` union | `Creature` / `Object` structs |
| linked lists (`l_next/l_prev`, attach/detach) | slices + `attach(&s, x)`-style helpers (prepend, remove by identity) |
| `coord*` aliasing (t_dest) | `*Coord` pointers into live structs (kept) |
| function pointers (daemons, options, nameit) | enum IDs → handler tables on `gameData` (daemons + effect dispatch); closures (options, nameit) |
| `when`/`otherwise`/`until` macros | `case`/`default`/`for !cond` |
| `goto` (retry loops in pack.c, move.c, passages.c) | labeled loops / restructured `for` |
| char-indexed tables (`monsters[type-'A']`) | same, `monsterTable[t.Type-'A']` |
| damage strings `"3x4/1x2"` | parsed once into a `DiceSpec` (`[]DiceRoll`) by `ParseDice` at table time |
| curses stdscr/hw windows | `Window` cell buffers over tcell |
| `mvinch` screen reads | `Window.Inch` from the buffer |
| signal handlers (SIGHUP autosave, SIGTSTP, SIGINT) | `os/signal` goroutine → channel checked in ReadChar; tcell handles TSTP/resize |
| `setjmp`-free exits (`my_exit`, `exit()` everywhere) | `myExit` restores the terminal and calls `os.Exit(0)`; one game run is one process |
| `vsprintf` message building | `fmt.Sprintf` |
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
| DES wizard password | `ROGUE_WIZARD=1` env check |
| `md_*` platform shims | stdlib (`os`, `os/user`, `time`) or deleted |
**Exit discipline** deserves a note: the C code calls `exit()` from deep inside
call chains (death, victory, save). The port threads a `g.gameOver(reason)` that
sets `Playing=false` and unwinds via normal returns to `Run` — the one
structural change made everywhere, since `os.Exit` would skip tcell teardown and
defers.
call chains (death, victory, save). One game run is one process, so the port
does the same — `myExit` restores the terminal (via `Screen.Fini`) and calls
`os.Exit(0)`. `Run` therefore does not return; the game ends by exiting. (An
earlier iteration unwound a `gameEnd` panic recovered in `Run`; step 8 replaced
it with the direct exit.)
## 7. Naming conventions
- C function `do_move` → method `(g *RogueGame) DoMove(dy, dx int)`; the C name
is preserved in CamelCase so cross-referencing Part 1 is mechanical.
Statics/internal helpers are lowerCamel (`g.doadd``g.addMsgV`).
- Entity-intrinsic functions become entity methods where they read no game
state: `inv_name(obj, drop)` needs ItemLore →
`(g *RogueGame) InvName(o *Object, drop bool) string`; but `is_magic(obj)`
`(o *Object) IsMagic() bool`.
- Boolean C `bool`/`int` returns stay `bool`; `char` returns become `byte`.
The initial port preserved C names in CamelCase for mechanical
cross-referencing. The idiomatic-refactor phase (steps 18) then renamed symbols
to descriptive Go names, keeping the C name as a `(file.c func_name)` breadcrumb
in each doc comment. So `do_move` is `moveHero`, not `DoMove`; `inv_name` is
`inventoryName`; `roll_em` is `rollAttacks`. The notable mappings are tabulated
in §7.1.
- Exported vs. unexported: `RogueGame`, the constructor `New` (taking a `Params`
struct), `Run`, `Restore`, and the type/field vocabulary are exported; the
ported command and effect methods are unexported (`moveHero`, `quaff`, ...) —
they are internals, not API.
- Entity-intrinsic functions become entity methods where they fit: `find_obj`
`(l *Level) ObjectAt`, pack surgery → `Player` methods, the message machinery
`MessageLine` methods (step 6).
- Boolean C `bool`/`int` returns become `bool`; the C `-1/0` status codes
(`attack`, `move_monst`, `do_chase`) become named `bool` results (step 5).
`char` returns become `byte`.
- `coord*` out-params become `Coord` returns (or `(Coord, bool)` for find-style
functions like `fallpos`/`find_floor`).
### 7.1 C name → Go name
Renames worth cross-referencing against Part 1 (the full set lives in the
`(file.c name)` breadcrumbs on each definition):
| C source name | Go name | Where |
| -------------------------- | ------------------------------------------ | ----------------- |
| `NewGame`/`Config`\* | `New` / `Params` | game.go |
| `my_exit` | `myExit` (→ `os.Exit`) | rip.go |
| `do_move` | `moveHero` | move.go |
| `be_trapped` | `springTrap` | move.go |
| `rndmove` | `randomStep` | move.go |
| `do_run` | `startRun` | move.go |
| `move_stuff` | `finishMove` | move.go |
| `chg_str` | `changeStrength` | misc.go |
| `get_dir` | `promptDirection` | misc.go |
| `do_rooms` / `do_maze` | `digRooms` / `digMaze` | rooms.go |
| `rnd_room` / `rnd_pos` | `randomRoom` / `randomPos` | rooms.go |
| `do_passages` / `conn` | `digPassages` / `connectRooms` | passages.go |
| `putpass` / `numpass` | `putPassage` / `numberPassage` | passages.go |
| `roomin` / `cansee` | `roomIn` / `canSee` | chase.go |
| `runto` / `move_monst` | `runTo` / `moveMonster` | chase.go |
| `do_chase` / `set_oldch` | `chaseStep` / `setOldChar` | chase.go |
| `find_obj` | `(l *Level).ObjectAt` | level.go |
| `roll_em` | `rollAttacks` | fight.go |
| `do_pot` | `applyPotionFuse` | potions.go |
| `get_item` | `promptPackItem` (→ `(obj, ok)`) | pack.go |
| `inv_name` | `inventoryName` | things.go |
| `struct msg` statics | `MessageLine` | io.go |
| `o_type` (byte) | `Object.Kind` (`ObjectKind`) | object.go |
| `o_arm` (overloaded) | `ArmorClass`/`Charges`/`GoldValue`/`Bonus` | object.go |
| `s_arm` | `Stats.ArmorClass` | types.go |
| `amulet` / `ntraps` | `HasAmulet` / `Level.TrapCount` | game.go/level.go |
| `ISHUH`/`SEEMONST`/`ISRUN` | `Confused`/`SenseMonsters`/`Awake` | types.go (step 1) |
\* `Restore` keeps its C name but also takes `Params`.
## 8. Porting order (each step compiles, is committed, and is testable)
1. **Foundation:** `go.mod`, types.go, object.go, creature.go, level.go, rng.go,
@@ -1609,6 +1696,13 @@ defers.
7. **Endgame:** rip.go, score.go, save.go, cmd/rogue. Full game loop, save,
scores.
Once the transliterated port was complete and playable, an **idiomatic-refactor
phase** followed (one feature branch per step, suite green throughout): typed
kinds and split `o_arm` fields; descriptive renames and `goto` removal; the
god-object extraction onto `Player`/`Level`/`MessageLine`; effect-dispatch
handler tables on `gameData`; the `New(Params)` constructor; and direct process
exit. Those are the steps referenced above (e.g. "step 5", "step 7").
## 9. Dropped C functionality (deliberate)
| Dropped | Why | Replacement |
@@ -1629,11 +1723,14 @@ defers.
chars + room/passage metadata) to text fixtures; any diff = a porting bug in
rooms/passages/newlevel. Fixtures generated once from instrumented C, or
accepted from the first correct Go run and guarded thereafter.
- **Combat math tests:** `swing`/`rollEm` against hand-computed cases,
- **Combat math tests:** `swing`/`rollAttacks` against hand-computed cases,
str_plus/add_dam table spot checks.
- **Determinism test:** scripted input sequence against a fixed seed must
produce identical final state across runs (guards hidden nondeterminism — map
iteration, time dependence).
- **Save round-trip:** New → play N scripted turns → save → restore → compare
full state.
- `go vet` + `gofmt` clean at every commit.
- **Save round-trip:** dirty a game's state, snapshot it to a file, `Restore`,
and compare — including the pointer fixups (equipment aliasing, monster chase
targets, map monster index). Because game-over now exits the process (step 8),
this goes through `saveFile`/`Restore` directly rather than driving the `S`
command to completion.
- `go vet` + `gofmt` + `golangci-lint` clean at every commit.

View File

@@ -73,8 +73,12 @@ cmd/rogue/ the executable
```
The engine package is fully headless-testable: `go test ./game/` runs scripted
game sessions, dungeon-generation golden checks, and an RNG compatibility test
against the original C generator.
command sequences, dungeon-generation golden checks, and an RNG compatibility
test against the original C generator.
For development, the `Makefile` wraps the toolchain: `make fmt` (gofmt +
prettier), `make lint` (golangci-lint), `make test`, and `make check` (all
three).
## License