use glam::UVec2; use std::ops::{Index, IndexMut}; /// A 2D grid-based map structure optimized for tile-based games. /// /// Provides efficient access to tiles using 2D coordinates (UVec2) while maintaining /// cache-friendly contiguous memory layout. Supports generic tile types that implement Copy. /// /// # Type Parameters /// * `T` - The tile value type. Must implement `Copy` for efficient access. /// /// # Examples /// ``` /// use glam::UVec2; /// use borders_core::game::TileMap; /// /// let mut map = TileMap::::new(10, 10); /// map[UVec2::new(5, 5)] = 42; /// assert_eq!(map[UVec2::new(5, 5)], 42); /// ``` #[derive(Clone, Debug)] pub struct TileMap { tiles: Box<[T]>, width: u32, height: u32, } impl TileMap { /// Creates a new TileMap with the specified dimensions and default value. /// /// # Arguments /// * `width` - The width of the map in tiles /// * `height` - The height of the map in tiles /// * `default` - The default value to initialize all tiles with pub fn with_default(width: u32, height: u32, default: T) -> Self { let capacity = (width * height) as usize; let tiles = vec![default; capacity].into_boxed_slice(); Self { tiles, width, height } } /// Creates a TileMap from an existing vector of tile data. /// /// # Arguments /// * `width` - The width of the map in tiles /// * `height` - The height of the map in tiles /// * `data` - Vector containing tile data in row-major order /// /// # Panics /// Panics if `data.len() != width * height` pub fn from_vec(width: u32, height: u32, data: Vec) -> Self { assert_eq!(data.len(), (width * height) as usize, "Data length must match width * height"); Self { tiles: data.into_boxed_slice(), width, height } } /// Converts the position to a flat array index. /// /// # Safety /// Debug builds will assert that the position is in bounds. /// Release builds skip the check for performance. #[inline] pub fn pos_to_index(&self, pos: UVec2) -> usize { debug_assert!(pos.x < self.width && pos.y < self.height); (pos.y * self.width + pos.x) as usize } /// Converts a flat array index to a 2D position. #[inline] pub fn index_to_pos(&self, index: usize) -> UVec2 { debug_assert!(index < self.tiles.len()); UVec2::new((index as u32) % self.width, (index as u32) / self.width) } /// Checks if a position is within the map bounds. #[inline] pub fn in_bounds(&self, pos: UVec2) -> bool { pos.x < self.width && pos.y < self.height } /// Gets the tile value at the specified position. /// /// Returns `None` if the position is out of bounds. pub fn get(&self, pos: UVec2) -> Option { if self.in_bounds(pos) { Some(self.tiles[self.pos_to_index(pos)]) } else { None } } /// Sets the tile value at the specified position. /// /// Returns `true` if the position was in bounds and the value was set, /// `false` otherwise. pub fn set(&mut self, pos: UVec2, tile: T) -> bool { if self.in_bounds(pos) { let idx = self.pos_to_index(pos); self.tiles[idx] = tile; true } else { false } } /// Returns the width of the map. #[inline] pub fn width(&self) -> u32 { self.width } /// Returns the height of the map. #[inline] pub fn height(&self) -> u32 { self.height } /// Returns the total number of tiles in the map. #[inline] pub fn len(&self) -> usize { self.tiles.len() } /// Returns `true` if the map contains no tiles. #[inline] pub fn is_empty(&self) -> bool { self.tiles.is_empty() } /// Returns an iterator over all valid cardinal neighbors of a position. /// /// Yields positions for up, down, left, and right neighbors that are within bounds. pub fn neighbors(&self, pos: UVec2) -> impl Iterator + '_ { const CARDINAL_DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (0, -1), (-1, 0)]; let pos_i32 = (pos.x as i32, pos.y as i32); let width = self.width; let height = self.height; CARDINAL_DIRECTIONS.iter().filter_map(move |(dx, dy)| { let nx = pos_i32.0 + dx; let ny = pos_i32.1 + dy; if nx >= 0 && ny >= 0 && nx < width as i32 && ny < height as i32 { Some(UVec2::new(nx as u32, ny as u32)) } else { None } }) } /// Calls a closure for each valid cardinal neighbor of a position. /// /// This is more efficient than using the `neighbors()` iterator when you don't /// need to collect the neighbors. pub fn on_neighbors(&self, pos: UVec2, mut closure: F) where F: FnMut(UVec2), { if pos.x > 0 { closure(UVec2::new(pos.x - 1, pos.y)); } if pos.x < self.width - 1 { closure(UVec2::new(pos.x + 1, pos.y)); } if pos.y > 0 { closure(UVec2::new(pos.x, pos.y - 1)); } if pos.y < self.height - 1 { closure(UVec2::new(pos.x, pos.y + 1)); } } /// Calls a closure for each neighbor using tile indices instead of positions. /// /// This is useful when working with systems that still use raw indices. pub fn on_neighbor_indices(&self, index: usize, mut closure: F) where F: FnMut(usize), { let width = self.width as usize; let height = self.height as usize; let x = index % width; let y = index / width; if x > 0 { closure(index - 1); } if x < width - 1 { closure(index + 1); } if y > 0 { closure(index - width); } if y < height - 1 { closure(index + width); } } /// Returns an iterator over all positions and their tile values. pub fn iter(&self) -> impl Iterator + '_ { (0..self.height).flat_map(move |y| { (0..self.width).map(move |x| { let pos = UVec2::new(x, y); (pos, self[pos]) }) }) } /// Returns an iterator over just the tile values. pub fn iter_values(&self) -> impl Iterator + '_ { self.tiles.iter().copied() } /// Returns an iterator over all positions in the map. pub fn positions(&self) -> impl Iterator + '_ { (0..self.height).flat_map(move |y| (0..self.width).map(move |x| UVec2::new(x, y))) } /// Returns an iterator over tile indices, positions, and values. pub fn enumerate(&self) -> impl Iterator + '_ { self.tiles.iter().enumerate().map(move |(idx, &value)| { let pos = self.index_to_pos(idx); (idx, pos, value) }) } /// Returns a reference to the underlying tile data as a slice. pub fn as_slice(&self) -> &[T] { &self.tiles } /// Returns a mutable reference to the underlying tile data as a slice. pub fn as_mut_slice(&mut self) -> &mut [T] { &mut self.tiles } } impl TileMap { /// Creates a new TileMap with the specified dimensions, using T::default() for initialization. pub fn new(width: u32, height: u32) -> Self { Self::with_default(width, height, T::default()) } } impl Index for TileMap { type Output = T; #[inline] fn index(&self, pos: UVec2) -> &Self::Output { &self.tiles[self.pos_to_index(pos)] } } impl IndexMut for TileMap { #[inline] fn index_mut(&mut self, pos: UVec2) -> &mut Self::Output { let idx = self.pos_to_index(pos); &mut self.tiles[idx] } } impl Index for TileMap { type Output = T; #[inline] fn index(&self, index: usize) -> &Self::Output { &self.tiles[index] } } impl IndexMut for TileMap { #[inline] fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.tiles[index] } } #[cfg(test)] mod tests { use super::*; #[test] fn test_new_with_default() { let map = TileMap::::with_default(10, 10, 42); assert_eq!(map.width(), 10); assert_eq!(map.height(), 10); assert_eq!(map[UVec2::new(0, 0)], 42); assert_eq!(map[UVec2::new(9, 9)], 42); } #[test] fn test_from_vec() { let data = vec![1u8, 2, 3, 4]; let map = TileMap::from_vec(2, 2, data); assert_eq!(map[UVec2::new(0, 0)], 1); assert_eq!(map[UVec2::new(1, 0)], 2); assert_eq!(map[UVec2::new(0, 1)], 3); assert_eq!(map[UVec2::new(1, 1)], 4); } #[test] fn test_pos_to_index() { let map = TileMap::::with_default(10, 10, 0); assert_eq!(map.pos_to_index(UVec2::new(0, 0)), 0); assert_eq!(map.pos_to_index(UVec2::new(5, 0)), 5); assert_eq!(map.pos_to_index(UVec2::new(0, 1)), 10); assert_eq!(map.pos_to_index(UVec2::new(3, 2)), 23); } #[test] fn test_index_to_pos() { let map = TileMap::::with_default(10, 10, 0); assert_eq!(map.index_to_pos(0), UVec2::new(0, 0)); assert_eq!(map.index_to_pos(5), UVec2::new(5, 0)); assert_eq!(map.index_to_pos(10), UVec2::new(0, 1)); assert_eq!(map.index_to_pos(23), UVec2::new(3, 2)); } #[test] fn test_in_bounds() { let map = TileMap::::with_default(10, 10, 0); assert!(map.in_bounds(UVec2::new(0, 0))); assert!(map.in_bounds(UVec2::new(9, 9))); assert!(!map.in_bounds(UVec2::new(10, 0))); assert!(!map.in_bounds(UVec2::new(0, 10))); } #[test] fn test_get_set() { let mut map = TileMap::::with_default(10, 10, 0); assert_eq!(map.get(UVec2::new(5, 5)), Some(0)); assert!(map.set(UVec2::new(5, 5), 42)); assert_eq!(map.get(UVec2::new(5, 5)), Some(42)); assert!(!map.set(UVec2::new(10, 10), 99)); assert_eq!(map.get(UVec2::new(10, 10)), None); } #[test] fn test_index_operators() { let mut map = TileMap::::with_default(10, 10, 0); map[UVec2::new(5, 5)] = 42; assert_eq!(map[UVec2::new(5, 5)], 42); } #[test] fn test_index_by_usize() { let mut map = TileMap::::with_default(10, 10, 0); map[23] = 42; assert_eq!(map[23], 42); assert_eq!(map[UVec2::new(3, 2)], 42); } #[test] fn test_neighbors_center() { let map = TileMap::::with_default(10, 10, 0); let neighbors: Vec<_> = map.neighbors(UVec2::new(5, 5)).collect(); assert_eq!(neighbors.len(), 4); assert!(neighbors.contains(&UVec2::new(5, 6))); assert!(neighbors.contains(&UVec2::new(6, 5))); assert!(neighbors.contains(&UVec2::new(5, 4))); assert!(neighbors.contains(&UVec2::new(4, 5))); } #[test] fn test_neighbors_corner() { let map = TileMap::::with_default(10, 10, 0); let neighbors: Vec<_> = map.neighbors(UVec2::new(0, 0)).collect(); assert_eq!(neighbors.len(), 2); assert!(neighbors.contains(&UVec2::new(1, 0))); assert!(neighbors.contains(&UVec2::new(0, 1))); } #[test] fn test_neighbors_edge() { let map = TileMap::::with_default(10, 10, 0); let neighbors: Vec<_> = map.neighbors(UVec2::new(0, 5)).collect(); assert_eq!(neighbors.len(), 3); assert!(neighbors.contains(&UVec2::new(0, 6))); assert!(neighbors.contains(&UVec2::new(1, 5))); assert!(neighbors.contains(&UVec2::new(0, 4))); } #[test] fn test_on_neighbors() { let map = TileMap::::with_default(10, 10, 0); let mut count = 0; map.on_neighbors(UVec2::new(5, 5), |_| count += 1); assert_eq!(count, 4); } #[test] fn test_on_neighbor_indices() { let map = TileMap::::with_default(10, 10, 0); let center_idx = map.pos_to_index(UVec2::new(5, 5)); let mut count = 0; map.on_neighbor_indices(center_idx, |_| count += 1); assert_eq!(count, 4); } #[test] fn test_iter() { let map = TileMap::::with_default(2, 2, 0); let positions: Vec<_> = map.iter().map(|(pos, _)| pos).collect(); assert_eq!(positions.len(), 4); assert!(positions.contains(&UVec2::new(0, 0))); assert!(positions.contains(&UVec2::new(1, 1))); } #[test] fn test_iter_values() { let map = TileMap::::with_default(2, 2, 42); let values: Vec<_> = map.iter_values().collect(); assert_eq!(values, vec![42, 42, 42, 42]); } #[test] fn test_positions() { let map = TileMap::::with_default(2, 2, 0); let positions: Vec<_> = map.positions().collect(); assert_eq!(positions.len(), 4); assert_eq!(positions[0], UVec2::new(0, 0)); assert_eq!(positions[3], UVec2::new(1, 1)); } #[test] fn test_enumerate() { let mut map = TileMap::::with_default(2, 2, 0); map[UVec2::new(1, 1)] = 42; let entries: Vec<_> = map.enumerate().collect(); assert_eq!(entries.len(), 4); assert_eq!(entries[3], (3, UVec2::new(1, 1), 42)); } #[test] fn test_generic_u16() { let mut map = TileMap::::with_default(5, 5, 0); assert_eq!(map[UVec2::new(0, 0)], 0); map[UVec2::new(2, 2)] = 65535; assert_eq!(map[UVec2::new(2, 2)], 65535); } #[test] fn test_generic_f32() { let mut map = TileMap::::with_default(5, 5, 1.5); assert_eq!(map[UVec2::new(0, 0)], 1.5); map[UVec2::new(2, 2)] = 2.7; assert_eq!(map[UVec2::new(2, 2)], 2.7); } }