owlchess 0.3.2

Yet another chess library for Rust
Documentation
//! Board that remembers previous moves
//!
//! Sometimes, using [`Board`](crate::board::Board) is not very convenient. The possible
//! reasons are as follows:
//!
//! - you cannot undo moves in a fast manner. You need to either create a new board after
//!   each move, or deal with [`RawUndo`](crate::moves::RawUndo) and `unsafe` manually
//! - you cannot detect draw by repetitions
//! - you cannot inspect all the previous moves or get the notation of the entire game
//!
//! To solve all these problems, there is a [`MoveChain`].
//!
//! # Example
//!
//! ```
//! # use owlchess::{Board, MoveChain};
//! # use owlchess::moves::make;
//! #
//! // Create an empty chain from the empty position
//! let mut chain = MoveChain::new_initial();
//!
//! // Push d2d4 move
//! chain.push(make::Uci("d2d4")).unwrap();
//! assert_eq!(
//!     chain.last().to_string(),
//!     "rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq d3 0 1".to_string(),
//! );
//!
//! // Undo the last move
//! chain.pop().unwrap();
//! assert_eq!(chain.last(), &Board::initial());
//!
//! // Push other moves
//! chain.push(make::Uci("e2e4")).unwrap();
//! chain.push(make::Uci("e7e5")).unwrap();
//!
//! // Get notation as a UCI move sequence
//! assert_eq!(chain.uci().to_string(), "e2e4 e7e5".to_string());
//! ```

use crate::board::{self, Board, RawBoard};
use crate::moves::{self, make, Make, Move, RawUndo};
use crate::types::{Color, DrawReason, GameStatus, Outcome, OutcomeFilter};

use std::collections::HashMap;
use std::fmt;

use thiserror::Error;

/// Error while parsing and applying UCI move sequence
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("cannot parse UCI move #{}: {}", .pos + 1, .source)]
pub struct UciParseError {
    /// The number of moves successfully applied before the error occurred
    pub pos: usize,
    /// The error happened when trying to apply the move
    pub source: moves::uci::ParseError,
}

/// Repetition table trait
///
/// It allows to customize [`MoveChain`] and to implement something better than
/// the built-in [`HashRepeat`] to detect draws by repetitions.
///
/// Technically, a repetition table is just a multiset that stored all the positions
/// occurred during the game.
pub trait Repeat: Default {
    /// Adds a board `b` to the set
    ///
    /// Note that this is a multiset, so there can be multiple instances of the same
    /// board in it.
    fn push(&mut self, b: &Board);

    /// Removes a board `b` from the set
    ///
    /// # Panics
    ///
    /// The function must panic if there is no such board `b`.
    fn pop(&mut self, b: &Board);

    /// Returns the number of times board `b` is present in the set.
    fn count(&self, b: &Board) -> usize;
}

/// Simple repetition table based on [`HashMap`]
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct HashRepeat(HashMap<u64, usize>);

impl Repeat for HashRepeat {
    fn push(&mut self, b: &Board) {
        self.0
            .entry(b.zobrist_hash())
            .and_modify(|x| *x += 1)
            .or_insert(1);
    }

    fn pop(&mut self, b: &Board) {
        let hash = b.zobrist_hash();
        let r = self.0.get_mut(&hash).unwrap();
        *r -= 1;
        if *r == 0 {
            self.0.remove(&hash);
        }
    }

    fn count(&self, b: &Board) -> usize {
        *self.0.get(&b.zobrist_hash()).unwrap_or(&0)
    }
}

/// Convenience instantiation of [`BaseMoveChain`] with default repetition table
pub type MoveChain = BaseMoveChain<HashRepeat>;

/// Board that remembers previous moves
///
/// See the [module docs](crate::chain) for more details.
///
/// This version allows customizing its repetition table. Use [`MoveChain`] if you don't
/// need this.
#[derive(Debug, Clone)]
pub struct BaseMoveChain<R: Repeat> {
    start: RawBoard,
    board: Board,
    repeat: R,
    stack: Vec<(Move, RawUndo)>,
    outcome: Option<Outcome>,
}

