2025-05-18 08:08:47 -07:00
|
|
|
// Eryn Wells <eryn@erynwells.me>
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
use crate::{Move, MoveRecord};
|
2025-05-23 10:00:20 -07:00
|
|
|
use chessfriend_board::{
|
2025-05-31 19:01:20 -07:00
|
|
|
castle::CastleEvaluationError, movement::Movement, Board, BoardProvider, PlacePieceError,
|
|
|
|
PlacePieceStrategy,
|
2025-05-23 10:00:20 -07:00
|
|
|
};
|
2025-05-21 09:51:16 -07:00
|
|
|
use chessfriend_core::{Color, Piece, Rank, Square, Wing};
|
2025-05-18 08:08:47 -07:00
|
|
|
use thiserror::Error;
|
|
|
|
|
2025-06-01 19:02:53 -07:00
|
|
|
pub type MakeMoveResult = Result<MoveRecord, MakeMoveError>;
|
2025-05-18 08:08:47 -07:00
|
|
|
|
|
|
|
#[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 },
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
#[error("{0} cannot make move")]
|
|
|
|
InvalidPiece(Piece),
|
|
|
|
|
2025-05-18 08:08:47 -07:00
|
|
|
#[error("cannot capture piece on {0}")]
|
|
|
|
InvalidCapture(Square),
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
#[error("cannot capture en passant on {0}")]
|
|
|
|
InvalidEnPassantCapture(Square),
|
|
|
|
|
2025-05-18 08:08:47 -07:00
|
|
|
#[error("no capture square")]
|
|
|
|
NoCaptureSquare,
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
#[error("no piece to capture on {0}")]
|
|
|
|
NoCapturePiece(Square),
|
|
|
|
|
2025-05-18 08:08:47 -07:00
|
|
|
#[error("{piece} on {origin} cannot move to {target}")]
|
|
|
|
NoMove {
|
|
|
|
piece: Piece,
|
|
|
|
origin: Square,
|
|
|
|
target: Square,
|
|
|
|
},
|
|
|
|
|
|
|
|
#[error("{0}")]
|
|
|
|
PlacePieceError(#[from] PlacePieceError),
|
2025-05-19 16:50:30 -07:00
|
|
|
|
|
|
|
#[error("{0}")]
|
|
|
|
CastleError(#[from] CastleEvaluationError),
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
#[error("cannot promote on {0}")]
|
|
|
|
InvalidPromotion(Square),
|
2025-05-21 09:51:16 -07:00
|
|
|
|
|
|
|
#[error("move to {0} requires promotion")]
|
|
|
|
PromotionRequired(Square),
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
pub trait MakeMove {
|
2025-05-31 20:17:18 -07:00
|
|
|
/// Make a move.
|
|
|
|
///
|
|
|
|
/// ## Errors
|
|
|
|
///
|
|
|
|
/// Returns one of [`MakeMoveError`] indicating why the move could not be
|
|
|
|
/// made.
|
|
|
|
///
|
2025-05-31 19:01:20 -07:00
|
|
|
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>;
|
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
fn advance_board_state(
|
|
|
|
&mut self,
|
|
|
|
en_passant_target: Option<Square>,
|
|
|
|
half_move_clock: HalfMoveClock,
|
|
|
|
);
|
2025-05-31 19:01:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<T: BoardProvider> MakeMove for T {
|
2025-05-18 08:08:47 -07:00
|
|
|
/// Make a move in the position.
|
|
|
|
///
|
|
|
|
/// ## Errors
|
|
|
|
///
|
2025-05-31 20:17:18 -07:00
|
|
|
/// If `validate` is [`ValidateMove::Yes`], perform validation of move
|
|
|
|
/// correctness prior to applying the move. See [`Position::validate_move`].
|
2025-05-31 19:01:20 -07:00
|
|
|
fn make_move(
|
2025-05-23 10:00:20 -07:00
|
|
|
&mut self,
|
|
|
|
ply: Move,
|
|
|
|
validate: ValidateMove,
|
2025-05-31 19:01:20 -07:00
|
|
|
) -> Result<MoveRecord, MakeMoveError> {
|
2025-05-18 08:08:47 -07:00
|
|
|
if ply.is_quiet() {
|
[board, moves, position] Implement KingMoveGenerator
Implement a move generator that emits moves for the king(s) of a particular color.
There will, of course, only ever be one king per side in any valid board, but
this iterator can (in theory) handle multiple kings on the board. This iterator
is almost entirely copypasta of the SliderMoveGenerator. The major difference is
castling.
Castle moves are emitted by a helper CastleIterator type. This struct collects
information about whether the given color can castle on each side of the board
and then emits moves for each side, if indicated.
Do some light refactoring of the castle-related methods on Board to accommodate
this move generator. Remove the dependency on internal state and rename the
"can_castle" method to color_can_castle.
In order to facilitate creating castling moves without relying on Board, remove
the origin and target squares from the encoded castling move. Code that makes
a castling move already looks up castling parameters to move the king and rook to
the right squares, so encoding those squares was redundant. This change
necessitated some updates to position.
Lastly, bring in a handful of unit tests courtesy of Claude. Apparently, it's my
new best coding friend. 🙃
2025-05-26 23:37:33 -07:00
|
|
|
self.validate_move(ply, validate)?;
|
2025-05-21 09:51:16 -07:00
|
|
|
return self.make_quiet_move(ply);
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
if ply.is_double_push() {
|
[board, moves, position] Implement KingMoveGenerator
Implement a move generator that emits moves for the king(s) of a particular color.
There will, of course, only ever be one king per side in any valid board, but
this iterator can (in theory) handle multiple kings on the board. This iterator
is almost entirely copypasta of the SliderMoveGenerator. The major difference is
castling.
Castle moves are emitted by a helper CastleIterator type. This struct collects
information about whether the given color can castle on each side of the board
and then emits moves for each side, if indicated.
Do some light refactoring of the castle-related methods on Board to accommodate
this move generator. Remove the dependency on internal state and rename the
"can_castle" method to color_can_castle.
In order to facilitate creating castling moves without relying on Board, remove
the origin and target squares from the encoded castling move. Code that makes
a castling move already looks up castling parameters to move the king and rook to
the right squares, so encoding those squares was redundant. This change
necessitated some updates to position.
Lastly, bring in a handful of unit tests courtesy of Claude. Apparently, it's my
new best coding friend. 🙃
2025-05-26 23:37:33 -07:00
|
|
|
self.validate_move(ply, validate)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
return self.make_double_push_move(ply);
|
|
|
|
}
|
|
|
|
|
2025-05-18 08:08:47 -07:00
|
|
|
if ply.is_capture() {
|
[board, moves, position] Implement KingMoveGenerator
Implement a move generator that emits moves for the king(s) of a particular color.
There will, of course, only ever be one king per side in any valid board, but
this iterator can (in theory) handle multiple kings on the board. This iterator
is almost entirely copypasta of the SliderMoveGenerator. The major difference is
castling.
Castle moves are emitted by a helper CastleIterator type. This struct collects
information about whether the given color can castle on each side of the board
and then emits moves for each side, if indicated.
Do some light refactoring of the castle-related methods on Board to accommodate
this move generator. Remove the dependency on internal state and rename the
"can_castle" method to color_can_castle.
In order to facilitate creating castling moves without relying on Board, remove
the origin and target squares from the encoded castling move. Code that makes
a castling move already looks up castling parameters to move the king and rook to
the right squares, so encoding those squares was redundant. This change
necessitated some updates to position.
Lastly, bring in a handful of unit tests courtesy of Claude. Apparently, it's my
new best coding friend. 🙃
2025-05-26 23:37:33 -07:00
|
|
|
self.validate_move(ply, validate)?;
|
2025-05-18 08:08:47 -07:00
|
|
|
return self.make_capture_move(ply);
|
|
|
|
}
|
|
|
|
|
2025-05-21 08:25:49 -07:00
|
|
|
if let Some(wing) = ply.castle_wing() {
|
2025-05-23 10:00:20 -07:00
|
|
|
return self.make_castle_move(ply, wing);
|
2025-05-19 16:50:30 -07:00
|
|
|
}
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
if ply.is_promotion() {
|
[board, moves, position] Implement KingMoveGenerator
Implement a move generator that emits moves for the king(s) of a particular color.
There will, of course, only ever be one king per side in any valid board, but
this iterator can (in theory) handle multiple kings on the board. This iterator
is almost entirely copypasta of the SliderMoveGenerator. The major difference is
castling.
Castle moves are emitted by a helper CastleIterator type. This struct collects
information about whether the given color can castle on each side of the board
and then emits moves for each side, if indicated.
Do some light refactoring of the castle-related methods on Board to accommodate
this move generator. Remove the dependency on internal state and rename the
"can_castle" method to color_can_castle.
In order to facilitate creating castling moves without relying on Board, remove
the origin and target squares from the encoded castling move. Code that makes
a castling move already looks up castling parameters to move the king and rook to
the right squares, so encoding those squares was redundant. This change
necessitated some updates to position.
Lastly, bring in a handful of unit tests courtesy of Claude. Apparently, it's my
new best coding friend. 🙃
2025-05-26 23:37:33 -07:00
|
|
|
self.validate_move(ply, validate)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
return self.make_promotion_move(ply);
|
|
|
|
}
|
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
unreachable!();
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
impl<T: BoardProvider> MakeMoveInternal for T {
|
2025-05-21 09:51:16 -07:00
|
|
|
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board_mut();
|
|
|
|
|
2025-05-21 09:51:16 -07:00
|
|
|
let origin = ply.origin_square();
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let piece = board
|
2025-05-21 09:51:16 -07:00
|
|
|
.get_piece(origin)
|
2025-05-18 08:08:47 -07:00
|
|
|
.ok_or(MakeMoveError::NoPiece(origin))?;
|
|
|
|
|
2025-05-21 09:51:16 -07:00
|
|
|
let target = ply.target_square();
|
2025-05-31 19:01:20 -07:00
|
|
|
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)?;
|
2025-05-21 09:51:16 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
board.remove_piece(origin);
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let record = MoveRecord::new(board, ply, None);
|
2025-05-23 10:00:20 -07:00
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
self.advance_board_state(None, HalfMoveClock::Advance);
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
Ok(record)
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board_mut();
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
let origin = ply.origin_square();
|
2025-05-31 19:01:20 -07:00
|
|
|
let piece = board
|
2025-05-20 19:29:02 -07:00
|
|
|
.remove_piece(origin)
|
|
|
|
.ok_or(MakeMoveError::NoPiece(origin))?;
|
|
|
|
|
|
|
|
let target = ply.target_square();
|
2025-05-31 19:01:20 -07:00
|
|
|
board
|
|
|
|
.place_piece(piece, target, PlacePieceStrategy::PreserveExisting)
|
|
|
|
.map_err(MakeMoveError::PlacePieceError)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-31 20:17:18 -07:00
|
|
|
// 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);
|
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
let en_passant_target = match target.rank() {
|
2025-06-03 20:25:53 -07:00
|
|
|
Rank::FOUR => Square::from_file_rank(target.file(), Rank::THREE),
|
|
|
|
Rank::FIVE => Square::from_file_rank(target.file(), Rank::SIX),
|
2025-05-20 19:29:02 -07:00
|
|
|
_ => unreachable!(),
|
2025-06-07 20:08:25 -07:00
|
|
|
};
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
self.advance_board_state(Some(en_passant_target), HalfMoveClock::Advance);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
Ok(record)
|
2025-05-20 19:29:02 -07:00
|
|
|
}
|
|
|
|
|
2025-05-18 08:08:47 -07:00
|
|
|
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
|
|
|
|
let origin_square = ply.origin_square();
|
2025-05-20 19:29:02 -07:00
|
|
|
let target_square = ply.target_square();
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board_mut();
|
|
|
|
|
|
|
|
let piece = board
|
|
|
|
.get_piece(origin_square)
|
|
|
|
.ok_or(MakeMoveError::NoPiece(origin_square))?;
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
if ply.is_en_passant() {
|
2025-05-31 19:01:20 -07:00
|
|
|
let en_passant_square = board
|
2025-06-03 20:25:53 -07:00
|
|
|
.en_passant_target()
|
2025-05-20 19:29:02 -07:00
|
|
|
.ok_or(MakeMoveError::NoCaptureSquare)?;
|
|
|
|
if target_square != en_passant_square {
|
|
|
|
return Err(MakeMoveError::InvalidEnPassantCapture(target_square));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board_mut();
|
|
|
|
|
2025-05-18 08:08:47 -07:00
|
|
|
let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?;
|
2025-05-31 19:01:20 -07:00
|
|
|
let captured_piece = board
|
2025-05-20 19:29:02 -07:00
|
|
|
.remove_piece(capture_square)
|
|
|
|
.ok_or(MakeMoveError::NoCapturePiece(capture_square))?;
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
board.remove_piece(origin_square).unwrap();
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
if let Some(promotion_shape) = ply.promotion_shape() {
|
|
|
|
let promoted_piece = Piece::new(piece.color, promotion_shape);
|
2025-05-31 19:01:20 -07:00
|
|
|
board.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
} else {
|
2025-05-31 19:01:20 -07:00
|
|
|
board.place_piece(piece, target_square, PlacePieceStrategy::Replace)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
}
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let record = MoveRecord::new(board, ply, Some(captured_piece));
|
2025-05-23 10:00:20 -07:00
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
self.advance_board_state(None, HalfMoveClock::Reset);
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
Ok(record)
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
2025-05-19 16:50:30 -07:00
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board_mut();
|
|
|
|
|
|
|
|
board.color_can_castle(wing, None)?;
|
2025-05-19 16:50:30 -07:00
|
|
|
|
2025-06-02 17:29:52 -07:00
|
|
|
let active_color = board.active_color();
|
[board, moves, position] Implement KingMoveGenerator
Implement a move generator that emits moves for the king(s) of a particular color.
There will, of course, only ever be one king per side in any valid board, but
this iterator can (in theory) handle multiple kings on the board. This iterator
is almost entirely copypasta of the SliderMoveGenerator. The major difference is
castling.
Castle moves are emitted by a helper CastleIterator type. This struct collects
information about whether the given color can castle on each side of the board
and then emits moves for each side, if indicated.
Do some light refactoring of the castle-related methods on Board to accommodate
this move generator. Remove the dependency on internal state and rename the
"can_castle" method to color_can_castle.
In order to facilitate creating castling moves without relying on Board, remove
the origin and target squares from the encoded castling move. Code that makes
a castling move already looks up castling parameters to move the king and rook to
the right squares, so encoding those squares was redundant. This change
necessitated some updates to position.
Lastly, bring in a handful of unit tests courtesy of Claude. Apparently, it's my
new best coding friend. 🙃
2025-05-26 23:37:33 -07:00
|
|
|
let parameters = Board::castling_parameters(wing, active_color);
|
2025-05-19 16:50:30 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let king = board.remove_piece(parameters.origin.king).unwrap();
|
|
|
|
board.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
|
2025-05-19 16:50:30 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let rook = board.remove_piece(parameters.origin.rook).unwrap();
|
|
|
|
board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
|
2025-05-19 16:50:30 -07:00
|
|
|
|
2025-05-31 20:17:18 -07:00
|
|
|
// Capture move record before revoking castling rights to ensure
|
|
|
|
// original board state is preserved.
|
2025-05-31 19:01:20 -07:00
|
|
|
let record = MoveRecord::new(board, ply, None);
|
2025-05-23 10:00:20 -07:00
|
|
|
|
2025-06-17 08:28:39 -07:00
|
|
|
board.revoke_castling_right_unwrapped(active_color, wing);
|
2025-05-31 20:17:18 -07:00
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
self.advance_board_state(None, HalfMoveClock::Advance);
|
2025-05-19 16:50:30 -07:00
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
Ok(record)
|
2025-05-19 16:50:30 -07:00
|
|
|
}
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board_mut();
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
let origin = ply.origin_square();
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let piece = board
|
|
|
|
.get_piece(origin)
|
|
|
|
.ok_or(MakeMoveError::NoPiece(origin))?;
|
2025-05-20 19:29:02 -07:00
|
|
|
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() {
|
2025-05-31 19:01:20 -07:00
|
|
|
board.remove_piece(origin);
|
2025-05-20 19:29:02 -07:00
|
|
|
let promoted_piece = Piece::new(piece.color, promotion_shape);
|
2025-05-31 19:01:20 -07:00
|
|
|
board.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
} else {
|
|
|
|
unreachable!(
|
|
|
|
"Cannot make a promotion move with a ply that has no promotion shape: {ply:?}",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let record = MoveRecord::new(board, ply, None);
|
2025-05-23 10:00:20 -07:00
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
self.advance_board_state(None, HalfMoveClock::Reset);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
Ok(record)
|
|
|
|
}
|
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
fn advance_board_state(
|
|
|
|
&mut self,
|
|
|
|
en_passant_target: Option<Square>,
|
|
|
|
half_move_clock: HalfMoveClock,
|
|
|
|
) {
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board_mut();
|
|
|
|
|
2025-05-18 08:08:47 -07:00
|
|
|
match half_move_clock {
|
2025-05-31 19:01:20 -07:00
|
|
|
HalfMoveClock::Reset => board.half_move_clock = 0,
|
|
|
|
HalfMoveClock::Advance => board.half_move_clock += 1,
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
|
2025-06-02 17:29:52 -07:00
|
|
|
let previous_active_color = board.active_color();
|
|
|
|
let active_color = previous_active_color.next();
|
|
|
|
board.set_active_color(active_color);
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-06-07 20:08:25 -07:00
|
|
|
board.set_en_passant_target_option(en_passant_target);
|
|
|
|
|
2025-06-02 17:29:52 -07:00
|
|
|
if active_color == Color::White {
|
2025-05-31 19:01:20 -07:00
|
|
|
board.full_move_number += 1;
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> {
|
|
|
|
if validate == ValidateMove::No {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2025-05-19 16:50:30 -07:00
|
|
|
let active_piece = self.validate_active_piece(ply)?;
|
2025-05-18 08:08:47 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board();
|
2025-05-19 16:50:30 -07:00
|
|
|
let origin_square = ply.origin_square();
|
2025-05-18 08:08:47 -07:00
|
|
|
let target_square = ply.target_square();
|
|
|
|
|
2025-05-19 16:50:30 -07:00
|
|
|
// 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.
|
2025-05-31 19:01:20 -07:00
|
|
|
let movement = active_piece.movement(origin_square, board);
|
2025-05-18 08:08:47 -07:00
|
|
|
if !movement.contains(target_square) {
|
|
|
|
return Err(MakeMoveError::NoMove {
|
|
|
|
piece: active_piece,
|
|
|
|
origin: origin_square,
|
|
|
|
target: target_square,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-05-19 16:50:30 -07:00
|
|
|
// TODO: En Passant capture.
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
if let Some(capture_square) = ply.capture_square() {
|
2025-05-31 19:01:20 -07:00
|
|
|
if let Some(captured_piece) = board.get_piece(capture_square) {
|
2025-05-18 08:08:47 -07:00
|
|
|
if captured_piece.color == active_piece.color {
|
2025-05-20 19:29:02 -07:00
|
|
|
return Err(MakeMoveError::InvalidCapture(capture_square));
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
} else {
|
2025-05-20 19:29:02 -07:00
|
|
|
return Err(MakeMoveError::NoPiece(capture_square));
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2025-05-19 16:50:30 -07:00
|
|
|
|
|
|
|
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError> {
|
|
|
|
let origin_square = ply.origin_square();
|
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
let board = self.board();
|
|
|
|
|
|
|
|
let active_piece = board
|
2025-05-19 16:50:30 -07:00
|
|
|
.get_piece(origin_square)
|
|
|
|
.ok_or(MakeMoveError::NoPiece(origin_square))?;
|
|
|
|
|
2025-06-02 17:29:52 -07:00
|
|
|
if active_piece.color != board.active_color() {
|
2025-05-19 16:50:30 -07:00
|
|
|
return Err(MakeMoveError::NonActiveColor {
|
|
|
|
piece: active_piece,
|
|
|
|
square: origin_square,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(active_piece)
|
|
|
|
}
|
2025-05-18 08:08:47 -07:00
|
|
|
}
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
|
|
enum HalfMoveClock {
|
|
|
|
Reset,
|
|
|
|
#[default]
|
|
|
|
Advance,
|
|
|
|
}
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2025-05-31 19:01:20 -07:00
|
|
|
use crate::{Move, PromotionShape};
|
|
|
|
use chessfriend_board::test_board;
|
2025-06-17 08:28:39 -07:00
|
|
|
use chessfriend_core::{Color, Square, piece};
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-23 10:00:20 -07:00
|
|
|
type TestResult = Result<(), MakeMoveError>;
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
#[test]
|
2025-05-23 10:00:20 -07:00
|
|
|
fn make_quiet_move() -> TestResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board!(White Pawn on C2);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
let ply = Move::quiet(Square::C2, Square::C3);
|
2025-05-31 19:01:20 -07:00
|
|
|
board.make_move(ply, ValidateMove::Yes)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.get_piece(Square::C2), None);
|
|
|
|
assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn)));
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::Black);
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.half_move_clock, 1);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-06-02 17:29:52 -07:00
|
|
|
board.set_active_color(Color::White);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
let ply = Move::quiet(Square::C3, Square::C4);
|
2025-05-31 19:01:20 -07:00
|
|
|
board.make_move(ply, ValidateMove::Yes)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.get_piece(Square::C3), None);
|
|
|
|
assert_eq!(board.get_piece(Square::C4), Some(piece!(White Pawn)));
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::Black);
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.half_move_clock, 2);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2025-05-21 09:51:16 -07:00
|
|
|
#[test]
|
|
|
|
fn make_invalid_quiet_pawn_move() {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board!(White Pawn on C2);
|
2025-05-21 09:51:16 -07:00
|
|
|
|
|
|
|
let ply = Move::quiet(Square::C2, Square::D2);
|
2025-05-31 19:01:20 -07:00
|
|
|
let result = board.make_move(ply, ValidateMove::Yes);
|
2025-05-21 09:51:16 -07:00
|
|
|
|
|
|
|
assert!(result.is_err());
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn)));
|
|
|
|
assert_eq!(board.get_piece(Square::D2), None);
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::White);
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.half_move_clock, 0);
|
2025-05-21 09:51:16 -07:00
|
|
|
}
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
#[test]
|
2025-05-23 10:00:20 -07:00
|
|
|
fn make_capture_move() -> TestResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board![
|
2025-05-20 19:29:02 -07:00
|
|
|
White Bishop on C2,
|
|
|
|
Black Rook on F5,
|
|
|
|
];
|
|
|
|
|
|
|
|
let ply = Move::capture(Square::C2, Square::F5);
|
2025-05-31 19:01:20 -07:00
|
|
|
let result = board.make_move(ply, ValidateMove::Yes)?;
|
|
|
|
|
|
|
|
assert_eq!(result.captured_piece, Some(piece!(Black Rook)));
|
2025-05-23 10:00:20 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.get_piece(Square::C2), None);
|
|
|
|
assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop)));
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::Black);
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.half_move_clock, 0);
|
2025-05-23 10:00:20 -07:00
|
|
|
|
|
|
|
Ok(())
|
2025-05-20 19:29:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2025-05-23 10:00:20 -07:00
|
|
|
fn make_en_passant_capture_move() -> TestResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board![
|
2025-05-20 19:29:02 -07:00
|
|
|
Black Pawn on F4,
|
|
|
|
White Pawn on E2
|
|
|
|
];
|
|
|
|
|
|
|
|
let ply = Move::double_push(Square::E2, Square::E4);
|
2025-05-31 20:17:18 -07:00
|
|
|
let record = board.make_move(ply, ValidateMove::Yes)?;
|
|
|
|
|
|
|
|
assert_eq!(record.en_passant_target, None);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.get_piece(Square::E2), None);
|
|
|
|
assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn)));
|
2025-05-20 19:29:02 -07:00
|
|
|
assert_eq!(
|
2025-06-03 20:25:53 -07:00
|
|
|
board.en_passant_target(),
|
2025-05-20 19:29:02 -07:00
|
|
|
Some(Square::E3),
|
|
|
|
"en passant square not set"
|
|
|
|
);
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::Black);
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.half_move_clock, 1);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
let ply = Move::en_passant_capture(Square::F4, Square::E3);
|
2025-05-31 19:01:20 -07:00
|
|
|
let result = board.make_move(ply, ValidateMove::Yes)?;
|
|
|
|
|
|
|
|
assert_eq!(result.captured_piece, Some(piece!(White Pawn)));
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.get_piece(Square::F4), None);
|
|
|
|
assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn)));
|
2025-05-20 19:29:02 -07:00
|
|
|
assert_eq!(
|
2025-05-31 19:01:20 -07:00
|
|
|
board.get_piece(Square::E4),
|
2025-05-20 19:29:02 -07:00
|
|
|
None,
|
|
|
|
"capture target pawn not removed"
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2025-05-21 09:51:16 -07:00
|
|
|
#[test]
|
|
|
|
fn make_last_rank_quiet_move_without_promotion() {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board!(
|
2025-05-21 09:51:16 -07:00
|
|
|
White Pawn on A7
|
|
|
|
);
|
|
|
|
|
|
|
|
let ply = Move::quiet(Square::A7, Square::A8);
|
2025-05-31 19:01:20 -07:00
|
|
|
let result = board.make_move(ply, ValidateMove::Yes);
|
2025-05-21 09:51:16 -07:00
|
|
|
|
|
|
|
assert!(result.is_err());
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::White);
|
2025-05-31 19:01:20 -07:00
|
|
|
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);
|
2025-05-21 09:51:16 -07:00
|
|
|
}
|
|
|
|
|
2025-05-20 19:29:02 -07:00
|
|
|
#[test]
|
2025-05-23 10:00:20 -07:00
|
|
|
fn make_promotion_move() -> TestResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board![
|
2025-05-20 19:29:02 -07:00
|
|
|
Black Pawn on E7,
|
|
|
|
White Pawn on F7,
|
|
|
|
];
|
|
|
|
|
|
|
|
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
|
2025-05-31 19:01:20 -07:00
|
|
|
board.make_move(ply, ValidateMove::Yes)?;
|
2025-05-20 19:29:02 -07:00
|
|
|
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.get_piece(Square::F7), None);
|
|
|
|
assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen)));
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::Black);
|
2025-05-31 19:01:20 -07:00
|
|
|
assert_eq!(board.half_move_clock, 0);
|
2025-05-20 19:29:02 -07:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2025-05-21 08:25:49 -07:00
|
|
|
|
|
|
|
#[test]
|
2025-05-23 10:00:20 -07:00
|
|
|
fn make_white_kingside_castle() -> TestResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board![
|
2025-05-21 08:25:49 -07:00
|
|
|
White Rook on H1,
|
|
|
|
White King on E1,
|
|
|
|
];
|
|
|
|
|
2025-06-11 08:15:06 -07:00
|
|
|
let ply = Move::castle(Color::White, Wing::KingSide);
|
2025-05-31 19:01:20 -07:00
|
|
|
board.make_move(ply, ValidateMove::Yes)?;
|
|
|
|
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::Black);
|
2025-05-31 19:01:20 -07:00
|
|
|
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)));
|
2025-06-17 08:28:39 -07:00
|
|
|
assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
|
2025-05-21 08:25:49 -07:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2025-05-23 10:00:20 -07:00
|
|
|
fn make_white_queenside_castle() -> TestResult {
|
2025-05-31 19:01:20 -07:00
|
|
|
let mut board = test_board![
|
2025-05-21 08:25:49 -07:00
|
|
|
White King on E1,
|
|
|
|
White Rook on A1,
|
|
|
|
];
|
|
|
|
|
2025-06-11 08:15:06 -07:00
|
|
|
let ply = Move::castle(Color::White, Wing::QueenSide);
|
2025-05-31 19:01:20 -07:00
|
|
|
board.make_move(ply, ValidateMove::Yes)?;
|
|
|
|
|
2025-06-02 17:29:52 -07:00
|
|
|
assert_eq!(board.active_color(), Color::Black);
|
2025-05-31 19:01:20 -07:00
|
|
|
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)));
|
2025-06-17 08:28:39 -07:00
|
|
|
assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
|
2025-05-21 08:25:49 -07:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2025-05-20 19:29:02 -07:00
|
|
|
}
|