HEXWORK — A Lantern in the Deep #
A 3D first-person NetHack-descendant in the terminal. Witcher-style low fantasy on the surface, retro-futurist machine-ruin underneath, rendered as truecolor 3D ASCII at 60fps on the CPU. No GPU required. Runs over SSH.
Full design bible: docs/ (vision, player experience,
lore, systems, architecture, renderer spec, implementation checklist).
Build #
cargo build --locked --release # binary: target/release/hexwork
./target/release/hexwork --help
./target/release/hexwork --seed 140
Requires Rust 1.75.0 or newer. The normal developer toolchain tracks stable;
select the minimum explicitly with cargo +1.75.0 .... Use the committed
lockfile (--locked) and build from the complete checkout, including vendor/.
HEXWORK maintains a metadata-only Rust 1.75 backport of the exact bincode 2.0.1
sources; upstream declares a newer minimum. See vendor/README.md
for source/license verification, maintenance obligations and feature/platform
limits. Do not bypass Cargo's Rust-version checks.
The verification scripts require Python 3.11+; the CI-workflow verifier also requires PyYAML 6.0.2. These are development checks, not game-runtime dependencies. Configured CI checks stable and 1.75 separately; configured platforms are not a claim of an unexecuted platform certification. No network calls at runtime, ever.
Fluid player correction status #
The approved Fluid HEXWORK correction contract is documented in
docs/fluid-player-design.md. It freezes one
signed-i64 millimetre actor authority, checked i128 projection into a
bounded i32 voxel margin, the 10 Hz/three-substep simulation, player
geometry and integer heading, action durations and pacing/death separation,
pending-effect ordering, resumable Inn work, and bounded lit observations.
This is an approved target, not implemented behavior. The current
executable remains the legacy grid executable until implementation advances;
cargo build --locked --release does not deliver Fluid. The future v5 schema
break is not emitted and no migration is authorized. The optional local SDL2
0.38/fontdue 0.9.3 path is intended but not delivered; ordinary terminal
SSH remains the intended client route.
For a safe baseline build, use a disposable detached worktree for the
annotated local legacy-grid-final commit. This does not modify the current
checkout, push anything, or migrate saves:
git worktree add --detach ../hexwork-fluid-player-legacy-grid-final \\
3f81e656bead175606b31a87cb15c9068f0d1c2b
cd ../hexwork-fluid-player-legacy-grid-final
git rev-parse HEAD # must print the exact commit above
cargo build --locked --release
./target/release/hexwork --help
Return to the original checkout and remove only that disposable worktree when
finished (cd - then git worktree remove ../hexwork-fluid-player-legacy-grid-final); do not run git push, merge,
rebase, or a save/schema migration as part of this check.
The previous first-person acceptance failure remains open: the first-session observation reported that the advertised Contracts key terminated play. The normal-brightness first-person acceptance route below is still pending human execution; automated tests and SVG/terminal artifacts do not certify font or monitor readability or full HEXWORK completion.
Terminal support matrix #
| Capability | Full | Degraded | Floor |
|---|---|---|---|
| Color | 24-bit truecolor | 256-color LUT | no-color density + bold (fully playable) |
| Unicode | box-drawing + half-blocks | ASCII-only (#|/-+) |
same, asserted playable |
| Size | 120×36+ recommended | 80×24 minimum | below 80×24: kind refusal + resize hint |
| Input | keys + mouse-drag orbit | keys only | keys only, full game completable |
| Latency | local 60fps | SSH adaptive (30fps floor) | turn-step mode (Shift advances one tick) |
HEXWORK_* environment overrides #
| Variable | Effect |
|---|---|
HEXWORK_FORCE_TC=1 |
Force truecolor path even if the probe disagrees |
HEXWORK_FORCE_256=1 |
Force 256-color LUT path |
HEXWORK_NO_COLOR=1 |
Force no-color density + bold path |
HEXWORK_FORCE_ASCII=1 |
Force ASCII-only glyphs (no box-drawing / half-blocks) |
HEXWORK_SCALE=0.5 |
Fixed render scale (disables adaptive scaling) |
Every automatic degradation is logged once to the message log. Set an override to silence the corresponding notice.
Controls #
The current slice's approved recovery controls (validation tracked in the interface audit):
| Context | Default keys | Effect |
|---|---|---|
| Perspective | Arrows / WASD / hjkl | Forward/back and strafe relative to horizontal camera heading |
| Perspective | Tab |
Toggle first-person/chase; each preserves its own angles |
| Play | x |
Spatial look; movement keys rotate 15° or tilt 10°, without a turn |
| Spatial look | 0; x / Esc |
Reset tilt and snap heading to nearest octant; leave look |
| Perspective | < / > |
Tilt down/up; chase changes orbit elevation |
| Play/map | M |
Toggle north-up tactical and restore saved perspective |
| Tactical | PageUp / PageDown; Home | Inspect Z slice; return to player floor |
| Tactical on player floor | Movement; Space / . |
World-direction movement; wait through ordinary action semantics |
| Inventory | i; Enter / x |
Open/close carried inventory; retain item detail/Wield/Tinker workflow |
| Play | g; Enter |
Pick up own-cell loot; contextual interaction |
| Normal play | Esc / q |
Save and quit; look/modal/map is dismissed first |
Story tactical pauses between accepted actions; Classic remains live. Movement on an inspected nonplayer floor is refused without a turn. New/load presentation faces down Milthollow's road (world −Y); old headless/replay direction commands remain world-relative. Six new camera actions are optional in legacy keymaps: missing actions stay unbound, and loaded bindings drive in-game hints. With no Home binding, close/reopen M to recenter; look-mode movement provides tilt.
Keyboard is sufficient. HEXWORK_MOUSE=1 opts into terminal mouse capture:
left-drag rotates/tilts perspective by 2° per column/row; capture is disabled on
exit. Capture command success is not terminal capability acknowledgement.
Live resize suspends gameplay below 80×24 and restores the same view on recovery.
Normal-brightness acceptance route (human result pending) #
Use the newly rebuilt, identified release binary—not an older installed copy. At normal terminal brightness, face the road, look left/right/up, exit look, identify an available service/exit/item by glyph, approach it, toggle chase and back, enter tactical, move on the player floor, inspect another floor and Home back, then return to perspective. Record binary identity, terminal dimensions, color mode, screenshots and explicit pass/fail. This is AC10; automated tests or SVG exports do not certify font/monitor readability or the full HEXWORK game.
Opt-in HEXWORK_INTERFACE_METADATA=/new/path.jsonl writes presentation-only
frame diagnostics outside save state (the path must not exist), including run,
seed, executable FNV identity, effective preferences, camera, player and canonical
hash/history counters. Keep this private unless intentionally sharing it.
HEXWORK_PTY_ARTIFACT_DIR enables sanitized test JSON/SVG exports; SVG uses a
recorded monospace cell aspect and is only a diagnostic aid.
Paying the Tinker for identification #
At Milthollow's Tinker interaction point, press the displayed Confirm key
(default Enter) to browse carried items. Select an unidentified item, inspect
it, then press Confirm to open a separate 15-copper confirmation. A fresh
Confirm pays; Esc cancels without charging. The browser shows your wallet,
exact item ID and Known/Unidentified status, using your loaded key bindings.
Full payment identifies that item template for the current run without advancing a simulation tick. Knowledge and payment survive save/load/replay; Story death still restores the existing checkpoint, so a later purchase may be rolled back. Underpayment, trading, mending and generic item-use effects are not implemented. Existing v4 saves remain readable; older binaries reject the new identification action tag rather than silently ignoring it.
Wielding a weapon #
Open Inventory with i, select a carried weapon, and press Enter to inspect it.
In detail, Enter opens a separate Wield confirmation; a fresh Enter confirms,
and Esc cancels. The UI uses your loaded bindings when remapped and shows the
current [wielded] item. Wielding records one zero-tick action without consuming
or moving the item.
The starter Ratcatcher pike now reaches two cells total in a clear cardinal lane on the same floor. Walls, blocked headroom and intervening living actors block that extended strike. Ordinary adjacent attacks keep their existing rules; a surviving enemy only returns a strike within its own one-cell reach. Damage remains 3. This does not yet implement shaft/grate/vertical reach, armor effects, consumables or the complete combat system.
New readers retain existing v4 saves/replays. Older binaries reject v4 records containing the new Wield action tag; forward-reader compatibility is not promised.