From cb1e302102c95855fffedead1b12da1277b78256 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 23 Jul 2026 08:40:15 +0700 Subject: [PATCH] Refresh ARCHITECTURE.md Part 2 for the post-refactor design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ARCHITECTURE.md | 351 ++++++++++++++++++++++++++++++------------------ README.md | 8 +- 2 files changed, 230 insertions(+), 129 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e8b9588..4364cd5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 1–8) 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. diff --git a/README.md b/README.md index 0e4e061..7dd6652 100644 --- a/README.md +++ b/README.md @@ -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