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}