Skip to main content

sashite_feen/
feen.rs

1//! The borrowing FEEN view: [`Feen`].
2
3use sashite_epin::Identifier as Piece;
4use sashite_sin::{Identifier as Style, Side};
5
6use crate::error::ParseError;
7use crate::hands::HandIter;
8use crate::parse::{self, Parsed};
9use crate::shape::Shape;
10use crate::token::epin_token;
11
12#[cfg(feature = "alloc")]
13use crate::limits::MAX_DIMENSIONS;
14#[cfg(feature = "alloc")]
15use sashite_qi::{Player, Qi};
16
17/// A validated FEEN position that borrows its source string.
18///
19/// `Feen` is produced by [`Feen::parse`] and exposes the position without
20/// allocating: the geometry and piece counts are precomputed, while board
21/// squares and hand items are yielded lazily by [`squares`](Feen::squares),
22/// [`first_hand`](Feen::first_hand), and [`second_hand`](Feen::second_hand).
23///
24/// # Examples
25///
26/// ```
27/// use sashite_feen::{Feen, Side};
28///
29/// let feen = Feen::parse("8/8/8/8/8/8/8/8 / W/w")?;
30/// assert_eq!(feen.square_count(), 64);
31/// assert_eq!(feen.piece_count(), 0);
32/// assert_eq!(feen.active_side(), Side::First);
33///
34/// assert_eq!(feen.squares().count(), 64);
35/// assert!(feen.squares().all(|square| square.is_none())); // an empty board
36/// # Ok::<(), sashite_feen::ParseError>(())
37/// ```
38#[derive(Debug, Clone, Copy)]
39pub struct Feen<'a> {
40    inner: Parsed<'a>,
41}
42
43impl<'a> Feen<'a> {
44    /// Validates `input` and returns a borrowing view of the position.
45    ///
46    /// # Errors
47    ///
48    /// Returns a [`ParseError`] if `input` is not a valid, canonical FEEN string.
49    pub fn parse(input: &'a str) -> Result<Self, ParseError> {
50        Ok(Self {
51            inner: parse::parse(input)?,
52        })
53    }
54
55    /// Reports whether `input` is a valid, canonical FEEN string.
56    #[must_use]
57    pub fn is_valid(input: &str) -> bool {
58        parse::parse(input).is_ok()
59    }
60
61    /// The board geometry.
62    #[must_use]
63    pub const fn shape(&self) -> Shape {
64        self.inner.shape
65    }
66
67    /// The total number of squares on the board.
68    #[must_use]
69    pub const fn square_count(&self) -> u32 {
70        self.inner.shape.square_count()
71    }
72
73    /// The total number of pieces, on the board and in both hands.
74    #[must_use]
75    pub const fn piece_count(&self) -> u32 {
76        self.inner
77            .board_pieces
78            .saturating_add(self.inner.hand_pieces)
79    }
80
81    /// The number of pieces on the board.
82    #[must_use]
83    pub const fn board_piece_count(&self) -> u32 {
84        self.inner.board_pieces
85    }
86
87    /// The number of pieces held across both hands.
88    #[must_use]
89    pub const fn hand_piece_count(&self) -> u32 {
90        self.inner.hand_pieces
91    }
92
93    /// The raw, canonical piece-placement field (field 1).
94    pub(crate) const fn placement_field(&self) -> &'a [u8] {
95        self.inner.placement
96    }
97
98    /// The raw, canonical hands field (field 2).
99    pub(crate) const fn hands_field(&self) -> &'a [u8] {
100        self.inner.hands
101    }
102
103    /// The side to move.
104    #[must_use]
105    pub const fn active_side(&self) -> Side {
106        self.inner.active.side()
107    }
108
109    /// The side not to move.
110    #[must_use]
111    pub const fn inactive_side(&self) -> Side {
112        self.inner.inactive.side()
113    }
114
115    /// The active player's style.
116    #[must_use]
117    pub const fn active_style(&self) -> Style {
118        self.inner.active
119    }
120
121    /// The inactive player's style.
122    #[must_use]
123    pub const fn inactive_style(&self) -> Style {
124        self.inner.inactive
125    }
126
127    /// The style associated with player side `first` (the uppercase token).
128    #[must_use]
129    pub const fn first_style(&self) -> Style {
130        if self.inner.active.is_first() {
131            self.inner.active
132        } else {
133            self.inner.inactive
134        }
135    }
136
137    /// The style associated with player side `second` (the lowercase token).
138    #[must_use]
139    pub const fn second_style(&self) -> Style {
140        if self.inner.active.is_second() {
141            self.inner.active
142        } else {
143            self.inner.inactive
144        }
145    }
146
147    /// Iterates the board squares in FEEN traversal order, yielding `None` for an
148    /// empty square and `Some(piece)` for an occupied one.
149    ///
150    /// The iterator yields exactly [`square_count`](Feen::square_count) items.
151    /// Dimensional separators carry no squares and are skipped; mapping the flat
152    /// order onto coordinates is the caller's board serialization scheme.
153    #[must_use]
154    pub fn squares(&self) -> SquareIter<'a> {
155        SquareIter::new(self.inner.placement)
156    }
157
158    /// Iterates the items in the First Player Hand, in canonical order.
159    #[must_use]
160    pub fn first_hand(&self) -> HandIter<'a> {
161        HandIter::new(split_hands(self.inner.hands).0)
162    }
163
164    /// Iterates the items in the Second Player Hand, in canonical order.
165    #[must_use]
166    pub fn second_hand(&self) -> HandIter<'a> {
167        HandIter::new(split_hands(self.inner.hands).1)
168    }
169
170    /// Materializes this view into an owned [`sashite_qi::Qi`] position.
171    ///
172    /// The board, hands, styles, and active player are copied into a `Qi` whose
173    /// piece type is [`sashite_epin::Identifier`] and whose style type is
174    /// [`sashite_sin::Identifier`]. This is the owned, transformable
175    /// representation of a position in the Sashité ecosystem; `Feen` itself
176    /// stays a read-only borrowing view.
177    ///
178    /// Because a `Feen` is only obtainable through [`parse`](Feen::parse) — which
179    /// validates the geometry, the canonical form, the shared square cap, and the
180    /// piece cardinality — every `Qi` invariant already holds, so the conversion
181    /// is total.
182    ///
183    /// # Panics
184    ///
185    /// Never for a `Feen` obtained from [`parse`](Feen::parse): the internal
186    /// `expect` is an invariant guard. Parsing enforces everything `Qi` requires
187    /// (geometry, canonical form, the shared square cap, and cardinality), so
188    /// each construction step succeeds. The section exists only because the
189    /// underlying `Qi` builders are fallible in the general case.
190    ///
191    /// Available with the `alloc` feature.
192    #[cfg(feature = "alloc")]
193    #[must_use]
194    pub fn to_qi(&self) -> Qi<Piece, Style> {
195        // FEEN stores dimension sizes as bytes; Qi takes `usize`. The shape has
196        // at most `MAX_DIMENSIONS` dimensions, so a fixed array avoids a heap
197        // allocation here (the board allocation happens inside `Qi`).
198        let shape = self.shape();
199        let dim_sizes = shape.dimensions();
200        let mut dims = [0usize; MAX_DIMENSIONS];
201        for (slot, &size) in dims.iter_mut().zip(dim_sizes) {
202            *slot = usize::from(size);
203        }
204
205        let turn = match self.active_side() {
206            Side::First => Player::First,
207            Side::Second => Player::Second,
208        };
209
210        // A fresh `Qi` board starts empty, so only occupied squares are placed.
211        let placements = self
212            .squares()
213            .enumerate()
214            .filter_map(|(index, square)| square.map(|piece| (index, Some(piece))));
215        let first_hand = self.first_hand().map(|item| {
216            (
217                item.piece(),
218                i32::try_from(item.count()).unwrap_or(i32::MAX),
219            )
220        });
221        let second_hand = self.second_hand().map(|item| {
222            (
223                item.piece(),
224                i32::try_from(item.count()).unwrap_or(i32::MAX),
225            )
226        });
227
228        Qi::new(
229            &dims[..dim_sizes.len()],
230            self.first_style(),
231            self.second_style(),
232        )
233        .and_then(|qi| qi.board_diff(placements))
234        .and_then(|qi| qi.first_hand_diff(first_hand))
235        .and_then(|qi| qi.second_hand_diff(second_hand))
236        .map(|qi| qi.with_turn(turn))
237        .expect("a validated FEEN always yields a valid Qi position")
238    }
239}
240
241/// Splits a validated hands field into its first/second slices on the single `/`.
242fn split_hands(hands: &[u8]) -> (&[u8], &[u8]) {
243    match hands.iter().position(|&b| b == b'/') {
244        Some(at) => (&hands[..at], &hands[at + 1..]),
245        // Defensive: a validated hands field always contains the delimiter.
246        None => (hands, &[]),
247    }
248}
249
250/// A lazy iterator over board squares in FEEN traversal order.
251///
252/// Yields `None` for an empty square and `Some(piece)` for an occupied one.
253#[derive(Debug)]
254pub struct SquareIter<'a> {
255    bytes: &'a [u8],
256    pos: usize,
257    empties: u32,
258}
259
260impl<'a> SquareIter<'a> {
261    pub(crate) const fn new(bytes: &'a [u8]) -> Self {
262        Self {
263            bytes,
264            pos: 0,
265            empties: 0,
266        }
267    }
268}
269
270impl Iterator for SquareIter<'_> {
271    type Item = Option<Piece>;
272
273    fn next(&mut self) -> Option<Self::Item> {
274        // Continue an in-progress run of empty squares.
275        if self.empties > 0 {
276            self.empties -= 1;
277            return Some(None);
278        }
279        // Dimensional separators carry no squares; skip them all.
280        while self.pos < self.bytes.len() && self.bytes[self.pos] == b'/' {
281            self.pos += 1;
282        }
283        if self.pos >= self.bytes.len() {
284            return None;
285        }
286        if self.bytes[self.pos].is_ascii_digit() {
287            // Empty-count run: emit the first empty now, queue the rest.
288            let mut value: u32 = 0;
289            while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_digit() {
290                value = value
291                    .saturating_mul(10)
292                    .saturating_add(u32::from(self.bytes[self.pos] - b'0'));
293                self.pos += 1;
294            }
295            self.empties = value.saturating_sub(1);
296            Some(None)
297        } else {
298            match epin_token(&self.bytes[self.pos..]) {
299                Some((len, id)) => {
300                    self.pos += len;
301                    Some(Some(id))
302                }
303                // Defensive: validated input never reaches here.
304                None => None,
305            }
306        }
307    }
308}