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

4.8 KiB
Raw Permalink Blame History

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.rsRng::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.rsBlackboard named typed-slot store.
  • Create: reikhelm-core/src/pass.rsGenContext, 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:
    • Blackboardnew, 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 Pipelinenew(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 Maps.
  • Snapshot fidelity: run(seed) and run_with_snapshots(seed) yield equal Maps; 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"