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) init.go per-game randomization (init.c)
command.go command dispatch (command.c) command.go command dispatch (command.c)
io.go message/status line (io.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) newlevel.go level orchestration (new_level.c)
rooms.go room/maze generation (rooms.c) rooms.go room/maze generation (rooms.c)
passages.go corridors (passages.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 potions.go, scrolls.go, rings.go, sticks.go, weapons.go, armor.go
misc.go look(), eat, direction input (misc.c) misc.go look(), eat, direction input (misc.c)
daemon.go scheduler (daemon.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) options.go options screen + ROGUEOPTS (options.c)
wizard.go wizard commands, identify (wizard.c) wizard.go wizard commands, identify (wizard.c)
rip.go tombstone, victory, scores (rip.c) rip.go tombstone, victory, scores (rip.c)
save.go save/restore via gob (save.c + state.c) save.go save/restore via gob (save.c + state.c)
score.go scoreboard (score.h + rip.c/save.c score I/O) 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 `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 Level Level // map, rooms, passages, level objects, monsters
Depth int // C `level` Depth int // C `level`
MaxDepth int // C `max_level` MaxDepth int // C `max_level`
Amulet bool HasAmulet bool
SeenStairs bool SeenStairs bool
// --- turn/command engine --- // --- turn/command engine ---
@@ -1155,7 +1156,7 @@ type RogueGame struct {
ToDeath, Kamikaze, HasHit bool ToDeath, Kamikaze, HasHit bool
MaxHit int MaxHit int
Take byte // glyph of thing picked up this move Take byte // glyph of thing picked up this move
Delta Coord // last direction from GetDir Delta Coord // last direction from promptDirection
DirCh byte DirCh byte
LastComm, LastDir byte // command repeat state ('a' command) LastComm, LastDir byte // command repeat state ('a' command)
LLastComm, LLastDir byte LLastComm, LLastDir byte
@@ -1166,9 +1167,9 @@ type RogueGame struct {
Daemons DaemonList // d_list[20] + `between` counter Daemons DaemonList // d_list[20] + `between` counter
// --- messages / UI --- // --- messages / UI ---
scr *Screen // tcell-backed terminal (stdscr + hw scratch window) scr *Screen // tcell-backed terminal (stdscr + hw scratch window)
Msgs MsgLine // msgbuf/newpos/mpos/huh/save state Msgs MessageLine // msgbuf/newpos/mpos/huh/save state
Options Options // terse, jump, seefloor, passgo, flush, tombstone, inv_type... Options Options // terse, jump, seefloor, passgo, flush, tombstone, inv_type...
StatMsg bool StatMsg bool
InvDescribe bool InvDescribe bool
Oldpos Coord // hero position before last look() Oldpos Coord // hero position before last look()
@@ -1182,13 +1183,16 @@ type RogueGame struct {
LastScore int LastScore int
AllScore bool AllScore bool
ScorePath string 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 Player-centric C globals move onto `Player`, world-centric onto `Level` (see
§4/§5); the remainder sit directly on `RogueGame` with the original semantics. §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`, 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 ### Entrypoints
@@ -1211,13 +1215,13 @@ exit code.
type Coord struct{ X, Y int } // value type; C `ce(a,b)` == `a == b` type Coord struct{ X, Y int } // value type; C `ce(a,b)` == `a == b`
type Stats struct { type Stats struct {
Str int // str_t; 3..31 Str int // str_t; 3..31
Exp int Exp int
Lvl int Lvl int
Arm int // armor class, lower is better ArmorClass int // armor class, lower is better (was s_arm)
HP int // s_hpt HP int // s_hpt
Dmg string // damage dice "1x4/1x2" — kept as string, parsed by rollEm Dmg DiceSpec // damage dice, parsed once by ParseDice (§4.3)
MaxHP int MaxHP int
} }
``` ```
@@ -1268,29 +1272,31 @@ both the AI and the save format rely on.
```go ```go
type Object struct { type Object struct {
Type byte // POTION '!', SCROLL '?', WEAPON ')', ... Kind ObjectKind // item category (was o_type); Glyph() gives the map char
Pos Coord Pos Coord
Text string // scroll gibberish Text string // scroll gibberish
Launch int // launcher weapon index, -1 none Launch WeaponKind // launcher weapon (noWeapon if none)
PackCh byte // inventory letter PackCh byte // inventory letter
Damage string // wield dice Damage DiceSpec // wield dice, parsed once
HurlDmg string // thrown dice HurlDmg DiceSpec // thrown dice
Count int // stack size Count int // stack size
Which int // subtype (P_HEALING, MACE, R_PROTECT, ...) Which int // subtype index (P_HEALING, MACE, R_PROTECT, ...)
HPlus, DPlus int HPlus, DPlus int
Arm int // OVERLOADED, as in C: armor class | charges | gold value ArmorClass int // armor only (was part of the overloaded o_arm)
Flags ObjFlags Charges int // wands/staffs
Group int // stack group id GoldValue int // gold piles
Label string // player-applied name 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: The single C `o_arm` field was overloaded (armor class | charges | gold value |
`o.Arm++`" keeps its original shape. 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 ### 4.4 Flags
@@ -1301,11 +1307,11 @@ type RoomFlags uint16
type PlaceFlags uint8 type PlaceFlags uint8
const ( const (
CanHuh CreatureFlags = 1 << iota // creature can confuse CanConfuse CreatureFlags = 1 << iota // ISHUH: creature can confuse
CanSee; IsBlind; IsCanc; IsFound; IsGreed; IsHaste; IsTarget CanSeeInvisible; Blind; Cancelled; Found; Greedy; Hasted; Targeted
IsHeld; IsHuh; IsInvis; IsMean; IsRegen; IsRun; IsFly; IsSlow Held; Confused; Invisible; Mean; Regenerates; Awake; Flying; Slowed
// hero aliases sharing C bit values are separate named constants: // 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 func (f CreatureFlags) Has(b CreatureFlags) bool
@@ -1313,10 +1319,11 @@ func (f *CreatureFlags) Set(b CreatureFlags)
func (f *CreatureFlags) Clear(b CreatureFlags) func (f *CreatureFlags) Clear(b CreatureFlags)
``` ```
C's `on(player, ISHALU)` ports to `g.Player.Flags.Has(IsHalu)`. The C bit The C `IS*`/`CAN*` macro names became descriptive Go constants in step 1
collisions (ISCANC/ISLEVIT, ISMEAN/ISHALU, ISFLY/SEEMONST) are preserved as (`ISHUH``Confused`, `SEEMONST``SenseMonsters`, `ISRUN``Awake`, ...). C's
equal values — they are disjoint by owner (monster vs hero), and the save format `on(player, ISHALU)` ports to `g.Player.On(Hallucinating)`. The C bit collisions
keeps working. (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 ### 4.5 Level and Place
@@ -1336,7 +1343,7 @@ type Level struct {
Objects []*Object // lvl_obj Objects []*Object // lvl_obj
Monsters []*Monster // mlist Monsters []*Monster // mlist
Stairs Coord Stairs Coord
NTraps int TrapCount int
} }
func (l *Level) At(y, x int) *Place // INDEX 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) FlagsAt(y, x int) *PlaceFlags // flat
func (l *Level) MonsterAt(y, x int) *Monster // moat 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) 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 ### 4.6 Room
```go ```go
@@ -1362,14 +1377,18 @@ type Room struct {
### 4.7 Static tables ### 4.7 Static tables
`tables.go` holds the immutable data as package-level **constants and `var` `tables.go` holds the immutable data from extern.c/init.c on a `gameData`
tables that are never written after init** (the only permitted package-level struct, built by `newGameData()` and hung on each game as `g.data`. Every game
data — they are ROM, not state): `monsterTable [26]MonsterKind` (name, carry %, carries its own copy, so the package keeps **no** non-constant package-level
flags, stats), `initDam` weapon table, base `objInfo` tables (copied into state (the linter's `gochecknoglobals` is clean): `monsterTable [26]MonsterKind`
`RogueGame.Items` at New so per-game mutation — probability resumming, (name, carry %, flags, stats), the `initWeaps` weapon table, base `ObjInfo`
`oi_know`, `oi_guess` — stays instance-local), `eLevels`, `rainbow`, `stones`, tables (copied into `RogueGame.Items` at New so per-game mutation — probability
`woods`, `metals`, `sylls`, trap names, help text, `lvlMons`/`wandMons` spawn resumming, `Know`, `Guess` — stays instance-local), `eLevels`, `rainbow`,
strings, `hNames`/`mNames` combat messages. `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 ```go
type ObjInfo struct { 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 r.Seed = r.Seed*11109 + 13849 // 32-bit wraparound, as C int
return int(r.Seed>>16) & 0xffff 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) rnd(rng int) int // rnd(): 0 if range==0 else abs(RN)%range
func (g *RogueGame) Roll(n, sides int) int func (g *RogueGame) roll(n, sides int) int
func (g *RogueGame) Spread(n int) int // misc.c: n ± 10% 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 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 ```go
type DaemonID int type DaemonID int
const ( 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 DDoctor; DStomach; DRunners; DSwander
DNohaste; DUnconfuse; DUnsee; DSight 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 { 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) Lengthen(f DaemonID, xtime int)
func (g *RogueGame) Extinguish(f DaemonID) func (g *RogueGame) Extinguish(f DaemonID)
func (g *RogueGame) KillDaemon(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) func (g *RogueGame) DoFuses(flag int)
``` ```
The 11 callbacks in daemons.go become methods (`g.doctor()`, `g.stomach()`, ...) The callbacks in daemons.go become methods (`g.doctor()`, `g.stomach()`, ...).
invoked from the dispatch switch. This is more robust than function values `runDaemon` looks the callback up in a `daemonHandlers` table on `gameData` (the
because it is comparable and serializable — the two properties the C design C `d_func` function pointers restored as method expressions, step 7). A
needed pointers for. `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 ### 5.3 Screen — curses → tcell
```go ```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 { type Screen struct {
T tcell.Screen term Terminal
Std *Window // stdscr equivalent (the game map is drawn here) Std *Window // stdscr equivalent (the game map is drawn here)
Hw *Window // the scratch window for inventory/help overlays Hw *Window // the scratch window for inventory/help overlays
} }
// Window is an in-memory cell buffer with a cursor, standout attribute, and // Window is an in-memory cell buffer with a cursor, standout attribute, and
@@ -1471,10 +1501,11 @@ type Screen struct {
type Window struct{ ... } type Window struct{ ... }
func (w *Window) Move(y, x int); func (w *Window) AddCh(ch byte) 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) 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 (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) Refresh() // blit stdscr to the device
func (s *Screen) ReadChar() byte // event loop → C char codes (arrows→hjkl, ^C→quit) 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)` Ported drawing code keeps its structure: `mvaddch(y, x, ch)`
@@ -1488,20 +1519,24 @@ SIGHUP/SIGTERM → `autoSave` via `os/signal`.
### 5.4 Messaging ### 5.4 Messaging
```go ```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 buf strings.Builder // msgbuf/newpos
Mpos int // cursor col on the message line Mpos int // cursor col on the message line
Huh string // last message (Ctrl-R repeat) Huh string // last message (Ctrl-R repeat)
SaveMsg bool; LowerMsg bool; MsgEsc bool 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 (m *MessageLine) Msg(format string, a ...any) int // msg(); ^Escape / Escape
func (g *RogueGame) AddMsg(format string, a ...any) func (m *MessageLine) Addf(format string, a ...any)
func (g *RogueGame) EndMsg() bool func (m *MessageLine) End() int
``` ```
C's `%s`+`vowelstr` message assembly ports directly on `fmt`. The io.c `RogueGame` keeps one-line `g.msg` / `g.addmsgf` / `g.endmsg` shorthands over
status-line shadow statics become fields of an unexported `statusCache` struct `g.Msgs`, so the ~400 ported call sites are unchanged. C's `%s`+`vowelstr`
on `RogueGame`. 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 ### 5.5 Options
@@ -1519,29 +1554,34 @@ holding closures.
### 5.6 Save/restore — gob replaces state.c ### 5.6 Save/restore — gob replaces state.c
The 2,134-line hand-rolled serializer exists because C cannot reflect. Go can: 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 the save file is `encoding/gob` of a `SaveState` snapshot struct (version-tagged
(version-tagged, gzip-compressed). C-format saves are **not** readable — the with `Release + "-go3"`; no compression). C-format saves are **not** readable —
formats were never cross-version compatible anyway (the file embeds the exact the formats were never cross-version compatible anyway (the file embeds the
version string). exact version string).
What still needs explicit code is exactly what state.c's `rs_fix_thing` did: `SaveState` is a flat, pointer-free mirror of the game rather than an embedded
pointers that alias other structures are encoded as indices — `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 ```go
type SaveState struct { type SaveState struct {
Version string Version string
Game RogueGame // gob-friendly: exported fields // ... the scalar game state, then value-copied worlds:
// pointer fixups: Player savedPlayer // Body savedCreature + equipment as pack indices
CurArmorIdx, CurWeaponIdx int // index into Player.Pack, -1 = nil Places []savedPlace // map cells without the monster pointer
CurRingIdx [2]int Objects []Object
MonstDest []DestRef // per monster: {Kind: hero|monster|object|gold, Index int} Monsters []savedCreature
PlaceMonst []int // per map cell: index into Level.Monsters 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 Save-file hygiene keeps the C behavior: the file is deleted on restore, and the
`Run` exits after a successful save. The XOR encryption and symlink checks are save command exits the process after a successful write (via `myExit`, §6). The
dropped (they were 1980s shared-machine anti-cheat). XOR encryption and symlink checks are dropped (they were 1980s shared-machine
anti-cheat).
### 5.7 Scoreboard ### 5.7 Scoreboard
@@ -1552,46 +1592,93 @@ Tombstone/victory screens port verbatim from rip.c.
## 6. C construct → Go construct map ## 6. C construct → Go construct map
| C construct | Go translation | | C construct | Go translation |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| global variable | `RogueGame` field (or Player/Level/subsystem field) | | global variable | `RogueGame` field (or Player/Level/subsystem field) |
| file-scope static | unexported field on `RogueGame` or subsystem struct | | file-scope static | unexported field on `RogueGame` or subsystem struct |
| `THING` union | `Creature` / `Object` structs | | `THING` union | `Creature` / `Object` structs |
| linked lists (`l_next/l_prev`, attach/detach) | slices + `attach(&s, x)`-style helpers (prepend, remove by identity) | | 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) | | `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) | | 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` | | `when`/`otherwise`/`until` macros | `case`/`default`/`for !cond` |
| `goto` (retry loops in pack.c, move.c, passages.c) | labeled loops / restructured `for` | | `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']` | | char-indexed tables (`monsters[type-'A']`) | same, `monsterTable[t.Type-'A']` |
| damage strings `"3x4/1x2"` | kept as strings; parsed by `rollEm` (`strings`/`strconv`) | | damage strings `"3x4/1x2"` | parsed once into a `DiceSpec` (`[]DiceRoll`) by `ParseDice` at table time |
| curses stdscr/hw windows | `Window` cell buffers over tcell | | curses stdscr/hw windows | `Window` cell buffers over tcell |
| `mvinch` screen reads | `Window.Inch` from the buffer | | `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 | | 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 | | `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` | | `vsprintf` message building | `fmt.Sprintf` |
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) | | XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
| DES wizard password | `ROGUE_WIZARD=1` env check | | DES wizard password | `ROGUE_WIZARD=1` env check |
| `md_*` platform shims | stdlib (`os`, `os/user`, `time`) or deleted | | `md_*` platform shims | stdlib (`os`, `os/user`, `time`) or deleted |
**Exit discipline** deserves a note: the C code calls `exit()` from deep inside **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 call chains (death, victory, save). One game run is one process, so the port
sets `Playing=false` and unwinds via normal returns to `Run` — the one does the same — `myExit` restores the terminal (via `Screen.Fini`) and calls
structural change made everywhere, since `os.Exit` would skip tcell teardown and `os.Exit(0)`. `Run` therefore does not return; the game ends by exiting. (An
defers. earlier iteration unwound a `gameEnd` panic recovered in `Run`; step 8 replaced
it with the direct exit.)
## 7. Naming conventions ## 7. Naming conventions
- C function `do_move` → method `(g *RogueGame) DoMove(dy, dx int)`; the C name The initial port preserved C names in CamelCase for mechanical
is preserved in CamelCase so cross-referencing Part 1 is mechanical. cross-referencing. The idiomatic-refactor phase (steps 18) then renamed symbols
Statics/internal helpers are lowerCamel (`g.doadd``g.addMsgV`). to descriptive Go names, keeping the C name as a `(file.c func_name)` breadcrumb
- Entity-intrinsic functions become entity methods where they read no game in each doc comment. So `do_move` is `moveHero`, not `DoMove`; `inv_name` is
state: `inv_name(obj, drop)` needs ItemLore → `inventoryName`; `roll_em` is `rollAttacks`. The notable mappings are tabulated
`(g *RogueGame) InvName(o *Object, drop bool) string`; but `is_magic(obj)` in §7.1.
`(o *Object) IsMagic() bool`.
- Boolean C `bool`/`int` returns stay `bool`; `char` returns become `byte`. - 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 - `coord*` out-params become `Coord` returns (or `(Coord, bool)` for find-style
functions like `fallpos`/`find_floor`). 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) ## 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, 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, 7. **Endgame:** rip.go, score.go, save.go, cmd/rogue. Full game loop, save,
scores. 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) ## 9. Dropped C functionality (deliberate)
| Dropped | Why | Replacement | | Dropped | Why | Replacement |
@@ -1629,11 +1723,14 @@ defers.
chars + room/passage metadata) to text fixtures; any diff = a porting bug in chars + room/passage metadata) to text fixtures; any diff = a porting bug in
rooms/passages/newlevel. Fixtures generated once from instrumented C, or rooms/passages/newlevel. Fixtures generated once from instrumented C, or
accepted from the first correct Go run and guarded thereafter. 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. str_plus/add_dam table spot checks.
- **Determinism test:** scripted input sequence against a fixed seed must - **Determinism test:** scripted input sequence against a fixed seed must
produce identical final state across runs (guards hidden nondeterminism — map produce identical final state across runs (guards hidden nondeterminism — map
iteration, time dependence). iteration, time dependence).
- **Save round-trip:** New → play N scripted turns → save → restore → compare - **Save round-trip:** dirty a game's state, snapshot it to a file, `Restore`,
full state. and compare — including the pointer fixups (equipment aliasing, monster chase
- `go vet` + `gofmt` clean at every commit. 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 The engine package is fully headless-testable: `go test ./game/` runs scripted
game sessions, dungeon-generation golden checks, and an RNG compatibility test command sequences, dungeon-generation golden checks, and an RNG compatibility
against the original C generator. 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 ## License