Skip to main content

sashite_feen/
shape.rs

1//! The board geometry: a small, fixed-size, `Copy` description of a regular
2//! multi-dimensional board.
3
4use crate::limits::MAX_DIMENSIONS;
5
6/// The shape of a regular board: the size of each of its dimensions.
7///
8/// Dimensions are ordered **outermost first**: for a 3D board, `dimensions()` is
9/// `[layers, ranks, files]`; for 2D, `[ranks, files]`; for 1D, `[files]`. This
10/// matches the FEEN separator depth — the deepest separator group splits the
11/// outermost dimension.
12///
13/// A `Shape` holds between 1 and [`MAX_DIMENSIONS`] dimensions, each of size 1
14/// to [`crate::MAX_DIMENSION_SIZE`].
15///
16/// # Invariant
17///
18/// Entries beyond the active dimension count are always zero, so the derived
19/// equality and hashing compare only the meaningful dimensions.
20#[derive(Clone, Copy, PartialEq, Eq, Hash)]
21pub struct Shape {
22    sizes: [u8; MAX_DIMENSIONS],
23    ndim: u8,
24}
25
26impl Shape {
27    /// Builds a shape from its dimension sizes, outermost first.
28    ///
29    /// Returns `None` if there are zero dimensions, more than [`MAX_DIMENSIONS`],
30    /// or any dimension of size zero. (Sizes above [`crate::MAX_DIMENSION_SIZE`]
31    /// are unrepresentable, since each is a `u8`.)
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use sashite_feen::Shape;
37    ///
38    /// let s = Shape::new(&[9, 9]).unwrap();
39    /// assert_eq!(s.dimensions(), &[9, 9]);
40    /// assert_eq!(s.dimension_count(), 2);
41    /// assert_eq!(s.square_count(), 81);
42    ///
43    /// assert!(Shape::new(&[]).is_none()); // need at least one dimension
44    /// assert!(Shape::new(&[2, 2, 2, 2]).is_none()); // too many dimensions
45    /// assert!(Shape::new(&[8, 0]).is_none()); // a dimension cannot be empty
46    /// ```
47    #[must_use]
48    pub const fn new(dimensions: &[u8]) -> Option<Self> {
49        let ndim = dimensions.len();
50        if ndim == 0 || ndim > MAX_DIMENSIONS {
51            return None;
52        }
53
54        let mut sizes = [0u8; MAX_DIMENSIONS];
55        let mut i = 0;
56        while i < ndim {
57            if dimensions[i] == 0 {
58                return None;
59            }
60            sizes[i] = dimensions[i];
61            i += 1;
62        }
63
64        #[allow(clippy::cast_possible_truncation)] // guarded: ndim <= MAX_DIMENSIONS
65        Some(Self {
66            sizes,
67            ndim: ndim as u8,
68        })
69    }
70
71    /// Builds a shape from a pre-validated size array and dimension count.
72    ///
73    /// The caller MUST guarantee that `1 <= ndim <= MAX_DIMENSIONS`, that
74    /// `sizes[..ndim]` are all non-zero, and that `sizes[ndim..]` are zero.
75    #[must_use]
76    pub(crate) const fn from_sizes(sizes: [u8; MAX_DIMENSIONS], ndim: u8) -> Self {
77        Self { sizes, ndim }
78    }
79
80    /// Returns the dimension sizes, outermost first (length 1 to
81    /// [`MAX_DIMENSIONS`]).
82    #[must_use]
83    pub fn dimensions(&self) -> &[u8] {
84        &self.sizes[..self.ndim as usize]
85    }
86
87    /// Returns the number of dimensions (1, 2, or 3).
88    #[must_use]
89    pub const fn dimension_count(&self) -> usize {
90        self.ndim as usize
91    }
92
93    /// Returns the total number of squares: the product of the dimension sizes.
94    ///
95    /// The maximum is `255^3 = 16_581_375`, which fits comfortably in a `u32`.
96    #[must_use]
97    pub const fn square_count(&self) -> u32 {
98        let mut product: u32 = 1;
99        let mut i = 0;
100        while i < self.ndim as usize {
101            product *= self.sizes[i] as u32;
102            i += 1;
103        }
104        product
105    }
106}
107
108impl core::fmt::Debug for Shape {
109    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110        f.debug_tuple("Shape").field(&self.dimensions()).finish()
111    }
112}