Files
wownerogue/docs/BALANCE_SIM.md
T
jwinterm 643bec4721 docs: pass for the 2026-07 build — balance sim, multi-level, render packs
- README: rewrite Difficulty section (per-network LEVELS ∝ block time, WOW=5min,
  multi-level descent, corrected casino size); document game_event + depth in the
  Socket.IO table.
- NEW docs/BALANCE_SIM.md: the block-timed edge math (exponential deadline), the
  headless sim over the real engine, what it proved (size self-cancels, fast monster
  fails on slow chains), and the per-network NETWORK_TUNING model.
- NEW docs/MULTI_LEVEL.md: descent mechanic, vault-only treasure, per-level
  provably-fair seeding, config, file map, open polish.
- NEW docs/RENDER_PACKS.md: modes-vs-packs, the RK.fx layer, iso/tiled/zoom/cat.
- docs/ARCHITECTURE.md: mark the block-time-difficulty gotcha RESOLVED; add render
  kit + balance/pacing sections and cross-links.
- docs/TODO.md: 2026-07 done/pending section.
2026-07-11 22:29:57 -04:00

83 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Balance simulation & per-network calibration
Wownerogue is a **block-timed** roguelike: a run is bounded by the blockchain, and the house edge
comes from whether you can clear the dungeon before the block lands. That makes the balance
un-eyeball-able — the deadline is a *random* variable and every chain has a different block time —
so difficulty is **measured**, not guessed. This doc covers the simulator that measures it and the
per-network tuning it produces.
## The timing model (why this is subtle)
- **Single-player runs are bounded by one block.** You enter on block *N* and die if you haven't
escaped before *N+1* (`queueManager.js`, `socketHandlers.js checkGamesTimeout`). Movement is
real-time within that window (~100 ms/move floor).
- **Block arrival is a Poisson process**, so the run window is **exponentially distributed** with
mean = the network's block time. Enter just after a block → a near-full interval; just before →
seconds. High variance is the core mechanic, intended.
- Therefore, for a run that escapes at wall-clock time `T`:
```
P(survive) = P(block lands after T) = exp(-T / meanBlockTime)
house_win(run) = caught|stuck ? 1 : (1 - exp(-T / meanBlockTime))
```
The random deadline is integrated out analytically — no Monte-Carlo over deadlines needed.
Block times (`src/chain/chainProfile.js`, `meanBlockTimeMs`): GRIN ~1 min · XMR ~2 · LTC ~2.5 ·
**WOW ~5 (measured on the live daemon)** · BTC ~10.
## The simulator (`src/sim/`)
Drives the **real engine** (`src/game/game.js` — same dungeon generator, monster AI, movement as
production) with headless bots to terminal outcomes, then reports the measured distribution. It
never reimplements game rules — it's a thin layer over `Game`.
| File | Role |
|------|------|
| `simulate.js` | Harness + CLI. Per preset: escape/treasure/caught/stuck rates, completion-move/-time percentiles, and **measured house-win per network** via the identity above. |
| `simBots.js` | Bot policies. `omniscient-*` (knows the full map, beelines) and `explorer-*` (fog-of-war, realistic). Neither *evades* the monster, so both **over-count catches** → true house-win is likely below the reported band. Read them as an upper band, not a point estimate. |
| `pathfind.js` | Dependency-free BFS (keeps the sim auditable). |
| `calibrate.js`| Solves tuning per network to a target house-win. |
Run:
```bash
node src/sim/simulate.js --runs 200 --bot explorer-greedy --presets normal,casino
node src/sim/calibrate.js --target 0.70 --preset casino
```
`--cadence` (ms/move, default 320) is the moves→seconds assumption the block-time math hinges on —
sweep it when calibrating for real players. `test/sim_ultra.test.js` guards the harness structurally
(it is **not** a balance assertion — those numbers are meant to move).
## What the sim proved (and the design it forced)
1. **Presets undershot their declared targets**, and the edge swung ~15 pts across chains from
*identical* dungeons — because `cryptoType` never sized anything.
2. **Dungeon size is not a clean edge lever.** A bigger map lengthens the run (timer edge ↑) but
gives the player more room to dodge (monster edge ↓); house-win self-cancels around ~60% until
the map becomes an unplayable slog.
3. **A fast monster is both distasteful and ineffective on slow chains** — WOW (5-min blocks) caps
~64% house-win even at a 2.2× monster, because a single dungeon clears in ~1 min ≪ the block, so
the timer barely bites.
**Conclusion → multi-level depth.** The fair, on-theme lever is *run length*: descend N levels
(each preset-sized, fair monster) so the cumulative timer provides the edge. Levels ∝ block time.
See `docs/MULTI_LEVEL.md` (mechanic) and `NETWORK_TUNING` in `src/game/difficultyConfig.js`.
## Per-network tuning (`NETWORK_TUNING`)
`difficultyConfig.js` folds the tuning onto the resolved preset. Precedence:
**preset → network tuning → env → explicit overrides** (so operators keep `DUNGEON_*`/`MONSTER_*`;
`NETWORK_TUNING_DISABLED=true` is the kill switch). Currently it sets **`levels`** per network
(the pacing lever); size and monster stay at the fair preset.
```
GRIN 1 · XMR 2 · LTC 2 · WOW 4 · BTC 8
```
**Pending:** the level counts are sim starting points. The simulator's multi-level support (bots
reset fog-of-war on descent) is in; a full re-calibration run to lock the per-network level counts
against a target house-win is the open follow-up. The `monsterSpeed` lever was intentionally
**dropped** (see finding 3).