mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-05 23:16:23 -06:00
442 lines
14 KiB
Rust
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);
|
|
}
|
|
}
|