55 lines
4.8 KiB
Markdown
55 lines
4.8 KiB
Markdown
# 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<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"`
|