# 04 — Software Architecture *Rust workspace, crates, sim design, data flow. The engineering build-to-print.* Target: Rust 1.75+ (stable), single static binary `hexwork`, no GPU, no async runtime in sim, deterministic fixed-tick simulation, 60fps CPU ASCII rendering in any modern terminal. --- ## 4.1 Workspace layout ``` hexwork/ Cargo.toml # workspace + release profile (lto, panic=abort? no — unwind for crash reports) docs/ # this bible (00–06) crates/ hexwork-core/ # ECS + sim tick + rules. NO I/O, NO terminal. 100% deterministic. hexwork-worldgen/ # floor/town generation (seeded RNG, anchor rooms + systemic fill) hexwork-content/ # data-file loading + validation (TOML/JSON monsters, items, hymns, contracts) hexwork-render/ # CPU raycaster → cell buffer → ANSI diff → terminal hexwork-term/ # terminal abstraction: raw mode, input, resize, truecolor detect, screenreader hook hexwork-audio/ # tiny beep/bell synth via terminal bell + optional rodio (feature-gated) hexwork-save/ # saves, lineage vault, .hexdemo replays hexwork-ui/ # screens, HUD, log, menus, look-mode, chargen (depends on core+content, not render internals) hexwork-cli/ # main.rs: arg parsing, game loop, panic handler, crash-report writer assets/ data/ # canonical content: monsters.toml, items.toml, hymns.toml, gods.toml, contracts/, floors/, towns/ palettes/ # colorblind/high-contrast/no-flicker themes tests/ # cross-crate integration: rule-chain tests, determinism replays, content validation ``` **Dependency rule (enforced by CI `cargo deny` + import lints):** `core ← worldgen, content ← save ← ui ← cli`; `render` and `term` are leaves consumed by `cli` only. `core` must build with `--no-default-features` and zero terminal/system deps (so we can fuzz + WASM-test the sim). Crates and single responsibilities: | Crate | Owns | Must never own | |-------|------|----------------| | core | entities, components, tick(), fluids/gas/fire/sound/light propagation, combat, hymn resolution, god ledgers | RNG seeding policy, file I/O, ANSI codes | | worldgen | seeded layouts, anchor-room placement, vertical connectors, faction seeding | balance numbers (those live in data files) | | content | schema structs, validation errors with file:line, hot-reload in debug | gameplay logic | | render | camera, raycast, glyph ramp, lighting, diff buffer | sim rules | | term | crossterm backend, input map, resize events, bracketed paste off | game state | | save | bincode + zstd snapshots, lineage vault (sled or JSON), demo event log | rule decisions | | ui | screens/state machine, HUD layout, keybinds, tutorial hints | pixels | | cli | loop, tick scheduling, panic → epitaph writer | — | ## 4.2 The sim — deterministic fixed-tick ECS **ECS choice:** `hecs` (small, no systems-scheduler magic) + handwritten tick order. Explicit tick phases beat parallel-scheduler nondeterminism for a NetHack-descendant: determinism > core-count bragging. **Tick:** 10 Hz fixed (`DT = 100ms`). Render interpolates actor positions between ticks (no sim effect). Holding `Shift` = advance exactly 1 tick (roguelike step mode). All randomness from a single `Xoshiro256**` stream seeded per-run; sub-streams per subsystem via `seed ^ domain_tag` (fluids, combat, worldgen, gossip) so adding a particle never reschedules combat. **Determinism contract:** same seed + same input-tick log = same state hash. `core::hash_state()` (SipHash over canonical entity dump) runs every 600 ticks in debug + on save; desync = crash with report. This powers `.hexdemo` replays (input log + seed) and Ironcartographer dailies. **Phases per tick (fixed order, each fully documented in code):** 1. `intent` (player + AI intents queued; AI is utility-scored, no allocations in hot path) 2. `hymns & bargains` (spell resolution, god ledger writes) 3. `move & collide` (continuous positions → voxel occupancy; shoves, slips, falls, ropes) 4. `fluids` (water/oil/blood downhill + fill-up; capped iterations/tick for perf) 5. `gas & scent` (rise/sink + draft advection) 6. `fire & heat` (ignite, spread, smoke spawn, melt) 7. `signal & electricity` (wire/plumbing graph BFS, capped depth) 8. `light` (recompute dirty cells only) 9. `sound` (emit → propagate → AI stimulus) 10. `structure` (load checks on marked-dirty supports only) 11. `rot & schedules` (corpses, spoilage, NPC clocks, faction clocks — cheap, staggered) 12. `death & epitaph` (rule-chain recorder writes cause links) **Performance budgets (slice gate):** tick ≤ 8ms p99 on M1-airs and 5-yr-old ThinkPads for a 96³ active volume; fluids/gas capped at 4k cell-updates/tick with spillover queue; sound/light use dirty-rect propagation. Bench harness `cargo bench -p hexwork-core` with criterion; CI fails on >10% tick regression. ## 4.3 World representation - **Voxels:** `ChunkedGrid<48³>` chunks, `Voxel { material: u8, occ: u8, fluid: FluidCell, gas: GasCell, light: u8, scent: u8, flags }` — 16 bytes/voxel. Active set = loaded 3×3×3 chunks around player + lit/settled far-field abstract sim for towns (schedules only). - **Actors:** continuous `Vec3` + radius + stance (stand/crouch/climb/swim/prone/rope). Voxel occupancy derived, never authoritative for actors (so falling/leaning/riding work). - **Vertical connectors as first-class entities:** Stairwell, Shaft, Well, Vent, ElevatorCar, Counterweight, RopeAnchor — each with load/length/anchor-quality; cutting/flooding/jamming handled by generic rules, not scripts. - **Rooms breathe:** each enclosed volume gets a pressure node; doors/vents/grates are edges. Draft field derived per tick for smoke/scent/gas advection. Secret vents are just edges flagged hidden until flour/smoke reveals them. ## 4.4 Content as data (designers edit TOML, not Rust) Every monster/item/hymn/god/contract is a validated data file. Example (shape is contract, exact fields in `assets/data/*.schema.md` to be written in slice): ```toml [monster.cable_rat] hp = 6; habits = ["gnaw_wire", "nest_in_walls"]; tell = "ozone smell + skritch above" uses = ["blood_pits_iron", "pelt_insulates", "wire_in_belly"] conducts = true; blood = "weak_acid"; fear_of = ["cats", "bells"] ``` Validation: `hexwork-content` build-time test loads ALL data, checks cross-refs (every `blood=` names a fluid, every contract names a reachable floor), and rejects single-use items (the 2-uses law is a TEST, not a guideline). Balance numbers carry `// intent:` comments. ## 4.5 Rendering pipeline (detail in doc 05) `sim snapshot (read-only) → camera → raycast/DDA → glyph+color cell buffer → ANSI diff → term`. Render thread reads via triple-buffered snapshot (copy of visible chunks + actor list, ~200KB); sim never blocks on render. Target: 80×24 minimum, 120×36 recommended, up to 240×70; degrades by render-scale, not sim fidelity. ## 4.6 Input, loop, and screens `term` normalizes kitty/wezterm/xterm/gnome-terminal + SSH: keys, mouse-drag orbit (optional), resize, focus. `ui` is a screen stack: Title → Chargen → Play (HUD+log+hint ribbon) → Look/Inventory/Contracts/Map/Chapel → Death/Epitaph/Replay. Full keyboard play mandatory; every mouse action has a key twin. Keymap in one TOML file, remappable in-game. Main loop pseudo-shape: `poll input → map to intents → tick accumulator (10Hz) → snapshot → render → log flush → autosave every 600 ticks`. Panic handler writes `crash-epitaph.md` (seed, tick, state hash, last 50 log lines) — a crash must still tell a story. ## 4.7 Saves, lineage, replays - **Save:** bincode+zstd snapshot + seed + input log tail; versioned (`save_version: u32`, migrations in `hexwork-save`, old saves load or explain why not). - **Lineage vault:** per-lineage JSON: mapped regions, recipes, translations, ghosts, auctioned gear. Never stores live run state (permadeath survives `rm` of the save, not of the vault — document this). - **`.hexdemo`:** seed + input-tick log + content pack hash; replay verifies hash then steps sim headless; `hexwork --replay file.hexdemo --export-ansi` dumps a shareable ANSI cast. Death replays auto-record rule-chain subtitles from the cause-link recorder. ## 4.8 Testing strategy (NetHack-descendant needs Net principal: trust no interaction) - **Rule-chain tests:** every §3.3 system has a test that asserts the *causal story*, e.g. `oil_under_door_grate_chokes_mimic`: pour → ignite → smoke advects through grate → mimic coughs → lootable. 100+ such tests at 1.0; slice ships 20. - **TDTTOE property tests:** proptest over material×hazard matrix (acid×{iron grate, iron helm, ...}) asserts consistent outcomes; deducibility lint: every hazard lists its rule refs. - **Determinism tests:** fixed seed + scripted inputs → state hash equality across 10k ticks, run in CI on 2 platforms. - **Content tests:** schema validation + 2-uses law + cross-ref integrity + epitaph count. - **Soak:** headless bot delver (random+greedy policy) runs 50k ticks nightly; asserts tick budget + no panics + SPS instrumentation hooks. - **Accessibility tests:** palette contrast ratios computed; screenreader event stream snapshot-tested. ## 4.9 Build, release, telemetry (opt-in only) Single `cargo build --release` binary; `rust-toolchain.toml` pins stable; `cargo dist` for GH releases (linux/mac/win); terminal feature matrix in README. No network calls in-game. Opt-in anonymous stats (SPS survey prompt after death — "was this death worth retelling? [y/n]") stored locally;毫不 exfiltrated. ## 4.10 The hard rules (PRs get rejected for these) 1. No gameplay logic outside `core` + data files. 2. No nondeterminism in `core` (no `Instant`, no threads, no HashMap iteration affecting outcomes — use `BTreeMap`/indexed pools). 3. No render-blocking-sim. 4. No single-use items. 5. No silent rules — every hazard emits a log line with its law the first time it matters ("The smoke is SUCKED through the grate — rooms breathe.").