minorhacks_chess 0.1.3

Fork of the `chess` crate (github.com/jordanbray/chess). This one has a unique set of bugs useful for certain applications; prefer to use the upstream crate.
Documentation
use crate::board::Board;
use crate::error::Error;
use crate::file::File;
use crate::movegen::MoveGen;
use crate::piece::Piece;
use crate::rank::Rank;
use crate::square::Square;

use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;

/// Represent a ChessMove in memory
#[derive(Clone, Copy, Eq, PartialEq, Default, Debug, Hash)]
pub struct ChessMove {
    source: Square,
    dest: Square,
    promotion: Option<Piece>,
}

impl ChessMove {
    /// Create a new chess move, given a source `Square`, a destination `Square`, and an optional
    /// promotion `Piece`
    #[inline]
    pub fn new(source: Square, dest: Square, promotion: Option<Piece>) -> ChessMove {
        ChessMove {
            source,
            dest,
            promotion,
        }
    }

    /// Get the source square (square the piece is currently on).
    #[inline]
    pub fn get_source(&self) -> Square {
        self.source
    }

    /// Get the destination square (square the piece is going to).
    #[inline]
    pub fn get_dest(&self) -> Square {
        self.dest
    }

    /// Get the promotion piece (maybe).
    #[inline]
    pub fn get_promotion(&self) -> Option<Piece> {
        self.promotion
    }
    /// Convert a SAN (Standard Algebraic Notation) move into a `ChessMove`
    ///
    /// ```
    /// use minorhacks_chess::{Board, ChessMove, Square};
    ///
    /// let board = Board::default();
    /// assert_eq!(
    ///     ChessMove::from_san(&board, "e4").expect("e4 is valid in the initial position"),
    ///     ChessMove::new(Square::E2, Square::E4, None)
    /// );
    /// ```
    pub fn from_san(board: &Board, move_text: &str) -> Result<ChessMove, Error> {
        // Castles first...
        if move_text.starts_with("O-O") {
            let rank = board.side_to_move().to_my_backrank();
            let source_file = File::E;
            let dest_file = if move_text.starts_with("O-O-O") {
                File::C
            } else {
                File::G
            };

            let m = ChessMove::new(
                Square::make_square(rank, source_file),
                Square::make_square(rank, dest_file),
                None,
            );
            if MoveGen::new_legal(&board).any(|l| l == m) {
                return Ok(m);
            } else {
                return Err(Error::InvalidSanMove);
            }
        }

        // forms of SAN moves
        // a4 (Pawn moves to a4)
        // exd4 (Pawn on e file takes on d4)
        // xd4 (Illegal, source file must be specified)
        // 1xd4 (Illegal, source file (not rank) must be specified)
        // Nc3 (Knight (or any piece) on *some square* to c3
        // Nb1c3 (Knight (or any piece) on b1 to c3
        // Nbc3 (Knight on b file to c3)
        // N1c3 (Knight on first rank to c3)
        // Nb1xc3 (Knight on b1 takes on c3)
        // Nbxc3 (Knight on b file takes on c3)
        // N1xc3 (Knight on first rank takes on c3)
        // Nc3+ (Knight moves to c3 with check)
        // Nc3# (Knight moves to c3 with checkmate)

        // Because I'm dumb, I'm wondering if a hash table of all possible moves would be stupid.
        // There are only 186624 possible moves in SAN notation.
        //
        // Would this even be faster?  Somehow I doubt it because caching, but maybe, I dunno...
        // This could take the form of a:
        // struct CheckOrCheckmate {
        //      Neither,
        //      Check,
        //      CheckMate,
        // }
        // struct FromSan {
        //      piece: Piece,
        //      source: Vec<Square>, // possible source squares
        //      // OR
        //      source_rank: Option<Rank>,
        //      source_file: Option<File>,
        //      dest: Square,
        //      takes: bool,
        //      check: CheckOrCheckmate
        // }
        //
        // This could be kept internally as well, and never tell the user about such an abomination
        //
        // I estimate this table would take around 2 MiB, but I had to approximate some things.  It
        // may be less

        // This can be described with the following format
        // [Optional Piece Specifier] ("" | "N" | "B" | "R" | "Q" | "K")
        // [Optional Source Specifier] ( "" | "a-h" | "1-8" | ("a-h" + "1-8"))
        // [Optional Takes Specifier] ("" | "x")
        // [Full Destination Square] ("a-h" + "0-8")
        // [Optional Promotion Specifier] ("" | "N" | "B" | "R" | "Q")
        // [Optional Check(mate) Specifier] ("" | "+" | "#")
        // [Optional En Passant Specifier] ("" | " e.p.")

        let error = Error::InvalidSanMove;
        let mut cur_index: usize = 0;
        let moving_piece = match move_text
            .get(cur_index..(cur_index + 1))
            .ok_or_else(|| error.clone())?
        {
            "N" => {
                cur_index += 1;
                Piece::Knight
            }
            "B" => {
                cur_index += 1;
                Piece::Bishop
            }
            "Q" => {
                cur_index += 1;
                Piece::Queen
            }
            "R" => {
                cur_index += 1;
                Piece::Rook
            }
            "K" => {
                cur_index += 1;
                Piece::King
            }
            _ => Piece::Pawn,
        };

        let mut source_file = match move_text
            .get(cur_index..(cur_index + 1))
            .ok_or_else(|| error.clone())?
        {
            "a" => {
                cur_index += 1;
                Some(File::A)
            }
            "b" => {
                cur_index += 1;
                Some(File::B)
            }
            "c" => {
                cur_index += 1;
                Some(File::C)
            }
            "d" => {
                cur_index += 1;
                Some(File::D)
            }
            "e" => {
                cur_index += 1;
                Some(File::E)
            }
            "f" => {
                cur_index += 1;
                Some(File::F)
            }
            "g" => {
                cur_index += 1;
                Some(File::G)
            }
            "h" => {
                cur_index += 1;
                Some(File::H)
            }
            _ => None,
        };

        let mut source_rank = match move_text
            .get(cur_index..(cur_index + 1))
            .ok_or_else(|| error.clone())?
        {
            "1" => {
                cur_index += 1;
                Some(Rank::First)
            }
            "2" => {
                cur_index += 1;
                Some(Rank::Second)
            }
            "3" => {
                cur_index += 1;
                Some(Rank::Third)
            }
            "4" => {
                cur_index += 1;
                Some(Rank::Fourth)
            }
            "5" => {
                cur_index += 1;
                Some(Rank::Fifth)
            }
            "6" => {
                cur_index += 1;
                Some(Rank::Sixth)
            }
            "7" => {
                cur_index += 1;
                Some(Rank::Seventh)
            }
            "8" => {
                cur_index += 1;
                Some(Rank::Eighth)
            }
            _ => None,
        };

        let _takes = if let Some("x") = move_text.get(cur_index..(cur_index + 1)) {
            cur_index += 1;
            true
        } else {
            false
        };

        let dest = if let Some(s) = move_text.get(cur_index..(cur_index + 2)) {
            if let Ok(q) = Square::from_str(s) {
                cur_index += 2;
                q
            } else {
                let sq = Square::make_square(
                    source_rank.ok_or_else(|| error.clone())?,
                    source_file.ok_or_else(|| error.clone())?,
                );
                source_rank = None;
                source_file = None;
                sq
            }
        } else {
            let sq = Square::make_square(
                source_rank.ok_or_else(|| error.clone())?,
                source_file.ok_or_else(|| error.clone())?,
            );
            source_rank = None;
            source_file = None;
            sq
        };

        // Ignore an '=' sign, for PGN compatibility
        if let Some("=") = move_text.get(cur_index..(cur_index + 1)) {
            cur_index += 1;
        }

        let promotion = if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
            match s {
                "N" => {
                    cur_index += 1;
                    Some(Piece::Knight)
                }
                "B" => {
                    cur_index += 1;
                    Some(Piece::Bishop)
                }
                "R" => {
                    cur_index += 1;
                    Some(Piece::Rook)
                }
                "Q" => {
                    cur_index += 1;
                    Some(Piece::Queen)
                }
                _ => None,
            }
        } else {
            None
        };

