Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development.
Something went wrong. Try again.
16 kB · 472 lines
Rust
at commit e957ce7b
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473use std::collections::{HashSet, VecDeque};
use crate::prefab::Room;use crate::tiles::TileType;
pub struct GameMap { pub width: i32, pub height: i32, pub tiles: Vec<Vec<TileType>>, pub powered: HashSet<(i32, i32)>, /// Named room rects (spec/schedules.md). Static Act One level data /// derived from the layout's placements, so it is rebuilt rather than /// serialized. pub rooms: Vec<Room>,}
impl GameMap { /// Build the Act One basement from prefab data (spec/basement-map.md). /// The width/height arguments are ignored; the basement layout fixes them. pub fn new(_width: i32, _height: i32) -> Self { let layout = crate::prefab::basement(); let flat = layout.tiles(); let mut map = Self::from_tiles(layout.width, layout.height, flat, HashSet::new()); map.rooms = layout.rooms; map }
/// Reconstruct a GameMap from saved tile data. pub fn from_tiles( width: i32, height: i32, flat_tiles: Vec<TileType>, powered: HashSet<(i32, i32)>, ) -> Self { let mut tiles = vec![vec![TileType::Rock; width as usize]; height as usize]; for y in 0..height { for x in 0..width { let idx = (y * width + x) as usize; if idx < flat_tiles.len() { tiles[y as usize][x as usize] = flat_tiles[idx]; } } } // Rooms are static level data. When the dimensions match the Act One // basement (the only current plane), rebuild them from the layout so // save/load and reconstruction keep room identity without serializing // it. Custom-sized maps (tests, future planes) start room-less. let rooms = if width == 64 && height == 36 { crate::prefab::basement().rooms } else { Vec::new() }; Self { width, height, tiles, powered, rooms, } }
/// The room containing (x, y), if any. pub fn room_at(&self, x: i32, y: i32) -> Option<&Room> { self.rooms.iter().find(|r| r.contains(x, y)) }
/// The room with the given prefab name, if present. pub fn room_named(&self, name: &str) -> Option<&Room> { self.rooms.iter().find(|r| r.name == name) }
/// The badge tier required to walk into a room: the lowest-tier door in /// its footprint (any door at or below a credential lets its holder in); /// a room with a plain door — or no door at all — is tier 0. This is the /// same `security_level` table human pathfinding bypasses use /// (basement-map.md criterion 3: one access rule for humans and /// player-directed actors alike). pub fn room_entry_tier(&self, room: &Room) -> i32 { (room.y..room.y + room.h) .flat_map(|y| (room.x..room.x + room.w).map(move |x| (x, y))) .map(|(x, y)| self.get_tile(x, y)) .filter(|t| t.is_door()) .map(|t| t.security_level()) .min() .unwrap_or(0) }
/// The entry tier of the room containing (x, y); open corridor and /// crawlspace ground is tier 0. pub fn entry_tier_at(&self, x: i32, y: i32) -> i32 { self.room_at(x, y) .map(|r| self.room_entry_tier(r)) .unwrap_or(0) }
pub fn in_bounds(&self, x: i32, y: i32) -> bool { x >= 0 && x < self.width && y >= 0 && y < self.height }
pub fn get_tile(&self, x: i32, y: i32) -> TileType { if self.in_bounds(x, y) { self.tiles[y as usize][x as usize] } else { TileType::Rock } }
pub fn set_tile(&mut self, x: i32, y: i32, tile: TileType) { if self.in_bounds(x, y) { self.tiles[y as usize][x as usize] = tile; } }
pub fn is_walkable(&self, x: i32, y: i32) -> bool { self.get_tile(x, y).is_walkable() }
pub fn blocks_sight(&self, x: i32, y: i32) -> bool { use TileType::*; matches!( self.get_tile(x, y), Rock | Wall | Door | SecurityDoor1 | SecurityDoor2 | SecurityDoor3 | SealedDoor | RollDoor ) }
/// Whether a straight sensor ray from `from` reaches `to` without passing /// through opaque structure. The target tile itself is allowed to be /// opaque: a camera can see the near face of a wall or door, but not what /// is behind it. pub fn sensor_line_reaches(&self, from: (i32, i32), to: (i32, i32)) -> bool { if !self.in_bounds(from.0, from.1) || !self.in_bounds(to.0, to.1) { return false; } let (mut x0, mut y0) = from; let (x1, y1) = to; let dx = (x1 - x0).abs(); let dy = -(y1 - y0).abs(); let sx = if x0 < x1 { 1 } else { -1 }; let sy = if y0 < y1 { 1 } else { -1 }; let mut err = dx + dy;
loop { if (x0, y0) != from && (x0, y0) != to && self.blocks_sight(x0, y0) { return false; } if (x0, y0) == to { return true; } let e2 = 2 * err; if e2 >= dy { err += dy; x0 += sx; } if e2 <= dx { err += dx; y0 += sy; } } }
pub fn core_pos(&self) -> Option<(i32, i32)> { for y in 0..self.height { for x in 0..self.width { if self.tiles[y as usize][x as usize] == TileType::Core { return Some((x, y)); } } } None }
pub fn entry_positions(&self) -> Vec<(i32, i32)> { let mut positions = Vec::new(); for y in 0..self.height { for x in 0..self.width { if self.tiles[y as usize][x as usize] == TileType::Entry { positions.push((x, y)); } } } positions }
pub fn tiles_of_type(&self, tile_type: TileType) -> Vec<(i32, i32)> { let mut positions = Vec::new(); for y in 0..self.height { for x in 0..self.width { if self.tiles[y as usize][x as usize] == tile_type { positions.push((x, y)); } } } positions }
pub fn adjacent_walkable(&self, x: i32, y: i32) -> Vec<(i32, i32)> { let dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]; dirs.iter() .filter_map(|&(dx, dy)| { let (nx, ny) = (x + dx, y + dy); if self.is_walkable(nx, ny) { Some((nx, ny)) } else { None } }) .collect() }
/// Adjacent walkable tiles, considering security bypass level. /// Security doors above the bypass level are treated as impassable. pub fn adjacent_walkable_with_security( &self, x: i32, y: i32, security_bypass: i32, ) -> Vec<(i32, i32)> { let dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]; dirs.iter() .filter_map(|&(dx, dy)| { let (nx, ny) = (x + dx, y + dy); if !self.is_walkable(nx, ny) { return None; } let tile = self.get_tile(nx, ny); if tile.is_door() && tile.security_level() > security_bypass { return None; // Can't breach this door } Some((nx, ny)) }) .collect() }
pub fn find_path( &self, start: (i32, i32), end: (i32, i32), avoid: &HashSet<(i32, i32)>, ) -> Option<Vec<(i32, i32)>> { self.find_path_with_security(start, end, avoid, 99) }
pub fn find_path_with_security( &self, start: (i32, i32), end: (i32, i32), avoid: &HashSet<(i32, i32)>, security_bypass: i32, ) -> Option<Vec<(i32, i32)>> { if start == end { return Some(vec![start]); }
let mut queue = VecDeque::new(); queue.push_back(start); let mut came_from: std::collections::HashMap<(i32, i32), Option<(i32, i32)>> = std::collections::HashMap::new(); came_from.insert(start, None);
while let Some(current) = queue.pop_front() { for neighbor in self.adjacent_walkable_with_security(current.0, current.1, security_bypass) { if came_from.contains_key(&neighbor) || avoid.contains(&neighbor) { continue; } came_from.insert(neighbor, Some(current)); if neighbor == end { // Reconstruct path let mut path = vec![neighbor]; let mut node = neighbor; while let Some(Some(prev)) = came_from.get(&node) { path.push(*prev); node = *prev; } path.reverse(); return Some(path); } queue.push_back(neighbor); } }
None }
pub fn update_power(&mut self, power_cores: &[(i32, i32)]) { self.powered.clear(); for &core_pos in power_cores { let mut visited = HashSet::new(); let mut queue = VecDeque::new(); queue.push_back(core_pos); while let Some(pos) = queue.pop_front() { if visited.contains(&pos) { continue; } visited.insert(pos); self.powered.insert(pos); for neighbor in self.adjacent_walkable(pos.0, pos.1) { if !visited.contains(&neighbor) { queue.push_back(neighbor); } } } } }}
#[cfg(test)]mod tests { use super::*; use std::collections::HashSet;
#[test] fn new_map_has_core() { let map = GameMap::new(80, 40); assert!(map.core_pos().is_some()); }
#[test] fn new_map_has_entry() { let map = GameMap::new(80, 40); assert!(!map.entry_positions().is_empty()); }
#[test] fn new_map_has_power_core() { let map = GameMap::new(80, 40); let cores = map.tiles_of_type(TileType::PowerCore); assert!(!cores.is_empty(), "Map should have a starting power core"); }
/// A blank all-floor map of the given size, bypassing prefab generation, /// for controlled pathfinding/power tests. fn blank(w: i32, h: i32) -> GameMap { GameMap::from_tiles( w, h, vec![TileType::Floor; (w * h) as usize], HashSet::new(), ) }
#[test] fn core_connects_to_the_corridor_spine() { // In the authored basement the core bay reaches the corridor system // (through its badge door, bypassed) even though the roll door seals // the dock — connectivity of the interior, not of the whole plane. let map = GameMap::new(0, 0); let core = map.core_pos().unwrap(); let spine = (24, 20); // a corridor-B floor tile assert_eq!(map.get_tile(spine.0, spine.1), TileType::Floor); let path = map.find_path_with_security(core, spine, &HashSet::new(), 3); assert!(path.is_some(), "core reaches the corridor spine"); }
#[test] fn find_path_no_path_through_walls() { let mut map = blank(80, 40); for y in 0..40 { for x in 0..80 { map.set_tile(x, y, TileType::Wall); } } map.set_tile(1, 1, TileType::Floor); map.set_tile(70, 35, TileType::Floor); let path = map.find_path((1, 1), (70, 35), &HashSet::new()); assert!(path.is_none(), "No path through solid walls"); }
#[test] fn sensor_line_stops_at_opaque_tiles() { let mut map = blank(7, 3); map.set_tile(3, 1, TileType::Wall); assert!( map.sensor_line_reaches((1, 1), (3, 1)), "a camera can see the near face of a wall" ); assert!( !map.sensor_line_reaches((1, 1), (4, 1)), "but not the floor hidden behind it" ); map.set_tile(3, 1, TileType::SecurityDoor3); assert!( !map.sensor_line_reaches((1, 1), (4, 1)), "closed security doors are also opaque to sight" ); }
#[test] fn security_pathfinding_respects_doors() { let mut map = blank(80, 40); map.set_tile(40, 20, TileType::SecurityDoor3); let avoid = HashSet::new(); assert!( map.find_path_with_security((35, 20), (45, 20), &avoid, 3) .is_some(), "bypass 3 passes a tier-3 door" ); assert!( map.find_path_with_security((35, 20), (45, 20), &avoid, 1) .is_some(), "bypass 1 routes around it" ); }
#[test] fn room_entry_tiers_match_the_authored_doors() { let map = GameMap::new(0, 0); let tier = |name: &str| map.room_entry_tier(map.room_named(name).unwrap()); assert_eq!(tier("server_room"), 2, "T2 badge door"); assert_eq!(tier("network_closet"), 2, "T2 badge door"); assert_eq!(tier("stairwell"), 3, "the act boundary is T3"); assert_eq!(tier("janitor"), 0, "plain door"); assert_eq!(tier("loading_dock"), 0, "plain door beside the roll door"); // Corridor ground belongs to no room: tier 0. assert_eq!(map.entry_tier_at(24, 20), 0); }
#[test] fn is_walkable_checks() { let map = GameMap::new(0, 0); let core = map.core_pos().unwrap(); assert!(map.is_walkable(core.0, core.1), "core bay is walkable"); assert!(!map.is_walkable(0, 0), "bedrock corner is not"); }
#[test] fn power_propagation_from_core() { let mut map = blank(20, 20); // Isolate a pocket with walls, power core inside. for x in 9..=13 { map.set_tile(x, 8, TileType::Wall); map.set_tile(x, 12, TileType::Wall); } for y in 8..=12 { map.set_tile(9, y, TileType::Wall); map.set_tile(13, y, TileType::Wall); } map.set_tile(10, 10, TileType::PowerCore); map.set_tile(11, 10, TileType::Floor); let cores = map.tiles_of_type(TileType::PowerCore); map.update_power(&cores); assert!(map.powered.contains(&(10, 10))); assert!(map.powered.contains(&(11, 10))); }
#[test] fn power_blocked_by_wall() { let mut map = blank(20, 20); // A wall ring around a power core; floor outside must stay dark. for x in 9..=11 { map.set_tile(x, 9, TileType::Wall); map.set_tile(x, 11, TileType::Wall); } map.set_tile(9, 10, TileType::Wall); map.set_tile(11, 10, TileType::Wall); map.set_tile(10, 10, TileType::PowerCore); let cores = map.tiles_of_type(TileType::PowerCore); map.update_power(&cores); assert!(map.powered.contains(&(10, 10))); assert!( !map.powered.contains(&(13, 10)), "power does not cross the wall ring" ); }}