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);
}
}
}
}
}
pub fn trim(&self, padding: usize) -> Self {
let mut min_x = usize::MAX;
let mut min_y = usize::MAX;
let mut max_x = usize::MIN;
let mut max_y = usize::MIN;
let mut has_content = false;
for y in 0..self.height {
for x in 0..self.width {
let tile = self.tiles[self.linearize_coords(GridCoord2D::new(x, y))];
if !tile.is_empty() {
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
has_content = true;
}
}
}
if !has_content {
return DungeonGrid::new(1, 1);
}
let content_min_x = min_x.saturating_sub(padding);
let content_min_y = min_y.saturating_sub(padding);
let content_max_x = (max_x + padding).min(self.width - 1);
let content_max_y = (max_y + padding).min(self.height - 1);
let final_width = (content_max_x - content_min_x + 1).max(1);
let final_height = (content_max_y - content_min_y + 1).max(1);
let mut final_grid = DungeonGrid::new(final_width, final_height);
for old_y in content_min_y..=content_max_y {
for old_x in content_min_x..=content_max_x {
let old_coord = GridCoord2D::new(old_x, old_y);
let tile = self.tiles[self.linearize_coords(old_coord)];
let new_x = old_x - content_min_x;
let new_y = old_y - content_min_y;
let new_coord = GridCoord2D::new(new_x, new_y);
final_grid.set(new_coord, tile);
}
}
if let Some(exit_coord) = self.exit {
let exit_x = (exit_coord.x as isize - content_min_x as isize)
.max(0)
.min(final_width as isize - 1) as usize;
let exit_y = (exit_coord.y as isize - content_min_y as isize)
.max(0)
.min(final_height as isize - 1) as usize;
final_grid.set_exit(GridCoord2D::new(exit_x, exit_y));
}
final_grid.place_walls();
final_grid.compute_edge_masks();
final_grid
}
}
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);
}
#[test]
fn test_trim_single_floor_tile() {
let mut grid = DungeonGrid::new(10, 10);
grid.set(GridCoord2D::new(5, 5), TileType::Floor);
grid.place_walls();
let trimmed = grid.trim(0);
assert_eq!(trimmed.width(), 3);
assert_eq!(trimmed.height(), 3);
assert_eq!(trimmed.get(GridCoord2D::new(1, 1)), Some(TileType::Floor));
}
#[test]
fn test_trim_small_cluster() {
let mut grid = DungeonGrid::new(20, 20);
for y in 6..=9 {
for x in 8..=10 {
grid.set(GridCoord2D::new(x, y), TileType::Floor);
}
}
grid.place_walls();
let trimmed = grid.trim(0);
assert_eq!(trimmed.width(), 5);
assert_eq!(trimmed.height(), 6);
}
#[test]
fn test_trim_preserves_exit() {
let mut grid = DungeonGrid::new(10, 10);
grid.set(GridCoord2D::new(5, 5), TileType::Floor);
grid.set_exit(GridCoord2D::new(5, 5));
grid.place_walls();
let trimmed = grid.trim(0);
let exit = trimmed.exit().unwrap();
assert_eq!(exit.x, 1);
assert_eq!(exit.y, 1);
}
#[test]
fn test_trim_with_padding() {
let mut grid = DungeonGrid::new(10, 10);
grid.set(GridCoord2D::new(5, 5), TileType::Floor);
grid.place_walls();
let trimmed = grid.trim(2);
assert_eq!(trimmed.width(), 7);
assert_eq!(trimmed.height(), 7);
assert_eq!(trimmed.get(GridCoord2D::new(3, 3)), Some(TileType::Floor));
}
#[test]
fn test_trim_recomputes_walls_at_edges() {
let mut grid = DungeonGrid::new(7, 7);
grid.set(GridCoord2D::new(3, 3), TileType::Floor);
grid.set(GridCoord2D::new(2, 3), TileType::Floor);
grid.set(GridCoord2D::new(4, 3), TileType::Floor);
let trimmed = grid.trim(1);
assert!(trimmed.width() >= 4);
assert!(trimmed.height() >= 3);
}
#[test]
fn test_trim_empty_grid() {
let grid = DungeonGrid::new(10, 10);
let trimmed = grid.trim(0);
assert_eq!(trimmed.width(), 1);
assert_eq!(trimmed.height(), 1);
}
#[test]
fn test_trim_full_grid_no_trim() {
let mut grid = DungeonGrid::new(5, 5);
for y in 0..5 {
for x in 0..5 {
grid.set(GridCoord2D::new(x, y), TileType::Floor);
}
}
grid.place_walls();
let trimmed = grid.trim(0);
assert_eq!(trimmed.width(), 5);
assert_eq!(trimmed.height(), 5);
}
#[test]
fn test_trim_exit_clamped_to_bounds() {
let mut grid = DungeonGrid::new(10, 10);
grid.set(GridCoord2D::new(0, 0), TileType::Floor);
grid.set_exit(GridCoord2D::new(9, 9));
grid.place_walls();
let trimmed = grid.trim(0);
let exit = trimmed.exit().unwrap();
assert!(exit.x < trimmed.width());
assert!(exit.y < trimmed.height());
}
#[test]
fn test_trim_preserves_floor_count() {
let mut grid = DungeonGrid::new(15, 15);
let original_floor_count = 10;
for i in 0..original_floor_count {
grid.set(GridCoord2D::new(5 + i, 7), TileType::Floor);
}
grid.place_walls();
let trimmed = grid.trim(0);
assert_eq!(trimmed.floor_count(), original_floor_count);
}
}