        #[allow(unused_assignments)]
        if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
            let _maybe_check_or_mate = match s {
                "+" => {
                    cur_index += 1;
                    Some(false)
                }
                "#" => {
                    cur_index += 1;
                    Some(true)
                }
                _ => None,
            };
        }

        // Ok, now we have all the data from the SAN move, in the following structures
        // moveing_piece, source_rank, source_file, taks, dest, promotion, maybe_check_or_mate, and
        // ep

        let mut found_move: Option<ChessMove> = None;
        for m in &mut MoveGen::new_legal(board) {
            // check that the move has the properties specified
            if board.piece_on(m.get_source()) != Some(moving_piece) {
                continue;
            }

            if let Some(rank) = source_rank {
                if m.get_source().get_rank() != rank {
                    continue;
                }
            }

            if let Some(file) = source_file {
                if m.get_source().get_file() != file {
                    continue;
                }
            }

            if m.get_dest() != dest {
                continue;
            }

            if m.get_promotion() != promotion {
                continue;
            }

            if found_move.is_some() {
                return Err(error);
            }

            found_move = Some(m);
        }

        found_move.ok_or(error)
    }
}

impl fmt::Display for ChessMove {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.promotion {
            None => write!(f, "{}{}", self.source, self.dest),
            Some(x) => write!(f, "{}{}{}", self.source, self.dest, x),
        }
    }
}

impl PartialOrd for ChessMove {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for ChessMove {
    fn cmp(&self, other: &ChessMove) -> Ordering {
        if self.source != other.source {
            self.source.cmp(&other.source)
        } else if self.dest != other.dest {
            self.dest.cmp(&other.dest)
        } else if self.promotion != other.promotion {
            match self.promotion {
                None => Ordering::Less,
                Some(x) => match other.promotion {
                    None => Ordering::Greater,
                    Some(y) => x.cmp(&y),
                },
            }
        } else {
            Ordering::Equal
        }
    }
}

/// Convert a UCI `String` to a move. If invalid, return `None`
/// ```
/// use minorhacks_chess::{ChessMove, Square, Piece};
/// use std::str::FromStr;
///
/// let mv = ChessMove::new(Square::E7, Square::E8, Some(Piece::Queen));
///
/// assert_eq!(ChessMove::from_str("e7e8q").expect("Valid Move"), mv);
/// ```
impl FromStr for ChessMove {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let source = Square::from_str(s.get(0..2).ok_or(Error::InvalidUciMove)?)?;
        let dest = Square::from_str(s.get(2..4).ok_or(Error::InvalidUciMove)?)?;

        let mut promo = None;
        if s.len() == 5 {
            promo = Some(match s.chars().last().ok_or(Error::InvalidUciMove)? {
                'q' => Piece::Queen,
                'r' => Piece::Rook,
                'n' => Piece::Knight,
                'b' => Piece::Bishop,
                _ => return Err(Error::InvalidUciMove),
            });
        }

        Ok(ChessMove::new(source, dest, promo))
    }
}

#[test]
fn test_basic_moves() {
    let board = Board::default();
    assert_eq!(
        ChessMove::from_san(&board, "e4").expect("e4 is valid in the initial position"),
        ChessMove::new(Square::E2, Square::E4, None)
    );
}