The port is complete, so the C sources and their build system (autoconf/automake inputs, Makefiles, Visual Studio project, doc templates, RPM spec, C build/modernization notes) leave this branch; they live on in master/modern-rogue for reference. Also ports the last missing wizard command (things.c pr_list/pr_spec, the '*' item-probability listing), rewrites README.md for the Go port, and updates ARCHITECTURE.md's layout notes (game/ has no third-party imports; tcell lives in term/). Kept: LICENSE.TXT, rogue.png, rogue.desktop, ARCHITECTURE.md.
100 KiB
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-roguebranch. - Part 2 — The Go Port Design: the object-oriented architecture for the Go
port (
sneak.berlin/go/rogue), 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 inrogue.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 embeddedl_next/l_prevpointers) 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
mvinchor theplaces[]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 numberdnum) 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
typedef struct { int x; int y; } coord;
Used for every position: hero, monsters, room corners, exits, deltas.
3.2 str_t — strength
typedef unsigned int str_t; /* strength 3..31 */
3.3 struct stats — fighting statistics (player and monsters)
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, ...).
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_armis overloaded three ways: armor class (armor), charge count (sticks, viao_charges), gold value (gold piles, viao_goldval).t_destaliases other entities' interiors: it points athero(player position), a room'sr_goldcoordinate, or another monster'st_pos. The save format encodes this as (kind, index) pairs.
3.5 PLACE — one map cell, and the places[] map
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
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
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
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
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 #defines 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 ( |
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 "; 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 " 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
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: sneak.berlin/go/rogue
1. Goals and principles
- 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.
- 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). TwoRogueGamevalues are two independent games. File-scope C statics (pagination counters, shadow copies, scratch buffers) become fields too — either onRogueGameor on the subsystem struct that replaces their file. - Modern data structures. Basic containers are NOT ported. The THING
doubly-linked lists become slices (
[]*Object,[]*Monster).list.cdisappears; 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. - OOP where the domain has objects, not everywhere. The THING union
splits into
ObjectandCreature(withPlayerandMonstervariants). 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. - 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. - 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 sneak.berlin/go/rogue
cmd/rogue/main.go package main — flag parsing, constructs RogueGame, calls Run
game/ package game — the port, one .go file per .c file
game.go RogueGame struct, NewGame, Run (main.c)
rng.go seeded LCG (main.c: rnd/roll + RN macro)
types.go Coord, Stats, flags, constants (rogue.h)
object.go Object type (rogue.h THING _o arm)
creature.go Creature/Monster/Player (rogue.h THING _t arm)
level.go Place, Level, map access (rogue.h places[] + macros)
tables.go monsters[26], obj_info tables, e_levels, rainbow/stones/wood/metal (extern.c)
init.go per-game randomization (init.c)
command.go command dispatch (command.c)
io.go message/status line (io.c)
screen.go Screen: tcell wrapper + Window (curses + mdport display shims)
newlevel.go level orchestration (new_level.c)
rooms.go room/maze generation (rooms.c)
passages.go corridors (passages.c)
move.go player movement, traps (move.c)
chase.go monster AI (chase.c)
monsters.go monster creation (monsters.c)
fight.go combat (fight.c)
things.go item naming/creation (things.c)
pack.go inventory (pack.c)
potions.go, scrolls.go, rings.go, sticks.go, weapons.go, armor.go
misc.go look(), eat, direction input (misc.c)
daemon.go scheduler (daemon.c)
daemons.go the 11 callbacks (daemons.c)
options.go options screen + ROGUEOPTS (options.c)
wizard.go wizard commands, identify (wizard.c)
rip.go tombstone, victory, scores (rip.c)
save.go save/restore via gob (save.c + state.c)
score.go scoreboard (score.h + rip.c/save.c score I/O)
mach_dep.c, mdport.c, xcrypt.c, state.c, list.c, vers.c have no
counterpart files: their few live responsibilities are absorbed (signals →
game.go; score file locking → score.go; version string → game.go;
serialization → save.go).
3. The RogueGame type
RogueGame is deliberately a god object: it is the C global namespace given
a receiver. Fields are grouped into embedded sub-structs where a subsystem is
coherent, but everything is reachable from g.
// RogueGame is one complete game of Rogue: all state that was global in the
// C implementation, plus the terminal it plays on. Construct with NewGame,
// then call Run.
type RogueGame struct {
// --- identity / RNG ---
Rng *Rng // the LCG; seed == C `seed`
Dnum int // dungeon number (the seed as shown to the user)
Whoami string // player name
Fruit string // favorite fruit
Home string // home directory (save default)
FileName string // save file path
Wizard bool // debug mode
NoScore bool // wizard was used / scoring disabled
// --- the world ---
Player Player
Level Level // map, rooms, passages, level objects, monsters
Depth int // C `level`
MaxDepth int // C `max_level`
Amulet bool
SeenStairs bool
// --- turn/command engine ---
Playing bool
After bool // this action consumes a turn
Again bool // repeating last command
Count int // command repeat count
NoCommand int // turns asleep
NoMove int // turns held
Quiet int // consecutive restful turns (heal rate)
Running bool
RunCh byte
DoorStop, Firstmove, MoveOn bool
ToDeath, Kamikaze, HasHit bool
MaxHit int
Take byte // glyph of thing picked up this move
Delta Coord // last direction from GetDir
DirCh byte
LastComm, LastDir byte // command repeat state ('a' command)
LLastComm, LLastDir byte
LastPick, LLastPick *Object
countCh, direction, newCount byte // command.c statics (as bool for newCount)
// --- daemons/fuses ---
Daemons DaemonList // d_list[20] + `between` counter
// --- messages / UI ---
scr *Screen // tcell-backed terminal (stdscr + hw scratch window)
Msgs MsgLine // msgbuf/newpos/mpos/huh/save state
Options Options // terse, jump, seefloor, passgo, flush, tombstone, inv_type...
StatMsg bool
InvDescribe bool
Oldpos Coord // hero position before last look()
Oldrp *Room
// --- item identity (per-game randomized appearance + discovery) ---
Items ItemLore // p_colors, s_names, r_stones, ws_made, ws_type,
// the 7 ObjInfo tables, `group` counter
// --- scores ---
LastScore int
AllScore bool
ScorePath string
}
Player-centric C globals move onto Player, world-centric onto Level
(see §4/§5); the remainder sit directly on RogueGame with the original
semantics. The command.c/io.c/things.c file-statics become lowercase fields
of RogueGame, MsgLine, and the inventory pagination helper respectively.
Entrypoints
func NewGame(opts Config) *RogueGame // Config: seed, name, ROGUEOPTS string, score path, wizard
func (g *RogueGame) Run() error // main.c main()+playit(): init tables, first level, daemons, loop
func (g *RogueGame) command() // one turn (command.c)
func Restore(path string, opts Config) (*RogueGame, error) // save.c restore()
cmd/rogue/main.go is a thin shell: flags (-s scores, -d death demo,
save-file arg, SEED/ROGUEOPTS env), NewGame(...) or Restore(...),
Run(), exit code.
4. Core types
4.1 Geometry and stats
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
// 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
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
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
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
type Room struct {
Pos Coord // upper-left
Max Coord // size
Gold Coord
GoldVal int
Flags RoomFlags // IsDark | IsGone | IsMaze
Exits []Coord // r_exit[12] + r_nexits → slice
}
4.7 Static tables
tables.go holds the immutable data as package-level constants and
var tables that are never written after init (the only permitted
package-level data — they are ROM, not state): monsterTable [26]MonsterKind
(name, carry %, flags, stats), initDam weapon table, base objInfo tables
(copied into RogueGame.Items at NewGame so per-game mutation — probability
resumming, oi_know, oi_guess — stays instance-local), eLevels,
rainbow, stones, woods, metals, sylls, trap names, help text,
lvlMons/wandMons spawn strings, hNames/mNames combat messages.
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
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:
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
// 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
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
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 —
type SaveState struct {
Version string
Game RogueGame // gob-friendly: exported fields
// pointer fixups:
CurArmorIdx, CurWeaponIdx int // index into Player.Pack, -1 = nil
CurRingIdx [2]int
MonstDest []DestRef // per monster: {Kind: hero|monster|object|gold, Index int}
PlaceMonst []int // per map cell: index into Level.Monsters
}
Save-file hygiene keeps the C behavior: the file is deleted on restore, and
Run exits after a successful save. The XOR encryption and symlink checks
are dropped (they were 1980s shared-machine anti-cheat).
5.7 Scoreboard
SCORE → ScoreEnt struct; the score file is a gob (or JSON) list at
~/.rogue_scores (overridable). Locking via os.OpenFile(O_CREATE|O_EXCL)
lock file with stale-age takeover, porting lock_sc/unlock_sc semantics.
Tombstone/victory screens port verbatim from rip.c.
6. C construct → Go construct map
| C construct | Go translation |
|---|---|
| global variable | RogueGame field (or Player/Level/subsystem field) |
| file-scope static | unexported field on RogueGame or subsystem struct |
THING union |
Creature / Object structs |
linked lists (l_next/l_prev, attach/detach) |
slices + attach(&s, x)-style helpers (prepend, remove by identity) |
coord* aliasing (t_dest) |
*Coord pointers into live structs (kept) |
| function pointers (daemons, options, nameit) | enum IDs + dispatch switch (daemons, save-relevant); closures (options, nameit) |
when/otherwise/until macros |
case/default/for !cond |
goto (retry loops in pack.c, move.c, passages.c) |
labeled loops / restructured for |
char-indexed tables (monsters[type-'A']) |
same, monsterTable[t.Type-'A'] |
damage strings "3x4/1x2" |
kept as strings; parsed by rollEm (strings/strconv) |
| curses stdscr/hw windows | Window cell buffers over tcell |
mvinch screen reads |
Window.Inch from the buffer |
| signal handlers (SIGHUP autosave, SIGTSTP, SIGINT) | os/signal goroutine → channel checked in ReadChar; tcell handles TSTP/resize |
setjmp-free exits (my_exit, exit() everywhere) |
error returns from Run + a small gameOver sentinel; no os.Exit inside game logic |
vsprintf message building |
fmt.Sprintf |
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
| DES wizard password | ROGUE_WIZARD=1 env check |
md_* platform shims |
stdlib (os, os/user, time) or deleted |
Exit discipline deserves a note: the C code calls exit() from deep
inside call chains (death, victory, save). The port threads a
g.gameOver(reason) that sets Playing=false and unwinds via normal
returns to Run — the one structural change made everywhere, since
os.Exit would skip tcell teardown and defers.
7. Naming conventions
- C function
do_move→ method(g *RogueGame) DoMove(dy, dx int); the C name is preserved in CamelCase so cross-referencing Part 1 is mechanical. Statics/internal helpers are lowerCamel (g.doadd→g.addMsgV). - Entity-intrinsic functions become entity methods where they read no game
state:
inv_name(obj, drop)needs ItemLore →(g *RogueGame) InvName(o *Object, drop bool) string; butis_magic(obj)→(o *Object) IsMagic() bool. - Boolean C
bool/intreturns staybool;charreturns becomebyte. coord*out-params becomeCoordreturns (or(Coord, bool)for find-style functions likefallpos/find_floor).
8. Porting order (each step compiles, is committed, and is testable)
- 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). - 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.
- Items: things.go, pack.go, init.go + ItemLore.
- Creatures & combat: monsters.go, chase.go, fight.go, daemon(s).go.
- UI: screen.go, io.go, options.go, misc.go look(), command.go, move.go. First playable build.
- Effects: potions.go, scrolls.go, rings.go, sticks.go, weapons.go, armor.go, wizard.go.
- 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/rollEmagainst hand-computed cases, str_plus/add_dam table spot checks. - Determinism test: scripted input sequence against a fixed seed must produce identical final state across runs (guards hidden nondeterminism — map iteration, time dependence).
- Save round-trip: NewGame → play N scripted turns → save → restore → compare full state.
go vet+gofmtclean at every commit.