sashite-feen 0.1.0

Field Expression Encoding Notation (FEEN): a compact, ASCII-only, no_std, zero-allocation validator and encoder for board-game positions in abstract strategy games, built on EPIN and SIN.
Documentation
//! The board geometry: a small, fixed-size, `Copy` description of a regular
//! multi-dimensional board.

use crate::limits::MAX_DIMENSIONS;

/// The shape of a regular board: the size of each of its dimensions.
///
/// Dimensions are ordered **outermost first**: for a 3D board, `dimensions()` is
/// `[layers, ranks, files]`; for 2D, `[ranks, files]`; for 1D, `[files]`. This
/// matches the FEEN separator depth — the deepest separator group splits the
/// outermost dimension.
///
/// A `Shape` holds between 1 and [`MAX_DIMENSIONS`] dimensions, each of size 1
/// to [`crate::MAX_DIMENSION_SIZE`].
///
/// # Invariant
///
/// Entries beyond the active dimension count are always zero, so the derived
/// equality and hashing compare only the meaningful dimensions.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Shape {
    sizes: [u8; MAX_DIMENSIONS],
    ndim: u8,
}

impl Shape {
    /// Builds a shape from its dimension sizes, outermost first.
    ///
    /// Returns `None` if there are zero dimensions, more than [`MAX_DIMENSIONS`],
    /// or any dimension of size zero. (Sizes above [`crate::MAX_DIMENSION_SIZE`]
    /// are unrepresentable, since each is a `u8`.)
    ///
    /// # Examples
    ///
    /// ```
    /// use sashite_feen::Shape;
    ///
    /// let s = Shape::new(&[9, 9]).unwrap();
    /// assert_eq!(s.dimensions(), &[9, 9]);
    /// assert_eq!(s.dimension_count(), 2);
    /// assert_eq!(s.square_count(), 81);
    ///
    /// assert!(Shape::new(&[]).is_none()); // need at least one dimension
    /// assert!(Shape::new(&[2, 2, 2, 2]).is_none()); // too many dimensions
    /// assert!(Shape::new(&[8, 0]).is_none()); // a dimension cannot be empty
    /// ```
    #[must_use]
    pub const fn new(dimensions: &[u8]) -> Option<Self> {
        let ndim = dimensions.len();
        if ndim == 0 || ndim > MAX_DIMENSIONS {
            return None;
        }

        let mut sizes = [0u8; MAX_DIMENSIONS];
        let mut i = 0;
        while i < ndim {
            if dimensions[i] == 0 {
                return None;
            }
            sizes[i] = dimensions[i];
            i += 1;
        }

        #[allow(clippy::cast_possible_truncation)] // guarded: ndim <= MAX_DIMENSIONS
        Some(Self {
            sizes,
            ndim: ndim as u8,
        })
    }

    /// Builds a shape from a pre-validated size array and dimension count.
    ///
    /// The caller MUST guarantee that `1 <= ndim <= MAX_DIMENSIONS`, that
    /// `sizes[..ndim]` are all non-zero, and that `sizes[ndim..]` are zero.
    #[must_use]
    pub(crate) const fn from_sizes(sizes: [u8; MAX_DIMENSIONS], ndim: u8) -> Self {
        Self { sizes, ndim }
    }

    /// Returns the dimension sizes, outermost first (length 1 to
    /// [`MAX_DIMENSIONS`]).
    #[must_use]
    pub fn dimensions(&self) -> &[u8] {
        &self.sizes[..self.ndim as usize]
    }

    /// Returns the number of dimensions (1, 2, or 3).
    #[must_use]
    pub const fn dimension_count(&self) -> usize {
        self.ndim as usize
    }

    /// Returns the total number of squares: the product of the dimension sizes.
    ///
    /// The maximum is `255^3 = 16_581_375`, which fits comfortably in a `u32`.
    #[must_use]
    pub const fn square_count(&self) -> u32 {
        let mut product: u32 = 1;
        let mut i = 0;
        while i < self.ndim as usize {
            product *= self.sizes[i] as u32;
            i += 1;
        }
        product
    }
}

impl core::fmt::Debug for Shape {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_tuple("Shape").field(&self.dimensions()).finish()
    }
}