docs(spec): address critic findings on viz playground spec

- C1.5: derive serde on DungeonConfig + 5 sub-configs (bridge needs it)
- C2: make "shape contains bounds.center()" a hard carver invariant (not a
  size heuristic the room-size sliders could defeat for Plus rooms)
- specify workspace placement (member + default-members excludes wasm from
  host cargo test/clippy)
- determinism crosscheck compares decoded tiles, excludes gen_ms

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Parley Hatch 2026-05-30 23:48:27 -06:00
parent e1c0612efc
commit 9d8d8b28f0

View file

@ -56,7 +56,9 @@ Concretely:
### 3.1 `reikhelm-core` changes (additive + one cleanup) ### 3.1 `reikhelm-core` changes (additive + one cleanup)
- **C1 — drop unused `rand` dep (WASM enabler + cleanup).** `reikhelm-core` lists `rand = "0.10"` but never uses it (the only reference is a doc comment; the RNG uses `rand_chacha::ChaCha8Rng` and reaches `rand_core` traits via `rand_chacha`'s re-export — *verified*). Removing it drops `getrandom` + `libc` from the tree, so the crate compiles to `wasm32-unknown-unknown` with **no getrandom backend gymnastics**. Tests must stay green afterward. - **C1 — drop unused `rand` dep (WASM enabler + cleanup).** `reikhelm-core` lists `rand = "0.10.1"` but never uses it (the only reference is a doc comment; the RNG uses `rand_chacha::ChaCha8Rng` and reaches `rand_core` traits via `rand_chacha`'s re-export — *verified, and the critic empirically compiled a rand-free core to `wasm32-unknown-unknown` with all 115 tests still green*). Removing it drops `getrandom` + `libc` from the tree, so the crate compiles to wasm32 with **no getrandom backend gymnastics**.
- **C1.5 — derive serde on the config types (additive prerequisite for the bridge).** `DungeonConfig` and its five sub-configs (`BspConfig`, `RoomConfig`, `ConnectConfig`, `DoorConfig`, and the new shape-weight config from C2) currently derive only `Clone/Copy/Debug/PartialEq`. The wasm bridge must deserialize a `DungeonConfig` from the page's JSON and serialize the default back out, so each gains `#[derive(Serialize, Deserialize)]` (additive, no behavior change). `serde_json` is presently a *dev*-dependency of core — that's fine; the configs only need the serde *derives* (already a normal core dep), and the bridge crate owns its own `serde_json`.
- **C2 — room shape variety (the headline generator change).** Extend `RoomCarver`/`RoomConfig` so each room, after its rectangle is chosen within the BSP leaf, is carved in one of several **shapes** selected per-room from the pass RNG with configurable weights: - **C2 — room shape variety (the headline generator change).** Extend `RoomCarver`/`RoomConfig` so each room, after its rectangle is chosen within the BSP leaf, is carved in one of several **shapes** selected per-room from the pass RNG with configurable weights:
- `Rect` — current behavior (the rectangle itself). - `Rect` — current behavior (the rectangle itself).
@ -64,7 +66,9 @@ Concretely:
- `Ellipse` — rounded room inscribed in the rectangle. - `Ellipse` — rounded room inscribed in the rectangle.
- `Plus` / cross — central cross with cut corners. - `Plus` / cross — central cross with cut corners.
Each shape is a predicate over the chosen rect (`cell ∈ shape`), so `cells` is populated with exactly the interior floor cells and `bounds` stays the tight AABB. **Contracts preserved:** determinism (all draws from the pass's own `rng`); the `DoorPlacer` pierce rule (`cells` = exact room interior); `MstConnect` uses `bounds.center()`, which for every shape above is an interior floor cell. Small rooms below a threshold fall back to `Rect` so tiny shapes don't degenerate. Each shape is a predicate over the chosen rect (`cell ∈ shape`), so `cells` is populated with exactly the interior floor cells and `bounds` stays the tight AABB. **Contracts preserved:** determinism (all draws from the pass's own `rng`); the `DoorPlacer` pierce rule (`cells` = exact room interior — the critic confirmed the pierce logic survives ellipse/octagon/plus, since a corridor runs center-to-center and a room's carved cells along any row form a contiguous run, so exactly one room→corridor transition is detected).
**`MstConnect`/`CorridorCarver` rely on `bounds.center()` being a carved floor cell. This is a *hard invariant the carver enforces*, not a size heuristic:** for `Octagon`/`Ellipse` the center is always carved, but a `Plus` at small slider-driven sizes can put `bounds.center()` (integer division) in a cut corner. So the carver's rule is: carve the chosen shape **only if its cell set contains `bounds.center()`; otherwise carve `Rect`.** (Equivalently, force the center cell carved.) Since the playground exposes room min/max as live sliders, this guard — not the recipe's `min_size` — is what guarantees every room's center is floor for *any* config.
- **C3 — iteration backlog (additive, no engine change; built in later rounds driven by the playground):** - **C3 — iteration backlog (additive, no engine change; built in later rounds driven by the playground):**
- `CellularAutomata` cave Shaper → organic blobs (`RegionKind::Cave`, additive variant). - `CellularAutomata` cave Shaper → organic blobs (`RegionKind::Cave`, additive variant).
@ -91,7 +95,9 @@ A thin `wasm-bindgen` bridge. Public surface (all *to be created*):
(Tiles sent as a compact integer grid, not the verbose serde `Map` JSON, to keep payloads small and the JS renderer simple. The envelope is a *wasm-crate* shape, not a core type — the core's serde `Map` is unchanged.) (Tiles sent as a compact integer grid, not the verbose serde `Map` JSON, to keep payloads small and the JS renderer simple. The envelope is a *wasm-crate* shape, not a core type — the core's serde `Map` is unchanged.)
- `default_config_json() -> String` — the default `DungeonConfig` as JSON, so the UI can populate sliders without hardcoding defaults. - `default_config_json() -> String` — the default `DungeonConfig` as JSON, so the UI can populate sliders without hardcoding defaults.
Dependencies: `reikhelm-core`, `wasm-bindgen`, `serde_json`. No `getrandom` (thanks to C1). Dependencies: `reikhelm-core`, `wasm-bindgen`, `serde_json`. No `getrandom` (thanks to C1). The bridge uses **only plain `#[wasm_bindgen]` functions** taking `u64`/`&str` and returning `String` — no `js-sys`/`web-sys` — so it also compiles on the host target.
**Workspace placement:** `reikhelm-wasm` joins the workspace `members`, but the workspace sets `default-members = ["reikhelm-core", "reikhelm-viz"]` so a bare `cargo test`/`cargo clippy` at the root (acceptance criterion 1) operates only on the host-native crates and never pulls `wasm-bindgen` into the default host build. The bridge is built deliberately with `cargo build -p reikhelm-wasm --target wasm32-unknown-unknown` (via wasm-pack). Phase 0 confirms `cargo build -p reikhelm-wasm` is also host-green as a safety net.
**Build:** `wasm-pack build reikhelm-wasm --target web --out-dir ../reikhelm-web/pkg`. Primary toolchain: `rustup target add wasm32-unknown-unknown` + `cargo install wasm-pack`. *Fallbacks if wasm-pack is unavailable:* (a) `cargo install wasm-bindgen-cli` + manual `cargo build --target wasm32-… && wasm-bindgen …`; (b) ultimate fallback — a native `--bin export` that writes the same JSON envelope to a file the page fetches, with Playwright re-running it to "reroll" (loses live sliders, keeps the loop). **Build:** `wasm-pack build reikhelm-wasm --target web --out-dir ../reikhelm-web/pkg`. Primary toolchain: `rustup target add wasm32-unknown-unknown` + `cargo install wasm-pack`. *Fallbacks if wasm-pack is unavailable:* (a) `cargo install wasm-bindgen-cli` + manual `cargo build --target wasm32-… && wasm-bindgen …`; (b) ultimate fallback — a native `--bin export` that writes the same JSON envelope to a file the page fetches, with Playwright re-running it to "reroll" (loses live sliders, keeps the loop).
@ -126,7 +132,7 @@ render.js ── distance field → light → draw floor/walls/doors → overla
<canvas> ←─ Playwright screenshots ─→ visual judgment → iterate <canvas> ←─ Playwright screenshots ─→ visual judgment → iterate
``` ```
Determinism crosscheck (test): for a fixed seed+config, the WASM envelope's tile grid must match the native core's output (compare WASM JSON vs. a native render of the same seed). Same core, same seed, same map — on the web too. Determinism crosscheck (test): for a fixed seed+config, the **decoded `tiles` grid** (and optionally `regions`/`edges`) from the WASM envelope must equal the native core's output — explicitly *excluding* the `gen_ms` timing field, which is wall-clock and differs every run (so this is a structured comparison of the tile array, never a raw JSON-string diff). Same core, same seed, same map — on the web too.
--- ---