mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-11-11 09:43:53 +01:00
Removes operator-private material from the shipped tree, corrects documentation
that had drifted from the code, deletes dead code, and applies a consistent
prose style.
Private material moved out of the public tree (docs/private/, git-ignored):
- the wallet keyfile recovery runbook and its helper scripts, which together
published the mainnet wallet file path, the service account, backup snapshot
filenames, and the wallet RPC listener state. The helper duplicated the same
constants, so relocating the document alone would not have removed them.
- the stagenet parallel-candidate runbook and its fixtures.
- the dated production-state record, the internal remediation plan, and the
TODO list. None are reference documentation for an outside reader.
Redacted or genericised:
- operator home-directory paths in shipped scripts and units. The asset build
script now takes KENNEY_ASSETS_DIR instead of a hard-coded path.
- private LAN addresses in tests, env examples and deploy docs, replaced with
RFC 5737 documentation addresses.
- the default alert sender and the notification hub example.
- the Monero wallet unit now matches the Wownero one: a dedicated service
account and a config file, instead of the operator's login account with the
wallet password in argv where any local user can read it from /proc.
Ignore rules now cover every .env variant rather than the bare name, plus local
agent state. The deployment docs instruct operators to create .env.mainnet and
.env.stagenet, which the exact-name rule would have allowed to be committed.
Documentation corrected against the code: per-chain dungeon level counts, the
premium unlock rule, Tavern chat scope, backup retention, the chain-profile
fallback, the environment template location, and several sections describing
shipped work as pending.
Dead code removed: configPersistence, rpccalls, a duplicate vendored ROT.js
bundle, the unused tile-picker overlay, the unreachable legacy game-size path
and its vestigial parameter, spectator stubs that shadowed the real
SpectatorManager, and commented-out debugging. nip98 now imports the shared
nostr loader rather than duplicating it.
Prose style: em-dashes removed throughout in favour of colons, semicolons and
commas; comments restated as present-tense descriptions of the code rather than
narration of past changes.
127 suites / 1015 tests pass.
89 lines
5.1 KiB
Markdown
89 lines
5.1 KiB
Markdown
# Multi-level dungeons
|
|
|
|
A run descends through `maxDepth` levels instead of playing a single map. Depth is the pacing lever
|
|
that lets the block timer supply the house edge on slow chains without an oversized dungeon or a
|
|
monster that moves faster than the player. See [`BALANCE_SIM.md`](BALANCE_SIM.md) for the balance
|
|
model behind that choice.
|
|
|
|
## Mechanic
|
|
|
|
- Each level is a normal preset-sized dungeon with its own entrance, exit, and a freshly placed
|
|
monster.
|
|
- Reaching a **non-final** exit takes the **stairs down**: a new level is generated, the player is
|
|
placed at its entrance, a new monster spawns, and fog of war resets. Descending is not a win.
|
|
- Reaching the **final** level's exit **escapes** the dungeon and wins the run.
|
|
- **Treasure exists only on the final level.** Intermediate levels are a pure race to the stairs, so
|
|
the treasure payout multiplier requires descending all the way and getting back out.
|
|
- `maxDepth = 1` is the single-level case: `levelSeed` returns the master seed unchanged at depth 1,
|
|
so a one-level run's layout is exactly what the master seed alone produces.
|
|
- The monster does not take its turn on the arrival move of a descent. `afterPlayerMove` returns
|
|
early on a `descend` result, so the player is never caught by a monster they have not yet seen.
|
|
|
|
## Provable fairness across the descent
|
|
|
|
Every level in a run regenerates from the one committed seed. `levelSeed(masterSeed, depth)` in
|
|
`provablyFair.js` returns the master seed verbatim for level 1 and salts it as `<seed>:L<depth>` for
|
|
deeper levels. Any level can be reproduced with:
|
|
|
|
```js
|
|
DungeonGenerator.regenerateFromSeed(levelSeed(seed, depth), cryptoType, generationOptions);
|
|
```
|
|
|
|
The `Game` constructor commits a layout fingerprint for **every** level of the run up front, before
|
|
play, in `gameProof.layoutFingerprints` (one entry per depth, each carrying `depth`,
|
|
`fingerprintVersion`, `generatorVersion`, and the SHA-256 `fingerprint`). `gameProof.layoutFingerprint`
|
|
remains as a level-1 alias for older verification clients. Because the manifest is complete at
|
|
creation time, a run that ends early still exposes a full, immutable record of the advertised
|
|
descent.
|
|
|
|
Each level is re-fingerprinted as it is actually generated during play, and a mismatch against the
|
|
committed manifest throws. Generator drift inside a live process becomes a hard failure rather than
|
|
an unverifiable served layout.
|
|
|
|
`src/game/fairnessVerifier.js` replays the whole manifest: it rejects a manifest whose depths are
|
|
incomplete or out of order, regenerates each level from its per-depth seed, nulls the treasure on
|
|
every non-final level to match play, and reports per-level fingerprint matches alongside entrance,
|
|
exit, treasure, and dimensions.
|
|
|
|
## Configuration
|
|
|
|
`maxDepth` comes from `difficultyConfig.levels`, set per chain by `NETWORK_TUNING` in
|
|
`src/game/difficultyConfig.js`. Depth scales with block time so that a slower chain gives a longer
|
|
run before the block lands:
|
|
|
|
| Chain | Approx. block time | Levels |
|
|
|---|---|---|
|
|
| GRIN | ~1 min | 1 |
|
|
| XMR | ~2 min | 1 |
|
|
| LTC | ~2.5 min | 2 |
|
|
| WOW | ~5 min | 2 |
|
|
| BTC | ~10 min | 3 |
|
|
|
|
Resolution order for the level count, lowest precedence first: preset (no `levels` key, so the
|
|
effective floor is 1) → `NETWORK_TUNING` for the chain → `customOverrides.levels` → the
|
|
`DUNGEON_LEVELS` environment variable, which always wins and is clamped to a minimum of 1.
|
|
|
|
`NETWORK_TUNING_DISABLED=true` skips per-network tuning entirely, which leaves the run at a single
|
|
level unless `DUNGEON_LEVELS` or an explicit override supplies a count. An unknown `cryptoType` is
|
|
also left untuned and therefore single-level.
|
|
|
|
Dungeon size and monster behaviour are not varied by depth: `applyNetworkTuning` folds only the
|
|
level count onto the resolved difficulty preset, and every level uses the same preset dimensions and
|
|
the same fair monster settings.
|
|
|
|
## Where it lives
|
|
|
|
| Concern | Location |
|
|
|---|---|
|
|
| Level generation and descent | `src/game/game.js`: `_generateLevel(depth)`, `_descend()`, the exit branch in `movePlayer` returning `event: 'descend'`, and `depth`/`maxDepth` in `getState` |
|
|
| All-level fingerprint manifest | `src/game/game.js`: `_buildLayoutFingerprintManifest()`, `getProofContext()` |
|
|
| Per-level seed | `src/game/provablyFair.js`: `levelSeed()` |
|
|
| Level count | `src/game/difficultyConfig.js`: `NETWORK_TUNING`, `applyNetworkTuning()`, `DUNGEON_LEVELS` |
|
|
| Verification | `src/game/fairnessVerifier.js` |
|
|
| Server wiring | `src/network/socketHandlers.js`: the `'descend'` branch in `afterPlayerMove` (no game over, no monster move on arrival) |
|
|
| Event emission | `src/game/movementManager.js`: emits `game_event` with `event`, `depth`, `maxDepth` after the `game_update` |
|
|
| Client state | `html/js/core/gameState.js`: clears accumulated map, explored, and visible tiles on a `depth` change so the previous level does not ghost through the new one |
|
|
| Client feedback | `html/js/network/socketHandlers.js`: the `game_event` handler renders the descend message and on-screen banner with `Level N / M` |
|
|
| Sim | `src/sim/simBots.js`: the explorer bot clears its known-tile set and remembered objectives on a depth change |
|
|
| Tests | `test/multiLevel_ultra.test.js`, `test/networkTuning_ultra.test.js` |
|