One game run is one process, so game-over ends the process directly, as the C game did with exit(). myExit now restores the terminal (via the new Terminal.Fini) and calls os.Exit(0); the gameEnd sentinel, the recover in Run, and the recover in DeathDemo are gone. Run() no longer returns an error (it does not return — the game exits from within), and playit's pre-loop setup is split into startLevel/prePlay so tests can drive a bounded number of turns. Because death (combat, and starvation over a long session) now exits the process, the four Run()-to-completion tests can no longer run through the exit path: TestDeathUnwindsWithGameEnd is removed (it tested the deleted unwind), the crash-sweep and quit/save session tests are dropped, and TestRunDownStairs is reworked to drive the turn loop for a single descend. Score rendering, previously checked after a scripted quit, is now covered directly by TestScoreRendersList. Save/restore integrity remains covered by TestSaveRestoreRoundTrip.
1640 lines
154 KiB
Markdown
1640 lines
154 KiB
Markdown
# Rogue 5.4.4 — Architecture
|
||
|
||
This document has two parts:
|
||
|
||
- **Part 1 — The C Program**: a complete structural map of the classic C
|
||
codebase (every file, every function, every type, every global) as it exists
|
||
on the `modern-rogue` branch.
|
||
- **Part 2 — The Go Port Design**: the object-oriented architecture for the Go
|
||
port (`git.eeqj.de/sneak/rgoue`), including how every C construct maps onto Go
|
||
types and methods.
|
||
|
||
---
|
||
|
||
# Part 1 — The C Program
|
||
|
||
## 1. Overview
|
||
|
||
Rogue 5.4.4 is a single-process, single-threaded, turn-based terminal game of
|
||
~16,800 lines of pre-C99 C across 36 files. Its architecture is that of a
|
||
classic 1980s BSD game:
|
||
|
||
- **All state is global.** ~120 global variables (defined in `extern.c`,
|
||
declared in `rogue.h`/`extern.h`) hold the entire game state: the player, the
|
||
level map, monster and object lists, UI state, and configuration.
|
||
- **One data type to rule them all.** A single C `union thing` (`THING`)
|
||
represents both creatures (player/monsters) and objects (weapons, potions,
|
||
...), discriminated by which union arm the code chooses to access. Doubly
|
||
linked lists of THINGs (via embedded `l_next`/`l_prev` pointers) form the
|
||
monster list, the level-object list, and every creature's pack.
|
||
- **The screen is the data structure.** Much game logic reads what character is
|
||
displayed at a map cell (via curses `mvinch` or the `places[]` shadow map) to
|
||
decide behavior — walls, doors, items, and monsters are identified by their
|
||
display glyph.
|
||
- **Turn engine via daemons and fuses.** A 20-slot table of function pointers
|
||
runs recurring per-turn callbacks ("daemons": healing, hunger, wandering
|
||
monsters) and one-shot countdowns ("fuses": potion effects wearing off), each
|
||
in a BEFORE or AFTER phase around the player's command.
|
||
- **Determinism by seeded LCG.** All randomness flows through `rnd()` using a
|
||
hand-rolled linear congruential generator (`seed = seed*11109+13849`), so a
|
||
given seed (dungeon number `dnum`) reproduces a dungeon exactly.
|
||
|
||
### Program flow
|
||
|
||
```
|
||
main()
|
||
├─ parse args/env (SEED, ROGUEOPTS, save file to restore, -s scores, -d death demo)
|
||
├─ init_probs/init_player/init_names/init_colors/init_stones/init_materials
|
||
├─ curses initscr(), setup() signals/tty
|
||
├─ new_level() # generate level 1
|
||
├─ start_daemon(runners/doctor/stomach/swander...), fuse(...)
|
||
└─ playit()
|
||
└─ while (playing)
|
||
command()
|
||
├─ do_daemons(BEFORE); do_fuses(BEFORE)
|
||
├─ status(); readchar() # input (with repeat counts, run prefixes)
|
||
├─ switch(ch) → do_move/search/quaff/read_scroll/wield/...
|
||
└─ if (after): do_daemons(AFTER); do_fuses(AFTER)
|
||
# the `runners` AFTER daemon moves all monsters
|
||
death()/total_winner()/quit() → score() → exit
|
||
```
|
||
|
||
## 2. Source file inventory
|
||
|
||
| File | Lines | Role |
|
||
| ------------- | ----- | ------------------------------------------------------------------ |
|
||
| `rogue.h` | 753 | Master header: all constants, macros, types, extern declarations |
|
||
| `extern.h` | 197 | Platform feature detection, mdport/mach_dep declarations |
|
||
| `score.h` | 26 | Scoreboard entry struct |
|
||
| `main.c` | 395 | Entry point, RNG, main loop, signals |
|
||
| `command.c` | 820 | Keyboard command dispatch |
|
||
| `io.c` | 277 | Message line, status line, input |
|
||
| `init.c` | 447 | Per-game randomization and data-table setup |
|
||
| `extern.c` | 391 | All global variable definitions + data tables |
|
||
| `new_level.c` | 231 | Level orchestration, item placement, treasure rooms |
|
||
| `rooms.c` | 472 | Room and maze generation, room enter/leave |
|
||
| `passages.c` | 424 | Corridor generation, passage numbering |
|
||
| `move.c` | 425 | Player movement, traps |
|
||
| `chase.c` | 541 | Monster AI (chase/flee/targeting), visibility |
|
||
| `monsters.c` | 252 | Monster creation, saving throws |
|
||
| `fight.c` | 686 | Combat resolution, special monster attacks |
|
||
| `things.c` | 713 | Item naming, random item creation, discoveries |
|
||
| `pack.c` | 503 | Inventory management, item selection UI |
|
||
| `potions.c` | 375 | 14 potion effects |
|
||
| `scrolls.c` | 329 | 18 scroll effects |
|
||
| `rings.c` | 204 | 14 ring types, wearing/removal |
|
||
| `sticks.c` | 431 | 14 wand/staff types, bolts |
|
||
| `weapons.c` | 288 | 9 weapon types, throwing |
|
||
| `armor.c` | 89 | 8 armor types, wearing |
|
||
| `misc.c` | 597 | look() display update, direction input, eat, level-up |
|
||
| `daemon.c` | 181 | Daemon/fuse scheduler |
|
||
| `daemons.c` | 295 | The 11 daemon/fuse callbacks |
|
||
| `list.c` | 113 | THING linked-list allocation |
|
||
| `rip.c` | 448 | Tombstone, victory, scoreboard display |
|
||
| `save.c` | 390 | Save/restore, XOR-encrypted I/O, score file I/O |
|
||
| `state.c` | 2134 | Hand-written serialization of every global |
|
||
| `options.c` | 501 | Options screen, ROGUEOPTS parsing |
|
||
| `wizard.c` | 284 | Debug/wizard commands, item identification |
|
||
| `mach_dep.c` | 457 | Score file locking, tty/signal setup, vestigial load checks |
|
||
| `mdport.c` | 1432 | Platform abstraction (the 900-line `md_readchar` keyboard decoder) |
|
||
| `xcrypt.c` | 707 | DES crypt(3) for the wizard password only |
|
||
| `vers.c` | 17 | Version string + XOR key strings |
|
||
|
||
## 3. Type system
|
||
|
||
### 3.1 `coord` — position
|
||
|
||
```c
|
||
typedef struct { int x; int y; } coord;
|
||
```
|
||
|
||
Used for every position: hero, monsters, room corners, exits, deltas.
|
||
|
||
### 3.2 `str_t` — strength
|
||
|
||
```c
|
||
typedef unsigned int str_t; /* strength 3..31 */
|
||
```
|
||
|
||
### 3.3 `struct stats` — fighting statistics (player and monsters)
|
||
|
||
```c
|
||
struct stats {
|
||
str_t s_str; /* strength (player: 3-31; monsters: fixed 10) */
|
||
int s_exp; /* experience value/points */
|
||
int s_lvl; /* level of mastery */
|
||
int s_arm; /* armor class (lower = better) */
|
||
int s_hpt; /* current hit points */
|
||
char s_dmg[13]; /* damage dice string, e.g. "1x4/1x2" */
|
||
int s_maxhp; /* max hit points */
|
||
};
|
||
```
|
||
|
||
### 3.4 `union thing` (`THING`) — the universal entity
|
||
|
||
The central type: a union of a creature arm (`_t`) and an object arm (`_o`),
|
||
with the two link pointers common to both. Access is via field macros (`t_pos` →
|
||
`_t._t_pos`, `o_type` → `_o._o_type`, ...).
|
||
|
||
```c
|
||
union thing {
|
||
struct { /* creature (player/monster) */
|
||
union thing *_l_next, *_l_prev; /* list links */
|
||
coord _t_pos; /* position */
|
||
bool _t_turn; /* slowed: is it our turn to move */
|
||
char _t_type; /* monster letter 'A'-'Z'; player '@' */
|
||
char _t_disguise; /* what a xeroc looks like */
|
||
char _t_oldch; /* map char under the monster */
|
||
coord *_t_dest; /* chase target (points at hero or gold) */
|
||
short _t_flags; /* ISRUN|ISHELD|ISINVIS|... state bits */
|
||
struct stats _t_stats;
|
||
struct room *_t_room; /* room the creature is in */
|
||
union thing *_t_pack; /* carried items (list) */
|
||
int _t_reserved; /* save-file fixup index */
|
||
} _t;
|
||
struct { /* object */
|
||
union thing *_l_next, *_l_prev; /* list links */
|
||
int _o_type; /* POTION/SCROLL/WEAPON/... glyph */
|
||
coord _o_pos; /* where it lies on the floor */
|
||
char *_o_text; /* scroll gibberish text */
|
||
int _o_launch; /* weapon needed to launch it */
|
||
char _o_packch; /* inventory letter */
|
||
char _o_damage[8]; /* wield damage "3x4" */
|
||
char _o_hurldmg[8]; /* thrown damage */
|
||
int _o_count; /* stack count */
|
||
int _o_which; /* subtype index (P_HEALING, MACE...) */
|
||
int _o_hplus, _o_dplus; /* hit/damage enchantment */
|
||
int _o_arm; /* armor class / charges / gold value (o_charges, o_goldval alias) */
|
||
int _o_flags; /* ISCURSED|ISKNOW|... */
|
||
int _o_group; /* stack group id */
|
||
char *_o_label; /* player-applied name */
|
||
} _o;
|
||
};
|
||
typedef union thing THING;
|
||
```
|
||
|
||
Key consequences the port must respect:
|
||
|
||
- `winat(y,x)` display logic: a monster's _disguise_ is shown, not its type.
|
||
- `o_arm` is overloaded three ways: armor class (armor), charge count (sticks,
|
||
via `o_charges`), gold value (gold piles, via `o_goldval`).
|
||
- `t_dest` aliases _other entities' interiors_: it points at `hero` (player
|
||
position), a room's `r_gold` coordinate, or another monster's `t_pos`. The
|
||
save format encodes this as (kind, index) pairs.
|
||
|
||
### 3.5 `PLACE` — one map cell, and the `places[]` map
|
||
|
||
```c
|
||
typedef struct {
|
||
char p_ch; /* base map character (wall, floor, door, item glyph) */
|
||
char p_flags; /* F_PASS|F_SEEN|F_DROPPED|F_LOCKED|F_REAL|F_PNUM|F_TMASK */
|
||
THING *p_monst; /* monster standing here, if any */
|
||
} PLACE;
|
||
|
||
PLACE places[MAXLINES*MAXCOLS]; /* column-major: INDEX(y,x) = &places[(x<<5)+y] */
|
||
```
|
||
|
||
Access macros: `chat(y,x)` (char), `flat(y,x)` (flags), `moat(y,x)` (monster),
|
||
`winat(y,x)` (what's visible: monster disguise or map char). Note the
|
||
column-major `(x << 5) + y` layout (32 = MAXLINES rows per column).
|
||
|
||
### 3.6 `struct room` — rooms and passages
|
||
|
||
```c
|
||
struct room {
|
||
coord r_pos; /* upper-left corner */
|
||
coord r_max; /* size */
|
||
coord r_gold; /* gold location */
|
||
int r_goldval;
|
||
short r_flags; /* ISDARK | ISGONE | ISMAZE */
|
||
int r_nexits;
|
||
coord r_exit[12]; /* door positions */
|
||
};
|
||
```
|
||
|
||
`rooms[MAXROOMS=9]` on a 3×3 grid; `passages[MAXPASS=13]` reuses the same struct
|
||
for corridor networks (exits = doors touched, flags = ISGONE|ISDARK).
|
||
|
||
### 3.7 `struct monster` — bestiary entry
|
||
|
||
```c
|
||
struct monster {
|
||
char *m_name; /* "aquator", "bat", ... */
|
||
int m_carry; /* % chance of carrying an item */
|
||
short m_flags; /* ISMEAN|ISFLY|ISREGEN|ISGREED|ISINVIS|CANHUH */
|
||
struct stats m_stats; /* starting stats incl. damage string */
|
||
};
|
||
extern struct monster monsters[26]; /* indexed by letter - 'A' */
|
||
```
|
||
|
||
### 3.8 `struct obj_info` — item class descriptor
|
||
|
||
```c
|
||
struct obj_info {
|
||
char *oi_name; /* true name ("healing", "mace") */
|
||
int oi_prob; /* generation probability (converted to cumulative) */
|
||
int oi_worth; /* score value */
|
||
char *oi_guess; /* player's "call it" name */
|
||
bool oi_know; /* has the player identified this type */
|
||
};
|
||
```
|
||
|
||
Seven tables: `things[7]` (category weights), `pot_info[14]`, `scr_info[18]`,
|
||
`ring_info[14]`, `ws_info[14]`, `weap_info[9+1]`, `arm_info[8]`.
|
||
|
||
### 3.9 `struct delayed_action` — daemon/fuse slot
|
||
|
||
```c
|
||
struct delayed_action {
|
||
int d_type; /* EMPTY / BEFORE / AFTER */
|
||
void (*d_func)(int); /* callback — identity of the entry */
|
||
int d_arg;
|
||
int d_time; /* DAEMON sentinel (-1) or countdown */
|
||
} d_list[MAXDAEMONS=20];
|
||
```
|
||
|
||
### 3.10 Others
|
||
|
||
- `struct h_list { char h_ch; char *h_desc; bool h_print; }` — help table rows.
|
||
- `STONE { char *st_name; int st_value; }` — ring stones with worth.
|
||
- `SCORE` (score.h) — scoreboard entry (uid, score, flags, monster, name, level,
|
||
time).
|
||
- `OPTION` (options.c) — option descriptor with get/put function pointers.
|
||
- `PACT` (potions.c) — potion type → flag/fuse-callback/duration.
|
||
- `SPOT` (rooms.c) — maze generation cell.
|
||
|
||
## 4. Constants and enumerations
|
||
|
||
All are `#define`s in rogue.h:
|
||
|
||
- **Geometry:** NUMLINES 24, NUMCOLS 80, MAXLINES 32, MAXCOLS 80, STATLINE 23.
|
||
- **Limits:** MAXROOMS 9, MAXTHINGS 9, MAXOBJ 9, MAXPACK 23, MAXTRAPS 10,
|
||
MAXPASS 13, MAXDAEMONS 20, AMULETLEVEL 26, NUMTHINGS 7, BORE_LEVEL 50.
|
||
- **Object type glyphs (`o_type`/map chars):** PASSAGE `#`, DOOR `+`, FLOOR `.`,
|
||
PLAYER `@`, TRAP `^`, STAIRS `%`, GOLD `*`, POTION `!`, SCROLL `?`, MAGIC `$`,
|
||
FOOD `:`, WEAPON `)`, ARMOR `]`, AMULET `,`, RING `=`, STICK `/`; pseudo-types
|
||
CALLABLE -1, R_OR_S -2 for item-prompt filters.
|
||
- **Subtype enums:** 8 traps (T_DOOR..T_MYST), 14 potions (P__), 18 scrolls
|
||
(S__), 9 weapons (MACE..SPEAR, +FLAME pseudo), 8 armors (LEATHER..PLATE_MAIL),
|
||
14 rings (R__), 14 sticks (WS__).
|
||
- **Creature flag bits:** CANHUH, CANSEE, ISBLIND, ISCANC, ISLEVIT, ISFOUND,
|
||
ISGREED, ISHASTE, ISTARGET, ISHELD, ISHUH, ISINVIS, ISMEAN, ISHALU, ISREGEN,
|
||
ISRUN, SEEMONST, ISFLY, ISSLOW (note deliberate collisions: ISCANC=ISLEVIT,
|
||
ISMEAN=ISHALU, SEEMONST=ISFLY — one set applies to monsters, other to hero).
|
||
- **Object flag bits:** ISCURSED, ISKNOW, ISMISL, ISMANY, ISFOUND (shared),
|
||
ISPROT.
|
||
- **Room flags:** ISDARK, ISGONE, ISMAZE.
|
||
- **Place flags:** F_PASS 0x80, F_SEEN 0x40, F_DROPPED/F_LOCKED 0x20, F_REAL
|
||
0x10, F_PNUM 0x0f (passage number), F_TMASK 0x07 (trap type).
|
||
- **Durations/tuning:** HEALTIME 30, HUHDURATION 20, SEEDURATION 850, HUNGERTIME
|
||
1300, MORETIME 150, STOMACHSIZE 2000, STARVETIME 850, BOLT_LENGTH 6, LAMPDIST
|
||
3, BEARTIME/SLEEPTIME/HOLDTIME/WANDERTIME/BEFORE/ AFTER as `spread()`
|
||
randomized durations.
|
||
- **Saving throws:** VS_POISON 0, VS_PARALYZATION 0, VS_DEATH 0, VS_BREATH 2,
|
||
VS_MAGIC 3.
|
||
- **Get-function returns:** NORM 0, QUIT 1, MINUS 2. **Inventory styles:**
|
||
INV_OVER 0, INV_SLOW 1, INV_CLEAR 2.
|
||
|
||
## 5. The macro layer
|
||
|
||
rogue.h defines a thick macro layer the port must translate to methods:
|
||
|
||
| Macro | Expansion | Port meaning |
|
||
| --------------------------------- | -------------------------------------------- | -------------------------------- |
|
||
| `when` / `otherwise` / `until(e)` | `break;case` / `break;default` / `while(!e)` | plain Go control flow |
|
||
| `hero` | `player.t_pos` | `g.Player.Pos` |
|
||
| `pstats` | `player.t_stats` | `g.Player.Stats` |
|
||
| `pack` | `player.t_pack` | `g.Player.Pack` |
|
||
| `proom` | `player.t_room` | `g.Player.Room` |
|
||
| `max_hp` | `player.t_stats.s_maxhp` | `g.Player.Stats.MaxHP` |
|
||
| `on(thing,flag)` | flag bit test | `t.Flags.Has(...)` |
|
||
| `INDEX/chat/flat/moat(y,x)` | places[] cell access | `g.Level.At(p).Ch/Flags/Monster` |
|
||
| `winat(y,x)` | monster disguise or map char | `g.Level.VisibleChar(p)` |
|
||
| `ce(a,b)` | coord equality | `a == b` (value type) |
|
||
| `ISRING(h,r)` / `ISWEARING(r)` | ring-worn tests | `g.Player.IsWearing(ring)` |
|
||
| `ISMULT(type)` | stackable type test | method on ObjType |
|
||
| `GOLDCALC` | `rnd(50+10*level)+2` | method on Level |
|
||
| `RN` | LCG step | `Rng.Next()` |
|
||
| `DISTANCE/dist` | squared distance | geometry helper |
|
||
| `attach/detach/free_list` | list ops via `&list` | slice operations |
|
||
|
||
## 6. Global variable census
|
||
|
||
Everything below lives in `extern.c` (game state) or as file-scope statics
|
||
(module state). In the Go port **all of these become fields of the `RogueGame`
|
||
struct** (user directive: no package-level globals).
|
||
|
||
- **Player & world:** `player` (THING), `places[]`, `rooms[9]`, `passages[13]`,
|
||
`mlist`, `lvl_obj`, `stairs`, `level`, `max_level`, `purse`, `amulet`,
|
||
`cur_armor`, `cur_weapon`, `cur_ring[2]`, `food_left`, `hungry_state`,
|
||
`ntraps`, `max_stats`, `oldpos`, `oldrp`, `delta`, `take`.
|
||
- **RNG/identity:** `seed`, `dnum`, `whoami`, `fruit`, `home`, `file_name`.
|
||
- **UI/messaging:** `huh`, `prbuf`, `mpos`, `msgbuf`+`newpos` (io.c statics),
|
||
`hw`, status-line shadow statics (io.c), discovery pagination statics
|
||
(things.c).
|
||
- **Command engine:** `count`, `after`, `again`, `door_stop`, `firstmove`,
|
||
`running`, `runch`, `last_comm`/`last_dir`/`last_pick` (+ `l_*` backups),
|
||
`no_command`, `no_move`, `quiet`, `to_death`, `kamikaze`, `max_hit`,
|
||
`has_hit`, `move_on`, `dir_ch`, `countch/direction/newcount` (command.c
|
||
statics).
|
||
- **Options:** `terse`, `fight_flush`, `jump`, `see_floor`, `passgo`,
|
||
`tombstone`, `inv_type`, `inv_describe`, `lower_msg`, `msg_esc`, `save_msg`,
|
||
`q_comm`, `stat_msg`.
|
||
- **Item identity tables:** `p_colors[]`, `s_names[]`, `r_stones[]`,
|
||
`ws_made[]`, `ws_type[]`, `a_class[]`, 7 obj_info tables, `pack_used[26]`,
|
||
`group`, `inpack`, `n_objs`, `no_food`.
|
||
- **Daemons:** `d_list[20]`, `between` (daemons.c static).
|
||
- **Scores/system:** `scoreboard`, `noscore`, `lastscore`, `numscores`,
|
||
`allscore`, `wizard`, `playing`, `in_shell`, `got_ltc`, `orig_dsusp`,
|
||
`seenstairs`, `vf_hit`, `e_levels[]`.
|
||
- **Static name data (immutable):** `monsters[26]`, `rainbow[]`, `stones[]`,
|
||
`wood[]`, `metal[]`, `sylls[]`, `inv_t_name[]`, `tr_name[]`, `helpstr[]`,
|
||
`release`, `encstr`, `statlist`, `version`, `h_names[]`/`m_names[]` (fight.c),
|
||
`init_dam[]` (weapons.c), `lvl_mons[]`/`wand_mons[]` (monsters.c), `uses[]`
|
||
(rings.c), `p_actions[]` (potions.c).
|
||
|
||
## 7. Function catalog by file
|
||
|
||
### main.c (396 lines) — Entry point and main game loop control
|
||
|
||
| Function | Signature | Description |
|
||
| --------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `main` | `int main(int argc, char **argv, char **envp)` | Entry point; initializes game state, terminal, initializes all subsystems (probabilities, player, names, colors, stones, materials), sets up windows and daemons, then calls `playit()`. Sets globals: `wizard`, `whoami`, `dnum`, `seed`, `file_name`, `home` |
|
||
| `endit` | `void endit(int sig)` | Signal handler for abnormal termination; calls `fatal()` with goodbye message |
|
||
| `fatal` | `void fatal(char *s)` | Prints message to status line, refreshes screen, ends curses, and exits |
|
||
| `rnd` | `int rnd(int range)` | Returns random number 0 to range-1 using macro `RN`; core randomness function |
|
||
| `roll` | `int roll(int number, int sides)` | Simulates rolling dice; calls `rnd()` for each die |
|
||
| `tstp` | `void tstp(int ignored)` | Signal handler for SIGTSTP/SIGCONT; saves cursor position, suspends curses, calls `md_tstpsignal()` and `md_tstpresume()`, restores screen state |
|
||
| `playit` | `void playit()` | Main game loop; runs daemons/fuses before and after each turn, reads `ROGUEOPTS` to set options, loops on `playing` flag calling `command()` |
|
||
| `quit` | `void quit(int sig)` | Confirm quit prompt; on 'y' displays final score and calls `score()`, reads/writes globals `purse`, `q_comm`, `count`, `to_death` |
|
||
| `leave` | `void leave(int sig)` | Quick exit during error; ends curses and exits cleanly |
|
||
| `shell` | `void shell()` | Shell escape; saves cursor, ends curses, calls `md_shellescape()`, restores curses; reads/writes global `in_shell` |
|
||
| `my_exit` | `void my_exit(int st)` | Proper exit wrapper; calls `resetltchars()` then `exit()` |
|
||
|
||
**Notes:** Globals initialized here from environment: `whoami`, `dnum`, `seed`,
|
||
`file_name`, `home`. Signal handlers registered: `endit()`, `tstp()`, `quit()`,
|
||
`leave()`. Heavy curses use (`initscr()`, `endwin()`, `newwin()`). Platform
|
||
shims: `md_init()`, `md_normaluser()`, `md_tstpsignal()`, `md_tstpresume()`.
|
||
|
||
### command.c (821 lines) — User command processing and game actions
|
||
|
||
| Function | Signature | Description |
|
||
| ------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `command` | `void command()` | Main command dispatcher; reads user input, handles counts/prefixes (digits for repeat), processes movement (hjklyubn), items (pick, drop, quaff, read, eat, wield, wear, take_off, ring_on, ring_off), special commands (search, help, identify, etc.). Uses static variables `countch`, `direction`, `newcount`. Handles daemons/fuses before/after. Modifies globals: `running`, `count`, `after`, `to_death`, `noscore`, `wizard`, `last_comm`, `last_dir`, etc. |
|
||
| `illcom` | `void illcom(int ch)` | Error handler for illegal commands; displays message via `msg()`, modifies `save_msg`, `count` |
|
||
| `search` | `void search()` | Search current square and adjacent 8; finds hidden doors/traps/passages with probability adjusted for hallucination/blindness; modifies `running`, `count` on discovery |
|
||
| `help` | `void help()` | Displays command help; reads from `helpstr[]` array, uses `hw` window for full help display |
|
||
| `identify` | `void identify()` | Identify objects/monsters by character; uses local `ident_list[]` and `monsters[]` array |
|
||
| `d_level` | `void d_level()` | Go down stairs; checks `levit_check()`, verifies stairs at current position, increments `level`, calls `new_level()` |
|
||
| `u_level` | `void u_level()` | Go up stairs; checks for stairs, checks `amulet` flag, decrements `level`, checks win condition `level==0`, calls `new_level()` |
|
||
| `levit_check` | `bool levit_check()` | Check levitation status; returns TRUE if player has ISLEVIT flag (and messages that you float up) |
|
||
| `call` | `void call()` | Name/rename items; calls `get_item()`, prompts for new name, allocates memory via `malloc()`, updates `oi_guess` in object info structures |
|
||
| `current` | `void current(THING *cur, char *how, char *where)` | Print currently equipped item (weapon/armor/rings); formats output using `inv_name()` |
|
||
|
||
**Notes:** Static variables `countch`, `direction`, `newcount` in `command()`
|
||
track repeat count state. Uses `when` macro for switch cases. Wizard mode
|
||
(MASTER) adds debug commands (Ctrl-key commands: create object, show map,
|
||
teleport, etc.). Command parsing via nested switch statements. Uses `readchar()`
|
||
for input, `msg()`/`addmsg()` for output.
|
||
|
||
### io.c (278 lines) — Input/output and terminal display
|
||
|
||
| Function | Signature | Description |
|
||
| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `msg` | `int msg(char *fmt, ...)` | Display message at top of screen; variadic wrapper using `va_list`, calls `doadd()`, `endmsg()`. Returns ESCAPE or ~ESCAPE. Clears line if format is empty string |
|
||
| `addmsg` | `void addmsg(char *fmt, ...)` | Append to current message buffer; variadic, calls `doadd()` |
|
||
| `endmsg` | `int endmsg()` | Finish message display; handles "--More--" continuation, auto-capitalizes first character unless `lower_msg`, saves message to `huh` for redisplay |
|
||
| `doadd` | `void doadd(char *fmt, va_list args)` | Internal: vsprintf into message buffer; checks overflow, concatenates to `msgbuf` |
|
||
| `step_ok` | `int step_ok(int ch)` | Check if terrain character allows stepping; returns FALSE for walls/space, TRUE for floor/doors/items (non-alpha chars) |
|
||
| `readchar` | `char readchar()` | Read character with error checking; calls `md_readchar()`, intercepts Ctrl-C (sends to `quit()`) |
|
||
| `status` | `void status()` | Display status line at STATLINE; shows level/gold/HP/armor/strength/experience/hunger; uses static tracking variables (`s_hp`, `s_pur`, etc.) to avoid redundant redraws |
|
||
| `wait_for` | `void wait_for(int ch)` | Block until specific character typed (or newline if ch is newline) |
|
||
| `show_win` | `void show_win(char *message)` | Display message in `hw` window and wait for space; used for full-screen displays |
|
||
|
||
**Notes:** Static `msgbuf[2*MAXMSG+1]`, `newpos` track accumulating multi-part
|
||
messages (`addmsg`/`msg`/`endmsg` protocol). `huh[]` global stores last message
|
||
for the `Ctrl-R` repeat command. Status line optimized with 8 static shadow
|
||
variables to minimize terminal writes.
|
||
|
||
### init.c (448 lines) — Initialization of game state and object tables
|
||
|
||
| Function | Signature | Description |
|
||
| ---------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `init_player` | `void init_player()` | Roll up player character; copies `max_stats` to `pstats`, creates starting items (food, ring mail +1, mace +1/+1, bow +1, 25-39 arrows), initializes `food_left`, sets `cur_armor` and `cur_weapon` |
|
||
| `init_colors` | `void init_colors()` | Randomize potion colors; assigns `p_colors[]` randomly from `rainbow[]` list, no duplicates |
|
||
| `init_names` | `void init_names()` | Generate random scroll names from `sylls[]` syllable fragments; stores mallocated names in `s_names[]` |
|
||
| `init_stones` | `void init_stones()` | Randomize ring stones; assigns `r_stones[]` from `stones[]` array (no duplicates), adds stone value to `ring_info[].oi_worth` |
|
||
| `init_materials` | `void init_materials()` | Initialize wand/staff materials; randomly picks from `metal[]` (wand) or `wood[]` (staff), sets `ws_type[]` and `ws_made[]` |
|
||
| `sumprobs` | `void sumprobs(struct obj_info *info, int bound)` | Converts item probability arrays to cumulative distributions (running sum) |
|
||
| `init_probs` | `void init_probs()` | Calls `sumprobs()` for all seven obj_info tables |
|
||
| `badcheck` | `void badcheck(char *name, struct obj_info *info, int bound)` | (MASTER) Verifies probabilities sum to 100; aborts with message if not |
|
||
| `pick_color` | `char *pick_color(char *col)` | Returns given color, or a random one if hero is hallucinating |
|
||
|
||
**Notes:** Static data defined here: `rainbow[]` (27 colors) + `cNCOLORS`,
|
||
`sylls[]` (syllables), `stones[]` (26 STONE entries with worth) + `cNSTONES`,
|
||
`wood[]` (33 woods) + `cNWOOD`, `metal[]` (22 metals) + `cNMETAL`. The name
|
||
arrays `p_colors[]`, `s_names[]`, `r_stones[]`, `ws_type[]`, `ws_made[]` are
|
||
defined in extern.c but populated here at game start (per-game randomization of
|
||
item appearance).
|
||
|
||
### extern.c (392 lines) — Global variable definitions
|
||
|
||
**Boolean flags:** `after` (want after-daemons), `again` (repeat last command),
|
||
`seenstairs`, `amulet` (found the Amulet), `door_stop` (stop running at doors),
|
||
`fight_flush` (flush typeahead on fight), `firstmove`, `got_ltc`, `has_hit`,
|
||
`in_shell`, `inv_describe`, `jump` (show running as jumps), `kamikaze`,
|
||
`lower_msg`, `move_on`, `msg_esc`, `passgo` (follow passage turns), `playing`,
|
||
`q_comm`, `running`, `save_msg`, `see_floor`, `stat_msg`, `terse`, `to_death`,
|
||
`tombstone`, `wizard` (MASTER), `pack_used[26]`.
|
||
|
||
**Char/string state:** `dir_ch`, `file_name[MAXSTR]`, `huh[MAXSTR]` (last
|
||
message), `prbuf[2*MAXSTR]` (sprintf scratch), `runch`, `take`,
|
||
`whoami[MAXSTR]`, `fruit[MAXSTR]` ("slime-mold"), `home[MAXSTR]`, `l_last_comm`,
|
||
`l_last_dir`, `last_comm`, `last_dir`; appearance-name arrays
|
||
`p_colors[MAXPOTIONS]`, `r_stones[MAXRINGS]`, `s_names[MAXSCROLLS]`,
|
||
`ws_made[MAXSTICKS]`, `ws_type[MAXSTICKS]`; fixed string tables `inv_t_name[]`
|
||
(Overwrite/Slow/Clear), `tr_name[]` (8 trap names), `release` (version).
|
||
|
||
**Int state:** `n_objs`, `ntraps`, `hungry_state` (0-3), `inpack`, `inv_type`,
|
||
`level`, `max_hit`, `max_level`, `mpos` (cursor pos on msg line), `no_food`,
|
||
`count` (command repeat), `food_left`, `lastscore`, `no_command` (turns asleep),
|
||
`no_move` (turns held), `noscore`, `purse` (gold), `quiet` (rest turns),
|
||
`vf_hit` (flytrap hits), `dnum` (dungeon number), `seed` (RNG seed),
|
||
`numscores`, `a_class[MAXARMORS]` (armor class table: 8 5 6 4 3 3 4 3...
|
||
actually per-type base AC), `e_levels[]` (experience thresholds: 10 20 40 80 ...
|
||
doubling to level 19), `orig_dsusp`.
|
||
|
||
**Coords/structural:** `delta` (last direction), `oldpos`, `stairs`;
|
||
`places[MAXLINES*MAXCOLS]` (the level map: PLACE = ch, flags, monster ptr);
|
||
`player` (THING); `cur_armor`, `cur_ring[2]`, `cur_weapon`, `l_last_pick`,
|
||
`last_pick` (THING ptrs); `lvl_obj` (level object list), `mlist` (monster list).
|
||
|
||
**Tables:** `rooms[MAXROOMS]`, `passages[MAXPASS]` (12 passage "rooms"),
|
||
`max_stats` (initial player stats: str 16, 0 exp, lvl 1, ac 10, 12 hp, "1x4"),
|
||
`oldrp`, `monsters[26]` (full A-Z monster table with names, carry %, flags,
|
||
stats), 7 `obj_info` tables: `things[NUMTHINGS]` (potion 26%, scroll 36%, food
|
||
16%, weapon 7%, armor 7%, ring 4%, stick 4%), `pot_info[MAXPOTIONS]`,
|
||
`scr_info[MAXSCROLLS]`, `ring_info[MAXRINGS]`, `ws_info[MAXSTICKS]`,
|
||
`weap_info[MAXWEAPONS+1]`, `arm_info[MAXARMORS]` — each entry: name,
|
||
probability, worth, guess ptr, know flag. `helpstr[]` (help text). `hw` (WINDOW*
|
||
scratch), `scoreboard` (FILE*).
|
||
|
||
**Notes:** Pure data file, no functions. The `monsters[]` table is the canonical
|
||
A-Z bestiary (Aquator, Bat, Centaur, Dragon, Emu, Venus Flytrap, Griffin,
|
||
Hobgoblin, Ice monster, Jabberwock, Kestrel, Leprechaun, Medusa, Nymph, Orc,
|
||
Phantom, Quagga, Rattlesnake, Snake, Troll, Black unicorn, Vampire, Wraith,
|
||
Xeroc, Yeti, Zombie) with per-monster carry probability, flag word
|
||
(ISMEAN/ISFLY/ISREGEN/ISGREED/ISINVIS/CANHUH), and stats (exp, level, AC, HP
|
||
dice, damage strings).
|
||
|
||
### mach_dep.c (458 lines) — Installation and machine-dependent setup
|
||
|
||
| Function | Signature | Description |
|
||
| ----------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||
| `init_check` | `void init_check()` | Check if system load/user count too high (compile-time optional; mostly vestigial) |
|
||
| `open_score` | `void open_score()` | Open score file for read/write; creates if missing; sets global `scoreboard` FILE* |
|
||
| `setup` | `void setup()` | Install signal handlers (`md_onsignal_autosave`), start checkout timer, set curses raw/noecho modes, fetch local tty chars |
|
||
| `getltchars` | `void getltchars()` | Save original tty dsusp char for later restore; sets `got_ltc`, `orig_dsusp` |
|
||
| `resetltchars` | `void resetltchars()` | Restore original tty chars (on exit/shell) |
|
||
| `playltchars` | `void playltchars()` | Set tty chars to in-game values |
|
||
| `start_score` | `void start_score()` | Stop checkout timer before scoring (vestigial) |
|
||
| `is_symlink` | `bool is_symlink(char *sp)` | lstat() check whether path is a symbolic link (save-file anti-cheat) |
|
||
| `too_much`/`author`/`checkout`/`chmsg`/`ucount` | various | Compile-time-guarded system-load limiting and author-exemption checks (vestigial on modern systems) |
|
||
| `lock_sc` | `bool lock_sc()` | Lock score file via lock file with retry/prompt and stale-lock aging |
|
||
| `unlock_sc` | `void unlock_sc()` | Release score file lock |
|
||
| `flush_type` | `void flush_type()` | Flush keyboard typeahead buffer (`flushinp()`) |
|
||
|
||
**Notes:** Almost everything here is either curses/tty plumbing or vestigial
|
||
1980s multi-user courtesy features. A Go port needs only: score-file open/lock
|
||
(use `os.OpenFile` + advisory locking or just best-effort), signal handling
|
||
(`os/signal`), and typeahead flush (tcell handles).
|
||
|
||
### mdport.c (1433 lines) — Platform abstraction layer for Unix/Windows/DOS
|
||
|
||
All functions are platform shims replaceable with Go stdlib:
|
||
|
||
| Function | Go equivalent |
|
||
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||
| `md_init` | none needed (tcell handles terminal init) |
|
||
| `md_onsignal_default/exit/autosave` | `os/signal.Notify` |
|
||
| `md_hasclreol`, `md_putchar`, `md_raw_standout/standend` | tcell rendering |
|
||
| `md_unlink`, `md_unlink_open_file`, `md_chmod` | `os.Remove`, `os.Chmod` |
|
||
| `md_normaluser` | omit (no setuid in Go port) |
|
||
| `md_getuid`, `md_getpid` | `os.Getuid()`, `os.Getpid()` |
|
||
| `md_getusername`, `md_getrealname` | `os/user.Current()` |
|
||
| `md_gethomedir` | `os.UserHomeDir()` |
|
||
| `md_sleep` | `time.Sleep` |
|
||
| `md_getshell`, `md_shellescape` | `os/exec` (or omit shell escape) |
|
||
| `md_crypt`, `md_getpass` | not needed (wizard password via env; no crypt) |
|
||
| `md_erasechar`, `md_killchar`, `md_dsuspchar`, `md_setdsuspchar`, `md_suspchar` | tcell key events |
|
||
| `md_readchar` | tcell `PollEvent` (the 900-line escape-sequence state machine disappears entirely) |
|
||
| `md_loadav`, `md_ignoreallsignals`, `md_tstp*`, checkout timers | omit / `os/signal` |
|
||
|
||
**Notes:** The dominant complexity is `md_readchar()` — a 900-line
|
||
keypad/escape-sequence decoder state machine for a dozen terminal types. tcell's
|
||
event model replaces all of it. Nothing in this file needs a line-by-line port.
|
||
|
||
### vers.c (18 lines) — Version and build information
|
||
|
||
| Variable | Value | Purpose |
|
||
| ---------- | ----------------------------- | ------------------------------------------------------------------- |
|
||
| `release` | "5.4.4" | Version number string (also written into save files for validation) |
|
||
| `encstr` | obfuscated bytes | XOR key stream #1 for save/score encryption |
|
||
| `statlist` | obfuscated bytes | XOR key stream #2 for save/score encryption |
|
||
| `version` | "rogue (rogueforge) 09/05/07" | Full version/build string |
|
||
|
||
**Notes:** `encstr`/`statlist` are the two XOR key strings consumed by
|
||
`encwrite()`/`encread()` in save.c.
|
||
|
||
### new_level.c (232 lines) — Dig and draw a new level
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `new_level` | `void new_level()` | Initializes a new dungeon level by clearing previous monsters and objects, drawing rooms and passages, placing traps and stairs, positioning monsters in their rooms, and placing the player. Updates max_level and resets hero position. |
|
||
| `rnd_room` | `int rnd_room()` | Selects and returns a random room index that actually exists (where ISGONE flag is not set). |
|
||
| `put_things` | `void put_things()` | Places potions, scrolls, and other items on the current level with 36% probability for each of MAXOBJ slots. Handles treasure rooms (1 in TREAS_ROOM chance) and places the amulet at level AMULETLEVEL if not yet found. |
|
||
| `treas_room` | `void treas_room()` | Creates a treasure room by selecting a random existing room and filling it with MINTREAS to MAXTREAS treasure items plus monsters from the next level down (incremented temporarily). |
|
||
|
||
**Notes:** File-scope constants: TREAS_ROOM (1/20 chance), MAXTREAS (10 items
|
||
max), MINTREAS (2 items min), MAXTRIES (10 tries limit). No static tables. Uses
|
||
macros: chat() for character at position, flat() for flags, F_REAL flag. Key
|
||
side effects: modifies global level, max_level, places[], mlist; calls helper
|
||
functions do_rooms(), do_passages(), put_things().
|
||
|
||
### rooms.c (473 lines) — Create the layout for the new level
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `do_rooms` | `void do_rooms()` | Creates all rooms on a level by partitioning space into 3x3 grid and generating random room sizes/positions. Marks some rooms as ISGONE, sets ISDARK and ISMAZE flags, places gold and monsters in each room based on probabilities. |
|
||
| `draw_room` | `void draw_room(struct room *rp)` | Draws walls (-, \|) around a room and lays down FLOOR characters inside; handles maze rooms by calling do_maze() instead. |
|
||
| `vert` | `void vert(struct room *rp, int startx)` | Draws a vertical wall line of '\|' characters from top to bottom of room. |
|
||
| `horiz` | `void horiz(struct room *rp, int starty)` | Draws a horizontal wall line of '-' characters across room width. |
|
||
| `do_maze` | `void do_maze(struct room *rp)` | Generates a maze within a room using recursive depth-first digging. Initializes maze SPOT array tracking visited positions and exits, sets up starting position, and calls dig() to carve passages. |
|
||
| `dig` | `void dig(int y, int x)` | Recursively digs maze passages by choosing random unvisited neighbors, accounting for exits, and calling putpass() to mark passage cells. Returns when no more valid neighbors exist. |
|
||
| `accnt_maze` | `void accnt_maze(int y, int x, int ny, int nx)` | Records a maze exit connection between adjacent SPOT cells, checking for duplicates before adding. |
|
||
| `rnd_pos` | `void rnd_pos(struct room *rp, coord *cp)` | Fills coord with random x,y within room bounds (excluding walls). |
|
||
| `find_floor` | `bool find_floor(struct room *rp, coord *cp, int limit, bool monst)` | Finds a valid floor/passage spot in given room or random room (if rp==NULL) within limit attempts. Returns TRUE if found, FALSE if limit exceeded. If monst==TRUE, checks for monster-valid position (empty with step_ok() char). |
|
||
| `enter_room` | `void enter_room(coord *cp)` | Called when player enters a room; sets proom, opens doors, and redraws non-dark rooms by displaying correct characters and monsters. Updates terminal display. |
|
||
| `leave_room` | `void leave_room(coord *cp)` | Called when player exits a room; clears room display by replacing FLOOR with space or passage chars, handles monsters with standout mode, restores doors. |
|
||
|
||
**Notes:** File-scope static variables: Maxy, Maxx, Starty, Startx (maze
|
||
bounds), and maze[NUMLINES/3+1][NUMCOLS/3+1] array (2D array of SPOT structures
|
||
tracking maze generation). SPOT typedef contains: nexits (count of exits),
|
||
exits[4] (array of exit coordinates), used (boolean flag). Uses "until" macro
|
||
with do-while loops for retries. Key macros: chat() and flat() for accessing
|
||
places[] array, FLOOR/DOOR/PASSAGE character constants, ISMAZE/ISDARK/ISGONE
|
||
room flags.
|
||
|
||
### passages.c (425 lines) — Draw the connecting passages
|
||
|
||
| Function | Signature | Description |
|
||
| ------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `do_passages` | `void do_passages()` | Draws all passages on a level using a connectivity graph algorithm. Builds spanning tree connecting all MAXROOMS rooms, then adds 0-4 random extra connections. Initializes rdes[] array and calls conn() to draw each passage, then calls passnum() to number them. |
|
||
| `conn` | `void conn(int r1, int r2)` | Draws a corridor between two rooms, determining direction (right or down based on room indices), finding door positions, and drawing passage tiles with optional turn points. Places DOOR characters at room entries. |
|
||
| `putpass` | `void putpass(coord *cp)` | Sets F_PASS flag at position and marks as PASSAGE character; randomly makes secret passages (unreal) at deeper levels. |
|
||
| `door` | `void door(struct room *rm, coord *cp)` | Records door position in room's r_exit array, increments r_nexits. For non-maze rooms, randomly creates secret doors (\|/- unreal) or regular DOOR characters. |
|
||
| `add_pass` | `void add_pass()` | [MASTER ifdef] Wizard command that displays all passages and doors on screen by iterating places[] and showing passages as PASSAGE, secret doors with standout. |
|
||
| `passnum` | `void passnum()` | Assigns passage numbers (0 to MAXPASS-1) by iterating through room exits and calling numpass() on each, resetting passage r_nexits. Uses static pnum and newpnum tracking. |
|
||
| `numpass` | `void numpass(int y, int x)` | Recursively numbers a passageway region by flood-fill; sets F_PNUM bits in flat flags, records door/secret door exits in passages[] array, and recurses on adjacent cells. |
|
||
|
||
**Notes:** File-scope static variables: pnum (current passage number), newpnum
|
||
(boolean flag). Static rdes[] array contains MAXROOMS room description
|
||
structures with conn[] (adjacency matrix for possible connections), isconn[]
|
||
(boolean array of actual connections made), and ingraph (boolean). The
|
||
connectivity matrix hardcodes a 3x3 room grid topology. Uses macros:
|
||
chat()/flat() for places[] access, F_PASS/F_PNUM flags, INDEX() macro for array
|
||
indexing, ce() for coordinate equality testing.
|
||
|
||
### move.c (426 lines) — Hero movement commands
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `do_run` | `void do_run(char ch)` | Initiates running mode by setting running=TRUE, runch=ch (direction), and after=FALSE. |
|
||
| `do_move` | `void do_move(int dy, int dx)` | Main movement handler; validates move legality (bounds, diagonal ok), handles confused movement, checks for traps/doors/monsters. Updates hero position, calls fight() for combat, handles room changes, updates display. Complex switch on destination cell character (DOOR/TRAP/PASSAGE/FLOOR/STAIRS/monster). |
|
||
| `turn_ok` | `bool turn_ok(int y, int x)` | Checks if position is a DOOR or both F_REAL and F_PASS flags set (valid turning point in corridors during runs). |
|
||
| `turnref` | `void turnref()` | Marks position F_SEEN and optionally refreshes screen if jump mode enabled (for passage turn display). |
|
||
| `door_open` | `void door_open(struct room *rp)` | Illuminates a room by iterating through all positions and waking any monsters found (calling wake_monster()). |
|
||
| `be_trapped` | `char be_trapped(coord *tc)` | Handles trap activation with switch on trap type: T_DOOR (level down), T_BEAR (no_move delay), T_MYST (random messages), T_SLEEP (no_command delay), T_ARROW (damage/rolls), T_TELEP (teleport), T_DART (poisoned damage), T_RUST (armor damage). Returns trap type. |
|
||
| `rndmove` | `coord *rndmove(THING *who)` | Generates random movement by adding rnd(3)-1 to current position; validates legality with diag_ok() and step_ok(), returns position or original if invalid. |
|
||
| `rust_armor` | `void rust_armor(THING *arm)` | Decrements armor AC (increases o_arm value) unless protected or wearing R_SUSTARM ring, with message output varying by terse flag. Returns if armor NULL, not ARMOR type, or already very rusty. |
|
||
|
||
**Notes:** File-scope global variable: nh (coord holding new hero position).
|
||
Uses macros: winat() for visible character, chat() for actual cell character,
|
||
flat() for flags, moat() for monster at position, INDEX() for place lookup. Trap
|
||
type constants: T_DOOR, T_BEAR (BEARTIME), T_MYST, T_SLEEP (SLEEPTIME), T_ARROW,
|
||
T_TELEP, T_DART, T_RUST. Uses "when" macro for compact case statements and "goto
|
||
over" for retry after confused move calculation.
|
||
|
||
### chase.c (542 lines) — Code for one creature to chase another
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `runners` | `void runners()` | Iterates through all monsters in mlist and moves each running monster (ISRUN flag) via move_monst(), handling fast monsters with second move and target flag updates. Clears has_hit message flag. |
|
||
| `move_monst` | `int move_monst(THING *tp)` | Executes one monster turn: calls do_chase() once for ISSLOW/ISHASTE alternate turns, toggles t_turn flag. Returns -1 if monster died. |
|
||
| `relocate` | `void relocate(THING *th, coord *new_loc)` | Updates monster position from th->t_pos to new_loc, refreshes moat() pointers, calls set_oldch() for display character, updates room pointer, recalculates destination if room changed, and redraws monster on screen. |
|
||
| `do_chase` | `int do_chase(THING *th)` | Primary chase logic: finds room of chaser and chasee, navigates to target by exiting current room and approaching chasee. For dragons, checks line-of-sight and fires flame bolt at hero if in range (DRAGONSHOT odds). Calls chase() to find best move, handles combat when goal reached, calls relocate(). Returns 0. |
|
||
| `set_oldch` | `void set_oldch(THING *tp, coord *cp)` | Sets tp->t_oldch to the screen character at cp for proper redrawing; accounts for darkness, lamp range, and blind status to choose correct background. |
|
||
| `see_monst` | `bool see_monst(THING *mp)` | Returns TRUE if hero can see monster: checks ISBLIND, ISINVIS (requires CANSEE), LAMPDIST proximity, and room visibility (same room and not dark). |
|
||
| `runto` | `void runto(coord *runner)` | Sets monster at runner position to ISRUN flag, clears ISHELD, sets t_dest via find_dest(), starts monster chasing. |
|
||
| `chase` | `bool chase(THING *tp, coord *ee)` | Finds best move for tp toward target ee; if confused (ISHUH), makes random move via rndmove(); otherwise iterates 3x3 surrounding cells to find closest approach, avoiding scare scrolls and Xerocs. Returns TRUE if still chasing (curdist != 0), FALSE if reached goal. |
|
||
| `roomin` | `struct room *roomin(coord *cp)` | Locates which room contains coordinate cp by checking F_PASS flag first (returns passage room), then iterating rooms[] for overlap; returns NULL/aborts if not found. |
|
||
| `diag_ok` | `bool diag_ok(coord *sp, coord *ep)` | Validates move from sp to ep: checks bounds and allows non-diagonals always; for diagonal moves, checks that both adjacent cells (same row and same column) are step_ok(). |
|
||
| `cansee` | `bool cansee(int y, int x)` | Returns TRUE if hero can see coordinate: checks ISBLIND, LAMPDIST range with passage blocking checks, or same room with lit/lit conditions. |
|
||
| `find_dest` | `coord *find_dest(THING *tp)` | Determines monster target: returns &hero if carrying probability is low, monster in proom, or monster is visible; otherwise searches lvl_obj for items in monster's room not already targeted by other monsters, with probability check; defaults to &hero. |
|
||
| `dist` | `int dist(int y1, int x1, int y2, int x2)` | Returns squared Euclidean distance (x delta squared plus y delta squared) for comparison purposes. |
|
||
| `dist_cp` | `int dist_cp(coord *c1, coord *c2)` | Wrapper calling dist() with coordinate components extracted from c1 and c2. |
|
||
|
||
**Notes:** File-scope static variable: ch_ret (coord holding where chasing takes
|
||
you, returned by chase() and used by do_chase()). DRAGONSHOT macro constant (5,
|
||
odds for dragon breath). Uses macros: moat() for monster at position,
|
||
chat()/flat() for cell access, on() for flag testing, ce() for coordinate
|
||
equality. Key dragon logic: checks 8-directional line-of-sight (row match,
|
||
column match, or diagonal), BOLT_LENGTH range squared, and calls fire_bolt().
|
||
Monster types referenced: 'D' (dragon), 'P' (invisible stalker), 'B' (bat), 'F'
|
||
(flytrap), 'X' (xeroc).
|
||
|
||
### monsters.c (253 lines) — File with various monster functions
|
||
|
||
| Function | Signature | Description |
|
||
| -------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `randmonster` | `char randmonster(bool wander)` | Selects a monster type from either lvl_mons[] or wand_mons[] array based on wander flag. Calculates index from level offset with noise (rnd(10)-6), clamps to valid range, skips nulls in array. Returns monster letter 'A'-'Z'. |
|
||
| `new_monster` | `void new_monster(THING *tp, char type, coord *cp)` | Initializes new monster: sets type, disguise, position; attaches to mlist; looks up stats from monsters[] table using type-'A' index; adjusts level based on depth (level > AMULETLEVEL); sets flags, hit points via roll(). If ISWEARING R_AGGR ring, calls runto(). For 'X' (xeroc), randomizes disguise. |
|
||
| `exp_add` | `int exp_add(THING *tp)` | Calculates experience reward based on monster level and max HP; modifies by 20x for level > 9, 4x for level > 6, and divides HP by 6 (or 8 if level 1). |
|
||
| `wanderer` | `void wanderer()` | Creates a wandering monster at random floor position in different room than proom. Allocates THING via new_item(), calls randmonster(TRUE), new_monster(), displays if SEEMONST, and sets running via runto(). |
|
||
| `wake_monster` | `THING *wake_monster(int y, int x)` | Activates monster at position: checks 1/3 chance to start chasing if ISMEAN and conditions met (not held, not wearing stealth, etc.); handles Medusa ('M') gaze attack (confusion if not saved). For ISGREED monsters, sets guard target to gold position or hero. Returns monster pointer. |
|
||
| `give_pack` | `void give_pack(THING *tp)` | If at max_level and random check (%) against m_carry probability succeeds, attaches one random item to monster's pack via new_thing(). |
|
||
| `save_throw` | `int save_throw(int which, THING *tp)` | Calculates saving throw: need value = 14 + which - (s_lvl / 2), rolls 1d20, returns TRUE if roll >= need. |
|
||
| `save` | `int save(int which)` | Hero's save check: for VS_MAGIC, reduces difficulty by protection ring bonuses (left and right ring armor values), then calls save_throw() with &player. |
|
||
|
||
**Notes:** File-scope static arrays: lvl_mons[] (26 monsters by level 0-25: K E
|
||
B S H I R O Z L C Q A N Y F T W P X U M V G J D), wand_mons[] (same with 0s for
|
||
F U D where wands unavailable). Monster type encoding: 'A' through 'Z' map to
|
||
indices 0-25 via t_type-'A'. The monsters[] global array (defined elsewhere) is
|
||
indexed this way. Key monster types: 'M' (Medusa), 'X' (Xeroc), 'F' (Flytrap),
|
||
'D' (Dragon). References AMULETLEVEL constant for depth-scaling experience. Uses
|
||
macros: ISWEARING() for ring checks, on() for flag testing, rnd() for
|
||
probabilities, roll() for hit dice.
|
||
|
||
### wizard.c (285 lines) — Special wizard commands (some also non-wizard under strange circumstances)
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `whatis` | `void whatis(bool insist, int type)` | Identifies an item from pack by prompting with get_item() and setting ISKNOW flag. Calls set_know() with appropriate info array (scr_info, pot_info, ws_info, ring_info) based on o_type. If insist, loops until valid selection or user cancels. |
|
||
| `set_know` | `void set_know(THING *obj, struct obj_info *info)` | Sets oi_know=TRUE for object type in info array, sets obj ISKNOW flag, and frees/clears any oi_guess string for that type. |
|
||
| `type_name` | `char *type_name(int type)` | Returns string description for object type by searching static tlist[] array: "potion", "scroll", "food", "ring, wand or staff", "ring", "wand or staff", "weapon", "suit of armor". |
|
||
| `create_obj` | `void create_obj()` | [MASTER ifdef] Wizard command to create arbitrary items: prompts for type and which (0-f hex), handles blessing for weapons/armor/rings, calls init_weapon() or sets o_arm, processes special ring logic, calls fix_stick() for staves, and adds to pack. |
|
||
| `teleport` | `void teleport()` | Instantly moves hero to random valid floor position, redraws old position with floor_at(), updates proom if changed rooms (leave_room/enter_room), and resets movement/combat flags (no_move, count, running). Resets ISHELD flag if held by flytrap (resets vf_hit and damage dice). |
|
||
| `passwd` | `int passwd()` | [MASTER ifdef] Prompts for wizard password, reads line with backspace/kill character handling, returns TRUE if strcmp(input, md_crypt(input, "mT")) matches PASSWD constant. |
|
||
| `show_map` | `void show_map()` | [MASTER ifdef] Displays full level map in hw window: iterates places[] drawing each cell with standout for non-real (F_REAL unset) cells, then shows "---More---" prompt. |
|
||
|
||
**Notes:** File-scope static data: tlist[] array in type_name() with h_list
|
||
structures mapping type constants to descriptions. Functions marked [MASTER
|
||
ifdef] are wizard-only debug/cheat commands (create_obj, passwd, show_map). The
|
||
teleport() function is non-wizard and can be invoked by T_TELEP trap or other
|
||
game mechanics. Uses macros: on() for flag testing, chat()/flat()/moat() for
|
||
places[] access, INDEX() for place lookup, ISWEARING() for ring checks.
|
||
References global variables: pack (player inventory), hero (player position),
|
||
proom (player's current room), player (player THING), monsters[] (for name
|
||
lookups in wizard feedback).
|
||
|
||
### things.c (714 lines) — Item naming and creation
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `inv_name` | `char* inv_name(THING *obj, bool drop)` | Formats item name for inventory display; handles all item types with modifiers like "(being worn)" or "(weapon in hand)"; writes to global `prbuf` |
|
||
| `drop` | `void drop()` | Player drops item from pack; checks floor availability, calls `leave_pack()`, attaches to level object list, updates display and global `amulet` flag |
|
||
| `dropcheck` | `bool dropcheck(THING *obj)` | Validates item can be dropped (checks cursed status and equipment status); handles ring stat adjustments like R_ADDSTR; returns FALSE if cursed |
|
||
| `new_thing` | `THING* new_thing()` | Creates random item for dungeon level; probabilistically selects type (potion/scroll/food/weapon/armor/ring/stick); initializes enchantments and cursing |
|
||
| `pick_one` | `int pick_one(struct obj_info *info, int nitems)` | Selects random item from cumulative probability table; returns index of selected item |
|
||
| `discovered` | `void discovered()` | Prompts player for which item type to list discoveries for; calls `print_disc()` and `end_line()` |
|
||
| `print_disc` | `void print_disc(char type)` | Lists all discovered items of given type (known or guessed); builds temporary THING for name formatting |
|
||
| `set_order` | `void set_order(int *order, int numthings)` | Fisher-Yates shuffle of array indices for randomized discovery list order |
|
||
| `add_line` | `char add_line(char *fmt, char *arg)` | Adds line to discovery window display with pagination; handles INV_SLOW vs INV_OVER modes; manages window creation and "Press space" prompts |
|
||
| `end_line` | `void end_line()` | Finalizes discovery list display; clears pagination state |
|
||
| `nothing` | `char* nothing(char type)` | Constructs "nothing found" message for specific item type |
|
||
| `nameit` | `void nameit(THING *obj, char *type, char *which, struct obj_info *op, char *(*prfunc)(THING *))` | Helper to format potion/ring/stick names with color/material and charge status via function pointer |
|
||
| `nullstr` | `char* nullstr(THING *ignored)` | Returns empty string; used as null function pointer in `nameit()` calls |
|
||
| `pr_list` | `void pr_list()` | MASTER-only: lists all possible items by type for wizard debugging |
|
||
| `pr_spec` | `void pr_spec(struct obj_info *info, int nitems)` | MASTER-only: prints all items in category with probability percentages |
|
||
|
||
**Notes:** Static file-scope variables `line_cnt`, `newpage`, `lastfmt`,
|
||
`lastarg` manage discovery display pagination. Function-pointer callback pattern
|
||
in `nameit()` (`nullstr`, `ring_num`, `charge_str`).
|
||
|
||
### pack.c (504 lines) — Pack management and item selection interface
|
||
|
||
| Function | Signature | Description |
|
||
| ------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `add_pack` | `void add_pack(THING *obj, bool silent)` | Adds item to player pack or picks up from floor; handles grouped items by incrementing count; keeps pack sorted by type; updates monster dest pointers and `amulet` flag |
|
||
| `pack_room` | `bool pack_room(bool from_floor, THING *obj)` | Validates pack space (`inpack` vs MAXPACK); removes from floor and updates display if successful |
|
||
| `leave_pack` | `THING* leave_pack(THING *obj, bool newobj, bool all)` | Removes item from pack; decrements `inpack`; splits stacks if not removing all; updates `pack_used` |
|
||
| `pack_char` | `char pack_char()` | Returns next unused pack slot letter (a-z); marks in `pack_used` array |
|
||
| `inventory` | `bool inventory(THING *list, int type)` | Lists pack items filtered by type; uses `add_line()` pagination; sets `n_objs` |
|
||
| `pick_up` | `void pick_up(char ch)` | Handles item pickup by type (gold vs objects); calls `add_pack()` or `money()` |
|
||
| `move_msg` | `void move_msg(THING *obj)` | Prints message for stepping onto object without picking it up (move_on mode) |
|
||
| `picky_inven` | `void picky_inven()` | Shows single item from pack by inventory letter |
|
||
| `get_item` | `THING* get_item(char *purpose, int type)` | Prompts player to select item from pack by letter or '*' for full list; handles ESCAPE and `again` repeat via `last_pick` |
|
||
| `money` | `void money(int value)` | Adds/subtracts gold from `purse`; updates display floor character |
|
||
| `floor_ch` | `char floor_ch()` | Returns appropriate floor display character based on room state and `show_floor()` |
|
||
| `floor_at` | `char floor_at()` | Returns character at hero position accounting for floor visibility |
|
||
| `reset_last` | `void reset_last()` | Resets saved last command state (`last_comm`, `last_dir`, `last_pick`) from backup copies |
|
||
|
||
**Notes:** Pack is a linked list anchored at global `pack` (macro for
|
||
`player.t_pack`); pack letters stored in `o_packch`, allocation tracked in
|
||
`pack_used[26]`. Grouped items (arrows etc.) stack by `o_group`. Uses `goto` for
|
||
pack insertion flow.
|
||
|
||
### potions.c (376 lines) — Potion effects
|
||
|
||
| Function | Signature | Description |
|
||
| ------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `quaff` | `void quaff()` | Drinks potion from pack; switch dispatching all 14 potion types (confusion, LSD, poison, strength, see-invisible, healing, monster-find, magic-find, raise-level, extra-heal, haste, restore, blind, levitate); calls `do_pot()` for fuse-based effects; updates `pot_info` known/guess state |
|
||
| `is_magic` | `bool is_magic(THING *obj)` | Tests if object radiates magic (enchanted armor/weapons, or any potion/scroll/stick/ring/amulet) |
|
||
| `invis_on` | `void invis_on()` | Enables CANSEE flag; re-displays invisible monsters |
|
||
| `turn_see` | `bool turn_see(bool turn_off)` | Toggles SEEMONST magic monster detection; redisplays monsters; handles hallucination display |
|
||
| `seen_stairs` | `bool seen_stairs()` | Checks if stairs are visible on map, under hero, or held by a visible monster |
|
||
| `raise_level` | `void raise_level()` | Sets experience to next level threshold; calls `check_level()` |
|
||
| `do_pot` | `void do_pot(int type, bool knowit)` | Standard potion setup: marks known, sets player flag from `p_actions[]` table, starts fuse or lengthens if already active |
|
||
|
||
**Notes:** `PACT` struct array `p_actions[]` maps potion types → player flag,
|
||
cleanup daemon function pointer (`unconfuse`, `come_down`, `sight`, etc.), and
|
||
duration.
|
||
|
||
### scrolls.c (330 lines) — Scroll effects
|
||
|
||
| Function | Signature | Description |
|
||
| ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `read_scroll` | `void read_scroll()` | Reads scroll from pack; switch on all 18 scroll types: confuse-monster (sets CANHUH on hero), magic-map (reveals level), hold-monster, sleep-self, enchant-armor, identify x5 varieties, scare-monster (message only when read), food-detection, teleport, enchant-weapon, create-monster, remove-curse, aggravate, protect-armor; calls `call_it()` to offer naming |
|
||
| `uncurse` | `void uncurse(THING *obj)` | Clears ISCURSED flag on item |
|
||
|
||
**Notes:** S_MAP iterates the entire level updating passage/door/floor tiles
|
||
into seen state; S_HOLD scans 3x3 neighborhood for monsters to hold; S_CREATE
|
||
places monster adjacent; S_SCARE when _picked up_ is handled in pack code
|
||
(scroll on floor scares monsters via chase.c check).
|
||
|
||
### rings.c (205 lines) — Ring equipping and effects
|
||
|
||
| Function | Signature | Description |
|
||
| ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `ring_on` | `void ring_on()` | Equips ring on left or right hand; calls `gethand()` if needed; switch dispatch for immediate effects (R_ADDSTR strength bonus, R_SEEINVIS, R_AGGR awakening) |
|
||
| `ring_off` | `void ring_off()` | Removes ring; validates which hand; `dropcheck()` for curse validation |
|
||
| `gethand` | `int gethand()` | Prompts player for left or right hand; loops until valid L/R or ESCAPE |
|
||
| `ring_eat` | `int ring_eat(int hand)` | Returns extra food consumption for ring on given hand; static `uses[]` table maps ring type to hunger impact (R_DIGEST negative/saves, R_REGEN +2, etc.) |
|
||
| `ring_num` | `char* ring_num(THING *obj)` | Returns bonus string "[+n]" if known (protection/add-str/add-dam/add-hit rings) |
|
||
|
||
**Notes:** Static `uses[]` array encodes per-ring metabolism cost. Rings worn
|
||
tracked in `cur_ring[LEFT]`/`cur_ring[RIGHT]`.
|
||
|
||
### sticks.c (432 lines) — Wand and staff zapping
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `fix_stick` | `void fix_stick(THING *cur)` | Initializes wand/staff damage strings and charges; staffs 2x3 melee damage, wands 1x1; WS_LIGHT gets 10-19 charges, others 3-7 |
|
||
| `do_zap` | `void do_zap()` | Zaps wand in aimed direction; switch over all 14 stick types: light (illuminate room), drain-life, invisibility (make monster invisible), polymorph, teleport-away, teleport-to, cancellation, magic missile, haste-monster, slow-monster, lightning/fire/cold bolts, nop; decrements charges |
|
||
| `drain` | `void drain()` | Drains half hero's HP and spreads equal damage across all monsters in room/passage; static `drainee[]` target array |
|
||
| `fire_bolt` | `void fire_bolt(coord *start, coord *dir, char *name)` | Fires elemental bolt: travels BOLT_LENGTH, bounces off walls (reversing delta components), damages monsters (with save for half... actually save to avoid), can hit hero on bounce-back; animates with directional characters ( | - / \\); dragons immune to flame |
|
||
| `charge_str` | `char* charge_str(THING *obj)` | Returns "[n charges]" string if known |
|
||
|
||
**Notes:** `fire_bolt` uses static `bolt` THING and `spotpos[]` path array;
|
||
bounce logic flips direction on wall hit. WS_POLYMORPH preserves monster's pack
|
||
through transformation. Used both by player zaps and dragon breath ("flame").
|
||
|
||
### weapons.c (289 lines) — Weapon initialization and throwing
|
||
|
||
| Function | Signature | Description |
|
||
| ------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `missile` | `void missile(int ydelta, int xdelta)` | Throws selected weapon: validates, leaves pack (splitting stacks), animates via `do_motion()`, tests hit with `hit_monster()`, falls to ground on miss |
|
||
| `do_motion` | `void do_motion(THING *obj, int ydelta, int xdelta)` | Animates projectile across screen one cell at a time until blocked (wall/door/monster) |
|
||
| `fall` | `void fall(THING *obj, bool pr)` | Places fallen projectile on ground via `fallpos()`; attaches to `lvl_obj` or discards if no space |
|
||
| `init_weapon` | `void init_weapon(THING *weap, int which)` | Sets weapon properties from static `init_dam[]` table (wield damage, hurl damage, launcher, flags); arrows/darts/shurikens get count 8-15 and shared group id |
|
||
| `hit_monster` | `int hit_monster(int y, int x, THING *obj)` | Wrapper calling `fight()` for missile combat at position |
|
||
| `num` | `char* num(int n1, int n2, char type)` | Formats bonus as "+h,+d" / "+a" strings for inventory |
|
||
| `wield` | `void wield()` | Equips weapon; validates not wearing/cursed current; updates `cur_weapon` |
|
||
| `fallpos` | `bool fallpos(coord *pos, coord *newpos)` | Finds random empty adjacent square (9 cells) for dropped item; reservoir-samples among valid spots |
|
||
|
||
**Notes:** Static `init_dam[]` table: mace 2x4, sword 3x4, bow 1x1, arrow
|
||
1x1/2x3 (launched by bow), dagger 1x6/1x4, two-handed sword 4x4, dart 1x1/1x3,
|
||
shuriken 1x2/2x4, spear 2x3/1x6. Global `group` counter assigns stack ids.
|
||
|
||
### armor.c (90 lines) — Armor wearing
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||
| `wear` | `void wear()` | Equips armor from pack; rejects if already wearing; sets ISKNOW; calls `waste_time()` (1 turn) |
|
||
| `take_off` | `void take_off()` | Removes armor; validates exists and not cursed via `dropcheck()` |
|
||
| `waste_time` | `void waste_time()` | Runs one turn of daemons/fuses (BEFORE and AFTER) without player action — lets hunger/healing tick during multi-turn actions |
|
||
|
||
### misc.c (598 lines) — Look/display update, direction input, utilities
|
||
|
||
| Function | Signature | Description |
|
||
| ------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `look` | `void look(bool wakeup)` | Core visibility update: redraws area around player (3x3 in corridors/dark, or whole room), erases old lamp area, handles door-stop run termination logic, wakes monsters if wakeup, applies hallucination via `trip_ch()`; maintains `oldpos`/`oldrp` |
|
||
| `trip_ch` | `int trip_ch(int y, int x, int ch)` | If hallucinating, replaces item characters with random `rnd_thing()` character |
|
||
| `erase_lamp` | `void erase_lamp(coord *pos, struct room *rp)` | Erases 3x3 lamp-lit area around old position in dark rooms |
|
||
| `show_floor` | `bool show_floor()` | Returns whether floor under hero should be shown (dark room + not blind + see_floor option) |
|
||
| `find_obj` | `THING* find_obj(int y, int x)` | Linear search of `lvl_obj` for object at position (fatal error if none in MASTER) |
|
||
| `eat` | `void eat()` | Eats food from pack; sets `food_left` to HUNGERTIME (with random reduction); 30% chance fruit vs ration message; may grant experience |
|
||
| `check_level` | `void check_level()` | Checks experience against `e_levels[]`; on level-up increases HP by roll(diff,10) and messages "Welcome to level N" |
|
||
| `chg_str` | `void chg_str(int amt)` | Modifies strength with R_SUSTSTR/ring accounting; tracks `max_stats.s_str` |
|
||
| `add_str` | `void add_str(str_t *sp, int amt)` | Adds to strength value with bounds (min 3, max 31) |
|
||
| `add_haste` | `bool add_haste(bool potion)` | Adds ISHASTE; if already hasted, faint 1-8 turns instead; starts `nohaste` fuse if potion |
|
||
| `aggravate` | `void aggravate()` | Calls `runto()` on all monsters — everything wakes and hunts |
|
||
| `vowelstr` | `char* vowelstr(char *str)` | Returns "n" if string starts with vowel (for "a"/"an") |
|
||
| `is_current` | `bool is_current(THING *obj)` | Checks if item is currently equipped; messages "in use" |
|
||
| `get_dir` | `bool get_dir()` | Prompts for direction key (hjklyubn); sets `delta`/`dir_ch`; handles `again` repeat; randomizes if confused/hallucinating; FALSE on ESCAPE |
|
||
| `sign` | `int sign(int nm)` | Returns -1/0/1 |
|
||
| `spread` | `int spread(int nm)` | Returns nm ±10% random spread (used for durations) |
|
||
| `call_it` | `void call_it(struct obj_info *info)` | Prompts player to name an unidentified item; stores in `oi_guess` |
|
||
| `rnd_thing` | `char rnd_thing()` | Picks random thing display character (includes AMULET only at deep levels); static `thing_list[]` |
|
||
| `choose_str` | `char* choose_str(char *ts, char *ns)` | Returns first string if hallucinating, else second |
|
||
|
||
**Notes:** `look()` is the heart of FOV/display maintenance and the trickiest
|
||
function here (passage-count door-stop logic using `sumhero`/`diffhero` diagonal
|
||
tests). Static `last_dt` in `get_dir()` for repeats.
|
||
|
||
### daemon.c (182 lines) — Daemon and fuse scheduler
|
||
|
||
| Function | Signature | Description |
|
||
| -------------- | ----------------------------------------------------------- | -------------------------------------------------------------- |
|
||
| `d_slot` | `struct delayed_action* d_slot()` | Finds first EMPTY slot in `d_list[20]` |
|
||
| `find_slot` | `struct delayed_action* find_slot(void (*func)())` | Finds occupied slot by function pointer |
|
||
| `start_daemon` | `void start_daemon(void (*func)(int), int arg, int type)` | Registers repeating daemon (d_time = DAEMON sentinel) |
|
||
| `kill_daemon` | `void kill_daemon(void (*func)())` | Removes daemon by function pointer |
|
||
| `do_daemons` | `void do_daemons(int flag)` | Runs all daemons matching BEFORE/AFTER phase |
|
||
| `fuse` | `void fuse(void (*func)(int), int arg, int time, int type)` | Registers one-shot countdown fuse |
|
||
| `lengthen` | `void lengthen(void (*func)(), int xtime)` | Extends existing fuse's remaining time |
|
||
| `extinguish` | `void extinguish(void (*func)())` | Cancels fuse by function pointer |
|
||
| `do_fuses` | `void do_fuses(int flag)` | Decrements matching fuses; fires and empties any reaching zero |
|
||
|
||
**Notes:** `d_list[MAXDAEMONS=20]` of
|
||
`struct delayed_action {type, func ptr, arg, time}`. Function pointers are the
|
||
identity of daemons/fuses — the Go port must replace this with named identifiers
|
||
(also required by save format, which already maps func ptrs to small ints).
|
||
|
||
### daemons.c (296 lines) — Daemon and fuse callbacks
|
||
|
||
| Function | Signature | Description |
|
||
| ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `doctor` | `void doctor()` | Healing daemon: heals based on level and `quiet` rest counter; R_REGEN ring bonus; caps at max_hp |
|
||
| `swander` | `void swander()` | Fuse that starts the `rollwand` daemon |
|
||
| `rollwand` | `void rollwand()` | Every turn after 4: 1-in-6 roll to spawn wandering monster via `wanderer()`, then re-fuse `swander` |
|
||
| `unconfuse` | `void unconfuse()` | Fuse: clears ISHUH; message |
|
||
| `unsee` | `void unsee()` | Fuse: clears CANSEE; re-hides invisible monsters |
|
||
| `sight` | `void sight()` | Fuse: cures blindness; re-lights room |
|
||
| `nohaste` | `void nohaste()` | Fuse: clears ISHASTE |
|
||
| `stomach` | `void stomach()` | Hunger daemon: decrements `food_left` (+1 per ring worn, adjusted by ring_eat); thresholds → hungry (300)/weak (150)/faint (0 = random paralysis, starvation death) |
|
||
| `come_down` | `void come_down()` | Fuse: ends hallucination; kills `visuals` daemon; redraws everything correctly |
|
||
| `visuals` | `void visuals()` | Hallucination daemon: continuously randomizes displayed item/monster glyphs |
|
||
| `land` | `void land()` | Fuse: ends levitation |
|
||
|
||
**Notes:** Static `between` counter in `rollwand()`. These 11 functions are
|
||
exactly the set the save format maps to integer ids 1-9 (+2).
|
||
|
||
### list.c (114 lines) — Doubly-linked list allocation
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | ----------------------------------------- | ----------------------------------------------------------------------- |
|
||
| `_detach` | `void _detach(THING **list, THING *item)` | Removes item from linked list (head pointer updated via double pointer) |
|
||
| `_attach` | `void _attach(THING **list, THING *item)` | Pushes item onto head of list |
|
||
| `_free_list` | `void _free_list(THING **ptr)` | Discards entire list |
|
||
| `discard` | `void discard(THING *item)` | free()s one THING |
|
||
| `new_item` | `THING* new_item()` | calloc()s a zeroed THING with NULL links |
|
||
|
||
**Notes:** Macro API `attach(a,b)`/`detach(a,b)`/`free_list(a)` pass `&list`. In
|
||
Go this becomes slices or container/list — the port will use slices of typed
|
||
pointers.
|
||
|
||
### fight.c (687 lines) — Combat system
|
||
|
||
| Function | Signature | Description |
|
||
| ------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `fight` | `int fight(coord *mp, THING *weap, bool thrown)` | Player attacks monster at coordinates with optional weapon. Xeroc disguise revelation, hit/miss messaging, confuse-monster ring/scroll effect (CANHUH), monster death via `killed()` |
|
||
| `attack` | `int attack(THING *mp)` | Monster attacks player. Handles damage, player death, and per-monster special effects: A=rust armor, I=freeze (no_command), R=weaken strength (poison, save or -1 STR), W=wraith XP drain, V=vampire maxhp drain, F=flytrap hold (accumulating vf_hit damage), L=leprechaun gold steal, N=nymph item steal |
|
||
| `set_mname` | `char *set_mname(THING *tp)` | Returns display name: "something" if unseen, random monster if hallucinating, else "the <name>"; static buffer |
|
||
| `swing` | `int swing(int at_lvl, int op_arm, int wplus)` | d20 to-hit roll: `res + wplus >= (20 - at_lvl) - op_arm` |
|
||
| `roll_em` | `bool roll_em(THING *thatt, THING *thdef, THING *weap, bool hurl)` | Rolls damage for an attack. Parses damage strings "NxM/NxM", strength to-hit/damage bonus tables, launcher matching for hurled ammo, ring add-hit/add-dam for player; applies to defender's `s_hpt` (min 0). Returns true if any swing hit |
|
||
| `prname` | `char *prname(char *mname, bool upper)` | Combatant name: "you" for NULL, optionally capitalized; static buffer |
|
||
| `thunk` | `void thunk(THING *weap, char *mname, bool noend)` | "the dagger hits <m>" message for thrown weapons |
|
||
| `hit` | `void hit(char *er, char *ee, bool noend)` | Prints hit message from `h_names[]` variants (terse-aware) |
|
||
| `miss` | `void miss(char *er, char *ee, bool noend)` | Prints miss message from `m_names[]` variants |
|
||
| `bounce` | `void bounce(THING *weap, char *mname, bool noend)` | "the dagger misses" message for thrown weapons |
|
||
| `remove_mon` | `void remove_mon(coord *mp, THING *tp, bool waskill)` | Removes monster from level: drops its pack (fall if killed), clears map cell + moat, detaches from mlist, resets to_death/kamikaze if it was target |
|
||
| `killed` | `void killed(THING *tp, bool pr)` | Awards experience, special deaths (F releases held hero, L drops gold), removes monster, "defeated the X" message, `check_level()` |
|
||
|
||
**Notes:** Static strength adjustment tables: `str_plus[]` (to-hit bonus for STR
|
||
3-31: -7..+3) and `add_dam[]` (damage bonus: -7..+6). `h_names[]`/`m_names[]`
|
||
message variant arrays. Careful NULL discipline where L/N monsters remove
|
||
themselves during their own attack (use-after-free avoidance).
|
||
|
||
### rip.c (449 lines) — Death, victory, and scoreboard
|
||
|
||
| Function | Signature | Description |
|
||
| -------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `score` | `void score(int amount, int flags, char monst)` | Scoreboard maintenance: reads top-ten via `rd_score()`, inserts player entry if it qualifies (one score per uid unless allscore), prints ranked list with standout for new entry, writes back via `wr_score()` under `lock_sc()` |
|
||
| `death` | `void death(char monst)` | Player death: purse -= 10%, draws tombstone ASCII art with name/gold/killer/year, calls `score()`, exits |
|
||
| `center` | `int center(char *str)` | Column to center a string on the tombstone |
|
||
| `total_winner` | `void total_winner()` | Victory: banner, itemized inventory with gold values (worth calculations per item type: weapons +bonus*100, armor by AC delta, unidentified halved, etc.), score(flags=2), exit |
|
||
| `killname` | `char *killname(char monst, bool doart)` | Death cause → string: A-Z monster names; lowercase specials from `nlist[]` (a=arrow, b=bolt, d=dart, h=hypothermia, s=starvation) |
|
||
| `death_monst` | `char death_monst()` | Random death cause for the -d demo flag |
|
||
|
||
**Notes:** Static `rip[]` tombstone art; `nlist[]` special death causes. SCORE
|
||
fields: uid, score, flags (0=killed 1=quit 2=winner 3=killed-with-amulet),
|
||
monster, name, level, time. Scoreboard file XOR-encrypted with same scheme as
|
||
saves.
|
||
|
||
### save.c (391 lines) — Save/restore and encrypted I/O
|
||
|
||
| Function | Signature | Description |
|
||
| ----------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `save_game` | `void save_game()` | Interactive save: prompt for filename (default `file_name`), confirm overwrite, open and `save_file()`, exit |
|
||
| `auto_save` | `void auto_save(int sig)` | SIGHUP/SIGTERM handler: silently saves to `file_name` and exits |
|
||
| `save_file` | `void save_file(FILE *savef)` | Writes version string + "LINES x COLS" header (encrypted), then `rs_save_file()`; chmod 0400; exits |
|
||
| `restore` | `bool restore(char *file, char **envp)` | Validates version + screen size, rejects symlinks/multi-link files (anti-cheat), deletes save file (no re-use), `rs_restore_file()`, re-enters `playit()` |
|
||
| `encwrite` | `size_t encwrite(char *start, size_t size, FILE *outf)` | XOR-encrypts stream: byte ^ encstr[i] ^ statlist[j] ^ feedback byte, cycling both key strings |
|
||
| `encread` | `size_t encread(char *start, size_t size, FILE *inf)` | Mirror decryption of `encwrite` |
|
||
| `rd_score` | `void rd_score(SCORE *top_ten)` | Reads/decrypts scoreboard entries (name string + sscanf'd numeric line) |
|
||
| `wr_score` | `void wr_score(SCORE *top_ten)` | Writes/encrypts scoreboard entries |
|
||
|
||
**Notes:** Save anti-cheat: file deleted on restore, symlink/hardlink rejected.
|
||
Go port decision point: keep byte-compatible format vs. clean format (see Part
|
||
2).
|
||
|
||
### state.c (2135 lines) — Save-state serialization layer
|
||
|
||
Machine-generated-style marshaling of every global into an endian-stable,
|
||
XOR-encrypted binary stream with per-block magic markers (0xABCD0001-style
|
||
RSID_* ids).
|
||
|
||
| Family | Functions | Notes |
|
||
| ---------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| Primitives | `rs_write/rs_read` (raw), `_int`, `_short`, `_uint`, `_chars`, `_boolean(s)`, `_shorts`, `_marker` | Little-endian normalized; count-prefixed arrays; `format_error`/`read_error`/`write_error` flags |
|
||
| Strings | `_string`, `_new_string`, `_string_index`, `_strings` | Length-prefixed; `_string_index` stores index into a master table (e.g. rainbow colors) instead of text |
|
||
| Game types | `_coord`, `_str_t`, `_stats`, `_room(s)`, `_monsters`, `_obj_info`, `_daemons`, `_window` | `_daemons` maps function pointers ↔ integer ids (rollwand=1, doctor=2, stomach=3, runners=4, swander=5, nohaste=6, unconfuse=7, unsee=8, sight=9); `_window` dumps curses screen contents cell by cell |
|
||
| THINGs | `_object`, `_object_list`, `_object_reference`, `_thing`, `_thing_list`, `rs_fix_thing(_list)` | Lists serialized with size prefix and rebuilt; intra-structure pointers (cur_weapon, monster t_dest) stored as list indices; monster chase targets encoded as (kind, index) pairs and fixed up post-load via `t_reserved` |
|
||
| Map | `_places` | All MAXLINES*MAXCOLS cells: ch, flags, monster reference index |
|
||
| Helpers | `get_list_item`, `find_list_ptr`, `list_size`, `find_room_coord`, `find_thing_coord`, `find_object_coord` | Pointer→index translation for serialization |
|
||
| Top level | `rs_save_file`, `rs_restore_file` | Fixed ordered sequence of ALL globals: 27 bools, ~25 ints, strings/name arrays, coords, player THING, equipment references, lvl_obj list, mlist, places[], rooms/passages, monsters[], all 7 obj_info tables, d_list daemons, and the curses screen |
|
||
|
||
**Notes:** This file exists because C can't reflect. The Go port replaces nearly
|
||
all of it with `encoding/gob` (or JSON) over a serializable snapshot struct —
|
||
only the pointer→index encoding of references (equipment, chase targets) needs
|
||
explicit code.
|
||
|
||
### options.c (502 lines) — Options menu and ROGUEOPTS parsing
|
||
|
||
| Function | Signature | Description |
|
||
| ------------------------------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `option` | `void option()` | Interactive options screen editing the `optlist[]` entries in place; per-option get/put function pointers; MINUS backs up, ESCAPE quits |
|
||
| `pr_optname` | `void pr_optname(OPTION *op)` | Prints "prompt (name): " |
|
||
| `put_bool` / `put_str` / `put_inv_t` | `void put_*(void *)` | Display current value (True/False, string, inventory-type name) |
|
||
| `get_bool` | `int get_bool(void *vp, WINDOW *win)` | Interactive boolean input (t/f/Enter/Esc/-) |
|
||
| `get_sf` | `int get_sf(void *vp, WINDOW *win)` | see_floor wrapper around get_bool with lamp redraw side effects |
|
||
| `get_str` | `int get_str(void *vopt, WINDOW *win)` | Line editor: backspace, kill-char, ~ home expansion; MAXINP=50 cap |
|
||
| `get_inv_t` | `int get_inv_t(void *vp, WINDOW *win)` | Inventory style selection (Overwrite/Slow/Clear) |
|
||
| `get_num` | `int get_num(void *vp, WINDOW *win)` | (MASTER) numeric via get_str + atoi |
|
||
| `parse_opts` | `void parse_opts(char *str)` | Parses ROGUEOPTS env var: comma-separated `flag`, `noflag`, `name=value` |
|
||
| `strucpy` | `void strucpy(char *s1, char *s2, int len)` | Copies only printable chars, capped |
|
||
|
||
**Notes:** `OPTION` struct: name, prompt, value pointer, put function pointer,
|
||
get function pointer. Options: terse, flush, jump, seefloor, passgo, tombstone,
|
||
inven, name, fruit, file. In Go this becomes a slice of interface-typed option
|
||
descriptors or simple closures.
|
||
|
||
### xcrypt.c (708 lines) — DES crypt(3) implementation
|
||
|
||
Full portable DES implementation (`xcrypt(key, setting)`) with precomputed
|
||
permutation tables (~100KB of lookup tables), supporting classic 2-char-salt
|
||
crypt and extended-format crypt.
|
||
|
||
**Usage in the game:** only `md_crypt()` → wizard-password check (`passwd()` in
|
||
wizard.c). NOT used for save encryption (that's the simple XOR in save.c). The
|
||
Go port can drop this file entirely and gate wizard mode on an environment
|
||
variable instead.
|
||
|
||
### score.h (27 lines) — Scoreboard entry structure
|
||
|
||
```c
|
||
struct sc_ent {
|
||
unsigned int sc_uid; /* user id */
|
||
int sc_score; /* final gold */
|
||
unsigned int sc_flags; /* 0=killed, 1=quit, 2=winner, 3=killed with amulet */
|
||
unsigned short sc_monster; /* killer: A-Z monster, or a/b/d/h/s special */
|
||
char sc_name[MAXSTR]; /* player name */
|
||
int sc_level; /* deepest level */
|
||
unsigned int sc_time; /* timestamp */
|
||
};
|
||
typedef struct sc_ent SCORE;
|
||
```
|
||
|
||
Declares `rd_score(SCORE*)` / `wr_score(SCORE*)` (implemented in save.c).
|
||
|
||
---
|
||
|
||
# Part 2 — The Go Port Design
|
||
|
||
Module: **`git.eeqj.de/sneak/rgoue`**
|
||
|
||
## 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 reference implementation lives on the `master`/`modern-rogue` branches;
|
||
this branch is Go-only. The tcell wrapper ended up in its own `term` package so
|
||
that `game` itself has no third-party imports:
|
||
|
||
```
|
||
go.mod module git.eeqj.de/sneak/rgoue
|
||
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, New, 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 New,
|
||
// 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 New(params Params) *RogueGame // Params: seed, name, ROGUEOPTS string, score path, wizard
|
||
func (g *RogueGame) Run() // main.c main()+playit(): init tables, first level, daemons, loop
|
||
func (g *RogueGame) command() // one turn (command.c)
|
||
func Restore(path string, params Params) (*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), `New(...)` 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 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.
|
||
|
||
```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) | `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.
|
||
|
||
## 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:** New → play N scripted turns → save → restore → compare
|
||
full state.
|
||
- `go vet` + `gofmt` clean at every commit.
|