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.
15 kB · 475 lines
Rust
at commit 4e7bfe58
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476use std::collections::{HashSet, VecDeque};
use crate::tiles::TileType;
pub struct GameMap { pub width: i32, pub height: i32, pub tiles: Vec<Vec<TileType>>, pub powered: HashSet<(i32, i32)>,}
impl GameMap { pub fn new(width: i32, height: i32) -> Self { let mut map = Self { width, height, tiles: vec![vec![TileType::Rock; width as usize]; height as usize], powered: HashSet::new(), }; map.generate_initial_lair(); map }
/// Reconstruct a GameMap from saved tile data. pub fn from_tiles( width: i32, height: i32, flat_tiles: Vec<TileType>, powered: HashSet<(i32, i32)>, _throne: (i32, i32), _entries: Vec<(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]; } } } Self { width, height, tiles, powered, } }
fn generate_initial_lair(&mut self) { let cx = self.width / 2; let cy = self.height / 2;
// Fill interior with walls (diggable) for y in 1..self.height - 1 { for x in 1..self.width - 1 { self.tiles[y as usize][x as usize] = TileType::Wall; } }
// --- Carve rooms ---
// Central throne room (9x7) let throne_w = 9; let throne_h = 7; self.carve_room(cx, cy, throne_w, throne_h);
// Place throne in center self.tiles[cy as usize][cx as usize] = TileType::Throne;
// Starting power core in the throne room (right side) if cx + 2 < self.width { self.tiles[cy as usize][(cx + 2) as usize] = TileType::PowerCore; }
// Left chamber (5x5) — starting lab let left_cx = cx - 15; let left_cy = cy; self.carve_room(left_cx, left_cy, 5, 5); if self.in_bounds(left_cx, left_cy) { self.tiles[left_cy as usize][left_cx as usize] = TileType::Lab; }
// Right chamber (5x5) — starting guard post let right_cx = cx + 15; let right_cy = cy; self.carve_room(right_cx, right_cy, 5, 5); if self.in_bounds(right_cx, right_cy) { self.tiles[right_cy as usize][right_cx as usize] = TileType::GuardPost; }
// Bottom chamber (5x5) let bottom_cx = cx; let bottom_cy = cy + 10; self.carve_room(bottom_cx, bottom_cy, 5, 5);
// --- Carve main corridors connecting rooms to throne room ---
// Horizontal corridor: left chamber -> throne room self.carve_h_corridor(left_cx + 3, cx - throne_w / 2, left_cy);
// Horizontal corridor: throne room -> right chamber self.carve_h_corridor(cx + throne_w / 2, right_cx - 2, right_cy);
// Vertical corridor: throne room -> bottom chamber self.carve_v_corridor(cx, cy + throne_h / 2, bottom_cy - 2);
// --- Entry corridor from top ---
// Place entry point at top let entry_x = cx; self.tiles[1][entry_x as usize] = TileType::Entry;
// Carve corridor from entry down to throne room self.carve_v_corridor(entry_x, 2, cy - throne_h / 2);
// --- Pre-carved branch corridors (dead-end stubs for future expansion) ---
// Branch going up from left chamber self.carve_v_corridor(left_cx, left_cy - 3, left_cy - 6);
// Branch going down from right chamber self.carve_v_corridor(right_cx, right_cy + 3, right_cy + 6);
// Branch going left from bottom chamber self.carve_h_corridor(bottom_cx - 3, bottom_cx - 6, bottom_cy);
// Branch going right from bottom chamber self.carve_h_corridor(bottom_cx + 3, bottom_cx + 6, bottom_cy); }
/// Carve a rectangular room centered at (cx, cy) with given width and height. fn carve_room(&mut self, cx: i32, cy: i32, w: i32, h: i32) { for y in (cy - h / 2)..=(cy + h / 2) { for x in (cx - w / 2)..=(cx + w / 2) { if y > 0 && y < self.height - 1 && x > 0 && x < self.width - 1 { self.tiles[y as usize][x as usize] = TileType::Floor; } } } }
/// Carve a horizontal corridor from x1 to x2 (inclusive) at the given y. fn carve_h_corridor(&mut self, x1: i32, x2: i32, y: i32) { let start = x1.min(x2); let end = x1.max(x2); for x in start..=end { if y > 0 && y < self.height - 1 && x > 0 && x < self.width - 1 { self.tiles[y as usize][x as usize] = TileType::Floor; } } }
/// Carve a vertical corridor from y1 to y2 (inclusive) at the given x. fn carve_v_corridor(&mut self, x: i32, y1: i32, y2: i32) { let start = y1.min(y2); let end = y1.max(y2); for y in start..=end { if y > 0 && y < self.height - 1 && x > 0 && x < self.width - 1 { self.tiles[y as usize][x as usize] = TileType::Floor; } } }
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 throne_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::Throne { 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_throne() { let map = GameMap::new(80, 40); assert!(map.throne_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"); }
#[test] fn new_map_has_lab() { let map = GameMap::new(80, 40); let labs = map.tiles_of_type(TileType::Lab); assert!(!labs.is_empty(), "Map should have a starting lab"); }
#[test] fn find_path_basic() { let map = GameMap::new(80, 40); let avoid = HashSet::new(); // Path from entry to throne let entries = map.entry_positions(); let throne = map.throne_pos().unwrap(); if let Some(entry) = entries.first() { let path = map.find_path(*entry, throne, &avoid); assert!(path.is_some(), "Should find path from entry to throne"); let p = path.unwrap(); assert!(!p.is_empty()); assert_eq!(p[0], *entry); assert_eq!(*p.last().unwrap(), throne); } }
#[test] fn find_path_no_path_through_walls() { let mut map = GameMap::new(80, 40); // Fill everything with walls except two isolated floor tiles 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 avoid = HashSet::new(); let path = map.find_path((1, 1), (70, 35), &avoid); assert!(path.is_none(), "No path through solid walls"); }
#[test] fn security_pathfinding_respects_doors() { let mut map = GameMap::new(80, 40); // Clear to floor for y in 0..40 { for x in 0..80 { map.set_tile(x, y, TileType::Floor); } } // Place a security door 3 in the middle map.set_tile(40, 20, TileType::SecurityDoor3); let avoid = HashSet::new();
// Agent with bypass 3 can path through let path_high = map.find_path_with_security((35, 20), (45, 20), &avoid, 3); assert!( path_high.is_some(), "Agent with bypass 3 should path through sec door 3" );
// Agent with bypass 1 cannot path through directly, but can go around let path_low = map.find_path_with_security((35, 20), (45, 20), &avoid, 1); assert!( path_low.is_some(), "Agent with bypass 1 should find alternate route around door" ); }
#[test] fn is_walkable_checks() { let map = GameMap::new(80, 40); // Throne should be walkable let throne = map.throne_pos().unwrap(); assert!(map.is_walkable(throne.0, throne.1)); // Rock should not be walkable assert!(!map.is_walkable(0, 0)); }
#[test] fn new_map_has_guard_post() { let map = GameMap::new(80, 40); let guard_posts = map.tiles_of_type(TileType::GuardPost); assert!( !guard_posts.is_empty(), "Map should have a starting guard post" ); }
#[test] fn power_propagation_from_core() { let mut map = GameMap::new(80, 40); // Place a power core and adjacent floor map.set_tile(10, 10, TileType::PowerCore); map.set_tile(11, 10, TileType::Floor); map.set_tile(10, 11, 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))); assert!(map.powered.contains(&(10, 11))); }
#[test] fn power_blocked_by_wall() { let mut map = GameMap::new(80, 40); // Power core with wall between it and another floor map.set_tile(10, 10, TileType::PowerCore); map.set_tile(11, 10, TileType::Wall); map.set_tile(12, 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(&(12, 10)), "Power should not propagate through walls" ); }}