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}