4.8 KiB
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 intoGenContext/Map.reikhelm-core/src/lib.rs— append module declarations here.
Files
- Create:
reikhelm-core/src/blackboard.rs—Blackboardnamed 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. Nopub modlines yet — Tasks 7–11 append theirs. - Modify:
reikhelm-core/src/lib.rs— appendpub 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<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 }— plusadd_region(kind, bounds, cells) -> RegionId(appends; assignsRegionId(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 viaPipeline::runproduces the expected grid and aMapwith matchingseed/dimensions. - Determinism:
run(seed)twice yields equalMaps. - Snapshot fidelity:
run(seed)andrun_with_snapshots(seed)yield equalMaps; 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.
Blackboardstores/retrieves twoVec<i32>under different keys without collision;getwith the wrong type returnsNone.
Success Criteria
Passtrait signature is exactlyfn 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. runandrun_with_snapshotsproduce identical maps; snapshots capture tiles + regions + edges.Blackboardis name-keyed and type-checked.passes/mod.rsexists (nopub modlines); module declarations added tolib.rs.- All tests pass:
cargo test -p reikhelm-core(pass/blackboard)
Commit
"feat(core): generation engine — GenContext, Pass, Pipeline, Blackboard"