Add ARCHITECTURE.md Part 2: Go OOP port design
Design for the Go port (module sneak.berlin/go/rogue): - RogueGame instance type owns all former globals; no package globals - THING union split into Creature (Player/Monster) and Object - Slices replace THING linked lists; list.c is not ported - Daemon function pointers become serializable DaemonID enum + dispatch - tcell/v2 (sole third-party dep) replaces curses; Window cell buffers preserve the screen-as-data-structure idiom - encoding/gob snapshot replaces the 2,134-line state.c serializer - Exact-LCG RNG for seed-compatible dungeon generation, with golden tests - Per-file porting map, naming conventions, porting order, drop list
This commit is contained in:
612
ARCHITECTURE.md
612
ARCHITECTURE.md
@@ -882,3 +882,615 @@ typedef struct sc_ent SCORE;
|
|||||||
```
|
```
|
||||||
|
|
||||||
Declares `rd_score(SCORE*)` / `wr_score(SCORE*)` (implemented in save.c).
|
Declares `rd_score(SCORE*)` / `wr_score(SCORE*)` (implemented in save.c).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part 2 — The Go Port Design
|
||||||
|
|
||||||
|
Module: **`sneak.berlin/go/rogue`**
|
||||||
|
|
||||||
|
## 1. Goals and principles
|
||||||
|
|
||||||
|
1. **Faithful gameplay.** The port reproduces Rogue 5.4.4 behavior exactly:
|
||||||
|
same dungeon generation for a given seed, same combat math, same item
|
||||||
|
tables, same messages. The unit of porting is the C function; every C
|
||||||
|
function becomes a Go method (or function) with a recognizable name, ported
|
||||||
|
line by line unless a rule below says otherwise.
|
||||||
|
2. **No globals.** All formerly-global state lives in a single instance type,
|
||||||
|
**`RogueGame`**, which owns per-game state and exposes the game's main
|
||||||
|
entrypoint (`Run`). Two `RogueGame` values are two independent games.
|
||||||
|
File-scope C statics (pagination counters, shadow copies, scratch buffers)
|
||||||
|
become fields too — either on `RogueGame` or on the subsystem struct that
|
||||||
|
replaces their file.
|
||||||
|
3. **Modern data structures.** Basic containers are NOT ported. The THING
|
||||||
|
doubly-linked lists become slices (`[]*Object`, `[]*Monster`). `list.c`
|
||||||
|
disappears; where the C code's shape depends on list semantics
|
||||||
|
(attach-to-front, detach), small compat helpers keep the ported call sites
|
||||||
|
readable. calloc/free vanish under GC.
|
||||||
|
4. **OOP where the domain has objects, not everywhere.** The THING union
|
||||||
|
splits into `Object` and `Creature` (with `Player` and `Monster` variants).
|
||||||
|
Behavior that is intrinsically about one entity (naming an object, testing
|
||||||
|
a flag) is a method on that entity. Everything that touches wider game
|
||||||
|
state (most of the game) is a method on `*RogueGame`, mirroring the C
|
||||||
|
call graph one-to-one.
|
||||||
|
5. **Standard library first.** Exactly one third-party dependency:
|
||||||
|
`github.com/gdamore/tcell/v2` — the de facto standard Go terminal cell
|
||||||
|
library — replacing curses. Everything else (saves, scores, RNG, signals,
|
||||||
|
options) is stdlib.
|
||||||
|
6. **Dead weight is dropped, not ported.** `mdport.c`'s 900-line keyboard
|
||||||
|
decoder (tcell events replace it), `xcrypt.c`'s DES (wizard mode gates on
|
||||||
|
an env var instead), load-average checks, tty dsusp-char juggling, and the
|
||||||
|
XOR save encryption all disappear. Each drop is documented in §9.
|
||||||
|
|
||||||
|
## 2. Repository and package layout
|
||||||
|
|
||||||
|
The C sources remain in the repository root as the reference implementation.
|
||||||
|
Go code lives in subdirectories (Go will not build a package directory that
|
||||||
|
contains foreign `.c` files):
|
||||||
|
|
||||||
|
```
|
||||||
|
go.mod module sneak.berlin/go/rogue
|
||||||
|
cmd/rogue/main.go package main — flag parsing, constructs RogueGame, calls Run
|
||||||
|
game/ package game — the port, one .go file per .c file
|
||||||
|
game.go RogueGame struct, NewGame, Run (main.c)
|
||||||
|
rng.go seeded LCG (main.c: rnd/roll + RN macro)
|
||||||
|
types.go Coord, Stats, flags, constants (rogue.h)
|
||||||
|
object.go Object type (rogue.h THING _o arm)
|
||||||
|
creature.go Creature/Monster/Player (rogue.h THING _t arm)
|
||||||
|
level.go Place, Level, map access (rogue.h places[] + macros)
|
||||||
|
tables.go monsters[26], obj_info tables, e_levels, rainbow/stones/wood/metal (extern.c)
|
||||||
|
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)
|
||||||
|
newlevel.go level orchestration (new_level.c)
|
||||||
|
rooms.go room/maze generation (rooms.c)
|
||||||
|
passages.go corridors (passages.c)
|
||||||
|
move.go player movement, traps (move.c)
|
||||||
|
chase.go monster AI (chase.c)
|
||||||
|
monsters.go monster creation (monsters.c)
|
||||||
|
fight.go combat (fight.c)
|
||||||
|
things.go item naming/creation (things.c)
|
||||||
|
pack.go inventory (pack.c)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
`mach_dep.c`, `mdport.c`, `xcrypt.c`, `state.c`, `list.c`, `vers.c` have no
|
||||||
|
counterpart files: their few live responsibilities are absorbed (signals →
|
||||||
|
`game.go`; score file locking → `score.go`; version string → `game.go`;
|
||||||
|
serialization → `save.go`).
|
||||||
|
|
||||||
|
## 3. The `RogueGame` type
|
||||||
|
|
||||||
|
`RogueGame` is deliberately a god object: it is the C global namespace given
|
||||||
|
a receiver. Fields are grouped into embedded sub-structs where a subsystem is
|
||||||
|
coherent, but everything is reachable from `g`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// RogueGame is one complete game of Rogue: all state that was global in the
|
||||||
|
// C implementation, plus the terminal it plays on. Construct with NewGame,
|
||||||
|
// then call Run.
|
||||||
|
type RogueGame struct {
|
||||||
|
// --- identity / RNG ---
|
||||||
|
Rng *Rng // the LCG; seed == C `seed`
|
||||||
|
Dnum int // dungeon number (the seed as shown to the user)
|
||||||
|
Whoami string // player name
|
||||||
|
Fruit string // favorite fruit
|
||||||
|
Home string // home directory (save default)
|
||||||
|
FileName string // save file path
|
||||||
|
Wizard bool // debug mode
|
||||||
|
NoScore bool // wizard was used / scoring disabled
|
||||||
|
|
||||||
|
// --- the world ---
|
||||||
|
Player Player
|
||||||
|
Level Level // map, rooms, passages, level objects, monsters
|
||||||
|
Depth int // C `level`
|
||||||
|
MaxDepth int // C `max_level`
|
||||||
|
Amulet bool
|
||||||
|
SeenStairs bool
|
||||||
|
|
||||||
|
// --- turn/command engine ---
|
||||||
|
Playing bool
|
||||||
|
After bool // this action consumes a turn
|
||||||
|
Again bool // repeating last command
|
||||||
|
Count int // command repeat count
|
||||||
|
NoCommand int // turns asleep
|
||||||
|
NoMove int // turns held
|
||||||
|
Quiet int // consecutive restful turns (heal rate)
|
||||||
|
Running bool
|
||||||
|
RunCh byte
|
||||||
|
DoorStop, Firstmove, MoveOn bool
|
||||||
|
ToDeath, Kamikaze, HasHit bool
|
||||||
|
MaxHit int
|
||||||
|
Take byte // glyph of thing picked up this move
|
||||||
|
Delta Coord // last direction from GetDir
|
||||||
|
DirCh byte
|
||||||
|
LastComm, LastDir byte // command repeat state ('a' command)
|
||||||
|
LLastComm, LLastDir byte
|
||||||
|
LastPick, LLastPick *Object
|
||||||
|
countCh, direction, newCount byte // command.c statics (as bool for newCount)
|
||||||
|
|
||||||
|
// --- daemons/fuses ---
|
||||||
|
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...
|
||||||
|
StatMsg bool
|
||||||
|
InvDescribe bool
|
||||||
|
Oldpos Coord // hero position before last look()
|
||||||
|
Oldrp *Room
|
||||||
|
|
||||||
|
// --- item identity (per-game randomized appearance + discovery) ---
|
||||||
|
Items ItemLore // p_colors, s_names, r_stones, ws_made, ws_type,
|
||||||
|
// the 7 ObjInfo tables, `group` counter
|
||||||
|
|
||||||
|
// --- scores ---
|
||||||
|
LastScore int
|
||||||
|
AllScore bool
|
||||||
|
ScorePath string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Entrypoints
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewGame(opts Config) *RogueGame // Config: seed, name, ROGUEOPTS string, score path, wizard
|
||||||
|
func (g *RogueGame) Run() error // main.c main()+playit(): init tables, first level, daemons, loop
|
||||||
|
func (g *RogueGame) command() // one turn (command.c)
|
||||||
|
func Restore(path string, opts Config) (*RogueGame, error) // save.c restore()
|
||||||
|
```
|
||||||
|
|
||||||
|
`cmd/rogue/main.go` is a thin shell: flags (`-s` scores, `-d` death demo,
|
||||||
|
save-file arg, `SEED`/`ROGUEOPTS` env), `NewGame(...)` or `Restore(...)`,
|
||||||
|
`Run()`, exit code.
|
||||||
|
|
||||||
|
## 4. Core types
|
||||||
|
|
||||||
|
### 4.1 Geometry and stats
|
||||||
|
|
||||||
|
```go
|
||||||
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 The THING union → two types
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Creature is the _t arm of the C THING union: the player or a monster.
|
||||||
|
type Creature struct {
|
||||||
|
Pos Coord
|
||||||
|
Turn bool // slowed: is it our turn
|
||||||
|
Type byte // 'A'..'Z' monster letter; '@' for the player
|
||||||
|
Disguise byte // what a xeroc shows as
|
||||||
|
OldCh byte // map char underneath
|
||||||
|
Dest *Coord // chase target — aliases hero pos, another monster's pos, or room gold
|
||||||
|
Flags CreatureFlags
|
||||||
|
Stats Stats
|
||||||
|
Room *Room
|
||||||
|
Pack []*Object
|
||||||
|
}
|
||||||
|
|
||||||
|
type Monster struct{ Creature }
|
||||||
|
|
||||||
|
// Player embeds Creature and owns the player-only state that was global in C.
|
||||||
|
type Player struct {
|
||||||
|
Creature
|
||||||
|
CurArmor *Object // worn armor
|
||||||
|
CurWeapon *Object // wielded weapon
|
||||||
|
CurRing [2]*Object // LEFT, RIGHT
|
||||||
|
PackUsed [26]bool // inventory letters in use
|
||||||
|
Inpack int // pack item count
|
||||||
|
Purse int // gold
|
||||||
|
FoodLeft int
|
||||||
|
HungryState int // 0..3
|
||||||
|
NoFood int // levels since food seen
|
||||||
|
MaxStats Stats // potential (restore-strength target)
|
||||||
|
VfHit int // venus flytrap grip counter
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `hero`/`pstats`/`pack`/`proom`/`max_hp` macros become
|
||||||
|
`g.Player.Pos` / `g.Player.Stats` / `g.Player.Pack` / `g.Player.Room` /
|
||||||
|
`g.Player.Stats.MaxHP`.
|
||||||
|
|
||||||
|
`Dest *Coord` keeps the C aliasing on purpose: monster targeting works by
|
||||||
|
pointer identity (`m.Dest == &g.Player.Pos` means "hunting the hero"), which
|
||||||
|
both the AI and the save format rely on.
|
||||||
|
|
||||||
|
### 4.3 Object
|
||||||
|
|
||||||
|
```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, ...)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 4.4 Flags
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CreatureFlags uint32
|
||||||
|
type ObjFlags uint32
|
||||||
|
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
|
||||||
|
// hero aliases sharing C bit values are separate named constants:
|
||||||
|
IsLevit = IsCanc; IsHalu = IsMean; SeeMonst = IsFly
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f CreatureFlags) Has(b CreatureFlags) bool
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 4.5 Level and Place
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Place struct {
|
||||||
|
Ch byte // base map character
|
||||||
|
Flags PlaceFlags // FPass|FSeen|FDropped|FReal|FPnum|FTmask
|
||||||
|
Monst *Monster
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level is the current dungeon level. It is reset in place by NewLevel,
|
||||||
|
// matching the C reuse of the global arrays.
|
||||||
|
type Level struct {
|
||||||
|
Places [MaxLines * MaxCols]Place // same column-major INDEX(y,x) layout
|
||||||
|
Rooms [MaxRooms]Room
|
||||||
|
Passages [MaxPass]Room
|
||||||
|
Objects []*Object // lvl_obj
|
||||||
|
Monsters []*Monster // mlist
|
||||||
|
Stairs Coord
|
||||||
|
NTraps int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Level) At(y, x int) *Place // INDEX
|
||||||
|
func (l *Level) Char(y, x int) byte // chat
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 Room
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Room struct {
|
||||||
|
Pos Coord // upper-left
|
||||||
|
Max Coord // size
|
||||||
|
Gold Coord
|
||||||
|
GoldVal int
|
||||||
|
Flags RoomFlags // IsDark | IsGone | IsMaze
|
||||||
|
Exits []Coord // r_exit[12] + r_nexits → slice
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 NewGame 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.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ObjInfo struct {
|
||||||
|
Name string
|
||||||
|
Prob int // cumulative after initProbs
|
||||||
|
Worth int
|
||||||
|
Guess string // player's "call it" label
|
||||||
|
Know bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemLore struct {
|
||||||
|
PotColors [MaxPotions]string // p_colors
|
||||||
|
ScrNames [MaxScrolls]string // s_names
|
||||||
|
RingStones [MaxRings]string // r_stones
|
||||||
|
WandMade [MaxSticks]string // ws_made
|
||||||
|
WandType [MaxSticks]string // ws_type ("wand"/"staff")
|
||||||
|
Things [NumThings]ObjInfo
|
||||||
|
Potions [MaxPotions]ObjInfo
|
||||||
|
Scrolls [MaxScrolls]ObjInfo
|
||||||
|
Rings [MaxRings]ObjInfo
|
||||||
|
Sticks [MaxSticks]ObjInfo
|
||||||
|
Weapons [MaxWeapons + 1]ObjInfo
|
||||||
|
Armors [MaxArmors]ObjInfo
|
||||||
|
Group int // stack group counter
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Subsystems
|
||||||
|
|
||||||
|
### 5.1 RNG — exact LCG compatibility
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Rng struct{ Seed int32 }
|
||||||
|
|
||||||
|
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%
|
||||||
|
```
|
||||||
|
|
||||||
|
Same seed → same dungeon, byte for byte. This is the port's master
|
||||||
|
correctness oracle (§10).
|
||||||
|
|
||||||
|
### 5.2 Daemons and fuses — function pointers → identifiers
|
||||||
|
|
||||||
|
C identifies daemons by function pointer; the save format already maps them
|
||||||
|
to small integers. The port makes that mapping primary:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type DaemonID int
|
||||||
|
const (
|
||||||
|
DRollwand DaemonID = iota + 1 // matches state.c save ids
|
||||||
|
DDoctor; DStomach; DRunners; DSwander
|
||||||
|
DNohaste; DUnconfuse; DUnsee; DSight
|
||||||
|
DVisuals; DComeDown; DLand // extra fuses beyond the C save map
|
||||||
|
)
|
||||||
|
|
||||||
|
type delayedAction struct {
|
||||||
|
Type int // EMPTY / BEFORE / AFTER
|
||||||
|
Func DaemonID
|
||||||
|
Arg int
|
||||||
|
Time int // DAEMON sentinel or fuse countdown
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonList struct {
|
||||||
|
List [MaxDaemons]delayedAction
|
||||||
|
Between int // daemons.c rollwand static
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *RogueGame) StartDaemon(f DaemonID, arg, typ int)
|
||||||
|
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) 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.
|
||||||
|
|
||||||
|
### 5.3 Screen — curses → tcell
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Screen wraps tcell and exposes the curses vocabulary the ported code speaks.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window is an in-memory cell buffer with a cursor, standout attribute, and
|
||||||
|
// the handful of curses ops the game uses.
|
||||||
|
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) 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ported drawing code keeps its structure: `mvaddch(y, x, ch)` →
|
||||||
|
`g.scr.Std.MvAddCh(y, x, ch)`. Curses `mvinch` reads come from the Window
|
||||||
|
buffer, preserving the "screen is a data structure" idiom without touching
|
||||||
|
the real terminal. `md_readchar`'s escape decoding is deleted; tcell's
|
||||||
|
`EventKey` provides decoded keys and we translate to the byte codes
|
||||||
|
`command()` already handles (KeyUp → 'k', etc.). SIGTSTP/resume and resize
|
||||||
|
are handled by tcell; SIGHUP/SIGTERM → `autoSave` via `os/signal`.
|
||||||
|
|
||||||
|
### 5.4 Messaging
|
||||||
|
|
||||||
|
```go
|
||||||
|
type MsgLine 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Options struct {
|
||||||
|
Terse, FightFlush, Jump, SeeFloor, PassGo, Tombstone bool
|
||||||
|
InvType int // INV_OVER / INV_SLOW / INV_CLEAR
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ROGUEOPTS` parsing (`parse_opts`) and the interactive option screen port
|
||||||
|
as-is; the C per-option get/put function pointers become a slice of option
|
||||||
|
descriptors 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).
|
||||||
|
|
||||||
|
What still needs explicit code is exactly what state.c's `rs_fix_thing` did:
|
||||||
|
pointers that alias other structures are encoded as indices —
|
||||||
|
|
||||||
|
```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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
### 5.7 Scoreboard
|
||||||
|
|
||||||
|
`SCORE` → `ScoreEnt` struct; the score file is a gob (or JSON) list at
|
||||||
|
`~/.rogue_scores` (overridable). Locking via `os.OpenFile(O_CREATE|O_EXCL)`
|
||||||
|
lock file with stale-age takeover, porting `lock_sc`/`unlock_sc` semantics.
|
||||||
|
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) | error returns from `Run` + a small `gameOver` sentinel; no `os.Exit` inside game logic |
|
||||||
|
| `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.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
- `coord*` out-params become `Coord` returns (or `(Coord, bool)` for
|
||||||
|
find-style functions like `fallpos`/`find_floor`).
|
||||||
|
|
||||||
|
## 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, tables.go, game.go skeleton. Unit tests: RNG sequence vs C
|
||||||
|
formula, table sanity (probabilities sum to 100).
|
||||||
|
2. **Dungeon generation:** rooms.go, passages.go, newlevel.go + enough of
|
||||||
|
misc.go/monsters.go/things.go to place things. Test: fixed-seed level
|
||||||
|
render to text, eyeballed against C build.
|
||||||
|
3. **Items:** things.go, pack.go, init.go + ItemLore.
|
||||||
|
4. **Creatures & combat:** monsters.go, chase.go, fight.go, daemon(s).go.
|
||||||
|
5. **UI:** screen.go, io.go, options.go, misc.go look(), command.go, move.go.
|
||||||
|
First playable build.
|
||||||
|
6. **Effects:** potions.go, scrolls.go, rings.go, sticks.go, weapons.go,
|
||||||
|
armor.go, wizard.go.
|
||||||
|
7. **Endgame:** rip.go, score.go, save.go, cmd/rogue. Full game loop, save,
|
||||||
|
scores.
|
||||||
|
|
||||||
|
## 9. Dropped C functionality (deliberate)
|
||||||
|
|
||||||
|
| Dropped | Why | Replacement |
|
||||||
|
|---|---|---|
|
||||||
|
| `md_readchar` escape decoding | tcell decodes keys | key-event translation table |
|
||||||
|
| XOR save/score encryption | obscurity, not security | plain gob (file perms 0600) |
|
||||||
|
| save-file symlink/hardlink checks | single-user era anti-cheat | none |
|
||||||
|
| DES crypt wizard password | ditto | `ROGUE_WIZARD` env var |
|
||||||
|
| load-average / user-count gating (`too_much`, `ucount`, CHECKTIME) | 1980s timesharing courtesy | none |
|
||||||
|
| tty dsusp/ltc character juggling | tcell owns the tty | none |
|
||||||
|
| shell escape (`!`) setuid dance | no privileges to drop | plain `os/exec` shell |
|
||||||
|
| curses window save in save file | screen is derivable | redraw on restore |
|
||||||
|
|
||||||
|
## 10. Testing strategy
|
||||||
|
|
||||||
|
- **RNG oracle:** golden test locking the LCG sequence for known seeds.
|
||||||
|
- **Generation goldens:** for a set of seeds, render generated levels
|
||||||
|
(map 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,
|
||||||
|
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:** NewGame → play N scripted turns → save → restore →
|
||||||
|
compare full state.
|
||||||
|
- `go vet` + `gofmt` clean at every commit.
|
||||||
|
|||||||
Reference in New Issue
Block a user