impl<R: Repeat> BaseMoveChain<R> {
    /// Creates an empty move chain, starting with position `b`
    pub fn new(b: Board) -> Self {
        let mut res = BaseMoveChain {
            start: b.r,
            board: b,
            repeat: R::default(),
            stack: Vec::new(),
            outcome: None,
        };
        res.repeat.push(&res.board);
        res
    }

    /// Creates an empty move chain, starting with initial position
    #[inline]
    pub fn new_initial() -> Self {
        Self::new(Board::initial())
    }

    /// Creates a move chain starting from position `b` with UCI moves from the
    /// space-separated list `uci_list` applied
    #[inline]
    pub fn from_uci_list(b: Board, uci_list: &str) -> Result<Self, UciParseError> {
        let mut res = BaseMoveChain::new(b);
        res.push_uci_list(uci_list)?;
        Ok(res)
    }

    /// Creates an empty move chain, starting from position with FEN string `fen`
    #[inline]
    pub fn from_fen(fen: &str) -> Result<Self, board::FenParseError> {
        Ok(Self::new(Board::from_fen(fen)?))
    }

    /// Returns the position from which this chain starts
    #[inline]
    pub fn startpos(&self) -> &RawBoard {
        &self.start
    }

    /// Returns the current position
    #[inline]
    pub fn last(&self) -> &Board {
        &self.board
    }

    /// Returns the number of applied moves
    #[inline]
    pub fn len(&self) -> usize {
        self.stack.len()
    }

