# 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 7–11 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 7–11 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` 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`, `Region`, `ConnGraph`, `Edge`, `Map` (Task 5). - **Produces:** - `Blackboard` — `new`, `insert(&mut self, key: &str, value: T)`, `get(&self, key: &str) -> Option<&T>`, `take(&mut self, key: &str) -> Option`. - `struct GenContext { tiles: Grid, regions: Vec, 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, regions: Vec, edges: Vec }` - `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)`. ## 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` 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"`