Files
smart-rgb/crates/borders-core/src/game/tilemap.rs
2025-10-10 20:00:55 -05:00

442 lines
14 KiB
Rust

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::<u8>::new(10, 10);
/// map[UVec2::new(5, 5)] = 42;
/// assert_eq!(map[UVec2::new(5, 5)], 42);
/// ```
#[derive(Clone, Debug)]
pub struct TileMap<T: Copy> {
tiles: Box<[T]>,
width: u32,
height: u32,
}
impl<T: Copy> TileMap<T> {
/// 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<T>) -> 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<T> {
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<Item = UVec2> + '_ {
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<F>(&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<F>(&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<Item = (UVec2, T)> + '_ {
(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<Item = T> + '_ {
self.tiles.iter().copied()
}
/// Returns an iterator over all positions in the map.
pub fn positions(&self) -> impl Iterator<Item = UVec2> + '_ {
(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<Item = (usize, UVec2, T)> + '_ {
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<T: Copy + Default> TileMap<T> {
/// 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<T: Copy> Index<UVec2> for TileMap<T> {
type Output = T;
#[inline]
fn index(&self, pos: UVec2) -> &Self::Output {
&self.tiles[self.pos_to_index(pos)]
}
}
impl<T: Copy> IndexMut<UVec2> for TileMap<T> {
#[inline]
fn index_mut(&mut self, pos: UVec2) -> &mut Self::Output {
let idx = self.pos_to_index(pos);
&mut self.tiles[idx]
}
}
impl<T: Copy> Index<usize> for TileMap<T> {
type Output = T;
#[inline]
fn index(&self, index: usize) -> &Self::Output {
&self.tiles[index]
}
}
impl<T: Copy> IndexMut<usize> for TileMap<T> {
#[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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u16>::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::<f32>::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);
}
}