use crate::dungeon::TileType;
use crate::grid_coord_2d::{GetCoordinateBounds2D, GridCoord2D, LinearizeCoords2D};
use std::collections::HashSet;
use std::ops::Index;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DungeonGrid {
width: usize,
height: usize,
tiles: Vec<TileType>,
floor_positions: HashSet<GridCoord2D>,
exit: Option<GridCoord2D>,
edge_masks: Vec<u8>,
}
impl DungeonGrid {
pub fn new(width: usize, height: usize) -> Self {
let size = width
.checked_mul(height)
.expect("Grid dimensions overflow: width * height exceeds usize::MAX");
Self {
width,
height,
tiles: vec![TileType::Empty; size],
floor_positions: HashSet::new(),
exit: None,
edge_masks: vec![0; size],
}
}
pub fn get(&self, coord: GridCoord2D) -> Option<TileType> {
if coord.x >= self.width || coord.y >= self.height {
None
} else {
Some(self.tiles[self.linearize_coords(coord)])
}
}
pub fn set(&mut self, coord: GridCoord2D, tile: TileType) {
if coord.x < self.width && coord.y < self.height {
let idx = self.linearize_coords(coord);
let old_tile = self.tiles[idx];
self.tiles[idx] = tile;
if old_tile.is_passable() && !tile.is_passable() {
self.floor_positions.remove(&coord);
} else if !old_tile.is_passable() && tile.is_passable() {
self.floor_positions.insert(coord);
}
}
}
#[inline]
pub fn is_floor(&self, coord: GridCoord2D) -> bool {
self.floor_positions.contains(&coord)
}
pub fn floor_iter(&self) -> impl Iterator<Item = GridCoord2D> + '_ {
self.floor_positions.iter().copied()
}
#[inline]
pub fn floor_count(&self) -> usize {
self.floor_positions.len()
}
#[inline]
pub fn exit(&self) -> Option<GridCoord2D> {
self.exit
}
pub fn set_exit(&mut self, coord: GridCoord2D) {
self.exit = Some(coord);
}
pub fn edge_mask(&self, coord: GridCoord2D) -> u8 {
if coord.x < self.width && coord.y < self.height {
self.edge_masks[self.linearize_coords(coord)]
} else {
0
}
}
pub fn set_edge_mask(&mut self, coord: GridCoord2D, mask: u8) {
if coord.x < self.width && coord.y < self.height {
let idx = self.linearize_coords(coord);
self.edge_masks[idx] = mask;
}
}
pub fn compute_edge_masks(&mut self) {
for y in 0..self.height {
for x in 0..self.width {
let coord = GridCoord2D::new(x, y);
if !self.get(coord).unwrap().is_wall() {
continue;
}
let mut mask = 0u8;
if y == 0 || !self.get(GridCoord2D::new(x, y - 1)).unwrap().is_wall() {
mask |= 1;
}
if x + 1 >= self.width || !self.get(GridCoord2D::new(x + 1, y)).unwrap().is_wall() {
mask |= 2;
}
if y + 1 >= self.height || !self.get(GridCoord2D::new(x, y + 1)).unwrap().is_wall()
{
mask |= 4;
}
if x == 0 || !self.get(GridCoord2D::new(x - 1, y)).unwrap().is_wall() {
mask |= 8;
}
self.set_edge_mask(coord, mask);
}
}
}
pub fn place_walls(&mut self) {
let floor_coords: Vec<GridCoord2D> = self.floor_positions.iter().copied().collect();
for coord in floor_coords {
let neighbors = [
(coord.x, coord.y.wrapping_sub(1)), (coord.x + 1, coord.y), (coord.x, coord.y + 1), (coord.x.wrapping_sub(1), coord.y), ];
for (nx, ny) in neighbors {
if nx < self.width && ny < self.height {
let neighbor = GridCoord2D::new(nx, ny);
if self.get(neighbor).unwrap().is_empty() {
self.set(neighbor, TileType::Wall);
}
}
}
}
}
}
impl GetCoordinateBounds2D for DungeonGrid {
#[inline]
fn width(&self) -> usize {
self.width
}
#[inline]
fn height(&self) -> usize {
self.height
}
}
impl Index<GridCoord2D> for DungeonGrid {
type Output = TileType;
fn index(&self, coord: GridCoord2D) -> &Self::Output {
&self.tiles[self.linearize_coords(coord)]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_grid_is_empty() {
let grid = DungeonGrid::new(10, 10);
assert_eq!(grid.width(), 10);
assert_eq!(grid.height(), 10);
assert_eq!(grid.floor_count(), 0);
assert_eq!(grid.exit(), None);
}
#[test]
fn test_set_floor_updates_tracking() {
let mut grid = DungeonGrid::new(5, 5);
let coord = GridCoord2D::new(2, 2);
grid.set(coord, TileType::Floor);
assert_eq!(grid.floor_count(), 1);
assert!(grid.is_floor(coord));
grid.set(coord, TileType::Wall);
assert_eq!(grid.floor_count(), 0);
assert!(!grid.is_floor(coord));
}
#[test]
fn test_place_walls_around_floor() {
let mut grid = DungeonGrid::new(5, 5);
let center = GridCoord2D::new(2, 2);
grid.set(center, TileType::Floor);
grid.place_walls();
assert_eq!(grid[center], TileType::Floor);
assert_eq!(grid[GridCoord2D::new(2, 1)], TileType::Wall);
assert_eq!(grid[GridCoord2D::new(3, 2)], TileType::Wall);
assert_eq!(grid[GridCoord2D::new(2, 3)], TileType::Wall);
assert_eq!(grid[GridCoord2D::new(1, 2)], TileType::Wall);
}
#[test]
fn test_edge_mask_computation() {
let mut grid = DungeonGrid::new(3, 3);
grid.set(GridCoord2D::new(1, 1), TileType::Wall);
grid.set(GridCoord2D::new(1, 0), TileType::Floor); grid.set(GridCoord2D::new(2, 1), TileType::Floor); grid.set(GridCoord2D::new(1, 2), TileType::Floor); grid.set(GridCoord2D::new(0, 1), TileType::Floor);
grid.compute_edge_masks();
assert_eq!(grid.edge_mask(GridCoord2D::new(1, 1)), 15);
}
}