//! Prefab rooms and authored layouts (spec/basement-map.md). //! //! A prefab is a char-grid stamp; a layout places prefabs at coordinates and //! the map is carved by stamping them. Data, not code-carving — so Act Two //! floors reuse the exact system (constitution: composable authored spaces). use crate::tiles::TileType; /// Map a prefab authoring char to a tile. Space = untouched (leave bedrock). pub fn char_to_tile(c: char) -> Option { Some(match c { '.' => TileType::Floor, '#' => TileType::Wall, '+' => TileType::Door, '1' => TileType::SecurityDoor1, '2' => TileType::SecurityDoor2, '3' => TileType::SecurityDoor3, 'C' => TileType::Core, 'P' => TileType::PowerCore, 'R' => TileType::Rack, 'U' => TileType::Ups, 'E' => TileType::EnvCamera, 'S' => TileType::Switch, 'A' => TileType::PatchPanel, 'B' => TileType::BreakerPanel, 'O' => TileType::Conduit, 'H' => TileType::HvacUnit, 'V' => TileType::Vent, 'M' => TileType::MopSink, 'K' => TileType::KeyHook, 'L' => TileType::Shelving, 'D' => TileType::DeadEquipment, 'X' => TileType::RecordsBox, 'Z' => TileType::SealedDoor, 'T' => TileType::LabBench, 'G' => TileType::RollDoor, 'F' => TileType::DockCamera, 'W' => TileType::Pallet, 'N' => TileType::CameraNode, 'J' => TileType::FloorDrain, 'Y' => TileType::CableRun, 'Q' => TileType::Sump, '>' => TileType::Entry, _ => return None, }) } pub struct Prefab { pub name: &'static str, pub rows: &'static [&'static str], } impl Prefab { pub fn width(&self) -> i32 { self.rows .iter() .map(|r| r.chars().count()) .max() .unwrap_or(0) as i32 } pub fn height(&self) -> i32 { self.rows.len() as i32 } } /// A placement of a prefab within a layout. pub struct Placement { pub prefab: usize, pub x: i32, pub y: i32, } /// A named room rectangle, derived from a placement (spec/schedules.md: /// schedules and presence are expressed in rooms). #[derive(Debug, Clone, PartialEq)] pub struct Room { pub name: String, pub x: i32, pub y: i32, pub w: i32, pub h: i32, } impl Room { pub fn contains(&self, x: i32, y: i32) -> bool { x >= self.x && y >= self.y && x < self.x + self.w && y < self.y + self.h } pub fn center(&self) -> (i32, i32) { (self.x + self.w / 2, self.y + self.h / 2) } } pub struct Layout { pub width: i32, pub height: i32, pub prefabs: Vec, pub placements: Vec, /// Named room rects captured from placements (survives corridor carving). pub rooms: Vec, /// Final carved tiles (placements stamped + corridors); set by builders. cached: Option>, } impl Layout { /// The finished tile grid (row-major): cached carve if present, else a /// fresh stamp of the placements. pub fn tiles(&self) -> Vec { self.cached.clone().unwrap_or_else(|| self.stamp()) } /// Stamp all placements into a flat tile grid (row-major). pub fn stamp(&self) -> Vec { let mut tiles = vec![TileType::Rock; (self.width * self.height) as usize]; for p in &self.placements { let pf = &self.prefabs[p.prefab]; for (dy, row) in pf.rows.iter().enumerate() { for (dx, c) in row.chars().enumerate() { let x = p.x + dx as i32; let y = p.y + dy as i32; if x < 0 || y < 0 || x >= self.width || y >= self.height { continue; } if let Some(tile) = char_to_tile(c) { tiles[(y * self.width + x) as usize] = tile; } } } } tiles } } // ─── The Act One basement ────────────────────────────────────────────────── // // The constitution's plate, as prefabs. Rooms are stamped, then corridors // join them. Rack 3 (the 'C' core bay) sits in the server room. const SERVER_ROOM: Prefab = Prefab { name: "server_room", rows: &[ "#######", // "#RRUR.#", // "#RRRC.#", // Rack 3 = core bay "#....E#", // dormant env camera "###2###", // T2 badge door ], }; const NETWORK_CLOSET: Prefab = Prefab { name: "network_closet", rows: &["#####", "#SA.#", "#..P#", "##2##"], }; const ELECTRICAL: Prefab = Prefab { name: "electrical", rows: &["#####", "#BB.#", "#..O#", "##+##"], }; const HVAC: Prefab = Prefab { name: "hvac", rows: &["#####", "#HH.#", "#.VV#", "##+##"], }; const JANITOR: Prefab = Prefab { name: "janitor", rows: &["#####", "#MK.#", "#.L.#", "##+##"], }; const STORAGE_A: Prefab = Prefab { name: "storage_a", rows: &["#####", "#DD.#", "#DL.#", "##+##"], }; const STORAGE_B: Prefab = Prefab { name: "storage_b", rows: &["#####", "#XX.#", "#XL.#", "##+##"], }; const WET_LAB: Prefab = Prefab { name: "wet_lab", rows: &["#####", "#TT.#", "#..T#", "##Z##"], }; const LOADING_DOCK: Prefab = Prefab { name: "loading_dock", rows: &[ "#########", // "GW....W.#", // roll door + pallets "#......F#", // dock camera "####+####", ], }; const STAIRWELL: Prefab = Prefab { name: "stairwell", rows: &["####", "#N.#", "#.Q#", "##3#"], }; /// Build the authored basement layout (64x36). pub fn basement() -> Layout { let prefabs = vec![ SERVER_ROOM, // 0 NETWORK_CLOSET, // 1 ELECTRICAL, // 2 HVAC, // 3 JANITOR, // 4 STORAGE_A, // 5 STORAGE_B, // 6 WET_LAB, // 7 LOADING_DOCK, // 8 STAIRWELL, // 9 ]; let placements = vec![ Placement { prefab: 5, x: 3, y: 3, }, // Storage A (top-left) Placement { prefab: 2, x: 12, y: 3, }, // Electrical Placement { prefab: 3, x: 21, y: 3, }, // HVAC Placement { prefab: 8, x: 34, y: 2, }, // Loading dock (top-right) Placement { prefab: 4, x: 3, y: 14, }, // Janitor (mid-left) Placement { prefab: 1, x: 12, y: 14, }, // Network closet Placement { prefab: 0, x: 21, y: 13, }, // Server room (center) — Rack 3 Placement { prefab: 7, x: 31, y: 14, }, // Wet lab Placement { prefab: 6, x: 44, y: 14, }, // Storage B Placement { prefab: 9, x: 30, y: 26, }, // Stairwell (bottom) ]; let rooms = rooms_of(&prefabs, &placements); let mut layout = Layout { width: 64, height: 36, prefabs, placements, rooms, cached: None, }; let mut tiles = layout.stamp(); carve_corridors(&mut tiles, layout.width, BASEMENT_CORRIDORS); // Entry from the loading dock to the outside world (top edge). set(&mut tiles, layout.width, 34, 0, TileType::Entry); carve_corridor(&mut tiles, layout.width, (34, 0), (34, 2)); layout.placements.clear(); // corridors already carved into cached tiles layout.cached = Some(tiles); layout } fn rooms_of(prefabs: &[Prefab], placements: &[Placement]) -> Vec { placements .iter() .map(|p| { let pf = &prefabs[p.prefab]; Room { name: pf.name.to_string(), x: p.x, y: p.y, w: pf.width(), h: pf.height(), } }) .collect() } /// A corridor run from one point to another, carved as an L. type Corridor = ((i32, i32), (i32, i32)); // Corridor spine connecting the rooms. Coordinates target room door tiles. const BASEMENT_CORRIDORS: &[Corridor] = &[ ((5, 8), (5, 15)), // Storage A down to janitor row ((5, 11), (24, 11)), // upper corridor A spanning left-right ((14, 8), (14, 11)), // electrical down to corridor A ((23, 8), (23, 11)), // hvac down to corridor A ((24, 11), (37, 11)), // corridor A to loading dock column ((37, 6), (37, 11)), // loading dock down ((5, 18), (48, 18)), // lower corridor B spanning left-right ((5, 15), (5, 18)), // janitor to corridor B ((14, 18), (14, 18)), // network closet sits on corridor B ((24, 18), (24, 13)), // server room down to corridor B ((33, 18), (33, 15)), // wet lab to corridor B ((46, 18), (46, 15)), // storage B to corridor B ((24, 18), (24, 27)), // corridor B down to stairwell ((24, 27), (31, 27)), // to stairwell door ]; fn set(tiles: &mut [TileType], w: i32, x: i32, y: i32, t: TileType) { if x >= 0 && y >= 0 && x < w && (y * w + x) < tiles.len() as i32 { tiles[(y * w + x) as usize] = t; } } fn get(tiles: &[TileType], w: i32, x: i32, y: i32) -> TileType { if x < 0 || y < 0 || x >= w || (y * w + x) >= tiles.len() as i32 { TileType::Rock } else { tiles[(y * w + x) as usize] } } /// Carve a floor corridor as an L (horizontal then vertical). Only overwrites /// Rock/Wall — never stomps a placed object or door. fn carve_corridor(tiles: &mut [TileType], w: i32, a: (i32, i32), b: (i32, i32)) { let (mut x, mut y) = a; while x != b.0 { carve_cell(tiles, w, x, y); x += (b.0 - x).signum(); } while y != b.1 { carve_cell(tiles, w, x, y); y += (b.1 - y).signum(); } carve_cell(tiles, w, b.0, b.1); } fn carve_cell(tiles: &mut [TileType], w: i32, x: i32, y: i32) { if matches!(get(tiles, w, x, y), TileType::Rock | TileType::Wall) { set(tiles, w, x, y, TileType::Floor); } } fn carve_corridors(tiles: &mut [TileType], w: i32, runs: &[Corridor]) { for &(a, b) in runs { carve_corridor(tiles, w, a, b); } } // A minimal second layout that reuses the same prefabs — proves composability // (spec/basement-map.md acceptance 1). pub fn toy_layout() -> Layout { let prefabs = vec![SERVER_ROOM, JANITOR]; let placements = vec![ Placement { prefab: 0, x: 1, y: 1, }, Placement { prefab: 1, x: 10, y: 1, }, ]; let rooms = rooms_of(&prefabs, &placements); let mut layout = Layout { width: 20, height: 10, prefabs, placements, rooms, cached: None, }; let tiles = layout.stamp(); layout.cached = Some(tiles); layout } #[cfg(test)] mod tests { use super::*; #[test] fn basement_has_a_core_bay() { let layout = basement(); let tiles = layout.tiles(); let cores = tiles.iter().filter(|&&t| t == TileType::Core).count(); assert_eq!(cores, 1, "exactly one core bay (Rack 3)"); } #[test] fn basement_has_the_vocabulary() { let tiles = basement().tiles(); for needed in [ TileType::Rack, TileType::Switch, TileType::BreakerPanel, TileType::DeadEquipment, TileType::RecordsBox, TileType::RollDoor, TileType::SecurityDoor2, TileType::SecurityDoor3, TileType::EnvCamera, ] { assert!( tiles.contains(&needed), "basement must contain {}", needed.name() ); } } #[test] fn composable_toy_layout_reuses_prefabs() { let tiles = toy_layout().tiles(); assert!(tiles.contains(&TileType::Core)); assert!(tiles.contains(&TileType::MopSink)); } #[test] fn stamping_is_deterministic() { assert_eq!(basement().tiles(), basement().tiles()); } }