// Eryn Wells use crate::{Move, MoveRecord}; use chessfriend_board::{ castle::CastleEvaluationError, movement::Movement, Board, BoardProvider, PlacePieceError, PlacePieceStrategy, }; use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use thiserror::Error; pub type MakeMoveResult = Result; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum ValidateMove { #[default] No, Yes, } #[derive(Debug, Error, Eq, PartialEq)] pub enum MakeMoveError { #[error("no piece on {0}")] NoPiece(Square), #[error("{piece} on {square} is not of active color")] NonActiveColor { piece: Piece, square: Square }, #[error("{0} cannot make move")] InvalidPiece(Piece), #[error("cannot capture piece on {0}")] InvalidCapture(Square), #[error("cannot capture en passant on {0}")] InvalidEnPassantCapture(Square), #[error("no capture square")] NoCaptureSquare, #[error("no piece to capture on {0}")] NoCapturePiece(Square), #[error("{piece} on {origin} cannot move to {target}")] NoMove { piece: Piece, origin: Square, target: Square, }, #[error("{0}")] PlacePieceError(#[from] PlacePieceError), #[error("{0}")] CastleError(#[from] CastleEvaluationError), #[error("cannot promote on {0}")] InvalidPromotion(Square), #[error("move to {0} requires promotion")] PromotionRequired(Square), } pub trait MakeMove { /// Make a move. /// /// ## Errors /// /// Returns one of [`MakeMoveError`] indicating why the move could not be /// made. /// fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult; } trait MakeMoveInternal { fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult; fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult; fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult; fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult; fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult; fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError>; fn validate_active_piece(&self, ply: Move) -> Result; fn advance_board_state( &mut self, en_passant_target: Option, half_move_clock: HalfMoveClock, ); } impl MakeMove for T { /// Make a move in the position. /// /// ## Errors /// /// If `validate` is [`ValidateMove::Yes`], perform validation of move /// correctness prior to applying the move. See [`Position::validate_move`]. fn make_move( &mut self, ply: Move, validate: ValidateMove, ) -> Result { if ply.is_quiet() { self.validate_move(ply, validate)?; return self.make_quiet_move(ply); } if ply.is_double_push() { self.validate_move(ply, validate)?; return self.make_double_push_move(ply); } if ply.is_capture() { self.validate_move(ply, validate)?; return self.make_capture_move(ply); } if let Some(wing) = ply.castle_wing() { return self.make_castle_move(ply, wing); } if ply.is_promotion() { self.validate_move(ply, validate)?; return self.make_promotion_move(ply); } unreachable!(); } } impl MakeMoveInternal for T { fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult { let board = self.board_mut(); let origin = ply.origin_square(); let piece = board .get_piece(origin) .ok_or(MakeMoveError::NoPiece(origin))?; let target = ply.target_square(); if piece.is_pawn() && target.rank().is_promotable_rank() { return Err(MakeMoveError::PromotionRequired(target)); } board .place_piece(piece, target, PlacePieceStrategy::PreserveExisting) .map_err(MakeMoveError::PlacePieceError)?; board.remove_piece(origin); let record = MoveRecord::new(board, ply, None); self.advance_board_state(None, HalfMoveClock::Advance); Ok(record) } fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult { let board = self.board_mut(); let origin = ply.origin_square(); let piece = board .remove_piece(origin) .ok_or(MakeMoveError::NoPiece(origin))?; let target = ply.target_square(); board .place_piece(piece, target, PlacePieceStrategy::PreserveExisting) .map_err(MakeMoveError::PlacePieceError)?; // Capture move record before setting the en passant square, to ensure // board state before the change is preserved. let record = MoveRecord::new(board, ply, None); let en_passant_target = match target.rank() { Rank::FOUR => Square::from_file_rank(target.file(), Rank::THREE), Rank::FIVE => Square::from_file_rank(target.file(), Rank::SIX), _ => unreachable!(), }; self.advance_board_state(Some(en_passant_target), HalfMoveClock::Advance); Ok(record) } fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult { let origin_square = ply.origin_square(); let target_square = ply.target_square(); let board = self.board_mut(); let piece = board .get_piece(origin_square) .ok_or(MakeMoveError::NoPiece(origin_square))?; if ply.is_en_passant() { let en_passant_square = board .en_passant_target() .ok_or(MakeMoveError::NoCaptureSquare)?; if target_square != en_passant_square { return Err(MakeMoveError::InvalidEnPassantCapture(target_square)); } } let board = self.board_mut(); let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?; let captured_piece = board .remove_piece(capture_square) .ok_or(MakeMoveError::NoCapturePiece(capture_square))?; board.remove_piece(origin_square).unwrap(); if let Some(promotion_shape) = ply.promotion_shape() { let promoted_piece = Piece::new(piece.color, promotion_shape); board.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?; } else { board.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; } let record = MoveRecord::new(board, ply, Some(captured_piece)); self.advance_board_state(None, HalfMoveClock::Reset); Ok(record) } fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult { let board = self.board_mut(); board.color_can_castle(wing, None)?; let active_color = board.active_color(); let parameters = Board::castling_parameters(wing, active_color); let king = board.remove_piece(parameters.origin.king).unwrap(); board.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?; let rook = board.remove_piece(parameters.origin.rook).unwrap(); board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?; // Capture move record before revoking castling rights to ensure // original board state is preserved. let record = MoveRecord::new(board, ply, None); board.revoke_castling_right_unwrapped(active_color, wing); self.advance_board_state(None, HalfMoveClock::Advance); Ok(record) } fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult { let board = self.board_mut(); let origin = ply.origin_square(); let piece = board .get_piece(origin) .ok_or(MakeMoveError::NoPiece(origin))?; if !piece.is_pawn() { return Err(MakeMoveError::InvalidPiece(piece)); } let target = ply.target_square(); if !target.rank().is_promotable_rank() { return Err(MakeMoveError::InvalidPromotion(target)); } if let Some(promotion_shape) = ply.promotion_shape() { board.remove_piece(origin); let promoted_piece = Piece::new(piece.color, promotion_shape); board.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?; } else { unreachable!( "Cannot make a promotion move with a ply that has no promotion shape: {ply:?}", ); } let record = MoveRecord::new(board, ply, None); self.advance_board_state(None, HalfMoveClock::Reset); Ok(record) } fn advance_board_state( &mut self, en_passant_target: Option, half_move_clock: HalfMoveClock, ) { let board = self.board_mut(); match half_move_clock { HalfMoveClock::Reset => board.half_move_clock = 0, HalfMoveClock::Advance => board.half_move_clock += 1, } let previous_active_color = board.active_color(); let active_color = previous_active_color.next(); board.set_active_color(active_color); board.set_en_passant_target_option(en_passant_target); if active_color == Color::White { board.full_move_number += 1; } } fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> { if validate == ValidateMove::No { return Ok(()); } let active_piece = self.validate_active_piece(ply)?; let board = self.board(); let origin_square = ply.origin_square(); let target_square = ply.target_square(); // Pawns can see squares they can't move to. So, calculating valid // squares requires a concept that includes Sight, but adds pawn pushes. // In ChessFriend, that concept is Movement. let movement = active_piece.movement(origin_square, board); if !movement.contains(target_square) { return Err(MakeMoveError::NoMove { piece: active_piece, origin: origin_square, target: target_square, }); } // TODO: En Passant capture. if let Some(capture_square) = ply.capture_square() { if let Some(captured_piece) = board.get_piece(capture_square) { if captured_piece.color == active_piece.color { return Err(MakeMoveError::InvalidCapture(capture_square)); } } else { return Err(MakeMoveError::NoPiece(capture_square)); } } Ok(()) } fn validate_active_piece(&self, ply: Move) -> Result { let origin_square = ply.origin_square(); let board = self.board(); let active_piece = board .get_piece(origin_square) .ok_or(MakeMoveError::NoPiece(origin_square))?; if active_piece.color != board.active_color() { return Err(MakeMoveError::NonActiveColor { piece: active_piece, square: origin_square, }); } Ok(active_piece) } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] enum HalfMoveClock { Reset, #[default] Advance, } #[cfg(test)] mod tests { use super::*; use crate::{Move, PromotionShape}; use chessfriend_board::test_board; use chessfriend_core::{Color, Square, piece}; type TestResult = Result<(), MakeMoveError>; #[test] fn make_quiet_move() -> TestResult { let mut board = test_board!(White Pawn on C2); let ply = Move::quiet(Square::C2, Square::C3); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.get_piece(Square::C2), None); assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn))); assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 1); board.set_active_color(Color::White); let ply = Move::quiet(Square::C3, Square::C4); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.get_piece(Square::C3), None); assert_eq!(board.get_piece(Square::C4), Some(piece!(White Pawn))); assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 2); Ok(()) } #[test] fn make_invalid_quiet_pawn_move() { let mut board = test_board!(White Pawn on C2); let ply = Move::quiet(Square::C2, Square::D2); let result = board.make_move(ply, ValidateMove::Yes); assert!(result.is_err()); assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::D2), None); assert_eq!(board.active_color(), Color::White); assert_eq!(board.half_move_clock, 0); } #[test] fn make_capture_move() -> TestResult { let mut board = test_board![ White Bishop on C2, Black Rook on F5, ]; let ply = Move::capture(Square::C2, Square::F5); let result = board.make_move(ply, ValidateMove::Yes)?; assert_eq!(result.captured_piece, Some(piece!(Black Rook))); assert_eq!(board.get_piece(Square::C2), None); assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop))); assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 0); Ok(()) } #[test] fn make_en_passant_capture_move() -> TestResult { let mut board = test_board![ Black Pawn on F4, White Pawn on E2 ]; let ply = Move::double_push(Square::E2, Square::E4); let record = board.make_move(ply, ValidateMove::Yes)?; assert_eq!(record.en_passant_target, None); assert_eq!(board.get_piece(Square::E2), None); assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn))); assert_eq!( board.en_passant_target(), Some(Square::E3), "en passant square not set" ); assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 1); let ply = Move::en_passant_capture(Square::F4, Square::E3); let result = board.make_move(ply, ValidateMove::Yes)?; assert_eq!(result.captured_piece, Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::F4), None); assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn))); assert_eq!( board.get_piece(Square::E4), None, "capture target pawn not removed" ); Ok(()) } #[test] fn make_last_rank_quiet_move_without_promotion() { let mut board = test_board!( White Pawn on A7 ); let ply = Move::quiet(Square::A7, Square::A8); let result = board.make_move(ply, ValidateMove::Yes); assert!(result.is_err()); assert_eq!(board.active_color(), Color::White); assert_eq!(board.get_piece(Square::A7), Some(piece!(White Pawn))); assert_eq!(board.get_piece(Square::A8), None); assert_eq!(board.half_move_clock, 0); } #[test] fn make_promotion_move() -> TestResult { let mut board = test_board![ Black Pawn on E7, White Pawn on F7, ]; let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.get_piece(Square::F7), None); assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen))); assert_eq!(board.active_color(), Color::Black); assert_eq!(board.half_move_clock, 0); Ok(()) } #[test] fn make_white_kingside_castle() -> TestResult { let mut board = test_board![ White Rook on H1, White King on E1, ]; let ply = Move::castle(Color::White, Wing::KingSide); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.active_color(), Color::Black); assert_eq!(board.get_piece(Square::E1), None); assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); Ok(()) } #[test] fn make_white_queenside_castle() -> TestResult { let mut board = test_board![ White King on E1, White Rook on A1, ]; let ply = Move::castle(Color::White, Wing::QueenSide); board.make_move(ply, ValidateMove::Yes)?; assert_eq!(board.active_color(), Color::Black); assert_eq!(board.get_piece(Square::E1), None); assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); Ok(()) } }