Merge docs-refresh (ARCHITECTURE.md Part 2 + rename table)

This commit is contained in:
2026-07-23 08:40:55 +07:00
3 changed files with 250 additions and 143 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
@@ -1167,7 +1168,7 @@ type RogueGame struct {
// --- messages / UI ---
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...
StatMsg bool
InvDescribe bool
@@ -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
@@ -1214,9 +1218,9 @@ type Stats struct {
Str int // str_t; 3..31
Exp int
Lvl int
Arm int // armor class, lower is better
ArmorClass int // armor class, lower is better (was s_arm)
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
}
```
@@ -1268,29 +1272,31 @@ both the AI and the save format rely on.
```go
type Object struct {
Type byte // POTION '!', SCROLL '?', WEAPON ')', ...
Kind ObjectKind // item category (was o_type); Glyph() gives the map char
Pos Coord
Text string // scroll gibberish
Launch int // launcher weapon index, -1 none
Launch WeaponKind // launcher weapon (noWeapon if none)
PackCh byte // inventory letter
Damage string // wield dice
HurlDmg string // thrown dice
Damage DiceSpec // wield dice, parsed once
HurlDmg DiceSpec // thrown dice
Count int // stack size
Which int // subtype (P_HEALING, MACE, R_PROTECT, ...)
Which int // subtype index (P_HEALING, MACE, R_PROTECT, ...)
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)
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,21 +1468,30 @@ 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
term Terminal
Std *Window // stdscr equivalent (the game map is drawn here)
Hw *Window // the scratch window for inventory/help overlays
}
@@ -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
// ... 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
@@ -1553,17 +1593,17 @@ 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) |
| 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"` | 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 |
| `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 |
@@ -1574,24 +1614,71 @@ Tombstone/victory screens port verbatim from rip.c.
| `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

34
TODO.md
View File

@@ -29,11 +29,24 @@ Refactor ground rules:
# Next Step
Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the post-refactor
names; add the C name → Go name rename table.
Playtest hardening pass: play several full games with the tcell binary and
extend run_test.go to drive a deeper multi-level playthrough (descend past level
5, use potions, scrolls, zapping, save/restore). Note: game-over now exits the
process (step 8), so scripted drives must stay bounded and avoid
death/quit/save-command; exercise the exit paths (death display, 'S' save)
another way if needed. Fix any panics, message mismatches, or divergences from
the C behavior that this uncovers, with regression tests.
# Completed Steps
- 2026-07-23 Docs refresh (docs-refresh): rewrote ARCHITECTURE.md Part 2 (the
pre-implementation design sketch) to match the final code — current type/field
names (ObjectKind, DiceSpec, split o_arm, step-1 flag names, TrapCount, Level
list methods), the static tables now on the per-game gameData struct, the
daemon/effect handler tables, the MessageLine extraction, the Terminal
interface, the flat gob SaveState, and the New(Params) + os.Exit design. Added
§7.1, a C-name → Go-name rename table, and a README note on the make targets.
- 2026-07-23 Refactor step 8 (refactor/constructor-style): constructor and exit
pass. NewGame(Config) → New(Params) and Restore takes Params, so the package's
primary type gets the canonical New() constructor with a named-field Params
@@ -144,24 +157,17 @@ names; add the C name → Go name rename table.
# Future Steps
1. Playtest hardening pass: play several full games with the tcell binary and
extend run_test.go to drive a deeper multi-level playthrough (descend past
level 5, use potions, scrolls, zapping, save/restore). Note: game-over now
exits the process (step 8), so scripted drives must stay bounded and avoid
death/quit/save-command; exercise the exit paths (death display, 'S' save)
another way if needed. Fix any panics, message mismatches, or divergences
from the C behavior that this uncovers, with regression tests.
2. Verify the seed-compatibility claim against the C reference on c-master: same
1. Verify the seed-compatibility claim against the C reference on c-master: same
seed, same dungeon, same item tables, for several seeds.
3. Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
2. Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
wizard commands).
4. Tag a release once a full game (Amulet retrieval and score entry) completes
3. Tag a release once a full game (Amulet retrieval and score entry) completes
without defects.
5. Full-terminal-size support (deferred by explicit decision 2026-07-06):
4. Full-terminal-size support (deferred by explicit decision 2026-07-06):
per-game dungeon dimensions instead of the 80x24 constants; open design
questions are resize policy, gameplay tuning at larger sizes, and a --classic
80x24 mode.
6. Note: this repo is exempt from the standard policy scaffold. A minimal dev
5. Note: this repo is exempt from the standard policy scaffold. A minimal dev
Makefile (fmt/fmt-check/lint/test/check targets) exists per sneak's
2026-07-07 request, but do not add a Dockerfile, CI config, or
REPO_POLICIES.md.