    /// Returns `true` if there is no applied moves
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.stack.is_empty()
    }

    /// Iterates over all the applied moves, from first to last
    ///
    /// If you need previous positions alongside with the moves, you need to use
    /// [`BaseMoveChain::walk()`] instead.
    #[inline]
    pub fn iter(&self) -> impl Iterator<Item = Move> + '_ {
        self.stack.iter().map(|(m, _)| *m)
    }

    /// Returns the `idx`th applied move
    ///
    /// # Panics
    ///
    /// The function panics if `idx` is out of range.
    #[inline]
    pub fn get(&self, idx: usize) -> Move {
        self.stack[idx].0
    }

    /// Returns the `idx`th applied move
    ///
    /// # Safety
    ///
    /// The behavior is undefined if `idx` is out of range.
    #[inline]
    pub unsafe fn get_unchecked(&self, idx: usize) -> Move {
        self.stack.get_unchecked(idx).0
    }

    /// Returns the game outcome stored in the chain
    #[inline]
    pub fn outcome(&self) -> &Option<Outcome> {
        &self.outcome
    }

    /// Returns `true` if there is a game outcome stored
    #[inline]
    pub fn is_finished(&self) -> bool {
        self.outcome.is_some()
    }

    /// Removes the stored game outcome
    #[inline]
    pub fn clear_outcome(&mut self) {
        self.outcome = None;
    }

    /// Sets the game outcome to `outcome`
    ///
    /// # Panics
    ///
    /// The function panics is the outcome was already set.
    #[inline]
    pub fn set_outcome(&mut self, outcome: Outcome) {
        assert!(!self.is_finished());
        self.outcome = Some(outcome);
    }

    /// Resets the outcome to `outcome`
    ///
    /// If the outcome was already set, then it is just replaced by `outcome`
    #[inline]
    pub fn reset_outcome(&mut self, outcome: Option<Outcome>) {
        self.outcome = outcome;
    }

    /// Calculates the current outcome of the game
    ///
    /// This function ignores the value stored in [`BaseMoveChain::outcome()`] and uses
    /// just current position and repetition table for calculations.
    ///
    /// This function can be computationally expensive, as it calls
    /// [`movegen::has_legal_moves`](crate::movegen::has_legal_moves).
    ///
    /// Unlike [`Board::calc_outcome()`], it is able to detect draw by repetitions.
    ///
    /// For the priority of different outcomes, see the docs for [`Board::calc_outcome()`]
    pub fn calc_outcome(&self) -> Option<Outcome> {
        // We need to handle the priority of different outcomes carefully.
        //
        // Checkmate and stalemate are definitely preferred over all the other
        // outcomes, though they cannot happen at the same time with draw by
        // repetitions.
        //
        // Next, all the strict outcomes must be checked before the non-strict ones.
        // For example, if there is both `DrawReason::Moves50` and `DrawReason::Repeat5`,
        // the latter must be preferred, as it's strict. Still, we have no priority
        // between `DrawReason::Moves75` and `DrawReason::Repeat5` or between
        // `DrawReason::Moves50` and `DrawReason::Repeat3`.
        //
        // So, we can do the following:
        // - first, call `Board::calc_outcome()` and check for strict outcomes
        // - then, check for draw by repetitions
        // - finally, return a non-strict outcome from `Board::calc_outcome()`, if any

        let outcome = self.board.calc_outcome();
        if let Some(out) = outcome {
            if out.passes(OutcomeFilter::Strict) {
                return outcome;
            }
        }

        let rep = self.repeat.count(&self.board);
        if rep >= 5 {
            return Some(Outcome::Draw(DrawReason::Repeat5));
        }
        if rep >= 3 {
            return Some(Outcome::Draw(DrawReason::Repeat3));
        }

        outcome
    }

    /// Calculates the game outcome using [`BaseMoveChain::calc_outcome()`] and sets the calculated
    /// outcome if it passes the filter `filter`
    ///
    /// This function can be computationally expensive, as it calls
    /// [`movegen::has_legal_moves`](crate::movegen::has_legal_moves).
    ///
    /// # Panics
    ///
    /// The function panics is the outcome was already set.
    pub fn set_auto_outcome(&mut self, filter: OutcomeFilter) -> Option<Outcome> {
        assert!(!self.is_finished());
        if let Some(outcome) = self.calc_outcome() {
            if outcome.passes(filter) {
                self.set_outcome(outcome);
            }
        }
        self.outcome
    }

    #[inline]
    fn do_finish_push(&mut self, mv: Move, u: RawUndo) {
        self.repeat.push(&self.board);
        self.stack.push((mv, u));
    }

    /// Pushes a move `mv` to the chain
    ///
    /// # Safety
    ///
    /// The move must be either legal or null, and the move cannot be null if the king is in
    /// check. Also, game outcome must be unset. Otherwise, the behavior is undefined.
    #[inline]
    pub unsafe fn push_unchecked(&mut self, mv: Move) {
        let u = moves::make_move_unchecked(&mut self.board, mv);
        self.do_finish_push(mv, u);
    }

    /// Pushes a move-like object `m` to the chain
    ///
    /// If `m` doesn't represent a legal move, then nothing is pushed, and the error
    /// is returned.
    #[inline]
    pub fn push<M: Make>(&mut self, m: M) -> Result<(), M::Err> {
        assert!(!self.is_finished());
        let (mv, undo) = m.make_raw(&mut self.board)?;
        self.do_finish_push(mv, undo);
        Ok(())
    }

    /// Pushes a space-separated list of moves `uci_list` to the chain
    ///
    /// The string is first split into tokens. Then, the function tries to apply each token as
    /// a UCI move. It stops when either all the moves were applied or an error occurred. So,
    /// in case of errors only a prefix of `uci_list` is pushed.
    ///
    /// To determine how much moves were pushed in case of error, you need to inspect
    /// [`UciParseError`].
    pub fn push_uci_list(&mut self, uci_list: &str) -> Result<(), UciParseError> {
        for (pos, token) in uci_list.split_ascii_whitespace().enumerate() {
            self.push(make::Uci(token))
                .map_err(|source| UciParseError { pos, source })?;
        }
        Ok(())
    }

    /// Removes the last pushed move from the chain and returns the removed move
    ///
    /// If the chain doesn't contain any moves, it remains unchanged and `None` is
    /// returned.
    pub fn pop(&mut self) -> Option<Move> {
        let (m, u) = self.stack.pop()?;
        self.repeat.pop(&self.board);
        self.clear_outcome();
        unsafe { moves::unmake_move_unchecked(&mut self.board, m, u) };
        Some(m)
    }

    /// Returns the wrapper which helps to format the move chain as a space-separated
    /// UCI move list
    ///
    /// The resulting wrapper implements [`fmt::Display`], so can be used with
    /// `write!()`, `println!()`, or `ToString::to_string`.
    ///
    /// # Example
    ///
    /// ```
    /// # use owlchess::{board::Board, chain::MoveChain};
    /// #
    /// let chain = MoveChain::from_uci_list(Board::initial(), "e2e4 e7e5 g1f3").unwrap();
    /// assert_eq!(format!("Your UCI chain is {}", chain.uci()), "Your UCI chain is e2e4 e7e5 g1f3");
    /// ```
    #[inline]
    pub fn uci(&self) -> UciList<'_, R> {
        UciList(self)
    }

    /// Creates a [`Walker`] over the current move chain
    ///
    /// [`Walker`] looks similar to iterator, but is bidirectional and can iterate over moves
    /// alongside with positions preceding these moves.
    ///
    /// The returned walker is initially at the beginning of the move chain.
    #[inline]
    pub fn walk(&self) -> Walker<'_> {
        Walker {
            board: self.board.clone(),
            stack: &self.stack,
            pos: 0,
            board_pos: self.stack.len(),
        }
    }

    /// Returns the wrapper which helps to format the move chain as a styled move list
    ///
    /// The resulting wrapper implements [`fmt::Display`], so can be used with
    /// `write!()`, `println!()`, or `ToString::to_string`.
    ///
    /// The formatting is customizable. `nums` indicate whether and how move numbers must
    /// be formatted, `style` indicates the move formatting style, and `status` indicates
    /// whether game status must be placed in the end of the list.
    ///
    /// # Example
    ///
    /// ```
    /// # use owlchess::{
    /// #     board::Board,
    /// #     chain::{MoveChain, NumberPolicy, GameStatusPolicy},
    /// #     moves::Style,
    /// #     types::OutcomeFilter,
    /// # };
    /// #
    /// let mut chain = MoveChain::from_uci_list(
    ///     Board::initial(),
    ///     "e2e4 e7e5 f1c4 b8c6 d1h5 g8f6 h5f7",
    /// ).unwrap();
    /// chain.set_auto_outcome(OutcomeFilter::Strict).unwrap();
    ///
    /// let formatted = chain.styled(
    ///     NumberPolicy::FromBoard,
    ///     Style::San,
    ///     GameStatusPolicy::Show
    /// ).to_string();
    ///
    /// assert_eq!(formatted, "1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6 4. Qxf7# 1-0".to_string());
    /// ```
    #[inline]
    pub fn styled(
        &self,
        nums: NumberPolicy,
        style: moves::Style,
        status: GameStatusPolicy,
    ) -> StyledList<'_, R> {
        StyledList {
            inner: self,
            nums,
            style,
            status,
        }
    }
}

