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 borrowing FEEN view: [`Feen`].

use sashite_epin::Identifier as Piece;
use sashite_sin::{Identifier as Style, Side};

use crate::error::ParseError;
use crate::hands::HandIter;
use crate::parse::{self, Parsed};
use crate::shape::Shape;
use crate::token::epin_token;

#[cfg(feature = "alloc")]
use crate::limits::MAX_DIMENSIONS;
#[cfg(feature = "alloc")]
use sashite_qi::{Player, Qi};

/// A validated FEEN position that borrows its source string.
///
/// `Feen` is produced by [`Feen::parse`] and exposes the position without
/// allocating: the geometry and piece counts are precomputed, while board
/// squares and hand items are yielded lazily by [`squares`](Feen::squares),
/// [`first_hand`](Feen::first_hand), and [`second_hand`](Feen::second_hand).
///
/// # Examples
///
/// ```
/// use sashite_feen::{Feen, Side};
///
/// let feen = Feen::parse("8/8/8/8/8/8/8/8 / W/w")?;
/// assert_eq!(feen.square_count(), 64);
/// assert_eq!(feen.piece_count(), 0);
/// assert_eq!(feen.active_side(), Side::First);
///
/// assert_eq!(feen.squares().count(), 64);
/// assert!(feen.squares().all(|square| square.is_none())); // an empty board
/// # Ok::<(), sashite_feen::ParseError>(())
/// ```
#[derive(Debug, Clone, Copy)]
pub struct Feen<'a> {
    inner: Parsed<'a>,
}

impl<'a> Feen<'a> {
    /// Validates `input` and returns a borrowing view of the position.
    ///
    /// # Errors
    ///
    /// Returns a [`ParseError`] if `input` is not a valid, canonical FEEN string.
    pub fn parse(input: &'a str) -> Result<Self, ParseError> {
        Ok(Self {
            inner: parse::parse(input)?,
        })
    }

    /// Reports whether `input` is a valid, canonical FEEN string.
    #[must_use]
    pub fn is_valid(input: &str) -> bool {
        parse::parse(input).is_ok()
    }

    /// The board geometry.
    #[must_use]
    pub const fn shape(&self) -> Shape {
        self.inner.shape
    }

    /// The total number of squares on the board.
    #[must_use]
    pub const fn square_count(&self) -> u32 {
        self.inner.shape.square_count()
    }

    /// The total number of pieces, on the board and in both hands.
    #[must_use]
    pub const fn piece_count(&self) -> u32 {
        self.inner
            .board_pieces
            .saturating_add(self.inner.hand_pieces)
    }

    /// The number of pieces on the board.
    #[must_use]
    pub const fn board_piece_count(&self) -> u32 {
        self.inner.board_pieces
    }

    /// The number of pieces held across both hands.
    #[must_use]
    pub const fn hand_piece_count(&self) -> u32 {
        self.inner.hand_pieces
    }

