commit 08e99e38a3a6aed29b7e00bcfa8099b4af54d681 Author: Parley Hatch Date: Thu May 28 20:56:24 2026 -0600 chore: scaffold reikhelm cargo workspace (core + viz crates) Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.dev/2026-05-28-procgen-map-core/README.md b/.dev/2026-05-28-procgen-map-core/README.md new file mode 100644 index 0000000..57691a5 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/README.md @@ -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` → 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: , 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 ;`. 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 ;` + `pub use`. The pass tasks run in parallel, so the orchestrator merges these append-only lines at integration. diff --git a/.dev/2026-05-28-procgen-map-core/dependencies.md b/.dev/2026-05-28-procgen-map-core/dependencies.md new file mode 100644 index 0000000..0e7ead1 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/dependencies.md @@ -0,0 +1,37 @@ +# Dependencies + +``` +Task 1: Workspace scaffold (no deps) +├── Task 2: Geometry (1) +│ └── Task 3: Grid (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` 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. diff --git a/.dev/2026-05-28-procgen-map-core/files.md b/.dev/2026-05-28-procgen-map-core/files.md new file mode 100644 index 0000000..fb4cb39 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/files.md @@ -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` 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 ;` by Tasks 2, 3, 4, 5, 6, 12. +- `reikhelm-core/src/passes/mod.rs` — appended with `pub mod ;` by Tasks 7–11 (parallel; orchestrator merges). diff --git a/.dev/2026-05-28-procgen-map-core/spec/design.md b/.dev/2026-05-28-procgen-map-core/spec/design.md new file mode 100644 index 0000000..24f926c --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/spec/design.md @@ -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 +│ ├── 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, // base terrain layer: what occupies each cell + pub regions: Vec, // 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 { width: u32, height: u32, cells: Vec } +``` +- 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` is the map; `Grid`/`Grid` 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, // 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 { 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, + pub regions: Vec, + 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> } +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); +} +``` +- 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 = "#"` 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::>("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`. +- **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=Wall, ... } + root Rng::from_seed(seed) + │ each pass below receives ctx + its own forked sub-stream (root.fork("#")) + │ + ├─ 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). diff --git a/.dev/2026-05-28-procgen-map-core/state/progress.md b/.dev/2026-05-28-procgen-map-core/state/progress.md new file mode 100644 index 0000000..59b52f3 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/state/progress.md @@ -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. diff --git a/.dev/2026-05-28-procgen-map-core/state/smell-ledger.md b/.dev/2026-05-28-procgen-map-core/state/smell-ledger.md new file mode 100644 index 0000000..3efa715 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/state/smell-ledger.md @@ -0,0 +1,5 @@ +# Smell Ledger — reikhelm + +Patterns surfaced by implementers/reviewers. Format: pattern — where found — where fixed — where it remains. + +_(none yet)_ diff --git a/.dev/2026-05-28-procgen-map-core/tasks/01-workspace-scaffold.md b/.dev/2026-05-28-procgen-map-core/tasks/01-workspace-scaffold.md new file mode 100644 index 0000000..0ea3cc7 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/01-workspace-scaffold.md @@ -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)"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/02-geometry.md b/.dev/2026-05-28-procgen-map-core/tasks/02-geometry.md new file mode 100644 index 0000000..7a2e220 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/02-geometry.md @@ -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` (all contained cells). + - `Line { from: Point, to: Point }` — `cells() -> impl Iterator` 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)"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/03-grid.md b/.dev/2026-05-28-procgen-map-core/tasks/03-grid.md new file mode 100644 index 0000000..6a3ddc6 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/03-grid.md @@ -0,0 +1,42 @@ +# Task 3: Grid + +**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`. +- 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` in row-major order. Generic on purpose: it's the map (`Grid`) and also scratch buffers (`Grid`, `Grid`) 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` 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`, `neighbors8(...)` — in-bounds only + - `iter() -> impl Iterator` + - `iter_rect(Rect) -> impl Iterator` (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` 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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/04-rng.md b/.dev/2026-05-28-procgen-map-core/tasks/04-rng.md new file mode 100644 index 0000000..4485c4a --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/04-rng.md @@ -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(&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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/05-data-model.md b/.dev/2026-05-28-procgen-map-core/tasks/05-data-model.md new file mode 100644 index 0000000..6f3901a --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/05-data-model.md @@ -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`. +- `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` (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` (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 }` — **fields `pub`** (read across modules by passes and viz). + - `struct Edge { a: RegionId, b: RegionId, at: Option }` — **fields `pub`** (constructed by `MstConnect`, `at` written by `DoorPlacer`, read by viz). + - `struct ConnGraph { edges: Vec }` — `new`, `add_edge(a, b) -> ()` (pushes `at: None`), `edges() -> &[Edge]`, **`edges_mut() -> &mut [Edge]`** (so `DoorPlacer` can populate `at`), `neighbors(RegionId) -> impl Iterator`. + - `struct Map { tiles: Grid, regions: Vec, 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`. +- [ ] 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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/06-engine.md b/.dev/2026-05-28-procgen-map-core/tasks/06-engine.md new file mode 100644 index 0000000..b930430 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/06-engine.md @@ -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` filled with `Wall`. + +`Blackboard` uses **named** typed slots (spec §4.6 fix), not bare `TypeId`, so multiple passes can store the same type under different keys. + +`run_with_snapshots` must produce a `Map` **identical** to `run` for the same seed (spec §7) — snapshotting only reads context state and must not consume the RNG. Each `Snapshot` captures tiles **and** region bounds + graph edges (so partition/connect stages, which don't change tiles, are still visible to the viz scrubber). + +## Interface Dependencies +- **Consumes:** `Rng` (Task 4); `Grid`, `Tile`, `Region`, `ConnGraph`, `Edge`, `Map` (Task 5). +- **Produces:** + - `Blackboard` — `new`, `insert(&mut self, key: &str, value: T)`, `get(&self, key: &str) -> Option<&T>`, `take(&mut self, key: &str) -> Option`. + - `struct GenContext { tiles: Grid, regions: Vec, graph: ConnGraph, blackboard: Blackboard }` — plus `add_region(kind, bounds, cells) -> RegionId` (appends; assigns `RegionId(regions.len())` so id == index). Regions are append-only: there is no remove/reorder, preserving the id-as-index invariant (Task 5) that edges and lookups depend on. + - `trait Pass { fn name(&self) -> &str; fn apply(&self, ctx: &mut GenContext, rng: &mut Rng); }` + - `struct Snapshot { label: String, tiles: Grid, regions: Vec, edges: Vec }` + - `struct Pipeline` — `new(width: u32, height: u32) -> Self`, `then(self, pass: impl Pass + 'static) -> Self`, `run(&self, seed: u64) -> Map`, `run_with_snapshots(&self, seed: u64) -> (Map, Vec)`. + +## Tests +- A trivial test `Pass` (e.g. "fill all Floor") run via `Pipeline::run` produces the expected grid and a `Map` with matching `seed`/dimensions. +- **Determinism:** `run(seed)` twice yields equal `Map`s. +- **Snapshot fidelity:** `run(seed)` and `run_with_snapshots(seed)` yield equal `Map`s; snapshot count equals pass count. +- **RNG isolation:** a pipeline `[A, B]` and `[A, X, B]` (X is a no-draw pass, or draws then discards) yield the same output from A and B — i.e. inserting a pass does not change other passes' streams. (Use two test passes that record their first draw into the blackboard.) +- **Occurrence keying:** two instances of the same-named test pass receive different sub-streams. +- `Blackboard` stores/retrieves two `Vec` under different keys without collision; `get` with the wrong type returns `None`. + +## Success Criteria +- [ ] `Pass` trait signature is exactly `fn apply(&self, ctx: &mut GenContext, rng: &mut Rng)`. +- [ ] Pipeline forks a per-pass sub-stream keyed by `name#occurrence`; passes never touch a shared RNG. +- [ ] `run` and `run_with_snapshots` produce identical maps; snapshots capture tiles + regions + edges. +- [ ] `Blackboard` is name-keyed and type-checked. +- [ ] `passes/mod.rs` exists (no `pub mod` lines); module declarations added to `lib.rs`. +- [ ] All tests pass: `cargo test -p reikhelm-core` (pass/blackboard) + +## Commit +`"feat(core): generation engine — GenContext, Pass, Pipeline, Blackboard"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/07-bsp-partition.md b/.dev/2026-05-28-procgen-map-core/tasks/07-bsp-partition.md new file mode 100644 index 0000000..a7f7c06 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/07-bsp-partition.md @@ -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: `, `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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/08-room-carver.md b/.dev/2026-05-28-procgen-map-core/tasks/08-room-carver.md new file mode 100644 index 0000000..530394c --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/08-room-carver.md @@ -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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/09-mst-connect.md b/.dev/2026-05-28-procgen-map-core/tasks/09-mst-connect.md new file mode 100644 index 0000000..175db64 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/09-mst-connect.md @@ -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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/10-corridor-carver.md b/.dev/2026-05-28-procgen-map-core/tasks/10-corridor-carver.md new file mode 100644 index 0000000..8da72f8 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/10-corridor-carver.md @@ -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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/11-door-placer.md b/.dev/2026-05-28-procgen-map-core/tasks/11-door-placer.md new file mode 100644 index 0000000..7f2e4e1 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/11-door-placer.md @@ -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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/12-dungeon-recipe.md b/.dev/2026-05-28-procgen-map-core/tasks/12-dungeon-recipe.md new file mode 100644 index 0000000..576210e --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/12-dungeon-recipe.md @@ -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`. + +## 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"` diff --git a/.dev/2026-05-28-procgen-map-core/tasks/13-viz-renderer.md b/.dev/2026-05-28-procgen-map-core/tasks/13-viz-renderer.md new file mode 100644 index 0000000..c211201 --- /dev/null +++ b/.dev/2026-05-28-procgen-map-core/tasks/13-viz-renderer.md @@ -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"` diff --git a/.dev/conventions.md b/.dev/conventions.md new file mode 100644 index 0000000..3e3b325 --- /dev/null +++ b/.dev/conventions.md @@ -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) + ``` +- 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 ;` 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..964b555 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3e13537 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["reikhelm-core", "reikhelm-viz"] diff --git a/reikhelm-core/Cargo.toml b/reikhelm-core/Cargo.toml new file mode 100644 index 0000000..6db2ae3 --- /dev/null +++ b/reikhelm-core/Cargo.toml @@ -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"] } diff --git a/reikhelm-core/src/lib.rs b/reikhelm-core/src/lib.rs new file mode 100644 index 0000000..e44e64c --- /dev/null +++ b/reikhelm-core/src/lib.rs @@ -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. diff --git a/reikhelm-viz/Cargo.toml b/reikhelm-viz/Cargo.toml new file mode 100644 index 0000000..aa49374 --- /dev/null +++ b/reikhelm-viz/Cargo.toml @@ -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" } diff --git a/reikhelm-viz/src/main.rs b/reikhelm-viz/src/main.rs new file mode 100644 index 0000000..76b70e7 --- /dev/null +++ b/reikhelm-viz/src/main.rs @@ -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"); +}