reikhelm/reikhelm-core/src/region.rs
Parley Hatch 659ef00017 feat(core): data model + Map output contract + ASCII renderer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:10:47 -06:00

175 lines
6.7 KiB
Rust

//! Semantic regions and connectivity (spec §4.5).
//!
//! Where [`crate::grid::Grid`] answers *what tile is at a cell*, this module
//! answers *what areas exist and how they connect*. A [`Region`] is a named
//! semantic area (a room, a corridor) with a bounding box and the exact set of
//! cells it occupies; a [`ConnGraph`] records which regions are linked and,
//! once doors are placed, where the link physically sits.
//!
//! The shared vocabulary here is deliberately map-type agnostic: a dungeon is
//! rooms joined by corridor edges, while a future town would be districts and
//! buildings joined by road edges — same types, different passes producing
//! them.
//!
//! All types derive `serde::{Serialize, Deserialize}` so they can travel inside
//! a [`crate::map::Map`] through JSON (spec §3).
use serde::{Deserialize, Serialize};
use crate::geometry::{Point, Rect};
/// A handle to a [`Region`], equal to its index in the region list.
///
/// **Invariant:** `RegionId(n)` is always the index `n` of that region in
/// `ctx.regions` / [`crate::map::Map::regions`]. Regions are append-only — never
/// removed or reordered — so this id-as-index relationship holds for the life of
/// the map. Edges in the [`ConnGraph`] and any lookup that turns an id back into
/// a region rely on it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RegionId(pub usize);
/// What kind of semantic area a [`Region`] represents.
///
/// Starts minimal; new variants are additive (later: `District`, `Plaza`,
/// `Building`). Passes match only on the kinds they care about.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegionKind {
/// A room: a carved, typically rectangular open area.
Room,
/// A corridor: a thin passage carved to connect rooms.
Corridor,
}
/// A semantic area of the map: an id, a kind, a bounding box, and its cells.
///
/// `bounds` is the axis-aligned bounding box; `cells` is the exact set of
/// occupied cells, which lets non-rectangular regions (e.g. cave blobs) be
/// represented faithfully rather than only as rectangles. Fields are `pub`
/// because passes and visualization read them directly across modules.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Region {
/// This region's stable id, equal to its index in the region list.
pub id: RegionId,
/// The semantic category of this region.
pub kind: RegionKind,
/// The axis-aligned bounding box enclosing all of `cells`.
pub bounds: Rect,
/// The exact cells this region occupies, in the order they were added.
pub cells: Vec<Point>,
}
/// A connection between two regions in the [`ConnGraph`].
///
/// `a` and `b` are the linked regions. `at` is the physical location of the
/// connection (the door cell): it is [`None`] when the edge is first created by
/// `MstConnect`, and is filled in later by `DoorPlacer`. Refines spec §4.5,
/// where `at` was sketched as a bare `Point` — the door location simply is not
/// known until placement runs. Fields are `pub` because `DoorPlacer` writes `at`
/// and visualization reads all three.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edge {
/// One endpoint region.
pub a: RegionId,
/// The other endpoint region.
pub b: RegionId,
/// Where the connection physically sits, once known (e.g. the door cell).
pub at: Option<Point>,
}
/// The region connectivity graph: nodes are [`RegionId`]s, edges are [`Edge`]s.
///
/// Built incrementally — `MstConnect` adds the edges that should exist (with no
/// location yet), then `DoorPlacer` populates each edge's `at`. The graph stores
/// edges in insertion order, which is deterministic and the order
/// [`ConnGraph::edges`] yields them in.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConnGraph {
edges: Vec<Edge>,
}
impl ConnGraph {
/// Creates an empty connectivity graph.
pub fn new() -> Self {
ConnGraph::default()
}
/// Records that regions `a` and `b` are connected.
///
/// The new edge's location is left as `at: None`; `DoorPlacer` fills it in
/// later. Edges are appended, so the order they were added is preserved.
pub fn add_edge(&mut self, a: RegionId, b: RegionId) {
self.edges.push(Edge { a, b, at: None });
}
/// Returns the edges in insertion order.
pub fn edges(&self) -> &[Edge] {
&self.edges
}
/// Returns the edges mutably, so `DoorPlacer` can populate each `at`.
pub fn edges_mut(&mut self) -> &mut [Edge] {
&mut self.edges
}
/// Iterates the regions directly connected to `id`.
///
/// For every edge touching `id`, yields the *other* endpoint. An undirected
/// link `{a, b}` therefore appears from both sides: `neighbors(a)` yields `b`
/// and `neighbors(b)` yields `a`. Self-loops (`a == b == id`) yield `id`. The
/// iteration order follows the edges' insertion order.
pub fn neighbors(&self, id: RegionId) -> impl Iterator<Item = RegionId> + '_ {
self.edges.iter().filter_map(move |e| {
if e.a == id {
Some(e.b)
} else if e.b == id {
Some(e.a)
} else {
None
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_edge_stores_none_location() {
let mut g = ConnGraph::new();
g.add_edge(RegionId(0), RegionId(1));
assert_eq!(g.edges().len(), 1);
let e = g.edges()[0];
assert_eq!(e.a, RegionId(0));
assert_eq!(e.b, RegionId(1));
assert_eq!(e.at, None, "MstConnect leaves the door location unknown");
}
#[test]
fn neighbors_returns_both_endpoints_counterparts() {
let mut g = ConnGraph::new();
g.add_edge(RegionId(0), RegionId(1));
g.add_edge(RegionId(1), RegionId(2));
// From region 1 we can reach both of its neighbors.
let n1: Vec<RegionId> = g.neighbors(RegionId(1)).collect();
assert_eq!(n1, vec![RegionId(0), RegionId(2)]);
// The link is undirected: each endpoint sees the other.
let n0: Vec<RegionId> = g.neighbors(RegionId(0)).collect();
assert_eq!(n0, vec![RegionId(1)]);
let n2: Vec<RegionId> = g.neighbors(RegionId(2)).collect();
assert_eq!(n2, vec![RegionId(1)]);
}
#[test]
fn edges_mut_lets_door_placer_populate_location() {
let mut g = ConnGraph::new();
g.add_edge(RegionId(0), RegionId(1));
// Simulate DoorPlacer writing the door cell into the edge.
g.edges_mut()[0].at = Some(Point::new(4, 7));
assert_eq!(g.edges()[0].at, Some(Point::new(4, 7)));
}
}