    /// The raw, canonical piece-placement field (field 1).
    pub(crate) const fn placement_field(&self) -> &'a [u8] {
        self.inner.placement
    }

    /// The raw, canonical hands field (field 2).
    pub(crate) const fn hands_field(&self) -> &'a [u8] {
        self.inner.hands
    }

    /// The side to move.
    #[must_use]
    pub const fn active_side(&self) -> Side {
        self.inner.active.side()
    }

    /// The side not to move.
    #[must_use]
    pub const fn inactive_side(&self) -> Side {
        self.inner.inactive.side()
    }

    /// The active player's style.
    #[must_use]
    pub const fn active_style(&self) -> Style {
        self.inner.active
    }

    /// The inactive player's style.
    #[must_use]
    pub const fn inactive_style(&self) -> Style {
        self.inner.inactive
    }

    /// The style associated with player side `first` (the uppercase token).
    #[must_use]
    pub const fn first_style(&self) -> Style {
        if self.inner.active.is_first() {
            self.inner.active
        } else {
            self.inner.inactive
        }
    }

    /// The style associated with player side `second` (the lowercase token).
    #[must_use]
    pub const fn second_style(&self) -> Style {
        if self.inner.active.is_second() {
            self.inner.active
        } else {
            self.inner.inactive
        }
    }

    /// Iterates the board squares in FEEN traversal order, yielding `None` for an
    /// empty square and `Some(piece)` for an occupied one.
    ///
    /// The iterator yields exactly [`square_count`](Feen::square_count) items.
    /// Dimensional separators carry no squares and are skipped; mapping the flat
    /// order onto coordinates is the caller's board serialization scheme.
    #[must_use]
    pub fn squares(&self) -> SquareIter<'a> {
        SquareIter::new(self.inner.placement)
    }

    /// Iterates the items in the First Player Hand, in canonical order.
    #[must_use]
    pub fn first_hand(&self) -> HandIter<'a> {
        HandIter::new(split_hands(self.inner.hands).0)
    }

    /// Iterates the items in the Second Player Hand, in canonical order.
    #[must_use]
    pub fn second_hand(&self) -> HandIter<'a> {
        HandIter::new(split_hands(self.inner.hands).1)
    }

    /// Materializes this view into an owned [`sashite_qi::Qi`] position.
    ///
    /// The board, hands, styles, and active player are copied into a `Qi` whose
    /// piece type is [`sashite_epin::Identifier`] and whose style type is
    /// [`sashite_sin::Identifier`]. This is the owned, transformable
    /// representation of a position in the Sashité ecosystem; `Feen` itself
    /// stays a read-only borrowing view.
    ///
    /// Because a `Feen` is only obtainable through [`parse`](Feen::parse) — which
    /// validates the geometry, the canonical form, the shared square cap, and the
    /// piece cardinality — every `Qi` invariant already holds, so the conversion
    /// is total.
    ///
    /// # Panics
    ///
    /// Never for a `Feen` obtained from [`parse`](Feen::parse): the internal
    /// `expect` is an invariant guard. Parsing enforces everything `Qi` requires
    /// (geometry, canonical form, the shared square cap, and cardinality), so
    /// each construction step succeeds. The section exists only because the
    /// underlying `Qi` builders are fallible in the general case.
    ///
    /// Available with the `alloc` feature.
    #[cfg(feature = "alloc")]
    #[must_use]
    pub fn to_qi(&self) -> Qi<Piece, Style> {
        // FEEN stores dimension sizes as bytes; Qi takes `usize`. The shape has
        // at most `MAX_DIMENSIONS` dimensions, so a fixed array avoids a heap
        // allocation here (the board allocation happens inside `Qi`).
        let shape = self.shape();
        let dim_sizes = shape.dimensions();
        let mut dims = [0usize; MAX_DIMENSIONS];
        for (slot, &size) in dims.iter_mut().zip(dim_sizes) {
            *slot = usize::from(size);
        }

        let turn = match self.active_side() {
            Side::First => Player::First,
            Side::Second => Player::Second,
        };

        // A fresh `Qi` board starts empty, so only occupied squares are placed.
        let placements = self
            .squares()
            .enumerate()
            .filter_map(|(index, square)| square.map(|piece| (index, Some(piece))));
        let first_hand = self.first_hand().map(|item| {
            (
                item.piece(),
                i32::try_from(item.count()).unwrap_or(i32::MAX),
            )
        });
        let second_hand = self.second_hand().map(|item| {
            (
                item.piece(),
                i32::try_from(item.count()).unwrap_or(i32::MAX),
            )
        });

        Qi::new(
            &dims[..dim_sizes.len()],
            self.first_style(),
            self.second_style(),
        )
        .and_then(|qi| qi.board_diff(placements))
        .and_then(|qi| qi.first_hand_diff(first_hand))
        .and_then(|qi| qi.second_hand_diff(second_hand))
        .map(|qi| qi.with_turn(turn))
        .expect("a validated FEEN always yields a valid Qi position")
    }
}

/// Splits a validated hands field into its first/second slices on the single `/`.
fn split_hands(hands: &[u8]) -> (&[u8], &[u8]) {
    match hands.iter().position(|&b| b == b'/') {
        Some(at) => (&hands[..at], &hands[at + 1..]),
        // Defensive: a validated hands field always contains the delimiter.
        None => (hands, &[]),
    }
}

/// A lazy iterator over board squares in FEEN traversal order.
///
/// Yields `None` for an empty square and `Some(piece)` for an occupied one.
#[derive(Debug)]
pub struct SquareIter<'a> {
    bytes: &'a [u8],
    pos: usize,
    empties: u32,
}

impl<'a> SquareIter<'a> {
    pub(crate) const fn new(bytes: &'a [u8]) -> Self {
        Self {
            bytes,
            pos: 0,
            empties: 0,
        }
    }
}

impl Iterator for SquareIter<'_> {
    type Item = Option<Piece>;

    fn next(&mut self) -> Option<Self::Item> {
        // Continue an in-progress run of empty squares.
        if self.empties > 0 {
            self.empties -= 1;
            return Some(None);
        }
        // Dimensional separators carry no squares; skip them all.
        while self.pos < self.bytes.len() && self.bytes[self.pos] == b'/' {
            self.pos += 1;
        }
        if self.pos >= self.bytes.len() {
            return None;
        }
        if self.bytes[self.pos].is_ascii_digit() {
            // Empty-count run: emit the first empty now, queue the rest.
            let mut value: u32 = 0;
            while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_digit() {
                value = value
                    .saturating_mul(10)
                    .saturating_add(u32::from(self.bytes[self.pos] - b'0'));
                self.pos += 1;
            }
            self.empties = value.saturating_sub(1);
            Some(None)
        } else {
            match epin_token(&self.bytes[self.pos..]) {
                Some((len, id)) => {
                    self.pos += len;
                    Some(Some(id))
                }
                // Defensive: validated input never reaches here.
                None => None,
            }
        }
    }
}