chore: scaffold reikhelm cargo workspace (core + viz crates)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
08e99e38a3
27 changed files with 1718 additions and 0 deletions
32
.dev/2026-05-28-procgen-map-core/README.md
Normal file
32
.dev/2026-05-28-procgen-map-core/README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# reikhelm — Procedural 2D Map Generator: Implementation Plan
|
||||
|
||||
**Goal:** Build a Rust Cargo workspace whose pure `reikhelm-core` library generates deterministic 2D dungeon maps from composable passes, with a `reikhelm-viz` macroquad binary that renders them interactively.
|
||||
|
||||
**Architecture:** `reikhelm-core` is a pure library (no rendering deps) layered as geometry → `Grid<T>` → semantic regions + connectivity graph → a `GenContext`/`Pass`/`Pipeline` engine → a vocabulary of passes (Partitioner / Shaper / Connector / Placer) → a `dungeon` recipe. The engine owns per-pass RNG forking so editing the pipeline never reshuffles an existing seed. The library's sole output is a plain-data `Map`; `reikhelm-viz` consumes a `Map` and paints it — it contains no generation logic. The dependency direction is enforced: `viz → core`, never the reverse.
|
||||
|
||||
**Tech Stack:** Rust (Cargo workspace), `rand` + `rand_chacha` (ChaCha8 deterministic RNG), `serde` (derive on `Map`), `macroquad` (viz renderer).
|
||||
|
||||
**Spec:** [`spec/design.md`](spec/design.md)
|
||||
|
||||
## v1 Scope
|
||||
|
||||
Ship a dungeon recipe end-to-end (BSP → rooms → MST-connect → corridors → doors), an ASCII debug renderer, and an interactive macroquad window with seed reroll. Towns, caves, additional tiles/decorators, sprite rendering, and serialization tooling are designed-for but explicitly deferred (spec §11).
|
||||
|
||||
## Inter-Pass Data Contract (read before any pass task)
|
||||
|
||||
Passes never call each other; they communicate only through `GenContext` fields. The v1 dungeon contract:
|
||||
|
||||
| Pass | Reads | Writes |
|
||||
|------|-------|--------|
|
||||
| `BspPartition` | (empty grid) | `ctx.regions`: one placeholder `Region { kind: Room, bounds: <leaf rect>, cells: [] }` per BSP leaf |
|
||||
| `RoomCarver` | `ctx.regions` (placeholder Rooms) | carves `Floor` into `ctx.tiles`; shrinks each Room's `bounds` to its actual room rect and fills `cells` |
|
||||
| `MstConnect` | `ctx.regions` (kind=Room, non-empty `cells`) centers | `ctx.graph`: edges `{ a, b, at: None }` forming an MST over rooms + a few extra loop edges |
|
||||
| `CorridorCarver` | `ctx.graph` edges + Room region bounds/centers | carves L-shaped `Floor` corridors into `ctx.tiles`; appends `Region { kind: Corridor, .. }` per corridor |
|
||||
| `DoorPlacer` | `ctx.tiles` + `ctx.regions` + `ctx.graph` | converts boundary `Wall` cells (room↔corridor) to `Door`; sets the corresponding `edge.at = Some(point)` |
|
||||
|
||||
This is the integration contract. Each pass is unit-tested by constructing a `GenContext` in its required input state, so all five passes are implementable in parallel against this table.
|
||||
|
||||
## Shared Wiring Files (parallel-edit note)
|
||||
|
||||
- `reikhelm-core/src/lib.rs` — each module-creating task appends its own `pub mod <name>;`. These tasks are mostly sequential, so appends don't conflict.
|
||||
- `reikhelm-core/src/passes/mod.rs` — created empty (with the contract doc comment) by Task 6; each pass task (7–11) appends one `pub mod <name>;` + `pub use`. The pass tasks run in parallel, so the orchestrator merges these append-only lines at integration.
|
||||
37
.dev/2026-05-28-procgen-map-core/dependencies.md
Normal file
37
.dev/2026-05-28-procgen-map-core/dependencies.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Dependencies
|
||||
|
||||
```
|
||||
Task 1: Workspace scaffold (no deps)
|
||||
├── Task 2: Geometry (1)
|
||||
│ └── Task 3: Grid<T> (1, 2)
|
||||
│ └── Task 5: Data model — Tile/Region/Map/ASCII (2, 3)
|
||||
│ └── Task 6: Engine — GenContext/Pass/Pipeline/Blackboard (4, 5)
|
||||
│ ├── Task 7: BspPartition pass (6)
|
||||
│ ├── Task 8: RoomCarver pass (6)
|
||||
│ ├── Task 9: MstConnect pass (6)
|
||||
│ ├── Task 10: CorridorCarver pass (6)
|
||||
│ └── Task 11: DoorPlacer pass (6)
|
||||
│ └── Task 12: Dungeon recipe + integration tests (7,8,9,10,11)
|
||||
│ └── Task 13: reikhelm-viz renderer (5, 12)
|
||||
└── Task 4: RNG (1) ← independent of the geometry/grid chain
|
||||
```
|
||||
|
||||
## Parallelizable batches
|
||||
|
||||
- After Task 1: **[2, 4]** in parallel.
|
||||
- Task 3 after 2 (4 may still be running alongside).
|
||||
- Task 5 after 3.
|
||||
- Task 6 after **both** 4 and 5.
|
||||
- After Task 6: **[7, 8, 9, 10, 11]** — the five passes, all parallel. This is the largest parallel batch. They share only the append-only `passes/mod.rs` wiring file (orchestrator merges) and the documented inter-pass data contract (README); each is unit-tested against a hand-built `GenContext`, so none blocks another.
|
||||
- Task 12 after all of 7–11 (it wires them into the pipeline and runs integration tests).
|
||||
- Task 13 after 12 (it calls the `dungeon` recipe).
|
||||
|
||||
## Critical path
|
||||
|
||||
1 → 2 → 3 → 5 → 6 → (any one pass) → 12 → 13.
|
||||
|
||||
## Integration notes
|
||||
|
||||
- `Pass` trait + `GenContext` (Task 6) is the keystone contract every pass consumes. Lock its signature before dispatching 7–11.
|
||||
- The `Edge.at: Option<Point>` field (set to `None` by Task 9, populated by Task 11) is the one cross-pass field write — verify it survives the pipeline in Task 12's tests.
|
||||
- Determinism is verified at two levels: per-pass tests assert reproducibility given a fixed sub-stream; Task 12 asserts whole-map reproducibility and `run` == `run_with_snapshots` for the same seed.
|
||||
40
.dev/2026-05-28-procgen-map-core/files.md
Normal file
40
.dev/2026-05-28-procgen-map-core/files.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Files
|
||||
|
||||
All files created or modified across the plan. One-line purpose each.
|
||||
|
||||
## Workspace root (Task 1)
|
||||
|
||||
- Create `Cargo.toml` — workspace manifest; `members = ["reikhelm-core", "reikhelm-viz"]`, `resolver = "2"`.
|
||||
- Create `.gitignore` — ignore `/target`.
|
||||
- Create `reikhelm-core/Cargo.toml` — core lib manifest; deps: `rand`, `rand_chacha`, `serde` (derive).
|
||||
- Create `reikhelm-core/src/lib.rs` — crate root; module declarations (appended to by later tasks).
|
||||
- Create `reikhelm-viz/Cargo.toml` — viz bin manifest; deps: `reikhelm-core` (path), `macroquad`.
|
||||
- Create `reikhelm-viz/src/main.rs` — stub `main` (compiles; fleshed out in Task 13).
|
||||
|
||||
## reikhelm-core modules
|
||||
|
||||
- Create `reikhelm-core/src/geometry.rs` (Task 2) — `Point`, `Rect`, `Line`.
|
||||
- Create `reikhelm-core/src/grid.rs` (Task 3) — generic `Grid<T>` with neighbor/iterator queries.
|
||||
- Create `reikhelm-core/src/rng.rs` (Task 4) — `Rng` (ChaCha8 wrapper) with deterministic `fork` and helpers.
|
||||
- Create `reikhelm-core/src/region.rs` (Task 5) — `RegionId`, `RegionKind`, `Region`, `Edge`, `ConnGraph`.
|
||||
- Create `reikhelm-core/src/map.rs` (Task 5) — `Tile`, `Map` (the output contract), serde derives.
|
||||
- Create `reikhelm-core/src/ascii.rs` (Task 5) — ASCII renderer (`Map::to_ascii` / `print_ascii`).
|
||||
- Create `reikhelm-core/src/blackboard.rs` (Task 6) — `Blackboard` named typed-slot scratch store.
|
||||
- Create `reikhelm-core/src/pass.rs` (Task 6) — `GenContext`, `Pass` trait, `Pipeline`, `Snapshot`.
|
||||
- Create `reikhelm-core/src/passes/mod.rs` (Task 6) — passes module root + inter-pass contract doc; pass `pub mod` lines appended by Tasks 7–11.
|
||||
- Create `reikhelm-core/src/passes/bsp.rs` (Task 7) — `BspConfig`, `BspPartition` pass.
|
||||
- Create `reikhelm-core/src/passes/room.rs` (Task 8) — `RoomConfig`, `RoomCarver` pass.
|
||||
- Create `reikhelm-core/src/passes/connect.rs` (Task 9) — `ConnectConfig`, `MstConnect` pass.
|
||||
- Create `reikhelm-core/src/passes/corridor.rs` (Task 10) — `CorridorCarver` pass.
|
||||
- Create `reikhelm-core/src/passes/door.rs` (Task 11) — `DoorPlacer` pass.
|
||||
- Create `reikhelm-core/src/recipes/mod.rs` (Task 12) — recipes module root.
|
||||
- Create `reikhelm-core/src/recipes/dungeon.rs` (Task 12) — `DungeonConfig`, `ConfigError`, `dungeon()` recipe + integration tests.
|
||||
|
||||
## reikhelm-viz
|
||||
|
||||
- Modify `reikhelm-viz/src/main.rs` (Task 13) — macroquad window: render `Map`, reroll key, seed display, stretch snapshot scrubber.
|
||||
|
||||
## Shared files modified by multiple tasks
|
||||
|
||||
- `reikhelm-core/src/lib.rs` — appended with `pub mod <name>;` by Tasks 2, 3, 4, 5, 6, 12.
|
||||
- `reikhelm-core/src/passes/mod.rs` — appended with `pub mod <name>;` by Tasks 7–11 (parallel; orchestrator merges).
|
||||
260
.dev/2026-05-28-procgen-map-core/spec/design.md
Normal file
260
.dev/2026-05-28-procgen-map-core/spec/design.md
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
# reikhelm — Procedural 2D Map Generator (Core + Viz)
|
||||
|
||||
**Status:** Draft spec for review
|
||||
**Date:** 2026-05-28
|
||||
**Scope of this spec:** The core generation library and a first interactive renderer. Ships a dungeon recipe end-to-end. Towns, caves, and other map types are explicitly designed-for but deferred.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal & Philosophy
|
||||
|
||||
Build a procedural 2D map generator as a set of **small, composable, deterministic primitives** that combine into recipes for different map types (dungeons first; towns, caves later).
|
||||
|
||||
The organizing principle:
|
||||
|
||||
> A map is built by composing small, deterministic **passes** over a shared **context**. Towns vs. dungeons are different **recipes** drawn from one shared **vocabulary** of passes.
|
||||
|
||||
This is a learning-oriented, exploratory project (the author is new to procgen and learning Rust). Architecture should be idiomatic, debuggable, and expandable without rewrites — adding a new map type should mean "write a few new passes and a recipe," never "restructure the engine."
|
||||
|
||||
Non-goals for this spec: a full game, gameplay logic, pathfinding/AI, networking, persistence beyond optional serialization, performance tuning.
|
||||
|
||||
---
|
||||
|
||||
## 2. Workspace Layout
|
||||
|
||||
A Cargo workspace with a strict dependency direction (`viz` depends on `core`; `core` depends on nothing rendering-related).
|
||||
|
||||
```
|
||||
reikhelm/ (cargo workspace)
|
||||
├── Cargo.toml (workspace manifest)
|
||||
├── reikhelm-core/ pure library: no rendering dependencies
|
||||
│ ├── geometry Point, Rect, Line
|
||||
│ ├── grid Grid<T>
|
||||
│ ├── rng deterministic seeded RNG + named sub-streams
|
||||
│ ├── region Region, RegionKind, connectivity graph
|
||||
│ ├── map Map (the output type — the core/viz contract)
|
||||
│ ├── pass Pass trait, GenContext, Pipeline
|
||||
│ ├── passes/ the primitive vocabulary (partitioners, shapers, ...)
|
||||
│ ├── recipes/ dungeon() recipe
|
||||
│ └── ascii ASCII renderer (debug + tests, lives in core)
|
||||
└── reikhelm-viz/ binary: macroquad window, consumes reikhelm-core
|
||||
```
|
||||
|
||||
**Why this split:** `reikhelm-core` compiles fast, is fully unit-testable without a window, and is reusable by *any* future consumer — a different renderer, a CLI, or a game loop (macroquad or Bevy). The renderer is swappable; generation never changes when you change how maps are drawn.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Core/Viz Contract — `Map` (load-bearing)
|
||||
|
||||
The single most important interface in the project. The core's entire job is to produce a `Map`; a renderer's entire job is to read one. The `Map` describes **what is where** and contains **no rendering information** (no colors, sprites, or pixels).
|
||||
|
||||
```rust
|
||||
pub struct Map {
|
||||
pub tiles: Grid<Tile>, // base terrain layer: what occupies each cell
|
||||
pub regions: Vec<Region>, // semantic areas (Room #3 = this rect, kind = Room)
|
||||
pub graph: ConnGraph, // how regions connect (edge = door/corridor/road)
|
||||
pub seed: u64, // the seed that produced this map (reproducibility)
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
```
|
||||
|
||||
- Renderers map data → visuals. The same `Map` can be drawn as ASCII characters, colored rectangles, or sprites — without the core changing.
|
||||
- **Sprite/autotiling support is a property of the renderer, not the core.** Renderers pick a sprite for a cell by querying that cell's neighbors via `Grid` (e.g. wall-corner vs. wall-edge). The core supplies neighbor queries; all art logic stays in the renderer.
|
||||
- `Map` and its constituent types **derive `serde::{Serialize, Deserialize}`** (cheap), so a generated map can be saved/loaded as JSON. This keeps the door open for tooling and cross-program use. *(Serialization round-trip is a nice-to-have, not a v1 acceptance gate.)*
|
||||
|
||||
---
|
||||
|
||||
## 4. Component Design
|
||||
|
||||
### 4.1 Geometry (`geometry`)
|
||||
Integer grid-coordinate types:
|
||||
- `Point { x: i32, y: i32 }` — arithmetic, neighbor offsets.
|
||||
- `Rect { x, y, w, h }` — `center()`, `contains(Point)`, `intersects(Rect)`, `iter()` over contained points, inflate/deflate.
|
||||
- `Line` — for corridor/segment carving; iterate cells along a line.
|
||||
|
||||
### 4.2 Grid (`grid`)
|
||||
```rust
|
||||
pub struct Grid<T> { width: u32, height: u32, cells: Vec<T> }
|
||||
```
|
||||
- Bounds-safe access: `get(Point) -> Option<&T>`, `get_mut`, `set`.
|
||||
- `in_bounds(Point) -> bool`, `index(Point)` internal.
|
||||
- Neighbors: `neighbors4(Point)`, `neighbors8(Point)` returning in-bounds points.
|
||||
- Iterators: `iter()` (point + value), `iter_rect(Rect)`.
|
||||
- Constructors: `new(w, h, fill)`, `from_fn(w, h, f)`.
|
||||
- Generic on purpose: `Grid<Tile>` is the map; `Grid<u32>`/`Grid<f32>` are scratch buffers (region-id maps, distance fields, noise) used during generation.
|
||||
|
||||
### 4.3 Tile (`map`)
|
||||
```rust
|
||||
pub enum Tile { Wall, Floor, Door } // extensible: Water, Lava, Road, Rubble, ... later
|
||||
```
|
||||
Starts minimal. New variants are additive; passes match on the variants they care about.
|
||||
|
||||
### 4.4 RNG (`rng`)
|
||||
- Wraps `rand_chacha::ChaCha8Rng` (reproducible across platforms; not `thread_rng`).
|
||||
- `Rng::from_seed(u64)`.
|
||||
- Convenience: `range(lo..hi)`, `chance(p)`, `choose(&[T])`, `shuffle(&mut [T])`.
|
||||
- **Named sub-streams:** `fork(label: &str) -> Rng` derives a child generator deterministically from the parent seed + label hash. The **`Pipeline` owns stream derivation, not individual passes** (see §4.6): `run` hands each pass its *own* forked sub-stream, keyed by pass name + occurrence index. This is what makes *"inserting or reordering a pass does not reshuffle unrelated randomness"* a structural guarantee rather than a convention each pass must remember to honor — a seed that made a good map keeps making it. Built in from the start because retrofitting it later invalidates all existing seeds.
|
||||
|
||||
### 4.5 Region & connectivity (`region`)
|
||||
```rust
|
||||
pub enum RegionKind { Room, Corridor, /* later: District, Plaza, Building */ }
|
||||
|
||||
pub struct Region {
|
||||
pub id: RegionId, // index/handle
|
||||
pub kind: RegionKind,
|
||||
pub bounds: Rect, // bounding box
|
||||
pub cells: Vec<Point>, // exact occupied cells (supports non-rect regions like caves)
|
||||
}
|
||||
|
||||
pub struct ConnGraph {
|
||||
// nodes = RegionId, edges carry the connection cell(s) (where the door/corridor is)
|
||||
edges: Vec<Edge>, // Edge { a: RegionId, b: RegionId, at: Point }
|
||||
}
|
||||
```
|
||||
The semantic layer that lets different map types share machinery: a dungeon is rooms + corridor edges; a town (later) is districts/buildings + road edges. Same types, different passes producing them.
|
||||
|
||||
### 4.6 Generation context & passes (`pass`)
|
||||
```rust
|
||||
pub struct GenContext {
|
||||
pub tiles: Grid<Tile>,
|
||||
pub regions: Vec<Region>,
|
||||
pub graph: ConnGraph,
|
||||
pub blackboard: Blackboard, // named typed scratch for inter-pass handoff
|
||||
}
|
||||
|
||||
pub trait Pass {
|
||||
fn name(&self) -> &str;
|
||||
fn apply(&self, ctx: &mut GenContext, rng: &mut Rng); // rng is this pass's OWN sub-stream
|
||||
}
|
||||
|
||||
pub struct Pipeline { passes: Vec<Box<dyn Pass>> }
|
||||
impl Pipeline {
|
||||
pub fn new() -> Self;
|
||||
pub fn then(self, pass: impl Pass + 'static) -> Self; // builder-style composition
|
||||
pub fn run(&self, seed: u64) -> Map; // produces final Map
|
||||
pub fn run_with_snapshots(&self, seed: u64) -> (Map, Vec<Snapshot>);
|
||||
}
|
||||
```
|
||||
- One responsibility per pass; passes communicate via `regions`/`graph`/`blackboard`.
|
||||
- **RNG isolation is owned by the pipeline, not the pass.** `run` creates a root `Rng::from_seed(seed)` and, for each pass, derives a dedicated sub-stream `root.fork(&key)` where `key = "<pass.name()>#<occurrence>"` and `occurrence` is a per-name counter owned by the run loop. So two `CorridorCarver`s (or a recipe using `RoomCarver` twice) each get a distinct, stable stream, and the `Pass` taking `rng: &mut Rng` cannot accidentally read a sibling's randomness. A pass needing multiple independent phases may further `rng.fork("phase")` internally. This is why `apply` takes `&self` (config is immutable; no per-pass occurrence state to track — the pipeline tracks it).
|
||||
- **`Blackboard`: named typed slots**, not bare `TypeId`. API shape: `bb.insert("bsp_leaves", leaves)` / `bb.get::<Vec<Rect>>("bsp_leaves")`. Named keys (not type-keyed) so multiple passes can stash values of the *same type* without silently overwriting each other — e.g. a town recipe with both a district partition and a building partition, each emitting `Vec<Rect>`.
|
||||
- **Snapshots:** `run_with_snapshots` records, after each pass, a label plus a copy of the tile grid **and** the region bounds + graph edges. Capturing regions/graph (not just tiles) matters because some passes change *only* the semantic layer — `BspPartition` populates regions while the grid is still all-`Wall`; `MstConnect` adds graph edges but carves nothing. A tile-only snapshot would render those (arguably the most interesting) stages as visually unchanged. Snapshotting is opt-in so the normal `run` path stays allocation-light.
|
||||
|
||||
### 4.7 The primitive vocabulary (`passes/`)
|
||||
Organized by role. v1 implements the **bold** ones; the rest are designed-for slots.
|
||||
|
||||
| Role | Responsibility | v1 | Later |
|
||||
|------|---------------|----|----|
|
||||
| **Partitioner** | divide space → produce regions | **`BspPartition`** | `GridPartition`, `Voronoi` |
|
||||
| **Shaper** | carve space within regions | **`RoomCarver`** | `CellularAutomata`, `RandomWalk` |
|
||||
| **Connector** | wire regions together | **`MstConnect`** + **`CorridorCarver`** | extra-loop connectors |
|
||||
| **Placer** | place features at chosen cells | **`DoorPlacer`** | `FeatureScatter` (poisson-disk) |
|
||||
| **Decorator** | finishing passes | *(none required v1)* | water, tagging, wall cleanup |
|
||||
|
||||
### 4.8 Recipes (`recipes/`)
|
||||
A recipe is a named function returning a configured `Pipeline`.
|
||||
```rust
|
||||
pub fn dungeon(cfg: DungeonConfig) -> Pipeline {
|
||||
Pipeline::new()
|
||||
.then(BspPartition::new(&cfg.partition))
|
||||
.then(RoomCarver::new(&cfg.rooms))
|
||||
.then(MstConnect::new(&cfg.connect))
|
||||
.then(CorridorCarver::default())
|
||||
.then(DoorPlacer::default())
|
||||
}
|
||||
```
|
||||
`DungeonConfig` exposes the knobs (map size, min/max room size, partition depth, extra-corridor chance) with sensible defaults.
|
||||
|
||||
### 4.9 ASCII renderer (`ascii`, in core)
|
||||
`map.to_ascii() -> String` and `map.print_ascii()`. Lives in core because it has no external deps and is the workhorse for unit tests and quick CLI checks (`#` wall, `.` floor, `+` door).
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
```
|
||||
seed: u64
|
||||
│
|
||||
▼
|
||||
Pipeline::run(seed)
|
||||
│ creates GenContext { empty Grid<Tile>=Wall, ... } + root Rng::from_seed(seed)
|
||||
│ each pass below receives ctx + its own forked sub-stream (root.fork("<name>#<n>"))
|
||||
│
|
||||
├─ BspPartition → fills ctx.regions with leaf rects (kind=Room placeholder)
|
||||
├─ RoomCarver → carves Floor inside each region; finalizes Room regions
|
||||
├─ MstConnect → builds ConnGraph edges (which rooms connect)
|
||||
├─ CorridorCarver → carves Floor corridors along graph edges; adds Corridor regions
|
||||
└─ DoorPlacer → sets Door tiles where corridors meet rooms; records edge.at
|
||||
│
|
||||
▼
|
||||
Map { tiles, regions, graph, seed, width, height }
|
||||
│
|
||||
▼
|
||||
reikhelm-viz → draw_rectangle per cell (color by Tile) [today]
|
||||
sprite per cell (chosen via neighbor query) [your art, later]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. The Viz Crate (`reikhelm-viz`)
|
||||
|
||||
A thin macroquad binary. Responsibilities only:
|
||||
- Call a recipe, get a `Map`.
|
||||
- Draw the grid: one colored rectangle per cell, color chosen by `Tile`.
|
||||
- **Reroll:** press a key (e.g. `Space`) → new seed → regenerate → redraw.
|
||||
- Display the current seed on screen.
|
||||
- **Stretch (in scope if time allows):** generate with `run_with_snapshots`; left/right arrows scrub through generation stages.
|
||||
|
||||
Viz contains **no generation logic** and **no algorithm knowledge** — only "given a `Map`, paint it." This is the proof that the contract holds: if the viz crate needs to reach into generation internals to draw, the boundary is wrong.
|
||||
|
||||
---
|
||||
|
||||
## 7. Determinism Contract
|
||||
|
||||
- Same `seed` → byte-identical `Map`, on any machine, every run.
|
||||
- All randomness flows through `Rng` (ChaCha8). No `HashMap` iteration order, system time, or `thread_rng` may influence generation.
|
||||
- The pipeline hands each pass a dedicated forked sub-stream keyed by name + occurrence (§4.6), so inserting or reordering a pass does not silently change other passes' output for an existing seed.
|
||||
- `run(seed)` and `run_with_snapshots(seed)` must produce identical `Map`s — snapshotting only *reads* context state after each pass and must never consume the RNG or otherwise perturb generation.
|
||||
|
||||
---
|
||||
|
||||
## 8. Error Handling
|
||||
|
||||
Generation is mostly infallible by construction, but inputs can be invalid:
|
||||
- **Config validation:** `DungeonConfig` validated at recipe construction — e.g. min room size must fit within the smallest possible BSP leaf, map dimensions > 0. Invalid config returns a `Result`/`ConfigError` rather than panicking deep in a pass.
|
||||
- **Degenerate outcomes** (e.g. a BSP leaf too small to hold a room): the pass skips that leaf gracefully and records nothing, rather than failing the whole generation. A map with fewer rooms is valid; a panic is not.
|
||||
- **In-pass bounds:** all grid writes go through bounds-safe `Grid` accessors; an out-of-bounds carve is a no-op, never a panic.
|
||||
- Internal invariant violations (a corridor referencing a nonexistent region id) use `debug_assert!` to catch bugs in development without costing release performance.
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Strategy
|
||||
|
||||
- **Determinism test:** generate twice with the same seed; assert `Map`s are equal (and ASCII output is identical).
|
||||
- **Connectivity test:** flood-fill the floor tiles of a generated dungeon; assert every `Room` region is reachable (no orphaned rooms).
|
||||
- **Bounds test:** assert no carve ever writes outside the grid; all rooms lie within map bounds.
|
||||
- **Geometry/Grid unit tests:** `Rect::intersects`, `contains`, neighbor queries, `Grid` get/set bounds behavior.
|
||||
- **Snapshot count test:** `run_with_snapshots` yields one snapshot per pass.
|
||||
- ASCII output is the primary human-readable assertion vehicle.
|
||||
|
||||
---
|
||||
|
||||
## 10. v1 Acceptance Criteria
|
||||
|
||||
1. `cargo build` / `cargo test` pass at the workspace root.
|
||||
2. `reikhelm-core` produces a connected dungeon `Map` from a seed, with rooms, corridors, and doors, fully deterministic.
|
||||
3. `map.print_ascii()` renders a recognizable dungeon.
|
||||
4. `cargo run -p reikhelm-viz` opens a window showing the dungeon in colored tiles; pressing the reroll key produces a new dungeon; the seed is visible.
|
||||
5. The dependency boundary holds: `reikhelm-core` has no rendering dependency; `reikhelm-viz` contains no generation logic.
|
||||
|
||||
---
|
||||
|
||||
## 11. Explicitly Deferred (designed-for, not built in v1)
|
||||
|
||||
- **Town recipe** (district partition, building placement, road network) — new passes + recipe, no architecture change.
|
||||
- **Cave recipe** (cellular automata, random walk, keep-largest-region).
|
||||
- Additional `Tile` variants (water, lava, road), decorators.
|
||||
- `FeatureScatter` / poisson-disk placement.
|
||||
- Sprite renderer + autotiling (the architecture supports it; building it is future art work).
|
||||
- Serialization round-trip tooling, CLI, performance tuning, any gameplay/engine integration (macroquad game or Bevy).
|
||||
24
.dev/2026-05-28-procgen-map-core/state/progress.md
Normal file
24
.dev/2026-05-28-procgen-map-core/state/progress.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Progress Ledger — reikhelm procgen core
|
||||
|
||||
Orchestrator's external memory. One entry per task as it completes.
|
||||
|
||||
## Environment notes
|
||||
- Rust 1.96.0 stable. `~/.cargo/bin` NOT on PATH — prepend `export PATH="$HOME/.cargo/bin:$PATH"` in every cargo shell call.
|
||||
- Resolved dep versions: _TBD by Task 1 (record rand major here — drives RNG API)._
|
||||
|
||||
## Execution schedule
|
||||
- Phase 0: Task 1 (scaffold) — direct dispatch, de-risk toolchain/version resolution.
|
||||
- Phase 1: Tasks 2→3→5 chain + Task 4 (RNG) — core library layer.
|
||||
- Phase 2: Task 6 (engine) — keystone Pass/Pipeline contract + validator.
|
||||
- Phase 2.5: orchestrator pre-stubs 5 pass files + wires passes/mod.rs.
|
||||
- Phase 3: Tasks 7–11 (passes) — parallel, worktree-isolated; then adversarial verify.
|
||||
- Phase 4: Task 12 (recipe + integration) — heavy determinism/connectivity verification.
|
||||
- Phase 5: Task 13 (viz) — build + boundary check.
|
||||
|
||||
## Task log
|
||||
|
||||
_(none yet)_
|
||||
|
||||
## Open concerns
|
||||
- Determinism is the highest-risk surface: RNG fork stability (T4) + pipeline per-pass forking (T6). Both get adversarial verification.
|
||||
- Parallel batch (7–11) shares passes/mod.rs — handled by orchestrator-owned pre-wiring + worktree isolation.
|
||||
5
.dev/2026-05-28-procgen-map-core/state/smell-ledger.md
Normal file
5
.dev/2026-05-28-procgen-map-core/state/smell-ledger.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Smell Ledger — reikhelm
|
||||
|
||||
Patterns surfaced by implementers/reviewers. Format: pattern — where found — where fixed — where it remains.
|
||||
|
||||
_(none yet)_
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Task 1: Workspace Scaffold
|
||||
|
||||
**Depends on:** none
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §2 (workspace layout), §3 (the `Map` contract these crates revolve around).
|
||||
- `.dev/2026-05-28-procgen-map-core/README.md` — architecture + dependency direction.
|
||||
|
||||
## Files
|
||||
- Create: `Cargo.toml` — workspace manifest.
|
||||
- Create: `.gitignore` — `/target`.
|
||||
- Create: `reikhelm-core/Cargo.toml` — core lib manifest.
|
||||
- Create: `reikhelm-core/src/lib.rs` — empty crate root (later tasks append `pub mod` lines).
|
||||
- Create: `reikhelm-viz/Cargo.toml` — viz binary manifest.
|
||||
- Create: `reikhelm-viz/src/main.rs` — stub `main` that compiles.
|
||||
|
||||
## What to Build
|
||||
A two-crate Cargo workspace (spec §2). `reikhelm-core` is a library with **no rendering dependencies** — only `rand`, `rand_chacha`, and `serde` (with the `derive` feature). `reikhelm-viz` is a binary depending on `reikhelm-core` (by path) and `macroquad`. `git init` the repo first so per-task commits work. Use `resolver = "2"`. Pin recent stable crate versions. `lib.rs` starts empty (a crate doc comment is fine); `main.rs` is a trivial `fn main()` placeholder — Task 13 replaces it. The point of this task is a workspace where `cargo build` and `cargo test` succeed with zero modules.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Produces:** a compiling workspace; `reikhelm-core` crate (lib), `reikhelm-viz` crate (bin).
|
||||
|
||||
## Tests
|
||||
- No unit tests yet. The success check is that the workspace compiles.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `git init` done; `.gitignore` excludes `/target`.
|
||||
- [ ] `reikhelm-core` declares `rand`, `rand_chacha`, `serde` (derive); declares **no** rendering/GUI dependency.
|
||||
- [ ] `reikhelm-viz` depends on `reikhelm-core` by path and on `macroquad`.
|
||||
- [ ] `cargo build` succeeds from the workspace root.
|
||||
- [ ] `cargo test` succeeds (zero tests is fine).
|
||||
|
||||
## Commit
|
||||
`"chore: scaffold reikhelm cargo workspace (core + viz crates)"`
|
||||
36
.dev/2026-05-28-procgen-map-core/tasks/02-geometry.md
Normal file
36
.dev/2026-05-28-procgen-map-core/tasks/02-geometry.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Task 2: Geometry
|
||||
|
||||
**Depends on:** Task 1
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.1 (geometry).
|
||||
- `reikhelm-core/src/lib.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/geometry.rs` — `Point`, `Rect`, `Line`.
|
||||
- Modify: `reikhelm-core/src/lib.rs` — append `pub mod geometry;`.
|
||||
|
||||
## What to Build
|
||||
Integer grid-coordinate geometry types (spec §4.1). All types derive `Clone, Copy, Debug, PartialEq, Eq` and `serde::{Serialize, Deserialize}`. These are the foundation reused by the grid, regions, and every pass — keep them small and total (no panics on valid input).
|
||||
|
||||
## Interface Dependencies
|
||||
- **Produces:**
|
||||
- `Point { x: i32, y: i32 }` — `new`, `manhattan(&self, other: Point) -> i32`, and offset helpers for the four/eight neighbor directions.
|
||||
- `Rect { x: i32, y: i32, w: i32, h: i32 }` — `new`, `center() -> Point`, `contains(Point) -> bool`, `intersects(&Rect) -> bool`, `inflate(by: i32) -> Rect`, `iter() -> impl Iterator<Item = Point>` (all contained cells).
|
||||
- `Line { from: Point, to: Point }` — `cells() -> impl Iterator<Item = Point>` yielding the grid cells along the segment (a straight horizontal/vertical run is sufficient for v1 corridors; document the behavior).
|
||||
|
||||
## Tests
|
||||
- `Rect::contains` is true for corners/interior and false just outside.
|
||||
- `Rect::intersects` is symmetric; true for overlap, false for adjacent-but-disjoint rects.
|
||||
- `Rect::iter` yields exactly `w * h` distinct points, all contained.
|
||||
- `Rect::center` returns an interior point.
|
||||
- `Point::manhattan` is correct and symmetric.
|
||||
- `Line::cells` yields a contiguous run from `from` to `to` inclusive for a horizontal and a vertical line.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] All listed types and methods exist with the signatures above and derive serde.
|
||||
- [ ] `pub mod geometry;` added to `lib.rs`.
|
||||
- [ ] All tests pass: `cargo test -p reikhelm-core geometry`
|
||||
|
||||
## Commit
|
||||
`"feat(core): geometry primitives (Point, Rect, Line)"`
|
||||
42
.dev/2026-05-28-procgen-map-core/tasks/03-grid.md
Normal file
42
.dev/2026-05-28-procgen-map-core/tasks/03-grid.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Task 3: Grid<T>
|
||||
|
||||
**Depends on:** Task 1, Task 2
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.2 (Grid).
|
||||
- `reikhelm-core/src/geometry.rs` — `Point`, `Rect` (indexing + region iteration).
|
||||
- `reikhelm-core/src/lib.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/grid.rs` — generic `Grid<T>`.
|
||||
- Modify: `reikhelm-core/src/lib.rs` — append `pub mod grid;`.
|
||||
|
||||
## What to Build
|
||||
A generic, bounds-safe 2D grid (spec §4.2) backed by a flat `Vec<T>` in row-major order. Generic on purpose: it's the map (`Grid<Tile>`) and also scratch buffers (`Grid<u32>`, `Grid<f32>`) during generation. All access is bounds-checked — out-of-bounds reads return `None`, out-of-bounds writes are no-ops (never panic). Neighbor queries return only in-bounds points. Derive `Clone, Debug`; derive `Serialize, Deserialize` gated on `T: Serialize/Deserialize`.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Point`, `Rect` from Task 2.
|
||||
- **Produces:** `Grid<T>` with:
|
||||
- `new(width: u32, height: u32, fill: T) -> Self where T: Clone`
|
||||
- `from_fn(width, height, f: impl Fn(Point) -> T) -> Self`
|
||||
- `width() -> u32`, `height() -> u32`
|
||||
- `in_bounds(Point) -> bool`
|
||||
- `get(Point) -> Option<&T>`, `get_mut(Point) -> Option<&mut T>`, `set(Point, T)` (no-op if OOB)
|
||||
- `neighbors4(Point) -> impl Iterator<Item = Point>`, `neighbors8(...)` — in-bounds only
|
||||
- `iter() -> impl Iterator<Item = (Point, &T)>`
|
||||
- `iter_rect(Rect) -> impl Iterator<Item = (Point, &T)>` (clipped to bounds)
|
||||
|
||||
## Tests
|
||||
- `get`/`set` round-trip for in-bounds points; `set` OOB is a no-op and `get` OOB is `None`.
|
||||
- `neighbors4`/`neighbors8` at a corner return only in-bounds neighbors (3 for corner with 8-conn).
|
||||
- `iter` visits exactly `width * height` cells once each.
|
||||
- `iter_rect` is clipped when the rect extends past the grid edge.
|
||||
- `from_fn` produces values matching the closure at sampled points.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `Grid<T>` exists with all listed methods; OOB access never panics.
|
||||
- [ ] `pub mod grid;` added to `lib.rs`.
|
||||
- [ ] All tests pass: `cargo test -p reikhelm-core grid`
|
||||
|
||||
## Commit
|
||||
`"feat(core): generic bounds-safe Grid<T>"`
|
||||
41
.dev/2026-05-28-procgen-map-core/tasks/04-rng.md
Normal file
41
.dev/2026-05-28-procgen-map-core/tasks/04-rng.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Task 4: Deterministic RNG
|
||||
|
||||
**Depends on:** Task 1
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.4 (RNG) and §7 (determinism contract).
|
||||
- `reikhelm-core/src/lib.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/rng.rs` — `Rng`.
|
||||
- Modify: `reikhelm-core/src/lib.rs` — append `pub mod rng;`.
|
||||
|
||||
## What to Build
|
||||
A deterministic RNG wrapping `rand_chacha::ChaCha8Rng` (spec §4.4). Reproducible across machines — never `thread_rng`. The keystone feature is `fork`: deriving an independent child stream from a **stable basis** (the originating seed) plus a label, so that forks are independent of draw order and of each other. The `Pipeline` (Task 6) calls `fork` once per pass; this is what makes editing the pipeline non-destructive to existing seeds (spec §7).
|
||||
|
||||
**Critical determinism detail:** `fork`'s label→seed mixing must be stable across runs and crate versions. Do **not** use `std::collections::hash_map::DefaultHasher` (unspecified, version-unstable). Use an explicit, fixed integer mix (e.g. a splitmix64 / FNV-style combine of the base seed and the label bytes). Document the chosen mixing.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Produces:** `Rng` with:
|
||||
- `from_seed(seed: u64) -> Self` (stores the seed as the fork basis)
|
||||
- `fork(&self, label: &str) -> Rng` — returns `Rng::from_seed(mix(self.basis_seed, label))`; **does not** advance `self`; order-independent.
|
||||
- `range(&mut self, lo: i32, hi: i32) -> i32` (half-open or inclusive — document which)
|
||||
- `chance(&mut self, p: f64) -> bool`
|
||||
- `choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T>`
|
||||
- `shuffle<T>(&mut self, items: &mut [T])`
|
||||
|
||||
## Tests
|
||||
- Same seed → identical sequence of `range` draws (reproducibility).
|
||||
- `fork` is **order-independent**: `r.fork("a")` produces the same stream regardless of how many values were drawn from `r` beforehand.
|
||||
- Distinct labels produce distinct (non-identical) streams: `fork("a") != fork("b")`.
|
||||
- `fork("name#0")` and `fork("name#1")` differ (the occurrence-keying scheme the pipeline relies on works).
|
||||
- `choose` on an empty slice returns `None`; `shuffle` is a permutation (same multiset).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `Rng` wraps ChaCha8; `fork` derives from a stable basis with an explicit (non-`DefaultHasher`) mix.
|
||||
- [ ] `fork` is order-independent and does not mutate the parent.
|
||||
- [ ] `pub mod rng;` added to `lib.rs`.
|
||||
- [ ] All tests pass: `cargo test -p reikhelm-core rng`
|
||||
|
||||
## Commit
|
||||
`"feat(core): deterministic ChaCha8 RNG with order-independent fork"`
|
||||
47
.dev/2026-05-28-procgen-map-core/tasks/05-data-model.md
Normal file
47
.dev/2026-05-28-procgen-map-core/tasks/05-data-model.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Task 5: Data Model & Output Contract (Tile, Region, Map, ASCII)
|
||||
|
||||
**Depends on:** Task 2, Task 3
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §3 (`Map` contract), §4.3 (Tile), §4.5 (Region & connectivity), §4.9 (ASCII renderer).
|
||||
- `reikhelm-core/src/geometry.rs` — `Point`, `Rect`.
|
||||
- `reikhelm-core/src/grid.rs` — `Grid<T>`.
|
||||
- `reikhelm-core/src/lib.rs` — append module declarations here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/region.rs` — `RegionId`, `RegionKind`, `Region`, `Edge`, `ConnGraph`.
|
||||
- Create: `reikhelm-core/src/map.rs` — `Tile`, `Map`.
|
||||
- Create: `reikhelm-core/src/ascii.rs` — ASCII renderer for `Map`.
|
||||
- Modify: `reikhelm-core/src/lib.rs` — append `pub mod region;`, `pub mod map;`, `pub mod ascii;`.
|
||||
|
||||
## What to Build
|
||||
The semantic data layer and the project's central output contract, `Map` (spec §3). `Map` is plain data describing *what is where* — **no colors, sprites, or rendering info**. Every type derives `serde::{Serialize, Deserialize}` (spec §3) so a `Map` can round-trip to JSON. The ASCII renderer (spec §4.9) lives in core because it has no external deps and is the workhorse for downstream tests.
|
||||
|
||||
Note the `Edge.at` field is `Option<Point>` (refines spec §4.5's `Point`): the door location is unknown until `DoorPlacer` runs, so `MstConnect` creates edges with `at: None`.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Point`, `Rect` (Task 2), `Grid<T>` (Task 3).
|
||||
- **Produces:**
|
||||
- `enum Tile { Wall, Floor, Door }` — `Default` = `Wall`; derives `Copy, Eq`.
|
||||
- `type RegionId` as a newtype `RegionId(usize)`. **Invariant:** `RegionId(n)` always equals the index `n` of that region in `ctx.regions`/`Map::regions`. Regions are append-only — never removed or reordered — so this id-as-index relationship holds for the life of the map (edges and lookups rely on it).
|
||||
- `enum RegionKind { Room, Corridor }` (extensible).
|
||||
- `struct Region { id: RegionId, kind: RegionKind, bounds: Rect, cells: Vec<Point> }` — **fields `pub`** (read across modules by passes and viz).
|
||||
- `struct Edge { a: RegionId, b: RegionId, at: Option<Point> }` — **fields `pub`** (constructed by `MstConnect`, `at` written by `DoorPlacer`, read by viz).
|
||||
- `struct ConnGraph { edges: Vec<Edge> }` — `new`, `add_edge(a, b) -> ()` (pushes `at: None`), `edges() -> &[Edge]`, **`edges_mut() -> &mut [Edge]`** (so `DoorPlacer` can populate `at`), `neighbors(RegionId) -> impl Iterator<Item = RegionId>`.
|
||||
- `struct Map { tiles: Grid<Tile>, regions: Vec<Region>, graph: ConnGraph, seed: u64, width: u32, height: u32 }` — fields `pub` (the output contract, spec §3).
|
||||
- `impl Map { fn to_ascii(&self) -> String; fn print_ascii(&self); }` — `Wall` → `#`, `Floor` → `.`, `Door` → `+`, one row per line.
|
||||
|
||||
## Tests
|
||||
- A small hand-built `Map` serializes to JSON and deserializes back equal (serde round-trip).
|
||||
- `ConnGraph::add_edge` stores `at: None`; `neighbors` returns both endpoints' counterparts.
|
||||
- `Map::to_ascii` on a hand-built 3×3 map with a known tile layout produces the exact expected string (including a door `+`).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] All types exist with the signatures above and derive serde; `Map` carries no rendering data.
|
||||
- [ ] `Edge.at` is `Option<Point>`.
|
||||
- [ ] ASCII output matches the documented glyph mapping.
|
||||
- [ ] Module declarations added to `lib.rs`.
|
||||
- [ ] All tests pass: `cargo test -p reikhelm-core` (region/map/ascii)
|
||||
|
||||
## Commit
|
||||
`"feat(core): data model + Map output contract + ASCII renderer"`
|
||||
55
.dev/2026-05-28-procgen-map-core/tasks/06-engine.md
Normal file
55
.dev/2026-05-28-procgen-map-core/tasks/06-engine.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# 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"`
|
||||
40
.dev/2026-05-28-procgen-map-core/tasks/07-bsp-partition.md
Normal file
40
.dev/2026-05-28-procgen-map-core/tasks/07-bsp-partition.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Task 7: BspPartition Pass
|
||||
|
||||
**Depends on:** Task 6
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.7 (Partitioner role), §5 (data flow).
|
||||
- `.dev/2026-05-28-procgen-map-core/README.md` — inter-pass data contract (this pass is the first row).
|
||||
- `reikhelm-core/src/pass.rs` — `Pass`, `GenContext` (Task 6).
|
||||
- `reikhelm-core/src/geometry.rs` — `Rect` (Task 2).
|
||||
- `reikhelm-core/src/passes/mod.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/passes/bsp.rs` — `BspConfig`, `BspPartition`.
|
||||
- Modify: `reikhelm-core/src/passes/mod.rs` — append `pub mod bsp;` and re-export `BspPartition`, `BspConfig`.
|
||||
|
||||
## What to Build
|
||||
A binary-space-partition partitioner (spec §4.7). Recursively splits the map rectangle into leaves, randomly choosing split axis and position within configured limits, stopping when a leaf can't be split without violating the minimum leaf size or when max depth is reached. For each final leaf it adds a **placeholder Room region** (`kind: Room`, `bounds: <leaf rect>`, `cells: []`) to `ctx.regions` via `ctx.add_region`. It does not carve tiles — the grid stays all `Wall`. All randomness comes from the passed `rng`.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Pass`, `GenContext`, `Rng` (Task 6); `Rect` (Task 2).
|
||||
- **Produces:**
|
||||
- `struct BspConfig { min_leaf: i32, max_depth: u32 }` (+ `Default`).
|
||||
- `struct BspPartition { /* holds BspConfig */ }` — `new(cfg: BspConfig) -> Self`; `impl Pass` with `name() == "bsp_partition"`.
|
||||
- **Writes to context:** one placeholder Room region per leaf in `ctx.regions`.
|
||||
|
||||
## Tests
|
||||
- After running on an empty `GenContext` (grid all `Wall`), `ctx.regions` is non-empty and every region is `kind: Room`.
|
||||
- Every leaf's `bounds` lies fully within the map and respects `min_leaf` (no dimension below the minimum).
|
||||
- Leaves do not overlap and stay within map bounds.
|
||||
- Determinism: same seed/sub-stream → identical leaf set (compare region bounds).
|
||||
- `max_depth: 0` yields a single leaf (the whole map).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `BspPartition` implements `Pass` and populates `ctx.regions` with non-overlapping in-bounds Room placeholders respecting `min_leaf`/`max_depth`.
|
||||
- [ ] Carves no tiles.
|
||||
- [ ] `pub mod bsp;` appended to `passes/mod.rs`.
|
||||
- [ ] Tests pass: `cargo test -p reikhelm-core bsp`
|
||||
|
||||
## Commit
|
||||
`"feat(core): BspPartition pass"`
|
||||
41
.dev/2026-05-28-procgen-map-core/tasks/08-room-carver.md
Normal file
41
.dev/2026-05-28-procgen-map-core/tasks/08-room-carver.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Task 8: RoomCarver Pass
|
||||
|
||||
**Depends on:** Task 6
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.7 (Shaper role), §5 (data flow).
|
||||
- `.dev/2026-05-28-procgen-map-core/README.md` — inter-pass data contract (RoomCarver row).
|
||||
- `reikhelm-core/src/pass.rs` — `Pass`, `GenContext` (Task 6).
|
||||
- `reikhelm-core/src/region.rs`, `reikhelm-core/src/map.rs` — `Region`, `RegionKind::Room`, `Tile::Floor`.
|
||||
- `reikhelm-core/src/passes/mod.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/passes/room.rs` — `RoomConfig`, `RoomCarver`.
|
||||
- Modify: `reikhelm-core/src/passes/mod.rs` — append `pub mod room;` and re-export `RoomCarver`, `RoomConfig`.
|
||||
|
||||
## What to Build
|
||||
A shaper (spec §4.7) that turns BSP leaves into actual rooms. For each existing `Room` region (the placeholders from `BspPartition`), it picks a room rectangle that fits inside the leaf `bounds` (random size within `RoomConfig` limits, leaving a margin from the leaf edge so corridors and walls have room), carves `Tile::Floor` into `ctx.tiles` over that rectangle, and updates the region's `bounds` to the actual room rect and fills `cells` with the carved points. A leaf too small to hold a `min_size` room is skipped gracefully: its region is **left in place with empty `cells` (never removed)** — removal would break the `RegionId`-as-index invariant (Task 5). A Room region with empty `cells` denotes "not actually a room"; downstream room consumers (MstConnect, CorridorCarver) treat only Room regions with non-empty `cells` as real rooms. (In the assembled dungeon recipe this case never fires — Task 12's config validation guarantees every leaf fits a room — but the pass must handle it without panicking per spec §8.)
|
||||
|
||||
This pass reads/writes `ctx.regions` and `ctx.tiles` only; it does **not** depend on `BspPartition`'s code, only on the documented "placeholder Room regions" precondition — so tests construct that precondition directly.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Pass`, `GenContext`, `Rng` (Task 6); `Region`, `Tile` (Task 5).
|
||||
- **Produces:**
|
||||
- `struct RoomConfig { min_size: i32, max_size: i32, margin: i32 }` (+ `Default`).
|
||||
- `struct RoomCarver { /* holds RoomConfig */ }` — `new(cfg) -> Self`; `impl Pass` with `name() == "room_carver"`.
|
||||
- **Writes to context:** `Floor` tiles; finalized Room region `bounds`/`cells`.
|
||||
|
||||
## Tests
|
||||
- Given a `GenContext` with two placeholder Room regions over a `Wall` grid, after running: each carved room's `bounds` lies within its original leaf bounds and within the map; the floor cell count matches `bounds` area.
|
||||
- Carved floors do not extend outside the originating leaf (margin respected).
|
||||
- A leaf smaller than `min_size` is skipped without panicking.
|
||||
- Determinism: same seed/sub-stream → identical carved rooms.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `RoomCarver` carves `Floor` rooms inside leaf bounds, respecting size/margin, and finalizes region `bounds`/`cells`.
|
||||
- [ ] Undersized leaves are handled gracefully (no panic).
|
||||
- [ ] `pub mod room;` appended to `passes/mod.rs`.
|
||||
- [ ] Tests pass: `cargo test -p reikhelm-core room`
|
||||
|
||||
## Commit
|
||||
`"feat(core): RoomCarver pass"`
|
||||
40
.dev/2026-05-28-procgen-map-core/tasks/09-mst-connect.md
Normal file
40
.dev/2026-05-28-procgen-map-core/tasks/09-mst-connect.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Task 9: MstConnect Pass
|
||||
|
||||
**Depends on:** Task 6
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.7 (Connector role), §4.5 (ConnGraph), §5 (data flow).
|
||||
- `.dev/2026-05-28-procgen-map-core/README.md` — inter-pass data contract (MstConnect row).
|
||||
- `reikhelm-core/src/pass.rs` — `Pass`, `GenContext` (Task 6).
|
||||
- `reikhelm-core/src/region.rs` — `Region`, `RegionKind::Room`, `ConnGraph`, `Edge` (Task 5).
|
||||
- `reikhelm-core/src/passes/mod.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/passes/connect.rs` — `ConnectConfig`, `MstConnect`.
|
||||
- Modify: `reikhelm-core/src/passes/mod.rs` — append `pub mod connect;` and re-export `MstConnect`, `ConnectConfig`.
|
||||
|
||||
## What to Build
|
||||
A connector (spec §4.7) that decides **which rooms connect**, without carving anything. It reads the real `Room` regions — those with `kind == Room` **and non-empty `cells`** (an empty-`cells` Room is an uncarved leaf; see Task 8) — builds a minimum spanning tree over their centers (edge weight = distance between centers, e.g. Manhattan), then adds a configurable fraction of extra edges between nearby rooms to create loops (so the dungeon isn't a strict tree). Each connection is pushed to `ctx.graph` as an `Edge { a, b, at: None }` — the door location is filled later by `DoorPlacer`. Operates only on `ctx.regions`/`ctx.graph`; carves no tiles.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Pass`, `GenContext`, `Rng` (Task 6); `Region`, `RegionKind`, `ConnGraph` (Task 5).
|
||||
- **Produces:**
|
||||
- `struct ConnectConfig { extra_edge_ratio: f64 }` (+ `Default`; `0.0` = pure MST).
|
||||
- `struct MstConnect { /* holds ConnectConfig */ }` — `new(cfg) -> Self`; `impl Pass` with `name() == "mst_connect"`.
|
||||
- **Writes to context:** MST + extra edges in `ctx.graph`, all with `at: None`.
|
||||
|
||||
## Tests
|
||||
- Given a `GenContext` with N (≥2) Room regions, after running: the MST portion has exactly N−1 edges and connects all rooms (graph is connected — verify by union-find/BFS over edges).
|
||||
- `extra_edge_ratio: 0.0` produces exactly N−1 edges; a positive ratio adds the expected extra count.
|
||||
- Every edge references valid `RegionId`s of `Room` regions and has `at == None`.
|
||||
- A single room (N=1) produces zero edges without panic.
|
||||
- Determinism: same seed/sub-stream → identical edge set.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `MstConnect` builds a connected MST over room centers plus configurable loop edges, all with `at: None`.
|
||||
- [ ] Carves no tiles; edge endpoints are valid Room region ids.
|
||||
- [ ] `pub mod connect;` appended to `passes/mod.rs`.
|
||||
- [ ] Tests pass: `cargo test -p reikhelm-core connect`
|
||||
|
||||
## Commit
|
||||
`"feat(core): MstConnect pass"`
|
||||
41
.dev/2026-05-28-procgen-map-core/tasks/10-corridor-carver.md
Normal file
41
.dev/2026-05-28-procgen-map-core/tasks/10-corridor-carver.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Task 10: CorridorCarver Pass
|
||||
|
||||
**Depends on:** Task 6
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.7 (Connector role), §5 (data flow).
|
||||
- `.dev/2026-05-28-procgen-map-core/README.md` — inter-pass data contract (CorridorCarver row).
|
||||
- `reikhelm-core/src/pass.rs` — `Pass`, `GenContext` (Task 6).
|
||||
- `reikhelm-core/src/region.rs`, `reikhelm-core/src/map.rs` — `ConnGraph`, `Region`, `RegionKind::Corridor`, `Tile::Floor`.
|
||||
- `reikhelm-core/src/geometry.rs` — `Line`, `Point` (Task 2).
|
||||
- `reikhelm-core/src/passes/mod.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/passes/corridor.rs` — `CorridorCarver`.
|
||||
- Modify: `reikhelm-core/src/passes/mod.rs` — append `pub mod corridor;` and re-export `CorridorCarver`.
|
||||
|
||||
## What to Build
|
||||
A connector (spec §4.7) that physically carves the connections chosen by `MstConnect`. For each edge in `ctx.graph`, carve an L-shaped corridor (one horizontal run + one vertical run, joined at an elbow; randomize which leg comes first) of `Tile::Floor` between the two rooms' centers. Append a `Region { kind: Corridor, .. }` describing each carved corridor's cells. All grid writes go through bounds-safe accessors (never panic). Carving over an existing `Floor` cell is fine (corridors may pass through or merge).
|
||||
|
||||
This pass does not set door tiles or `edge.at` — that's `DoorPlacer`. It reads `ctx.graph` + Room region centers and writes `ctx.tiles` + Corridor regions. Tests construct the precondition (rooms + a graph) directly.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Pass`, `GenContext`, `Rng` (Task 6); `ConnGraph`, `Region`, `Tile` (Task 5); `Line`, `Point` (Task 2).
|
||||
- **Produces:**
|
||||
- `struct CorridorCarver { /* unit or small config; Default */ }` — `default()`/`new()`; `impl Pass` with `name() == "corridor_carver"`.
|
||||
- **Writes to context:** `Floor` corridors in `ctx.tiles`; one `Corridor` region per edge.
|
||||
|
||||
## Tests
|
||||
- Given two rooms (carved Floor rects) and one graph edge between them, after running: a contiguous `Floor` path connects the two room interiors (verify by flood-fill: both room centers are in the same connected floor component).
|
||||
- An L-corridor's cells are all in-bounds; carving never panics even if a center is near the map edge.
|
||||
- One `Corridor` region is added per edge.
|
||||
- Determinism: same seed/sub-stream → identical carved corridors.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `CorridorCarver` carves L-shaped `Floor` corridors for every graph edge and adds Corridor regions; rooms become floor-connected.
|
||||
- [ ] Bounds-safe; sets no doors and does not touch `edge.at`.
|
||||
- [ ] `pub mod corridor;` appended to `passes/mod.rs`.
|
||||
- [ ] Tests pass: `cargo test -p reikhelm-core corridor`
|
||||
|
||||
## Commit
|
||||
`"feat(core): CorridorCarver pass"`
|
||||
42
.dev/2026-05-28-procgen-map-core/tasks/11-door-placer.md
Normal file
42
.dev/2026-05-28-procgen-map-core/tasks/11-door-placer.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Task 11: DoorPlacer Pass
|
||||
|
||||
**Depends on:** Task 6
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.7 (Placer role), §5 (data flow).
|
||||
- `.dev/2026-05-28-procgen-map-core/README.md` — inter-pass data contract (DoorPlacer row).
|
||||
- `reikhelm-core/src/pass.rs` — `Pass`, `GenContext` (Task 6).
|
||||
- `reikhelm-core/src/map.rs`, `reikhelm-core/src/region.rs` — `Tile`, `Region`, `ConnGraph`, `Edge` (Task 5).
|
||||
- `reikhelm-core/src/grid.rs` — neighbor queries (Task 3).
|
||||
- `reikhelm-core/src/passes/mod.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/passes/door.rs` — `DoorPlacer`.
|
||||
- Modify: `reikhelm-core/src/passes/mod.rs` — append `pub mod door;` and re-export `DoorPlacer`.
|
||||
|
||||
## What to Build
|
||||
A placer (spec §4.7) that puts doors where a corridor meets a room. Scan for boundary `Wall` cells that sit between a room interior and a corridor (a `Wall` whose opposite neighbors are both `Floor`, one belonging to a Room region and one to a Corridor region) and convert them to `Tile::Door`. For each graph edge, record a representative door location into `edge.at = Some(point)` via `ConnGraph::edges_mut()` (Task 5). Use `ctx.regions` to distinguish room floor from corridor floor (membership by `cells`/`bounds`), and `Grid` neighbor queries for adjacency. Bounds-safe; idempotent enough that it won't create doors mid-corridor or mid-room.
|
||||
|
||||
This is the final pass; it reads `ctx.tiles`/`ctx.regions`/`ctx.graph` and writes `Door` tiles + populates `edge.at`. Tests construct a small room+corridor+wall layout directly.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Pass`, `GenContext`, `Rng` (Task 6); `Tile`, `Region`, `ConnGraph`, `Edge` (Task 5); `Grid` neighbor queries (Task 3).
|
||||
- **Produces:**
|
||||
- `struct DoorPlacer { /* unit or small config; Default */ }` — `default()`/`new()`; `impl Pass` with `name() == "door_placer"`.
|
||||
- **Writes to context:** `Door` tiles at room↔corridor boundaries; `edge.at = Some(point)` for connected edges.
|
||||
|
||||
## Tests
|
||||
- Given a hand-built room rect + a corridor reaching its wall (with a single wall cell between room floor and corridor floor), after running: that wall cell becomes `Door`.
|
||||
- No `Door` is placed in the middle of a room or corridor (only at boundaries).
|
||||
- At least one connected `edge` has `at == Some(_)` after running on a connected layout.
|
||||
- Bounds-safe: never panics on doors near the map edge.
|
||||
- Determinism: same input → identical door placement.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `DoorPlacer` converts room↔corridor boundary walls to `Door` and sets `edge.at`.
|
||||
- [ ] No spurious doors inside rooms/corridors; bounds-safe.
|
||||
- [ ] `pub mod door;` appended to `passes/mod.rs`.
|
||||
- [ ] Tests pass: `cargo test -p reikhelm-core door`
|
||||
|
||||
## Commit
|
||||
`"feat(core): DoorPlacer pass"`
|
||||
44
.dev/2026-05-28-procgen-map-core/tasks/12-dungeon-recipe.md
Normal file
44
.dev/2026-05-28-procgen-map-core/tasks/12-dungeon-recipe.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Task 12: Dungeon Recipe + Integration Tests
|
||||
|
||||
**Depends on:** Task 7, Task 8, Task 9, Task 10, Task 11
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §4.8 (recipes), §8 (error handling), §9 (testing), §10 (acceptance).
|
||||
- `.dev/2026-05-28-procgen-map-core/README.md` — inter-pass data contract (the full chain).
|
||||
- `reikhelm-core/src/pass.rs` — `Pipeline` (Task 6).
|
||||
- `reikhelm-core/src/passes/mod.rs` — all five passes + their configs (Tasks 7–11).
|
||||
- `reikhelm-core/src/lib.rs` — append the module declaration here.
|
||||
|
||||
## Files
|
||||
- Create: `reikhelm-core/src/recipes/mod.rs` — recipes module root; append `pub mod dungeon;`.
|
||||
- Create: `reikhelm-core/src/recipes/dungeon.rs` — `DungeonConfig`, `ConfigError`, `dungeon()`; integration tests.
|
||||
- Modify: `reikhelm-core/src/lib.rs` — append `pub mod recipes;`.
|
||||
|
||||
## What to Build
|
||||
The dungeon recipe (spec §4.8): a function that validates a config and assembles the full pipeline — `BspPartition` → `RoomCarver` → `MstConnect` → `CorridorCarver` → `DoorPlacer` — sized to the config. `DungeonConfig` composes the per-pass configs and the map dimensions, with sensible `Default`s. Validation (spec §8) runs at recipe construction: dimensions > 0, and the room min size must be able to fit within the smallest possible BSP leaf (`min_leaf`), returning `ConfigError` rather than panicking. This task also carries the cross-pass **integration tests** (spec §9) that prove the whole chain works.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `Pipeline` (Task 6); all five passes + configs (Tasks 7–11).
|
||||
- **Produces:**
|
||||
- `struct DungeonConfig { width: u32, height: u32, bsp: BspConfig, rooms: RoomConfig, connect: ConnectConfig }` (+ `Default`).
|
||||
- `enum ConfigError { /* e.g. ZeroDimension, RoomLargerThanLeaf, ... */ }` (impl `std::error::Error`/`Display`).
|
||||
- `fn dungeon(cfg: DungeonConfig) -> Result<Pipeline, ConfigError>`.
|
||||
|
||||
## Tests (integration — spec §9)
|
||||
- **Determinism:** `dungeon(cfg)?.run(42)` twice produces equal `Map`s and identical `to_ascii()`.
|
||||
- **`run` == `run_with_snapshots`:** same seed yields the same `Map`; snapshot count == 5.
|
||||
- **Connectivity:** flood-fill the `Floor`/`Door` tiles of a generated dungeon; assert every `Room` region's interior is reachable (no orphaned rooms).
|
||||
- **Bounds:** no non-`Wall` tile lies outside the map; all rooms within bounds.
|
||||
- **Config validation:** zero dimensions and a room-min-size larger than `min_leaf` both return `Err(ConfigError)`, not a panic.
|
||||
- **Door reachability sanity:** at least one `Door` tile exists in a multi-room dungeon, and connected edges have `at == Some(_)`.
|
||||
- (Optional) print one ASCII dungeon in a `#[test]` behind `--nocapture` for eyeball confirmation.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `dungeon(cfg)` validates config and returns a `Pipeline` (or `ConfigError`) assembling all five passes in order.
|
||||
- [ ] A generated dungeon is fully connected, in-bounds, deterministic, and has doors.
|
||||
- [ ] `run` and `run_with_snapshots` agree; snapshot count is 5.
|
||||
- [ ] Module declarations added to `lib.rs`/`recipes/mod.rs`.
|
||||
- [ ] All tests pass: `cargo test -p reikhelm-core` (full suite green)
|
||||
|
||||
## Commit
|
||||
`"feat(core): dungeon recipe + end-to-end integration tests"`
|
||||
38
.dev/2026-05-28-procgen-map-core/tasks/13-viz-renderer.md
Normal file
38
.dev/2026-05-28-procgen-map-core/tasks/13-viz-renderer.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Task 13: reikhelm-viz Renderer
|
||||
|
||||
**Depends on:** Task 5, Task 12
|
||||
|
||||
## Context Files
|
||||
- `.dev/2026-05-28-procgen-map-core/spec/design.md` — §6 (viz crate), §3 (`Map` contract — renderer reads data only).
|
||||
- `reikhelm-core/src/map.rs` — `Map`, `Tile` (Task 5).
|
||||
- `reikhelm-core/src/recipes/dungeon.rs` — `dungeon()`, `DungeonConfig` (Task 12).
|
||||
- `reikhelm-core/src/pass.rs` — `Pipeline::run` / `run_with_snapshots` (Task 6).
|
||||
- `reikhelm-viz/Cargo.toml` — `macroquad` dependency (Task 1).
|
||||
|
||||
## Files
|
||||
- Modify: `reikhelm-viz/src/main.rs` — macroquad window rendering a `Map`.
|
||||
|
||||
## What to Build
|
||||
The interactive renderer (spec §6). A macroquad binary that builds a dungeon from the `dungeon` recipe at a fixed `DungeonConfig`, then each frame draws one colored rectangle per cell, colored by `Tile` (e.g. `Wall` dark, `Floor` light, `Door` accent). Pressing the reroll key (Space) increments the seed and regenerates; the current seed is drawn on screen. Cell pixel size derives from window size / map dimensions.
|
||||
|
||||
**Stretch (in scope if straightforward):** generate via `run_with_snapshots` and let Left/Right arrows scrub through the generation stages, drawing the snapshot at the current index (and a label). Because snapshots carry regions + edges too, the partition and connect stages are visible even though they don't change tiles — draw region outlines / edges for those frames.
|
||||
|
||||
The renderer must contain **no generation logic** and reach into no engine internals beyond the public `Map`/recipe API (spec §3, §6) — that boundary is the proof the architecture holds.
|
||||
|
||||
## Interface Dependencies
|
||||
- **Consumes:** `dungeon()`, `DungeonConfig` (Task 12); `Map`, `Tile` (Task 5); `Pipeline::run`/`run_with_snapshots` (Task 6).
|
||||
- **Produces:** the `reikhelm-viz` binary.
|
||||
|
||||
## Tests
|
||||
- Rendering is interactive/visual — no unit tests required. Validation is manual via the success criteria below.
|
||||
- (Optional) a `color_of(Tile) -> Color` helper can have a trivial unit test if extracted.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `cargo run -p reikhelm-viz` opens a window showing a dungeon as colored tiles.
|
||||
- [ ] Pressing Space generates a new dungeon (new seed); the seed is visible on screen.
|
||||
- [ ] `reikhelm-viz` contains no generation logic and uses only `reikhelm-core`'s public API.
|
||||
- [ ] `cargo build` of the whole workspace succeeds.
|
||||
- [ ] (Stretch) Left/Right scrub generation stages, with partition/connect stages visibly distinct.
|
||||
|
||||
## Commit
|
||||
`"feat(viz): macroquad dungeon renderer with seed reroll"`
|
||||
51
.dev/conventions.md
Normal file
51
.dev/conventions.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Project Conventions — reikhelm
|
||||
|
||||
Ground truth every dispatched agent reads. Plan lives at `.dev/2026-05-28-procgen-map-core/`.
|
||||
|
||||
## Toolchain & environment (CRITICAL)
|
||||
|
||||
- Rust **1.96.0** stable, `aarch64-apple-darwin`, via rustup.
|
||||
- **`~/.cargo/bin` is NOT on the non-interactive shell PATH.** Every shell command that runs `cargo`, `rustc`, or `rustup` MUST first export it:
|
||||
```sh
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
```
|
||||
Put this at the front of the same Bash invocation as your cargo command. If you forget it you'll get `command not found: cargo`.
|
||||
- Run all cargo commands from the workspace root `/Users/caspar/Projects/reikhelm`.
|
||||
|
||||
## Dependency versions
|
||||
|
||||
`rand`/`rand_chacha` are pinned by Task 1 — check `reikhelm-core/Cargo.toml` for the exact major version before writing RNG code. **rand 0.9 changed the API vs 0.8** (`gen_range`→`random_range`, `thread_rng`→`rng`, `SeedableRng` still `from_seed`/`seed_from_u64`). Match the API to the resolved version; `cargo build` is the arbiter.
|
||||
|
||||
## Git
|
||||
|
||||
- Repo is initialized in Task 1; work happens on the **default branch** (no feature branch — greenfield bring-up, explicitly authorized by the session goal).
|
||||
- Each task commits with the exact message given in its task file's `## Commit` section.
|
||||
- End every commit message body with this trailer (on its own line, preceded by a blank line):
|
||||
```
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
```
|
||||
- Commit only the files your task created/modified. Do not commit `/target` (gitignored).
|
||||
|
||||
## Module wiring ownership
|
||||
|
||||
- A task that CREATES a module file also appends its own `pub mod <name>;` to the parent (`lib.rs` or `passes/mod.rs` or `recipes/mod.rs`) — EXCEPT the parallel pass batch (Tasks 7–11): for those, the orchestrator pre-wires `passes/mod.rs` and pre-creates empty stub files, so pass implementers ONLY edit their own pass file and never touch `mod.rs`.
|
||||
|
||||
## Determinism rules (load-bearing — spec §7)
|
||||
|
||||
- All generation randomness flows through `reikhelm_core::rng::Rng` (ChaCha8). Never `thread_rng`/`rand::random()`.
|
||||
- **Never `std::collections::hash_map::DefaultHasher`** for anything that affects generation — it's version-unstable. RNG `fork` uses an explicit integer mix (splitmix64 / FNV-style).
|
||||
- No `HashMap`/`HashSet` *iteration order* may influence generated output (use sorted/`BTreeMap` or index order where order matters).
|
||||
- Same seed → byte-identical `Map`, every run, every machine.
|
||||
|
||||
## Rust style
|
||||
|
||||
- Derive traits rather than hand-impl where possible (`Clone, Copy, Debug, PartialEq, Eq`, serde).
|
||||
- Public items get a `///` doc comment. Keep functions small and total — no panics on valid input.
|
||||
- Grid writes go through bounds-safe accessors; OOB is a no-op, never a panic (spec §8).
|
||||
- Prefer iterators/combinators over index loops where it reads cleanly. This is a learning-Rust project: favor clear, idiomatic code over clever code.
|
||||
- Tests live in an inline `#[cfg(test)] mod tests { use super::*; ... }` at the bottom of the module file.
|
||||
|
||||
## Reporting (for dispatched implementers)
|
||||
|
||||
End your report with a STATUS line: `DONE`, `DONE_WITH_CORRECTIONS`, `NEEDS_CONTEXT`, or `BLOCKED`.
|
||||
Paste the **actual** `cargo test` output (not "tests pass"). List every file you created/modified. If you deviated from the task spec, say why.
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
696
Cargo.lock
generated
Normal file
696
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "fontdue"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-traits",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
||||
|
||||
[[package]]
|
||||
name = "macroquad"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f7d60318b52b19e909db1f01c522977da28a5e515127a73ecd8b7488498edb"
|
||||
dependencies = [
|
||||
"fontdue",
|
||||
"glam",
|
||||
"image",
|
||||
"macroquad_macro",
|
||||
"miniquad",
|
||||
"quad-rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macroquad_macro"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd"
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
|
||||
[[package]]
|
||||
name = "miniquad"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f64fd94ff70fdc425766c78a3fa9f263d02cb25725a49f255f6a66d8a186c3f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"ndk-sys",
|
||||
"objc-rs",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-rs"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quad-rand"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "reikhelm-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reikhelm-viz"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"macroquad",
|
||||
"reikhelm-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.11.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["reikhelm-core", "reikhelm-viz"]
|
||||
9
reikhelm-core/Cargo.toml
Normal file
9
reikhelm-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "reikhelm-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.10.1"
|
||||
rand_chacha = "0.10.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
4
reikhelm-core/src/lib.rs
Normal file
4
reikhelm-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
//! `reikhelm-core` — deterministic procedural 2D map generation.
|
||||
//!
|
||||
//! This crate holds the pure generation logic with no rendering or GUI
|
||||
//! dependencies. Modules are added by later tasks.
|
||||
8
reikhelm-viz/Cargo.toml
Normal file
8
reikhelm-viz/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "reikhelm-viz"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
macroquad = "0.4.15"
|
||||
reikhelm-core = { version = "0.1.0", path = "../reikhelm-core" }
|
||||
7
reikhelm-viz/src/main.rs
Normal file
7
reikhelm-viz/src/main.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! `reikhelm-viz` — visualization binary for generated maps.
|
||||
//!
|
||||
//! This is a placeholder entry point; Task 13 replaces it with the real
|
||||
//! macroquad-based viewer.
|
||||
fn main() {
|
||||
println!("reikhelm-viz: placeholder");
|
||||
}
|
||||
Loading…
Reference in a new issue