use std::collections::{HashSet, VecDeque}; use crate::tiles::TileType; pub struct GameMap { pub width: i32, pub height: i32, pub tiles: Vec>, 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, 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> { 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> { 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" ); } }