mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-19 15:18:33 +02:00
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.
This commit is contained in:
@@ -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 |
|
||||
|
||||
+17
-3
@@ -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)
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -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 (`<seed>:L<depth>`). 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=<n>`;
|
||||
`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.
|
||||
@@ -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: '<kind>'` 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`.
|
||||
@@ -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 ✅
|
||||
|
||||
Reference in New Issue
Block a user