reikhelm/.dev/2026-05-28-procgen-map-core/tasks/06-engine.md
Parley Hatch 08e99e38a3 chore: scaffold reikhelm cargo workspace (core + viz crates)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:56:24 -06:00

55 lines
4.8 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.

# Task 6: Generation Engine (GenContext, Pass, Pipeline, Blackboard)
**Depends on:** Task 4, Task 5
## Context Files
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.6 (context, passes, pipeline, blackboard, snapshots), §5 (data flow), §7 (determinism).
- `.dev/2026-05-28-procgen-map-core/README.md` — inter-pass data contract table.
- `reikhelm-core/src/rng.rs``Rng::fork` (Task 4).
- `reikhelm-core/src/map.rs`, `reikhelm-core/src/region.rs`, `reikhelm-core/src/grid.rs` — types assembled into `GenContext`/`Map`.
- `reikhelm-core/src/lib.rs` — append module declarations here.
## Files
- Create: `reikhelm-core/src/blackboard.rs``Blackboard` named typed-slot store.
- Create: `reikhelm-core/src/pass.rs``GenContext`, `Pass`, `Pipeline`, `Snapshot`.
- Create: `reikhelm-core/src/passes/mod.rs` — passes module root; include the inter-pass data contract (from README) as a module doc comment. **No `pub mod` lines yet** — Tasks 711 append theirs.
- Modify: `reikhelm-core/src/lib.rs` — append `pub mod blackboard;`, `pub mod pass;`, `pub mod passes;`.
## What to Build
The keystone engine (spec §4.6). This task locks the `Pass` contract every pass consumes, so its signatures must be exactly right before Tasks 711 dispatch.
**RNG isolation is owned here, not by passes (spec §4.6, §7).** `Pipeline::run` creates a root `Rng::from_seed(seed)`, then for each pass derives a dedicated sub-stream `root.fork(&key)` where `key = format!("{}#{}", pass.name(), occurrence)` and `occurrence` is a per-name counter the run loop maintains (so two passes with the same name get distinct, stable streams). Each pass receives only its own `&mut Rng`. This makes determinism-under-pipeline-edits structural.
`Pipeline` carries the canvas dimensions (refines spec §4.6's `new()`): the recipe sets them, and `run` builds the initial `Grid<Tile>` filled with `Wall`.
`Blackboard` uses **named** typed slots (spec §4.6 fix), not bare `TypeId`, so multiple passes can store the same type under different keys.
`run_with_snapshots` must produce a `Map` **identical** to `run` for the same seed (spec §7) — snapshotting only reads context state and must not consume the RNG. Each `Snapshot` captures tiles **and** region bounds + graph edges (so partition/connect stages, which don't change tiles, are still visible to the viz scrubber).
## Interface Dependencies
- **Consumes:** `Rng` (Task 4); `Grid<Tile>`, `Tile`, `Region`, `ConnGraph`, `Edge`, `Map` (Task 5).
- **Produces:**
- `Blackboard``new`, `insert<T: 'static>(&mut self, key: &str, value: T)`, `get<T: 'static>(&self, key: &str) -> Option<&T>`, `take<T: 'static>(&mut self, key: &str) -> Option<T>`.
- `struct GenContext { tiles: Grid<Tile>, regions: Vec<Region>, graph: ConnGraph, blackboard: Blackboard }` — plus `add_region(kind, bounds, cells) -> RegionId` (appends; assigns `RegionId(regions.len())` so id == index). Regions are append-only: there is no remove/reorder, preserving the id-as-index invariant (Task 5) that edges and lookups depend on.
- `trait Pass { fn name(&self) -> &str; fn apply(&self, ctx: &mut GenContext, rng: &mut Rng); }`
- `struct Snapshot { label: String, tiles: Grid<Tile>, regions: Vec<Region>, edges: Vec<Edge> }`
- `struct Pipeline``new(width: u32, height: u32) -> Self`, `then(self, pass: impl Pass + 'static) -> Self`, `run(&self, seed: u64) -> Map`, `run_with_snapshots(&self, seed: u64) -> (Map, Vec<Snapshot>)`.
## Tests
- A trivial test `Pass` (e.g. "fill all Floor") run via `Pipeline::run` produces the expected grid and a `Map` with matching `seed`/dimensions.
- **Determinism:** `run(seed)` twice yields equal `Map`s.
- **Snapshot fidelity:** `run(seed)` and `run_with_snapshots(seed)` yield equal `Map`s; snapshot count equals pass count.
- **RNG isolation:** a pipeline `[A, B]` and `[A, X, B]` (X is a no-draw pass, or draws then discards) yield the same output from A and B — i.e. inserting a pass does not change other passes' streams. (Use two test passes that record their first draw into the blackboard.)
- **Occurrence keying:** two instances of the same-named test pass receive different sub-streams.
- `Blackboard` stores/retrieves two `Vec<i32>` under different keys without collision; `get` with the wrong type returns `None`.
## Success Criteria
- [ ] `Pass` trait signature is exactly `fn apply(&self, ctx: &mut GenContext, rng: &mut Rng)`.
- [ ] Pipeline forks a per-pass sub-stream keyed by `name#occurrence`; passes never touch a shared RNG.
- [ ] `run` and `run_with_snapshots` produce identical maps; snapshots capture tiles + regions + edges.
- [ ] `Blackboard` is name-keyed and type-checked.
- [ ] `passes/mod.rs` exists (no `pub mod` lines); module declarations added to `lib.rs`.
- [ ] All tests pass: `cargo test -p reikhelm-core` (pass/blackboard)
## Commit
`"feat(core): generation engine — GenContext, Pass, Pipeline, Blackboard"`