impl<R: Repeat> Default for BaseMoveChain<R> {
    #[inline]
    fn default() -> Self {
        Self::new_initial()
    }
}

impl<R: Repeat + Eq> PartialEq<Self> for BaseMoveChain<R> {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        if self.board != other.board
            || self.repeat != other.repeat
            || self.stack.len() != other.stack.len()
        {
            return false;
        }
        self.stack
            .iter()
            .zip(other.stack.iter())
            .all(|((m1, _), (m2, _))| m1 == m2)
    }
}

impl<R: Repeat + Eq> Eq for BaseMoveChain<R> {}

/// Wrapper that helps to format [`BaseMoveChain`] as a UCI move list
///
/// See [`BaseMoveChain::uci()`] for more details.
pub struct UciList<'a, R: Repeat>(&'a BaseMoveChain<R>);

impl<'a, R: Repeat> fmt::Display for UciList<'a, R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        for (i, m) in self.0.iter().enumerate() {
            if i != 0 {
                write!(f, " ")?;
            }
            write!(f, "{}", m)?;
        }
        Ok(())
    }
}

/// Walks over the move chain, iterating over moves with their preceding positions
///
/// Walker works similar to bidirectional iterator, but doesn't implement [`Iterator`]
/// in fact. The reason is that [`Walker::next()`] returns returns reference to [`Board`]
/// which can live only before the iterator is changed. But the [`Iterator`] trait requires
/// that the references to items returned by `next()` must live at least as long as the
/// iterator itself.
///
/// Walker can be considered a stack, where [`next()`][Walker::next] pushes the next move
/// from the move chain, and [`prev()`][Walker::prev()] pops the last pushed move.
///
/// To create a walker, one needs to use [`BaseMoveChain::walk()`].
///
/// # Example
///
/// ```
/// # use owlchess::{Board, MoveChain, chain::Walker};
/// #
/// let chain = MoveChain::from_uci_list(Board::initial(), "e2e4 e7e5 g1f3").unwrap();
/// let mut walker = chain.walk();
///
/// // Push two moves into Walker
/// assert_eq!(walker.next().unwrap().1.to_string(), "e2e4".to_string());
/// assert_eq!(walker.next().unwrap().1.to_string(), "e7e5".to_string());
///
/// // Then, pop one of them (i.e. "e7e5")
/// assert_eq!(walker.prev().unwrap().1.to_string(), "e7e5".to_string());
///
/// // Push the rest of the moves
/// assert_eq!(walker.next().unwrap().1.to_string(), "e7e5".to_string());
/// assert_eq!(walker.next().unwrap().1.to_string(), "g1f3".to_string());
///
/// // We are at the end
/// assert_eq!(walker.next(), None);
/// ```
pub struct Walker<'a> {
    board: Board,
    stack: &'a [(Move, RawUndo)],
    pos: usize,
    board_pos: usize,
}

