From 643bec4721b84b9b535818ed4aefa0d5b531351f Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 11 Jul 2026 22:29:57 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20pass=20for=20the=202026-07=20build=20?= =?UTF-8?q?=E2=80=94=20balance=20sim,=20multi-level,=20render=20packs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- README.md | 46 ++++++++++++++++++++----- docs/ARCHITECTURE.md | 20 +++++++++-- docs/BALANCE_SIM.md | 82 ++++++++++++++++++++++++++++++++++++++++++++ docs/MULTI_LEVEL.md | 48 ++++++++++++++++++++++++++ docs/RENDER_PACKS.md | 48 ++++++++++++++++++++++++++ docs/TODO.md | 17 +++++++++ 6 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 docs/BALANCE_SIM.md create mode 100644 docs/MULTI_LEVEL.md create mode 100644 docs/RENDER_PACKS.md diff --git a/README.md b/README.md index 59cc9dc..c4df7d3 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,43 @@ crypto block as long as at least two players are queued. Supported economies: See `docs/MATCH_MODE.md` for full architecture, configuration, and API details. -## Difficulty Presets +## Difficulty & Pacing -| Preset | Dungeon Size | Monster Aggression | House Win Rate | -|--------|--------------|--------------------|----| -| `easy` | 30×15 | Low | ~30% | -| `normal` | 45×22 | Medium | ~55% | -| `hard` | 55×28 | High | ~65% | -| `casino` | 60×30 | Very High | ~70% | +Difficulty has two levers, both keyed to the crypto network so `cryptoType` actually shapes the +game (it used to be a dead parameter). -Set via `DIFFICULTY_PRESET` in `.env`. Defaults to `casino` for paid modes. +**Presets** (`DIFFICULTY_PRESET`) — the per-level shape: + +| Preset | Dungeon Size | Monster | Target House Win | +|--------|--------------|---------|------------------| +| `easy` | 30×15 | low | ~30% | +| `normal` | 45×22 | medium | ~55% | +| `hard` | 55×28 | high | ~65% | +| `casino` | 70×35 | very high | ~70% | + +Defaults to `normal` for free play, `casino` for paid. Every knob is `env`-overridable +(`DUNGEON_WIDTH`, `MONSTER_SPEED`, …). + +**Multi-level depth (`levels`) — the pacing lever, ∝ block time.** A run *descends* N levels (each a +preset-sized dungeon with a fair monster). Reaching a non-final exit takes the stairs down; only the +final exit escapes; the treasure sits in the vault (final level). Because total run length scales +with the chain's block time, the "race the block" **timer** provides the house edge — no giant map, +no cheating-fast monster. + +| Chain | Block time | Levels | +|-------|-----------|--------| +| GRIN | ~1 min | 1 | +| XMR | ~2 min | 2 | +| LTC | ~2.5 min | 2 | +| WOW | ~5 min (measured) | 4 | +| BTC | ~10 min | 8 | + +Levels live in `NETWORK_TUNING` (`src/game/difficultyConfig.js`); override with `DUNGEON_LEVELS`, +disable with `NETWORK_TUNING_DISABLED=true`. The counts are sim-derived starting points — see +[`docs/BALANCE_SIM.md`](docs/BALANCE_SIM.md) for how the balance is measured and calibrated. + +> **Note:** Wownero's block time is **~5 min** (measured on the live daemon), not 2 min as older +> docs/comments claimed. --- @@ -149,7 +176,8 @@ curl -X POST http://localhost:3000/api/admin/credits/adjust \ | Event | Description | |-------|-------------| | `game_start` | Game initialized with dungeon state | -| `game_update` | State after movement | +| `game_update` | State after movement (incl. `depth`/`maxDepth` for multi-level descent) | +| `game_event` | Discrete event: `treasure_found`, `descend` (took the stairs down), `escaped` | | `game_over` | Game ended | | `payment_created` | Payment address ready | | `credits_update` | Credit balance changed | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 85ab067..d3cdb57 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -44,9 +44,23 @@ XMR, WOW, GRIN: `decimalsFor`, `meanBlockTimeMsFor`, `familyFor` (monero / utxo `uriSchemeFor`, `atomicDivisor` (BigInt). Unknown chains fall back to a WOW-shaped default. Money math stays exact via `src/money/atomic.js` (BigInt, decimals-parameterized) — never floats. -> **Gotcha (open):** `difficultyConfig` threads `cryptoType` everywhere but doesn't yet key difficulty -> off mean block time — it keys off payment mode. Wiring block-time → dungeon size is a pending -> game-balance change (see RULESETS.md), and it bites when Grin (~1 min) / BTC (~10 min) ship. +> **Resolved (2026-07):** `cryptoType` now keys difficulty off block time via `NETWORK_TUNING` in +> `difficultyConfig.js` — the pacing lever is **multi-level depth** (levels ∝ block time), measured +> and calibrated with a headless balance simulator. Also corrected: **WOW is ~5 min/block**, not 2 +> (measured on the live daemon). See [BALANCE_SIM.md](BALANCE_SIM.md) and [MULTI_LEVEL.md](MULTI_LEVEL.md). + +## Client render kit + +`html/js/render/` (`RK`) draws the shared `Scene` in multiple techniques (Tiled / ASCII / Iso / 3D) +and interchangeable art **packs**, with a shared pure-canvas FX layer (torch/hearth fire, hazard +tiles), zoom, and an operator-gated cosmetic catalog. See [RENDER_PACKS.md](RENDER_PACKS.md). + +## Game balance & pacing + +Difficulty is **measured, not guessed**: single-player runs are bounded by a random (exponential) +block deadline, so the house edge is a simulation problem. `src/sim/` drives the real engine with +headless bots; `NETWORK_TUNING` + multi-level depth are the calibrated levers. See +[BALANCE_SIM.md](BALANCE_SIM.md) and [MULTI_LEVEL.md](MULTI_LEVEL.md). ## Regenerating the API reference (autodocs) diff --git a/docs/BALANCE_SIM.md b/docs/BALANCE_SIM.md new file mode 100644 index 0000000..a7febed --- /dev/null +++ b/docs/BALANCE_SIM.md @@ -0,0 +1,82 @@ +# 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). diff --git a/docs/MULTI_LEVEL.md b/docs/MULTI_LEVEL.md new file mode 100644 index 0000000..0808211 --- /dev/null +++ b/docs/MULTI_LEVEL.md @@ -0,0 +1,48 @@ +# Multi-level dungeons + +A run descends through `maxDepth` levels instead of a single map. This is the pacing lever that +makes the block-timer provide the house edge on slow chains without a giant dungeon or a cheating +monster (see [`BALANCE_SIM.md`](BALANCE_SIM.md) for why). + +## Mechanic + +- Each level is a normal preset-sized dungeon with its own entrance, exit, and a fair monster. +- Reaching a **non-final** exit takes the **stairs down** → a fresh level is generated, the player + is placed at its entrance, a new monster spawns, and fog-of-war resets. This is **not** a win. +- Reaching the **final** level's exit **escapes** (win). +- **Treasure lives only in the vault** (the final level). Intermediate levels are a race to the + stairs; the 2×/3× treasure payout requires descending all the way and getting back out. +- `maxDepth = 1` (single level) is byte-identical to the pre-multi-level game. + +## Provably fair across the descent + +The whole run regenerates from the one committed seed. Per-level seed +(`levelSeed(masterSeed, depth)` in `provablyFair.js`): **level 1 = the master seed verbatim** +(so single-level games are unchanged), deeper levels salt it (`:L`). Verify any level +with `DungeonGenerator.regenerateFromSeed(levelSeed(seed, depth), cryptoType)`. + +## Configuration + +`maxDepth` comes from `difficultyConfig.levels`, set by `NETWORK_TUNING` per chain +(GRIN 1 · XMR 2 · LTC 2 · WOW 4 · BTC 8, ∝ block time). Operator overrides: `DUNGEON_LEVELS=`; +`NETWORK_TUNING_DISABLED=true` forces a single level. + +## Where it lives + +| Concern | Location | +|---|---| +| Level generation + descent | `src/game/game.js` — `_generateLevel(depth)`, `_descend()`, the exit branch in `movePlayer` (returns `event: 'descend'`), `depth`/`maxDepth` in `getState` | +| Per-level seed | `src/game/provablyFair.js` — `levelSeed()` | +| Level count (pacing) | `src/game/difficultyConfig.js` — `NETWORK_TUNING`, `DUNGEON_LEVELS` | +| Server wiring | `src/network/socketHandlers.js` — `afterPlayerMove` `'descend'` branch (no game-over, no monster move on arrival) | +| Client | `html/js/core/gameState.js` — clears accumulated map/explored/visible tiles on a `depth` change so the old level doesn't ghost through the new one | +| Sim | `src/sim/simBots.js` — explorer bot resets fog-of-war on descent | +| Tests | `test/multiLevel_ultra.test.js` | + +## Open polish + +- Client **"Level N / M" indicator** + a "descended" toast (the server sends `depth`/`maxDepth` and + a `game_event: descend`; there is no client `game_event` handler yet). +- Per-network **level counts are sim starting points** — pending a full multi-level calibration run. +- **Escalating hazards per level** (level 1 tame → deep levels lava/poison) — the hazard-tile FX is + already wired (`RENDER_PACKS.md`); it needs the generator to place hazards + gameplay stakes. diff --git a/docs/RENDER_PACKS.md b/docs/RENDER_PACKS.md new file mode 100644 index 0000000..e6c4954 --- /dev/null +++ b/docs/RENDER_PACKS.md @@ -0,0 +1,48 @@ +# Render kit: modes, packs & FX + +The client render kit (`html/js/render/`, namespace `RK`) draws the **same `Scene`** in multiple +techniques and art styles, so the tavern and dungeon are rich, interchangeable environments and new +art can be added as products. + +## Modes vs. packs + +- **Render modes = techniques** (how a scene is drawn): `Tiled`, `ASCII`, `Iso`, `3D` + (`renderModes.js`). Each declares a `projection` (`topdown` / `iso` / `3d`). +- **Packs = sets/styles within a projection** (chosen via the pack picker): e.g. topdown *Roguelike + Interior* ↔ *Roguelike Dungeon*, iso *Dungeon* ↔ *Medieval*. Registered in `packRegistry.js` / + `assetPacks.js`. "Fancy" is **not** a mode — it's a pack (tiles + FX). +- A premium mode unlocks once the user has **any** unlocked pack for its projection. +- The scene model is **kind-based** (`floor`/`wall`/`table`/`torch`/`monster`/…), so one pack renders + both the tavern (`sceneFromTavern`) and the dungeon (`sceneFromGameState`) — that's what makes + packs interchangeable across both. + +Entitlement: packs are gated by the operator-owned cosmetic catalog (`cosmetic_catalog` table, +`src/multiplayer/entitlements.js`) — free / grant / lifetime-spend / subscription-tier. `?unlock=1` +sets a sticky local QA bypass. See `MONETIZATION.md`. + +## The FX layer (`fxLayer.js`, `RK.fx`) + +Pure-canvas animated FX (CSP-safe, no WebGL), shared by the tiled **and** iso renderers so a scene +lights up the same way in either projection: + +- `RK.fx.fire` — dancing two-layer flame + warm glow (torches, hearths). +- `RK.fx.hazard` — pulsing overlay for hazard tiles (`lava` / `poison` / `spikes`) with a + footprint-clip callback so it fits square cells and iso diamonds alike. +- `RK.fx.flicker` / `pulse` / `glow` — the primitives. + +A tile carries `fx: 'fire'` or `hazard: ''` in the scene legend (`sceneModel.js`); the renderer +draws only the tile's floor **base** in the static pass and lets `RK.fx` paint the animated layer in +the live loop. The tavern has wall braziers + a hearth; the dungeon hazard kinds are wired and render +the moment the generator emits their chars (`L`/`P`/`^`) — pending the generator + gameplay stakes. + +## Other pieces + +| File | Role | +|------|------| +| `isoRenderer.js` | Canvas isometric projection. Orientation-aware walls (`_S` for x-runs, `_W` for y-runs, corner tile), floor variety, contact shadows + vignette juice. Kenney iso dungeon set (`assets/kenney/iso-dungeon/`). | +| `tileRenderer.js`| Top-down atlas renderer. `over` compositing (object tiles drawn on a base tile), torch-lit vignette, ember/flicker loop, animated fire/hazard emitters. | +| `zoomControl.js` | `RK.attachZoom(host)` — wheel/pinch zoom (0.4–4.0), dblclick reset, pixelated scaling. | +| `catSprites.js` | Animated tavern cat (Pet Cats Pack idle strips). | +| `packRegistry.js`| Multi-pack-per-projection registry: active-pack selection persisted per projection, entitlement-gated, graceful fallback. Node-testable. | + +Tests: `test/renderPackResolver.test.js`, `test/packRegistry_ultra.test.js`. diff --git a/docs/TODO.md b/docs/TODO.md index 567fb1e..1840d4c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -16,6 +16,23 @@ All milestones implemented. See `docs/MATCH_MODE.md` for architecture and config # Wownerogue Development Todo +## Rendering, Balance & Multi-level (2026-07) + +Done: +- [x] **Render packs**: modes (technique) vs packs (style) split; "Fancy" retired to a topdown pack; multi-pack registry; Kenney iso dungeon set + orientation-aware walls; per-projection pack picker. See `RENDER_PACKS.md`. +- [x] **FX layer (`RK.fx`)**: shared canvas fire/hazard/glow; tavern braziers + hearth; dungeon hazard tiles wired. Zoom (`zoomControl.js`); animated cat. +- [x] **Balance sim** (`src/sim/`): headless harness over the real engine; measured house-win per network. See `BALANCE_SIM.md`. +- [x] **Per-network tuning**: `cryptoType` now shapes difficulty via `NETWORK_TUNING` (was a dead param). +- [x] **WOW block-time fix**: 2 min → **5 min** (measured on the live daemon); `chainProfile.js`. +- [x] **Multi-level dungeons**: descend N levels ∝ block time; provably-fair per level; treasure in the vault. See `MULTI_LEVEL.md`. + +Pending: +- [ ] **Multi-level calibration run**: lock per-network level counts against a target house-win (sim multi-level support is in; counts are currently starting points). +- [ ] **Block-bounded pot race** (flagship): consume `Ruleset.timing.blockDeadline`; add pot+rake economy (house = zero-risk rake-taker). Decisions pending: empty-pot rule, prize split. +- [ ] **PvP wiring**: connect the already-built `last-alive`/`score-attack` rulesets to a real queue. +- [ ] **Hazard gameplay**: generator places lava/poison/spikes (FX already renders them) + damage stakes; escalate per level. +- [ ] **Client polish**: "Level N/M" indicator + "descended" toast (server sends `depth`/`maxDepth` + `game_event: descend`). + ## High Priority - [x] **Verify Server Startup**: Server starts cleanly with `npm run dev` ✅ - [x] **Test Payment Flow**: Fixed critical bug where payment confirmation wasn't updating DB status ✅