175 lines
6.7 KiB
Rust
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)));
|
|
}
|
|
}
|