impl<'a> Walker<'a> {
    /// Returns the number of moves in the underlying move chain
    #[inline]
    pub fn len(&self) -> usize {
        self.stack.len()
    }

    /// Returns `true` if the underlying move chain has no moves
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.stack.is_empty()
    }

    /// Returns the number of moves pushed into the walker
    ///
    /// This is equal to the move index that will be fetched by the next call of
    /// [`Walker::next()`].
    #[inline]
    pub fn pos(&self) -> usize {
        self.pos
    }

    fn set_board_pos(&mut self, target: usize) {
        while self.board_pos > target {
            self.board_pos -= 1;
            let (mv, u) = self.stack[self.board_pos];
            unsafe {
                moves::unmake_move_unchecked(&mut self.board, mv, u);
            }
        }
        while self.board_pos < target {
            let (mv, _) = self.stack[self.board_pos];
            unsafe {
                moves::make_move_unchecked(&mut self.board, mv);
            }
            self.board_pos += 1;
        }
    }

    /// Tries to push the next move from chain into the walker
    ///
    /// If there are no moves to push (i. e. the walker is at the end of
    /// the chain), then `None` is returned.
    ///
    /// Otherwise, the pushed move is returned, alongside with the position
    /// immediately preceding this move.
    ///
    /// Note that the reference to [`Board`] is only active until the walker
    /// is mutated.
    #[allow(clippy::should_implement_trait)] // cannot implement Iterator
    pub fn next(&mut self) -> Option<(&Board, Move)> {
        if self.pos == self.stack.len() {
            return None;
        }
        self.pos += 1;
        self.set_board_pos(self.pos - 1);
        Some((&self.board, self.stack[self.pos - 1].0))
    }

    /// Tries to pop the last move from the walker
    ///
    /// If there are no moves to pop (i. e. the walker is at the beginning
    /// of the chain), then `None` is returned.
    ///
    /// Otherwise, the popped move is returned, alongside with the position
    /// immediately preceding this move.
    ///
    /// Note that the reference to [`Board`] is only active until the walker
    /// is mutated.
    pub fn prev(&mut self) -> Option<(&Board, Move)> {
        if self.pos == 0 {
            return None;
        }
        self.pos -= 1;
        self.set_board_pos(self.pos);
        Some((&self.board, self.stack[self.pos].0))
    }

    /// Pops all the moves from the walker
    ///
    /// In other words, the walker moves to the start of the chain.
    #[inline]
    pub fn start(&mut self) {
        self.pos = 0;
    }

    /// Pushes all the moves to the walker
    ///
    /// In other words, the walker moves to the end of the chain.
    #[inline]
    pub fn end(&mut self) {
        self.pos = self.stack.len();
    }
}

