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. forked from cameron.stream/misaligned
Something went wrong. Try again.
12 kB · 367 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368use 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) }
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 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 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 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" ); }}