//! 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, } /// 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, } /// 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, } 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 + '_ { 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 = 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 = g.neighbors(RegionId(0)).collect(); assert_eq!(n0, vec![RegionId(1)]); let n2: Vec = 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))); } }