// Eryn Wells mod captures; use crate::fen::{FromFenStr, FromFenStrError}; use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, ZobristState, }; use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::{ algebraic::AlgebraicMoveComponents, generators::{ AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, }, GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, UnmakeMoveResult, ValidateMove, }; use std::{collections::HashSet, fmt, sync::Arc}; #[must_use] #[derive(Clone, Debug, Default, Eq)] pub struct Position { pub board: Board, pub(crate) moves: Vec, pub(crate) captures: CapturesList, /// A set of hashes of board positions seen throughout the move record. boards_seen: HashSet, } impl Position { pub fn empty(zobrist: Option>) -> Self { Self::new(Board::empty(zobrist)) } /// Return a starting position. pub fn starting(zobrist: Option>) -> Self { Self::new(Board::starting(zobrist)) } pub fn new(board: Board) -> Self { Self { board, ..Default::default() } } } impl Position { /// Place a piece on the board. /// /// ## Errors /// /// See [`chessfriend_board::Board::place_piece`]. pub fn place_piece( &mut self, piece: Piece, square: Square, strategy: PlacePieceStrategy, ) -> Result, PlacePieceError> { self.board.place_piece(piece, square, strategy) } #[must_use] pub fn get_piece(&self, square: Square) -> Option { self.board.get_piece(square) } pub fn remove_piece(&mut self, square: Square) -> Option { self.board.remove_piece(square) } } impl Position { pub fn sight(&self, square: Square) -> BitBoard { self.board.sight(square) } pub fn movement(&self, square: Square) -> BitBoard { self.board.movement(square) } } impl Position { pub fn all_moves(&self, color: Option) -> AllPiecesMoveGenerator { AllPiecesMoveGenerator::new(&self.board, color) } /// Generate legal moves. /// /// ## Panics /// /// If the position failed to make a move generated by the internal move /// generator, this method will panic. #[must_use] pub fn all_legal_moves( &self, color: Option, ) -> Box + '_> { let generator = self.all_moves(color); let mut test_board = self.board.clone(); Box::new(generator.filter(move |ply| { let active_color_before_move = test_board.active_color(); let ply: Move = ply.clone().into(); let record = test_board .make_move(ply, ValidateMove::No) .unwrap_or_else(|err| { panic!( "unable to make generated move [{ply}]: {err}\n\n{}", test_board.display().highlight(ply.relevant_squares()) ); }); let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move)); test_board.unmake_move(&record).unwrap_or_else(|err| { panic!( "unable to unmake generated move [{ply}]: {err}\n\n{}", test_board.display().highlight(ply.relevant_squares()) ); }); move_is_legal })) } #[must_use] pub fn moves_for_piece( &self, square: Square, ) -> Option>> { self.get_piece(square) .map(|piece| Self::generator(&self.board, piece)) } #[must_use] fn generator(board: &Board, piece: Piece) -> Box> { match piece.shape { Shape::Pawn => Box::new(PawnMoveGenerator::new(board, Some(piece.color))), Shape::Knight => Box::new(KnightMoveGenerator::new(board, Some(piece.color))), Shape::Bishop => Box::new(BishopMoveGenerator::new(board, Some(piece.color))), Shape::Rook => Box::new(RookMoveGenerator::new(board, Some(piece.color))), Shape::Queen => Box::new(QueenMoveGenerator::new(board, Some(piece.color))), Shape::King => Box::new(KingMoveGenerator::new(board, Some(piece.color))), } } } impl Position { pub fn active_sight(&self) -> BitBoard { self.board.active_sight() } /// A [`BitBoard`] of all squares the given color can see. pub fn friendly_sight(&self, color: Color) -> BitBoard { self.board.friendly_sight(color) } /// A [`BitBoard`] of all squares visible by colors that oppose the given color. pub fn active_color_opposing_sight(&self) -> BitBoard { self.board.active_color_opposing_sight() } } impl Position { /// Make a move on the board and record it in the move list. Returns `true` /// if the board position has been seen before (i.e. it's a repetition). /// /// ## Errors /// /// Returns one of [`MakeMoveError`] if the move cannot be made. /// pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result { let record = self.board.make_move(ply, validate)?; if let Some(captured_piece) = record.captured_piece { self.captures.push(record.color, captured_piece); } let has_seen = if let Some(hash) = self.board.zobrist_hash() { // HashSet::insert() returns true if the value does not exist in the // set when it's called. !self.boards_seen.insert(hash) } else { false }; self.moves.push(record.clone()); Ok(has_seen) } /// Unmake the last move made on the board and remove its record from the /// move list. /// /// ## Errors /// /// Returns one of [`UnmakeMoveError`] if the move cannot be made. /// pub fn unmake_last_move(&mut self) -> UnmakeMoveResult { let last_move_record = self.moves.pop().ok_or(UnmakeMoveError::NoMove)?; let hash_before_unmake = self.board.zobrist_hash(); let unmake_result = self.board.unmake_move(&last_move_record); if unmake_result.is_ok() { if let Some(capture) = last_move_record.captured_piece { let popped_piece = self.captures.pop(last_move_record.color); debug_assert_eq!(Some(capture), popped_piece); } if let Some(hash_before_unmake) = hash_before_unmake { self.boards_seen.remove(&hash_before_unmake); } } else { self.moves.push(last_move_record); } unmake_result } /// Build a move given its origin, target, and possible promotion. Perform /// some minimal validation. If a move cannot be #[must_use] pub fn move_from_algebraic_components( &self, components: AlgebraicMoveComponents, ) -> Option { match components { AlgebraicMoveComponents::Null => Some(Move::null()), AlgebraicMoveComponents::Regular { origin, target, promotion, } => self.move_from_origin_target(origin, target, promotion), } } fn move_from_origin_target( &self, origin: Square, target: Square, promotion: Option, ) -> Option { let piece = self.get_piece(origin)?; let color = piece.color; // Pawn and King are the two most interesting shapes here, because of en // passant, castling and so on. So, let the move generators do their // thing and find the move that fits the parameters. For the rest of the // pieces, do something a little more streamlined. match piece.shape { Shape::Pawn => PawnMoveGenerator::new(&self.board, None) .find(|ply| { ply.origin() == origin && ply.target() == target && ply.promotion_shape() == promotion }) .map(std::convert::Into::into), Shape::King => KingMoveGenerator::new(&self.board, None) .find(|ply| ply.origin() == origin && ply.target() == target) .map(std::convert::Into::into), _ => { if color != self.board.active_color() { return None; } let target_bitboard: BitBoard = target.into(); if !(self.movement(origin) & target_bitboard).is_populated() { return None; } if self.get_piece(target).is_some() { return Some(Move::capture(origin, target)); } Some(Move::quiet(origin, target)) } } } } impl Position { #[must_use] pub fn zobrist_hash(&self) -> Option { self.board.zobrist_hash() } pub fn set_zobrist_state(&mut self, state: Arc) { self.board.set_zobrist_state(state); } } impl Position { pub fn display(&self) -> DiagramFormatter { self.board.display() } } impl FromFenStr for Position { type Error = FromFenStrError; fn from_fen_str(string: &str) -> Result { let board = Board::from_fen_str(string)?; Ok(Position::new(board)) } } impl ToFenStr for Position { type Error = ::Error; fn to_fen_str(&self) -> Result { self.board.to_fen_str() } } impl PartialEq for Position { fn eq(&self, other: &Self) -> bool { self.board == other.board } } impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.board.display())?; if !self.captures.is_empty() { write!(f, "\n\n{}", self.captures)?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::{test_position, Position}; use chessfriend_core::piece; #[test] fn piece_on_square() { let pos = test_position![ Black Bishop on F7, ]; let piece = pos.board.get_piece(Square::F7); assert_eq!(piece, Some(piece!(Black Bishop))); } #[test] fn piece_in_starting_position() { let pos = test_position!(starting); assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook))); assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook))); } }