# 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"`
