chessfriend/position/src/position/make_move.rs

412 lines
12 KiB
Rust
Raw Normal View History

// Eryn Wells <eryn@erynwells.me>
use crate::{movement::Movement, Position};
use chessfriend_board::{en_passant, CastleParameters, PlacePieceError, PlacePieceStrategy};
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
use chessfriend_moves::Move;
use thiserror::Error;
use super::CastleEvaluationError;
type MakeMoveResult = Result<(), 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),
}
pub enum UnmakeMoveError {}
impl Position {
/// 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`].
pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult {
self.validate_move(ply, validate)?;
if ply.is_quiet() {
return self.make_quiet_move(ply.origin_square(), ply.target_square());
}
if ply.is_double_push() {
return self.make_double_push_move(ply);
}
if ply.is_capture() {
return self.make_capture_move(ply);
}
if let Some(wing) = ply.castle() {
return self.make_castle_move(wing);
}
if ply.is_promotion() {
return self.make_promotion_move(ply);
}
Ok(())
}
pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> {
Ok(())
}
}
impl Position {
fn make_quiet_move(&mut self, origin: Square, target: Square) -> MakeMoveResult {
let piece = self
.board
.remove_piece(origin)
.ok_or(MakeMoveError::NoPiece(origin))?;
self.place_active_piece(piece, target)?;
self.advance_clocks(HalfMoveClock::Advance);
Ok(())
}
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult {
let origin = ply.origin_square();
let piece = self
.board
.remove_piece(origin)
.ok_or(MakeMoveError::NoPiece(origin))?;
let target = ply.target_square();
self.place_active_piece(piece, target)?;
self.board.en_passant_target = match target.rank() {
Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)),
Rank::FIVE => Some(Square::from_file_rank(target.file(), Rank::SIX)),
_ => unreachable!(),
};
self.advance_clocks(HalfMoveClock::Advance);
Ok(())
}
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
let origin_square = ply.origin_square();
let target_square = ply.target_square();
let piece = self.get_piece_for_move(origin_square)?;
if ply.is_en_passant() {
let en_passant_square = self
.board
.en_passant_target
.ok_or(MakeMoveError::NoCaptureSquare)?;
if target_square != en_passant_square {
return Err(MakeMoveError::InvalidEnPassantCapture(target_square));
}
}
let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?;
let captured_piece = self
.remove_piece(capture_square)
.ok_or(MakeMoveError::NoCapturePiece(capture_square))?;
// Register the capture
self.captures[piece.color as usize].push(captured_piece);
self.remove_piece(origin_square).unwrap();
if let Some(promotion_shape) = ply.promotion_shape() {
let promoted_piece = Piece::new(piece.color, promotion_shape);
self.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?;
} else {
self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?;
}
self.advance_clocks(HalfMoveClock::Reset);
Ok(())
}
fn make_castle_move(&mut self, wing: Wing) -> MakeMoveResult {
self.active_color_can_castle(wing)?;
let active_color = self.board.active_color;
let parameters = self.board.castling_parameters(wing);
let king = self.board.remove_piece(parameters.origin.king).unwrap();
self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
let rook = self.board.remove_piece(parameters.origin.rook).unwrap();
self.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
self.board.castling_rights.revoke(active_color, wing);
self.advance_clocks(HalfMoveClock::Advance);
Ok(())
}
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult {
let origin = ply.origin_square();
let piece = self.get_piece_for_move(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() {
self.remove_piece(origin);
let promoted_piece = Piece::new(piece.color, promotion_shape);
self.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?;
} else {
unreachable!(
"Cannot make a promotion move with a ply that has no promotion shape: {ply:?}",
);
}
self.advance_clocks(HalfMoveClock::Reset);
Ok(())
}
}
impl Position {
fn get_piece_for_move(&mut self, square: Square) -> Result<Piece, MakeMoveError> {
self.get_piece(square).ok_or(MakeMoveError::NoPiece(square))
}
fn place_active_piece(&mut self, piece: Piece, square: Square) -> MakeMoveResult {
self.place_piece(piece, square, PlacePieceStrategy::PreserveExisting)
.map_err(MakeMoveError::PlacePieceError)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum HalfMoveClock {
Reset,
#[default]
Advance,
}
impl Position {
fn advance_clocks(&mut self, half_move_clock: HalfMoveClock) {
match half_move_clock {
HalfMoveClock::Reset => self.board.half_move_clock = 0,
HalfMoveClock::Advance => self.board.half_move_clock += 1,
}
self.board.active_color = self.board.active_color.next();
if self.board.active_color == Color::White {
self.board.full_move_number += 1;
}
}
}
impl Position {
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 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, &self.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) = self.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 active_piece = self
.board
.get_piece(origin_square)
.ok_or(MakeMoveError::NoPiece(origin_square))?;
if active_piece.color != self.board.active_color {
return Err(MakeMoveError::NonActiveColor {
piece: active_piece,
square: origin_square,
});
}
Ok(active_piece)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_position, ValidateMove};
use chessfriend_core::{piece, Color, Square};
use chessfriend_moves::{Move, PromotionShape};
#[test]
fn make_quiet_move() -> MakeMoveResult {
let mut pos = test_position!(White Pawn on C2);
let ply = Move::quiet(Square::C2, Square::C3);
pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.get_piece(Square::C2), None);
assert_eq!(pos.get_piece(Square::C3), Some(piece!(White Pawn)));
assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.board.half_move_clock, 1);
pos.board.active_color = Color::White;
let ply = Move::quiet(Square::C3, Square::C4);
pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.get_piece(Square::C3), None);
assert_eq!(pos.get_piece(Square::C4), Some(piece!(White Pawn)));
assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.board.half_move_clock, 2);
Ok(())
}
#[test]
fn make_capture_move() {
let mut pos = test_position![
White Bishop on C2,
Black Rook on F5,
];
let ply = Move::capture(Square::C2, Square::F5);
assert_eq!(pos.make_move(ply, ValidateMove::Yes), Ok(()));
assert_eq!(pos.get_piece(Square::C2), None);
assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop)));
assert_eq!(pos.captures[Color::White as usize][0], piece!(Black Rook));
assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.board.half_move_clock, 0);
}
#[test]
fn make_en_passant_capture_move() -> MakeMoveResult {
let mut pos = test_position![
Black Pawn on F4,
White Pawn on E2
];
let ply = Move::double_push(Square::E2, Square::E4);
pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.get_piece(Square::E2), None);
assert_eq!(pos.get_piece(Square::E4), Some(piece!(White Pawn)));
assert_eq!(
pos.board.en_passant_target,
Some(Square::E3),
"en passant square not set"
);
assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.board.half_move_clock, 1);
let ply = Move::en_passant_capture(Square::F4, Square::E3);
pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.get_piece(Square::F4), None);
assert_eq!(pos.get_piece(Square::E3), Some(piece!(Black Pawn)));
assert_eq!(
pos.get_piece(Square::E4),
None,
"capture target pawn not removed"
);
assert_eq!(pos.captures[Color::Black as usize][0], piece!(White Pawn));
Ok(())
}
#[test]
fn make_promotion_move() -> MakeMoveResult {
let mut pos = test_position![
Black Pawn on E7,
White Pawn on F7,
];
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.get_piece(Square::F7), None);
assert_eq!(pos.get_piece(Square::F8), Some(piece!(White Queen)));
assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.board.half_move_clock, 0);
Ok(())
}
}