[moves, position] Implement unmaking moves on a board

Declare an UnmakeMove trait in the moves crate, just like the MakeMove trait from
an earlier commit. Implement this trait for all types that also implement
BoardProvider.

Bring in a whole pile of unit tests from Claude. (Holy shit, using Claude really
saves time on these tests…) Several of these tests failed, and all of those
failures revealed bugs in either MakeMove or UnmakeMove. Huzzah! Include fixes for
those bugs here.
This commit is contained in:
Eryn Wells 2025-05-31 20:17:18 -07:00
parent 40e8e055f9
commit f60cb8cf69
4 changed files with 522 additions and 10 deletions

View file

@ -61,6 +61,13 @@ pub enum MakeMoveError {
}
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;
}
@ -82,8 +89,8 @@ impl<T: BoardProvider> MakeMove for T {
///
/// ## Errors
///
/// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to
/// applying the move. See [`Position::validate_move`].
/// 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,
@ -158,14 +165,16 @@ impl<T: BoardProvider> MakeMoveInternal for T {
.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);
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!(),
};
let record = MoveRecord::new(board, ply, None);
self.advance_clocks(HalfMoveClock::Advance);
Ok(record)
@ -227,10 +236,12 @@ impl<T: BoardProvider> MakeMoveInternal for T {
let rook = board.remove_piece(parameters.origin.rook).unwrap();
board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
board.castling_rights.revoke(active_color, wing);
// Capture move record before revoking castling rights to ensure
// original board state is preserved.
let record = MoveRecord::new(board, ply, None);
board.castling_rights.revoke(active_color, wing);
self.advance_clocks(HalfMoveClock::Advance);
Ok(record)
@ -426,7 +437,9 @@ mod tests {
];
let ply = Move::double_push(Square::E2, Square::E4);
board.make_move(ply, ValidateMove::Yes)?;
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)));