chessfriend/moves/src/make_move.rs
Eryn Wells 37cb9bcaa0 [board, moves] Reorganize castling_right API on Board
Board::color_has_castling_right takes an Option<Color> which it unwraps. With that
unwrapped Color, it can call…

Board::color_has_castling_right_unwrapped, which performs the evaluation with a
non-Option Color.

Clean up some imports.
2025-06-17 08:28:39 -07:00

555 lines
17 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
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<MoveRecord, MakeMoveError>;
#[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<Piece, MakeMoveError>;
fn advance_board_state(
&mut self,
en_passant_target: Option<Square>,
half_move_clock: HalfMoveClock,
);
}
impl<T: BoardProvider> 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<MoveRecord, MakeMoveError> {
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<T: BoardProvider> 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<Square>,
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<Piece, MakeMoveError> {
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(())
}
}