liquidwar7core 0.2.0

Liquidwar7 core logic library, low-level things which are game-engine agnostic.
Documentation
// Copyright (C) 2025 Christian Mauduit <ufoot@ufoot.org>

//! Simple 3D grid for fighter occupation tracking.
//!
//! This module provides a straightforward w×h×d grid that tracks which
//! fighter occupies each cell. Walkability and gradient lookups are
//! delegated to QuadMesh.

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// A simple 3D grid for fighter occupation tracking.
///
/// Uses a flat `Vec<Option<u64>>` indexed by `x + y * width + z * width * height`.
/// `None` means empty, `Some(id)` means occupied by that fighter.
///
/// Walkability is determined by `QuadMesh.cell_at()` - this grid only tracks fighters.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Grid {
    width: usize,
    height: usize,
    depth: usize,
    /// Fighter at each cell. None = empty, Some(id) = occupied.
    fighters: Vec<Option<u64>>,
}

impl Grid {
    /// Creates a new grid with the given dimensions.
    /// All cells start empty (None).
    pub fn new(width: usize, height: usize, depth: usize) -> Self {
        let size = width * height * depth;
        Self {
            width,
            height,
            depth,
            fighters: vec![None; size],
        }
    }

    /// Returns the grid dimensions.
    pub fn dimensions(&self) -> (usize, usize, usize) {
        (self.width, self.height, self.depth)
    }

    /// Converts (x, y, z) to a flat index, returning None if out of bounds.
    #[inline]
    pub fn xyz_to_index(&self, x: usize, y: usize, z: usize) -> Option<usize> {
        if x < self.width && y < self.height && z < self.depth {
            Some(x + y * self.width + z * self.width * self.height)
        } else {
            None
        }
    }

    /// Converts a flat index to (x, y, z).
    #[inline]
    pub fn index_to_xyz(&self, index: usize) -> Option<(usize, usize, usize)> {
        if index < self.fighters.len() {
            let z = index / (self.width * self.height);
            let remainder = index % (self.width * self.height);
            let y = remainder / self.width;
            let x = remainder % self.width;
            Some((x, y, z))
        } else {
            None
        }
    }

    /// Returns the fighter at (x, y, z), if any.
    #[inline]
    pub fn fighter_at(&self, x: usize, y: usize, z: usize) -> Option<u64> {
        self.xyz_to_index(x, y, z)
            .and_then(|idx| self.fighters[idx])
    }

    /// Sets the fighter at (x, y, z). Returns false if out of bounds.
    #[inline]
    pub fn set_fighter(&mut self, x: usize, y: usize, z: usize, fighter_id: Option<u64>) -> bool {
        if let Some(idx) = self.xyz_to_index(x, y, z) {
            self.fighters[idx] = fighter_id;
            true
        } else {
            false
        }
    }

    /// Returns true if (x, y, z) has no fighter.
    /// Note: This doesn't check walkability - use QuadMesh.cell_at() for that.
    #[inline]
    pub fn is_cell_empty(&self, x: usize, y: usize, z: usize) -> bool {
        self.fighter_at(x, y, z).is_none()
    }

    /// Returns the 6-connected neighbors of (x, y, z).
    /// Note: This doesn't filter by walkability - caller should check QuadMesh.
    pub fn neighbors(&self, x: usize, y: usize, z: usize) -> impl Iterator<Item = (usize, usize, usize)> {
        let width = self.width;
        let height = self.height;
        let depth = self.depth;

        let candidates = [
            (x.wrapping_sub(1), y, z),
            (x + 1, y, z),
            (x, y.wrapping_sub(1), z),
            (x, y + 1, z),
            (x, y, z.wrapping_sub(1)),
            (x, y, z + 1),
        ];

        candidates
            .into_iter()
            .filter(move |(nx, ny, nz)| *nx < width && *ny < height && *nz < depth)
    }

    /// Returns the total number of cells.
    pub fn len(&self) -> usize {
        self.fighters.len()
    }

    /// Returns true if the grid has no cells (zero dimensions).
    pub fn is_empty(&self) -> bool {
        self.fighters.is_empty()
    }

    /// Clears all fighters from the grid.
    pub fn clear(&mut self) {
        self.fighters.fill(None);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_grid_new() {
        let grid = Grid::new(10, 8, 2);
        assert_eq!(grid.dimensions(), (10, 8, 2));
        assert_eq!(grid.len(), 160);
    }

    #[test]
    fn test_xyz_to_index() {
        let grid = Grid::new(10, 8, 2);
        assert_eq!(grid.xyz_to_index(0, 0, 0), Some(0));
        assert_eq!(grid.xyz_to_index(9, 0, 0), Some(9));
        assert_eq!(grid.xyz_to_index(0, 1, 0), Some(10));
        assert_eq!(grid.xyz_to_index(0, 0, 1), Some(80));
        assert_eq!(grid.xyz_to_index(10, 0, 0), None); // Out of bounds
    }

    #[test]
    fn test_index_to_xyz() {
        let grid = Grid::new(10, 8, 2);
        assert_eq!(grid.index_to_xyz(0), Some((0, 0, 0)));
        assert_eq!(grid.index_to_xyz(9), Some((9, 0, 0)));
        assert_eq!(grid.index_to_xyz(10), Some((0, 1, 0)));
        assert_eq!(grid.index_to_xyz(80), Some((0, 0, 1)));
        assert_eq!(grid.index_to_xyz(160), None); // Out of bounds
    }

    #[test]
    fn test_roundtrip() {
        let grid = Grid::new(10, 8, 2);
        for x in 0..10 {
            for y in 0..8 {
                for z in 0..2 {
                    let idx = grid.xyz_to_index(x, y, z).unwrap();
                    let (rx, ry, rz) = grid.index_to_xyz(idx).unwrap();
                    assert_eq!((x, y, z), (rx, ry, rz));
                }
            }
        }
    }

    #[test]
    fn test_set_and_get_fighter() {
        let mut grid = Grid::new(10, 10, 1);

        // Initially empty
        assert!(grid.is_cell_empty(5, 5, 0));
        assert_eq!(grid.fighter_at(5, 5, 0), None);

        // Place a fighter
        let fighter_id = 0x1234567890abcdef_u64;
        assert!(grid.set_fighter(5, 5, 0, Some(fighter_id)));
        assert!(!grid.is_cell_empty(5, 5, 0));
        assert_eq!(grid.fighter_at(5, 5, 0), Some(fighter_id));

        // Remove fighter
        grid.set_fighter(5, 5, 0, None);
        assert!(grid.is_cell_empty(5, 5, 0));
    }

    #[test]
    fn test_neighbors() {
        let grid = Grid::new(10, 10, 1);

        // Center cell should have 4 neighbors (z=0 only has 4, not 6)
        let neighbors: Vec<_> = grid.neighbors(5, 5, 0).collect();
        assert_eq!(neighbors.len(), 4);
        assert!(neighbors.contains(&(4, 5, 0)));
        assert!(neighbors.contains(&(6, 5, 0)));
        assert!(neighbors.contains(&(5, 4, 0)));
        assert!(neighbors.contains(&(5, 6, 0)));

        // Corner cell should have 2 neighbors
        let corner_neighbors: Vec<_> = grid.neighbors(0, 0, 0).collect();
        assert_eq!(corner_neighbors.len(), 2);
    }
}