Constant Dependency Design Doc
Execution checklist source of truth for Issue #227:
docs/ISSUE_227_EXECUTION_CHECKLIST.md
Goal
Understand which C header files are “leaf” constants files (safe to import from anywhere in JS without circular initialization risk), and which depend on constants from other files.
C Header Constant Cross-File Dependencies
Every case where a constant’s RHS references a constant defined in another file:
artifact.h
SPFX_XRAYusesRAY[objclass.h]TAMINGusesLAST_PROP[prop.h]
botl.h
BL_ATTCLR_MAX,HL_ATTCLR_*useCLR_MAX[color.h]MAXCOusesCOLNO[global.h]REASSESS_ONLYusesTRUE[global.h]
config.h
CONFIG_ERROR_SECUREusesTRUE[global.h]GEM_GRAPHICSusesGEM[objects.h]
dgn_file.h
D_ALIGN_CHAOTIC/LAWFUL/NEUTRALuseAM_*[align.h]
display.h
GLYPH_*_OFFuseMAXEXPCHARS[sym.h],WARNCOUNT[sym.h],MAXTCHARS[sym.h]- guard macro only [vision.h]
global.h
PANICTRACEand guard macros use constants from [config.h]NH_DEVEL_STATUSusesNH_STATUS_WIP[patchlevel.h]NH_STATUS_RELEASEDdefined in [patchlevel.h]
hack.h
SHOP_WALL_DMGusesACURRSTR[attrib.h]MAXLINFOusesMAXDUNGEON[global.h],MAXLEVEL[global.h]MM_NOWAITusesSTRAT_WAITMASK[monst.h]RLOC_NOMSGusesSTRAT_APPEARMSG[monst.h]BALL_IN_MON,CHAIN_IN_MONuseOBJ_FREE[obj.h]UNDEFINED_RACE/ROLEuseNON_PM[permonst.h]CC_SKIP_INACCSusesZAP_POS[rm.h]SYM_OFF_XusesWARNCOUNT[sym.h]
mextra.h
- guards use
AM_*[align.h] FCSIZusesCOLNO[global.h],ROWNO[global.h]
monsters.h
SEDUCTION_ATTACKS_*useNO_ATTK[artilist.h],AD_*[monattk.h],AT_*[monattk.h]
obj.h
OBJ_Hguard usesUNIX[config.h]
objects.h
B,P,S,PAPERuseWHACK[objclass.h],PIERCE[objclass.h],SLASH[objclass.h],LEATHER[objclass.h]
sp_lev.h
ICEDPOOLSusesICED_MOAT[rm.h],ICED_POOL[rm.h]
sym.h
MAXTCHARSusesTRAPNUM[trap.h]
you.h
ROLE_ALIGNMASK/CHAOTIC/LAWFUL/NEUTRALuseAM_*[align.h]
Pure Leaf Headers (no cross-file constant deps)
These headers define only self-contained constants and are safe to import from anywhere without risk of unresolved dependencies:
align.h artilist.h attrib.h context.h
coord.h decl.h defsym.h dungeon.h
engrave.h extern.h flag.h hacklib.h
integer.h lint.h mkroom.h monattk.h
mondata.h monst.h optlist.h patchlevel.h
quest.h rect.h region.h rm.h
savefile.h seffects.h spell.h stairs.h
trap.h vision.h warnings.h weight.h
youprop.h objclass.h obj.h (mostly) prop.h color.h
JS Equivalent Leaf Files
Mapping the above to JS: the files that define only pure constants and are safe to import from anywhere without circular init risk:
| C header | JS equivalent | Status |
|---|---|---|
| config.h + defsym.h + integer.h | const.js |
✅ leaf, no imports |
| monsters.h + monattk.h | monsters.js |
✅ near-leaf (→ attack_fields.js only) |
| objects.h + objclass.h | objects.js |
✅ leaf, no imports |
| trap.h | const.js (TT_* etc.) |
✅ |
| align.h | const.js (A_LAWFUL etc.) |
✅ |
| attrib.h | const.js (A_STR etc.) |
✅ |
| artilist.h | const.js / artifacts.js |
check |
| rm.h | const.js |
check |
| sym.h / defsym.h | const.js |
check |
| hacklib.h | hacklib.js |
✅ leaf |
Key Insight
The C constant dependency graph is shallow — only ~12 headers have any
cross-file constant dependencies, and those dependencies only go 1-2 levels
deep (e.g. hack.h → global.h → config.h → pure literals). There are
no cycles in the C constant dependency graph.
The JS equivalent should maintain this property: const.js, monsters.js,
objects.js, hacklib.js form the leaf tier and must never
import from gameplay modules. Everything else can freely import from these
without initialization risk.
The pervasive JS module cycles (trap ↔ hack ↔ vision etc.) are not a
constant initialization problem — they only involve function imports, which
are resolved by the time any function executes.
Target JS Architecture
The goal is a set of leaf files that together hold all exported capitalized constants and all static data tables. No other JS file exports capitalized constants — only functions and unexported locals. This means the rest of the codebase can have arbitrary circular dependencies between gameplay files without any constant initialization risk.
The leaf files
| File | Contents | Imports | Source |
|---|---|---|---|
version.js |
COMMIT_NUMBER — build artifact |
none | git hook |
const.js |
All hand-maintained capitalized constants: scalars, display tables, direction arrays, terrain/symbol/trap constants | version.js only |
hand-maintained |
objects.js |
Auto-generated object data table + initObjectData() |
const.js only |
gen_objects.py |
monsters.js |
Auto-generated monster data table | const.js only |
gen_monsters.py |
artifacts.js |
Auto-generated artifact data table (artilist[]) + ART_* / SPFX_* constants |
const.js only |
gen_artifacts.py |
symbols.js |
Late-bound symbol/glyph constants and deferred cross-leaf constants (display.h chain) |
const.js, objects.js, monsters.js, artifacts.js |
hand-maintained (prototype) |
game.js |
game singleton + all struct class definitions |
const.js only |
hand-written |
engrave_data.js |
Encrypted engrave strings (makedefs output) | none | build artifact |
epitaph_data.js |
Encrypted epitaph strings (makedefs output) | none | build artifact |
rumor_data.js |
Encrypted rumor strings (makedefs output) | none | build artifact |
storage.js |
DEFAULT_FLAGS, OPTION_DEFS — config data with 30+ consumers |
core leaf headers | hand-maintained |
config.js is merged into const.js.
symbols.js is reintroduced as a late-bound module for symbol/glyph chains
that depend on generated leaf data (objects.js/monsters.js/artifacts.js).
attack_fields.js (runtime alias shim) is deleted entirely once all call
sites use canonical C field names (Phase 2).
The rule
- Core leaf headers (
version.js,const.js): export capitalized constants; import only from each other. - Generated data leaf headers (
objects.js,monsters.js,artifacts.js): export capitalized data constants and tables; import only from core leaf headers. - State leaf (
game.js): exportsgamesingleton and struct classes; imports only from core leaf headers. - Build artifact leaves (
engrave_data.js,epitaph_data.js,rumor_data.js): export encrypted string blobs; no imports. - Options leaf (
storage.js): exportsDEFAULT_FLAGSandOPTION_DEFS; imports only from core leaf headers. - All other files: import freely from any leaf file; may have arbitrary circular imports between themselves (functions only); must not export capitalized constants.
Table-building helper functions
The generated data files use C macro idioms that become small pure helper functions in JS, defined inline at the top of the file that uses them — not exported:
| Helper | Defined in | What it does |
|---|---|---|
HARDGEM(n) |
objects.js |
n >= 8 ? 1 : 0 — gem toughness from Mohs hardness |
BITS(...) |
objects.js |
Unpacks object bitfield args into a plain object |
PHYS/DRLI/COLD/FIRE/ELEC/STUN/POIS(a,b) |
artifacts.js |
Attack struct shorthand |
NO_ATTK, NO_DFNS, NO_CARY |
artifacts.js |
Empty attack/defense/carry struct |
DFNS(c), CARY(c) |
artifacts.js |
Defense/carry struct shorthand |
If a helper is ever shared across multiple leaf files it moves to const.js.
The generators emit these as function calls (e.g. HARDGEM(9)) rather than
pre-evaluating them — keeping the JS tables readable and close to the C source.
Implementation steps
- Merge
config.js+symbols.js→const.js - Update the Python generators so
objects.js,monsters.js,artifacts.jsimport fromconst.jsonly; inline table-building helpers - Audit all other JS files and move any stray exported capitalized constants into the appropriate leaf file
- Update all import sites from
config.js/symbols.js→const.js
Audit command
Check for stray exported capitalized constants outside leaf files:
node --test test/unit/constants_export_policy.test.js
node --test test/unit/gen_constants_report.test.js
rg -n "^export (const|let|var) [A-Z]" js \
| rg -v "js/(const|objects|monsters|artifacts|symbols|version|storage|.*_data)\\.js:"
This should produce no output. If it does, move or unexport the offending constants.
Master Refactor Plan (Issue #227)
Four phases. Each phase keeps tests passing before moving to the next.
Design principle
Cyclic imports between JS modules are fine — ESM resolves function bindings lazily at call time, so gameplay modules can freely import from each other. The only constraint: no cycles in init-time constant computation. This is solved by the leaf header architecture (Phase 1): all exported capitalized constants live in leaf files that never import gameplay modules.
Phase 0 — Preflight and baseline (complete)
Inventory and baseline captured before code edits began.
docs/port-status/ISSUE_227_PHASE0_INVENTORY_2026-03-05.mddocs/port-status/ISSUE_227_PHASE0_BASELINE_2026-03-05.md
Phase 1 — Infrastructure Laydown + Constant Consolidation (complete)
Established leaf header architecture. All exported capitalized constants now
live in leaf files only. config.js and objclass.js deleted.
symbols.js is intentionally reintroduced as a leaf owner for display.h
glyph/symbol constants.
Constant export rule enforced via audit command.
Phase 2 — C Field Name Normalization (complete)
All struct field names normalized to C-canonical: attack fields (aatyp,
adtyp, damn, damd), permonst fields (mflags1/2/3, mresists,
mconveys, msound, cwt, cnutrit, mmove, mattk, mcolor,
maligntyp, mname), objclass fields (oc_name, oc_descr, oc_material,
etc.). Generators emit canonical names with backward-compat aliases.
Phase 3 — File-per-C-Source Reorganization
Move functions so each JS file aligns to its C source file. Cyclic imports between gameplay files are explicitly allowed — function bindings resolve lazily, so moving functions freely cannot create init-time cycles.
Target: dissolve JS “consolidation” files (combat.js, look.js,
monutil.js, stackobj.js, player.js, discovery.js, options_menu.js)
into their canonical C-source-named counterparts.
Status: COMPLETE (March 2026). All 6 consolidation files dissolved:
combat.js→exper.jsoptions_menu.js→options.jslook.js→pager.jsdiscovery.js→o_init.js,do_name.jsstackobj.js→invent.jsmonutil.js→display.js,mon.js,hack.js,steal.js,mhitm.js,do_name.js,monmove.js,invent.jsplayer.jsroles/races tables →role.js(Player class remains inplayer.js)
Exit gate:
- Each gameplay function is in its corresponding C-source-named file. ✓
- Ownership mapping in
docs/MODULES.mdreflects code reality. ✓ - No parity regression vs baseline. ✓
Phase 4 — Remove set*Context Wiring Hacks (complete)
Created gstate.js — a game state singleton (mirrors C’s global u/level/
flags). allmain.js calls setGame(this) once; modules read
gstate.game.player, gstate.game.map, gstate.game.display, etc.
Completed removals/replacements:
mkobj.js:setObjectMoves,setMklevObjectContext,setLevelDepthmakemon.js:setMakemonInMklevContext,setMakemonRoleContext,setMakemonLevelContext,setMakemonPlayerContextpline.js:setOutputContextdisplay.js: display-context override setter wiring removedtimeout.js: timer/current-turn setter wiring removedsp_lev.js:setLevelContext,setFinalizeContext,setSplevPlayerContext,setSpecialLevelDepthreplaced by scoped wrappers (withLevelContext,withFinalizeContext,withSpecialLevelDepth)getpos.js:set_getpos_contextreplaced by explicitgetpos_async(..., ctx)parameter passing
Remaining active setters pending migration or explicit design retention:
- None.
Exit gate:
- No
set*Context/set*Playerwiring remains (except explicitly accepted bootstrap-only cases). ✓ - No parity regression vs baseline envelope. ✓ (
26/34gameplay,2481unit pass)
Non-Negotiable Autonomy Rules
- No behavior changes mixed into pure-structure commits unless needed to keep tests passing; if needed, isolate the behavior fix in a separate commit.
- Never proceed to the next phase with unresolved regressions.
- Keep
CURRENT_WORK.mdupdated with:- active phase/subphase,
- current blockers,
- next concrete commit target.
- Push validated increments immediately; do not accumulate large local WIP.
C source files already matched 1:1 in JS (no work needed):
apply, artifact, ball, bones, botl, cmd, decl, detect, dig,
display, dog, dogmove, dungeon, engrave, explode, fountain,
hack, insight, invent, lock, mhitm, mhitu, mkobj, mkmaze,
mkroom, mon, mondata, monmove, mthrowu, music, objnam, pager,
pickup, pline, polyself, quest, rect, region, restore, save,
shk, shknam, sit, sounds, sp_lev, spell, steed, teleport,
timeout, topologize, trap, uhitm, vault, vision, weapon, were,
wield, wizard, worn, write, zap
C source files intentionally not ported to JS (blacklist — covered by JS built-ins or irrelevant in the browser):
| C file | Reason not needed in JS |
|---|---|
alloc.c |
malloc/free wrappers — JS has garbage collection |
cfgfiles.c |
nethackrc/config file parsing — JS uses in-game options without file I/O |
coloratt.c |
terminal color attribute tables — JS uses CSS/HTML rendering; const.js covers what’s needed |
date.c |
date/time utilities — JS has built-in Date |
dlb.c |
Data Library Binary file format for bundling game data — JS bundles data as ES modules |
drawing.c |
runtime init of defsyms/def_monsyms arrays — JS pre-declares these as constants in const.js |
files.c |
file I/O (save, bones, config) — JS uses storage.js and browser storage APIs |
mail.c |
Unix mail daemon in-game feature — not applicable in browser |
mdlib.c |
shared math/data utilities for NetHack toolchain — covered by JS built-ins |
nhlobj.c |
Lua object bindings for special levels — JS re-implements sp_lev natively |
nhlsel.c |
Lua selection bindings — same |
nhlua.c |
Lua scripting interface — same |
nhmd4.c |
MD4 hash for save file integrity — JS uses different save format |
report.c |
crash reporting / panic trace — not applicable in browser |
rnd.c |
C PRNG implementation — replaced by rng.js + xoshiro256.js |
selvar.c |
Lua selection variables for map gen — handled in JS sp_lev.js |
sfbase.c |
save file serialization base — JS uses storage.js with JSON |
sfstruct.c |
save file struct layout — same |
strutil.c |
C string buffer utilities — JS has built-in string methods |
sys.c |
system config (SYSCF, debug files) — not applicable in browser |
JS files with no C counterpart (JS infrastructure — keep as-is):
animation, browser_input, chargen, config, delay, headless,
input, keylog, nethack, render, replay_core, replay_compare,
rng, storage, xoshiro256
JS invented consolidation files (dissolved — Phase 3 complete):
— all deleted.
combat, look, monutil, stackobj, discovery, options_menu, mapplayer.js roles/races moved to role.js; Player class remains in player.js.
map.js elimination execution checklist (completed)
map.js previously exported four gameplay-facing symbols:
makeLocation, makeRoom, GameMap, and FILL_*.
Implemented destinations:
makeRoom+FILL_*->mkroom.js(source-of-truth forstruct mkroom).makeLocation-> level-state owner (game.js) with C-shape fields matchingstruct rm.GameMapconstructor/methods -> level-state owner (game.js), with room/door/flags collections asgame.level.*.- Importers (
dungeon.js,mklev.js,sp_lev.js,storage.js,chargen.js,extralev.js) switched to canonical owners. map.jsdeleted without compatibility re-exports.
C Field Name Normalization
Part of the same work: JS code uses non-C field names as aliases on data structs. These must be renamed to match the C source so that the autotranslator can emit correct code and ported functions look like the original.
See also: docs/STRUCTURES.md for the parallel effort on
global variable names (gX. → game.*).
Attack struct (struct attack — permonst.h)
| JS alias | C field | Uses | Files |
|---|---|---|---|
.at |
.aatyp |
1 | attack_fields.js |
.type (on attack obj) |
.aatyp |
6 | attack_fields.js, dogmove.js, mon.js, mondata.js |
.damage |
.adtyp |
2 | mhitu.js, mondata.js |
.ad |
.adtyp |
7 | artifact.js |
.dice |
.damn |
1 | artifact.js |
.sides |
.damd |
2 | artifact.js |
The generated monsters.js already emits canonical names (aatyp, adtyp,
damn, damd). attack_fields.js exists solely to paper over these aliases
at runtime. Once all call sites are normalized, attack_fields.js is deleted.
Monster data struct (struct permonst — permonst.h)
| JS alias | C field | Uses | Notes |
|---|---|---|---|
.speed |
.mmove |
11 | On monster instances; mmove is the permonst field |
.difficulty |
.mlevel |
~10 | Used interchangeably with .mlevel in some files |
.mlevel is already the C name; .difficulty is a JS invention used in some
files. Normalize to .mlevel everywhere.
Object struct (struct obj / struct objclass)
| JS alias | C field | Uses | Notes |
|---|---|---|---|
.name (on item) |
.oname |
~11 | User-assigned name; C uses oname char* |
.oc_name (the object class name) is already the correct C name where used.