/// Indicates whether to show move numbers in [`BaseMoveChain::styled()`]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum NumberPolicy {
    /// Do not show move numbers
    Omit,
    /// Show move numbers according to the number specified in the board
    FromBoard,
    /// Show move numbers, but start from the given number instead of the one
    /// specified in the board
    Custom(usize),
}

/// Indicates whether to show game status in [`BaseMoveChain::styled()`]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum GameStatusPolicy {
    /// Show game status
    Show,
    /// Hide game status
    Hide,
}

/// Wrapper which helps to format the move chain as a styled move list
///
/// See [`BaseMoveChain::styled()`] doc for more details.
pub struct StyledList<'a, R: Repeat> {
    inner: &'a BaseMoveChain<R>,
    nums: NumberPolicy,
    style: moves::Style,
    status: GameStatusPolicy,
}

impl<'a, R: Repeat> fmt::Display for StyledList<'a, R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        if self.inner.is_empty() {
            match self.status {
                GameStatusPolicy::Show => write!(f, "{}", GameStatus::from(*self.inner.outcome()))?,
                GameStatusPolicy::Hide => {}
            }
            return Ok(());
        }

        let mut walker = self.inner.walk();
        let (b, mv) = walker.next().unwrap();
        let real_start_num = b.raw().move_number as usize;
        let start_num = match self.nums {
            NumberPolicy::Omit => None,
            NumberPolicy::FromBoard => Some(real_start_num),
            NumberPolicy::Custom(u) => Some(u),
        };

        if let Some(num) = start_num {
            match b.side() {
                Color::White => write!(f, "{}. ", num)?,
                Color::Black => write!(f, "{}... ", num)?,
            }
        }
        write!(f, "{}", mv.styled(b, self.style).unwrap())?;

        while let Some((b, mv)) = walker.next() {
            if let Some(num) = start_num {
                if b.side() == Color::White {
                    write!(
                        f,
                        " {}.",
                        b.raw().move_number as usize - real_start_num + num
                    )?;
                }
            }
            write!(f, " {}", mv.styled(b, self.style).unwrap())?;
        }

        match self.status {
            GameStatusPolicy::Show => write!(f, " {}", GameStatus::from(*self.inner.outcome()))?,
            GameStatusPolicy::Hide => {}
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::board::Board;
    use crate::moves::make;
    use crate::types::{DrawReason, Outcome, OutcomeFilter, WinReason};

    #[test]
    fn test_simple() {
        let mut chain =
            MoveChain::from_fen("rnbqk1nr/ppp1bppp/3p4/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 4")
                .unwrap();
        assert_eq!(
            chain.last().as_fen(),
            "rnbqk1nr/ppp1bppp/3p4/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 4"
        );
        assert_eq!(
            chain.startpos().as_fen(),
            "rnbqk1nr/ppp1bppp/3p4/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 4"
        );
        assert_eq!(chain.len(), 0);

        chain.push(make::Uci("g8f6")).unwrap();
        assert_eq!(
            chain.last().as_fen(),
            "rnbqk2r/ppp1bppp/3p1n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 w kq - 4 5"
        );
        assert_eq!(
            chain.startpos().as_fen(),
            "rnbqk1nr/ppp1bppp/3p4/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 4"
        );
        assert_eq!(chain.len(), 1);

        chain.push(make::Uci("d2d4")).unwrap();
        assert_eq!(
            chain.last().as_fen(),
            "rnbqk2r/ppp1bppp/3p1n2/4p3/2BPP3/5N2/PPP2PPP/RNBQ1RK1 b kq d3 0 5"
        );
        assert_eq!(
            chain.startpos().as_fen(),
            "rnbqk1nr/ppp1bppp/3p4/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 4"
        );
        assert_eq!(chain.len(), 2);

        chain.pop().unwrap();
        assert_eq!(
            chain.last().as_fen(),
            "rnbqk2r/ppp1bppp/3p1n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 w kq - 4 5"
        );
        assert_eq!(
            chain.startpos().as_fen(),
            "rnbqk1nr/ppp1bppp/3p4/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 4"
        );
        assert_eq!(chain.len(), 1);

        chain.push(make::San("d4")).unwrap();
        assert_eq!(
            chain.last().as_fen(),
            "rnbqk2r/ppp1bppp/3p1n2/4p3/2BPP3/5N2/PPP2PPP/RNBQ1RK1 b kq d3 0 5"
        );
        assert_eq!(
            chain.startpos().as_fen(),
            "rnbqk1nr/ppp1bppp/3p4/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 4"
        );
        assert_eq!(chain.len(), 2);

        assert_eq!(
            chain.iter().map(|m| m.to_string()).collect::<Vec<_>>(),
            vec!["g8f6", "d2d4"]
        );
        assert_eq!(chain.get(0).to_string(), "g8f6");
        assert_eq!(chain.get(1).to_string(), "d2d4");
        assert_eq!(chain.uci().to_string(), "g8f6 d2d4");
    }

    #[test]
    fn test_repeat() {
        let mut chain = MoveChain::new_initial();
        assert!(!chain.is_finished());
        assert_eq!(chain.outcome(), &None);

        chain
            .push_uci_list("g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1 c6b8")
            .unwrap();
        assert_eq!(chain.outcome(), &None);
        assert_eq!(
            chain.calc_outcome(),
            Some(Outcome::Draw(DrawReason::Repeat3))
        );

        let _ = chain.set_auto_outcome(OutcomeFilter::Strict);
        assert_eq!(chain.outcome(), &None);

        chain
            .push_uci_list("g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1 c6b8")
            .unwrap();
        assert_eq!(chain.outcome(), &None);
        assert_eq!(
            chain.calc_outcome(),
            Some(Outcome::Draw(DrawReason::Repeat5))
        );

        let _ = chain.set_auto_outcome(OutcomeFilter::Strict);
        assert!(chain.is_finished());
        assert_eq!(chain.outcome(), &Some(Outcome::Draw(DrawReason::Repeat5)));

        chain.pop().unwrap();
        assert!(!chain.is_finished());
        assert_eq!(chain.outcome(), &None);

        chain.set_auto_outcome(OutcomeFilter::Relaxed);
        assert!(chain.is_finished());
        assert_eq!(chain.outcome(), &Some(Outcome::Draw(DrawReason::Repeat3)));

        assert_eq!(
            chain.uci().to_string(),
            "g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1"
        );
    }

    #[test]
    fn test_checkmate() {
        let mut chain = MoveChain::new_initial();
        chain.push_uci_list("g2g4 e7e5 f2f4 d8h4").unwrap();
        assert_eq!(
            chain.set_auto_outcome(OutcomeFilter::Force),
            Some(Outcome::Win {
                side: Color::Black,
                reason: WinReason::Checkmate
            }),
        );
        assert!(chain.is_finished());
        assert_eq!(
            chain.outcome(),
            &Some(Outcome::Win {
                side: Color::Black,
                reason: WinReason::Checkmate
            })
        );

        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::San,
                    GameStatusPolicy::Show,
                )
                .to_string(),
            "1. g4 e5 2. f4 Qh4# 0-1".to_string()
        );
    }

    #[test]
    fn test_walker() {
        let mut chain = MoveChain::new_initial();
        for mv in [
            "e4", "e5", "Nf3", "d6", "Bc4", "Bg4", "Nc3", "g6", "Nxe5", "Bxd1", "Bxf7", "Ke7",
            "Nd5#",
        ] {
            chain.push(make::San(mv)).unwrap();
        }

        let mut w = chain.walk();
        assert_eq!(w.len(), 13);

        assert_eq!(w.pos(), 0);
        assert_eq!(w.prev(), None);
        assert_eq!(w.pos(), 0);

        w.start();
        assert_eq!(w.pos(), 0);
        assert_eq!(w.prev(), None);
        assert_eq!(w.pos(), 0);

        w.end();
        assert_eq!(w.pos(), 13);
        assert_eq!(w.next(), None);
        assert_eq!(w.pos(), 13);

        w.prev().unwrap();
        w.prev().unwrap();
        let (b, mv) = w.prev().unwrap();
        assert_eq!(
            b.as_fen(),
            "rn1qkbnr/ppp2p1p/3p2p1/4N3/2B1P3/2N5/PPPP1PPP/R1BbK2R w KQkq - 0 6"
        );
        assert_eq!(mv.san(b).unwrap().to_string(), "Bxf7+");
        assert_eq!(w.pos(), 10);

        let (b, mv) = w.next().unwrap();
        assert_eq!(
            b.as_fen(),
            "rn1qkbnr/ppp2p1p/3p2p1/4N3/2B1P3/2N5/PPPP1PPP/R1BbK2R w KQkq - 0 6"
        );
        assert_eq!(mv.san(b).unwrap().to_string(), "Bxf7+");
        assert_eq!(w.pos(), 11);

        let (b, mv) = w.next().unwrap();
        assert_eq!(
            b.as_fen(),
            "rn1qkbnr/ppp2B1p/3p2p1/4N3/4P3/2N5/PPPP1PPP/R1BbK2R b KQkq - 0 6"
        );
        assert_eq!(mv.san(b).unwrap().to_string(), "Ke7");
        assert_eq!(w.pos(), 12);
    }

    #[test]
    fn test_styled() {
        let chain = MoveChain::from_uci_list(Board::initial(), "e2e4 e7e5 g1f3 d7d6 f1b5").unwrap();
        assert_eq!(chain.len(), 5);
        assert_eq!(
            chain.last().as_fen(),
            "rnbqkbnr/ppp2ppp/3p4/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 1 3"
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Omit,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "e4 e5 Nf3 d6 Bb5+".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "1. e4 e5 2. Nf3 d6 3. Bb5+".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Custom(42),
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "42. e4 e5 43. Nf3 d6 44. Bb5+".to_string()
        );

        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::SanUtf8,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "1. e4 e5 2. ♘f3 d6 3. ♗b5+".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::Uci,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "1. e2e4 e7e5 2. g1f3 d7d6 3. f1b5".to_string()
        );

        let chain =
            MoveChain::from_uci_list(Board::initial(), "e2e4 e7e5 g1f3 d7d6 f1b5 c7c6").unwrap();
        assert_eq!(
            chain.last().as_fen(),
            "rnbqkbnr/pp3ppp/2pp4/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 4"
        );
        assert_eq!(chain.len(), 6);
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Omit,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "e4 e5 Nf3 d6 Bb5+ c6".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "1. e4 e5 2. Nf3 d6 3. Bb5+ c6".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Custom(42),
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "42. e4 e5 43. Nf3 d6 44. Bb5+ c6".to_string()
        );

        let board = Board::from_fen("5K2/4P3/8/8/8/8/6p1/7k b - - 0 12").unwrap();
        let chain = MoveChain::from_uci_list(board.clone(), "g2g1q e7e8q").unwrap();
        assert_eq!(chain.last().as_fen(), "4QK2/8/8/8/8/8/8/6qk b - - 0 13");
        assert_eq!(chain.len(), 2);
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Omit,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "g1=Q e8=Q".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "12... g1=Q 13. e8=Q".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Custom(42),
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "42... g1=Q 43. e8=Q".to_string()
        );

        let board = Board::from_fen("5K2/4P3/8/8/8/8/6p1/7k b - - 0 12").unwrap();
        let chain = MoveChain::from_uci_list(board.clone(), "g2g1q e7e8q g1c5").unwrap();
        assert_eq!(chain.last().as_fen(), "4QK2/8/8/2q5/8/8/8/7k w - - 1 14");
        assert_eq!(chain.len(), 3);
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Omit,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "g1=Q e8=Q Qc5+".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "12... g1=Q 13. e8=Q Qc5+".to_string()
        );
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::Custom(42),
                    moves::Style::San,
                    GameStatusPolicy::Hide
                )
                .to_string(),
            "42... g1=Q 43. e8=Q Qc5+".to_string()
        );

        let mut chain =
            MoveChain::from_uci_list(Board::initial(), "e2e4 e7e5 g1f3 d7d6 f1b5").unwrap();
        chain.set_outcome(Outcome::Draw(DrawReason::Agreement));
        assert_eq!(
            chain
                .styled(
                    NumberPolicy::FromBoard,
                    moves::Style::San,
                    GameStatusPolicy::Show
                )
                .to_string(),
            "1. e4 e5 2. Nf3 d6 3. Bb5+ 1/2-1/2".to_string()
        );
    }
}