added undo_move, added make/undo testing
This commit is contained in:
parent
e7578dd0f0
commit
1ddc38165f
5 changed files with 226 additions and 53 deletions
69
src/board.rs
69
src/board.rs
|
|
@ -202,9 +202,7 @@ impl Board {
|
|||
self.rm_piece(to + (self.side_to_move as i8 * 16 - 8), !self.side_to_move);
|
||||
opt_captured_piece = Some(PieceType::Pawn);
|
||||
}
|
||||
_ => {
|
||||
panic!("unable to make_move: invalid flags: {}", flags);
|
||||
}
|
||||
_ => { panic!("unable to make_move: invalid flags: {}", flags); }
|
||||
}
|
||||
|
||||
// 5. Update the castling rights
|
||||
|
|
@ -246,4 +244,69 @@ impl Board {
|
|||
old_halfmove_clock,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn undo_move(&mut self, undo_info: UndoMove) {
|
||||
// 1. Restore all simple state from the UndoMove object
|
||||
self.castling_rights = undo_info.old_castling_rights;
|
||||
self.en_passant_target = undo_info.old_en_passant_square;
|
||||
self.halfmove_clock = undo_info.old_halfmove_clock;
|
||||
|
||||
// 2. Flip side_to_move *before* doing piece ops.
|
||||
self.side_to_move = !self.side_to_move;
|
||||
|
||||
// 3. Decrement fullmove number if it was Black's turn (which is now self.side_to_move)
|
||||
self.fullmove_number -= self.side_to_move as u16;
|
||||
|
||||
// 4. Extract move data
|
||||
let mv = undo_info.mv;
|
||||
let from = mv.get_from();
|
||||
let to = mv.get_to();
|
||||
let flags = mv.get_flags();
|
||||
|
||||
// 5. Reverse the piece movements based on the flag
|
||||
match flags {
|
||||
MOVE_FLAG_QUIET => {
|
||||
self.move_piece(to, from, self.side_to_move);
|
||||
}
|
||||
MOVE_FLAG_CAPTURE => {
|
||||
self.move_piece(to, from, self.side_to_move);
|
||||
self.put_piece(to, !self.side_to_move, undo_info.captured_piece.unwrap());
|
||||
}
|
||||
MOVE_FLAG_DOUBLE_PAWN => {
|
||||
self.move_piece(to, from, self.side_to_move);
|
||||
}
|
||||
MOVE_FLAG_PROMO_Q | MOVE_FLAG_PROMO_N | MOVE_FLAG_PROMO_B | MOVE_FLAG_PROMO_R => {
|
||||
self.rm_piece(to, self.side_to_move);
|
||||
self.put_piece(from, self.side_to_move, PieceType::Pawn);
|
||||
}
|
||||
MOVE_FLAG_PROMO_Q_CAP | MOVE_FLAG_PROMO_N_CAP | MOVE_FLAG_PROMO_B_CAP | MOVE_FLAG_PROMO_R_CAP => {
|
||||
self.rm_piece(to, self.side_to_move);
|
||||
self.put_piece(from, self.side_to_move, PieceType::Pawn);
|
||||
self.put_piece(to, !self.side_to_move, undo_info.captured_piece.unwrap());
|
||||
}
|
||||
MOVE_FLAG_WK_CASTLE => {
|
||||
self.move_piece(Square::G1, Square::E1, self.side_to_move);
|
||||
self.move_piece(Square::F1, Square::H1, self.side_to_move);
|
||||
}
|
||||
MOVE_FLAG_BK_CASTLE => {
|
||||
self.move_piece(Square::G8, Square::E8, self.side_to_move);
|
||||
self.move_piece(Square::F8, Square::H8, self.side_to_move);
|
||||
}
|
||||
MOVE_FLAG_WQ_CASTLE => {
|
||||
self.move_piece(Square::C1, Square::E1, self.side_to_move);
|
||||
self.move_piece(Square::D1, Square::A1, self.side_to_move);
|
||||
}
|
||||
MOVE_FLAG_BQ_CASTLE => {
|
||||
self.move_piece(Square::C8, Square::E8, self.side_to_move);
|
||||
self.move_piece(Square::D8, Square::A8, self.side_to_move);
|
||||
}
|
||||
MOVE_FLAG_EN_PASSANT => {
|
||||
self.move_piece(to, from, self.side_to_move);
|
||||
// Determine where the captured pawn was
|
||||
let captured_pawn_square = to + (self.side_to_move as i8 * 16 - 8);
|
||||
self.put_piece(captured_pawn_square, !self.side_to_move, PieceType::Pawn);
|
||||
}
|
||||
_ => { panic!("unable to unmake_move: invalid flags: {}", flags); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,51 +143,6 @@ impl Board {
|
|||
}
|
||||
}
|
||||
|
||||
impl Move {
|
||||
/// Converts a square index (0-63) to algebraic notation (e.g., 0 -> "a1", 63 -> "h8").
|
||||
fn square_val_to_alg(val: u16) -> String {
|
||||
let file = (b'a' + (val % 8) as u8) as char;
|
||||
let rank = (b'1' + (val / 8) as u8) as char;
|
||||
format!("{}{}", file, rank)
|
||||
}
|
||||
|
||||
/// Converts the move to coordinate notation (e.g., "e2e4", "e7e8q", "e1g1").
|
||||
pub fn to_algebraic(&self) -> String {
|
||||
let flags = self.get_flags();
|
||||
|
||||
// Handle castling first. In this new format, the "to" square is
|
||||
// the *king's* destination square (g1/c1 or g8/c8).
|
||||
// Your old implementation reading the file is still fine.
|
||||
if (flags == MOVE_FLAG_WK_CASTLE) || (flags == MOVE_FLAG_BK_CASTLE) {
|
||||
return "O-O".to_string();
|
||||
}
|
||||
if (flags == MOVE_FLAG_WQ_CASTLE) || (flags == MOVE_FLAG_BQ_CASTLE) {
|
||||
return "O-O-O".to_string();
|
||||
}
|
||||
|
||||
let from_val = self.0 & MOVE_FROM_MASK;
|
||||
let to_val = (self.0 & MOVE_TO_MASK) >> 6;
|
||||
|
||||
let from_str = Self::square_val_to_alg(from_val);
|
||||
let to_str = Self::square_val_to_alg(to_val);
|
||||
|
||||
// Check if it's any promotion type (1xxx)
|
||||
if (flags & 0b1000_0000_0000_0000) != 0 {
|
||||
let promo_char = match flags {
|
||||
MOVE_FLAG_PROMO_N | MOVE_FLAG_PROMO_N_CAP => 'n',
|
||||
MOVE_FLAG_PROMO_B | MOVE_FLAG_PROMO_B_CAP => 'b',
|
||||
MOVE_FLAG_PROMO_R | MOVE_FLAG_PROMO_R_CAP => 'r',
|
||||
MOVE_FLAG_PROMO_Q | MOVE_FLAG_PROMO_Q_CAP => 'q',
|
||||
_ => '?', // Should not happen
|
||||
};
|
||||
format!("{}{}{}", from_str, to_str, promo_char)
|
||||
} else {
|
||||
// This covers Quiet, DoublePawn, Capture, EnPassant
|
||||
format!("{}{}", from_str, to_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MoveList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", &self.iter().map(|mv| mv.to_algebraic()).collect::<Vec<String>>().join(" "))
|
||||
|
|
|
|||
10
src/move.rs
10
src/move.rs
|
|
@ -105,11 +105,11 @@ impl MoveList {
|
|||
}
|
||||
|
||||
pub struct UndoMove {
|
||||
mv: Move,
|
||||
captured_piece: Option<PieceType>,
|
||||
old_en_passant_square: Option<Square>,
|
||||
old_castling_rights: u8,
|
||||
old_halfmove_clock: u8,
|
||||
pub mv: Move,
|
||||
pub captured_piece: Option<PieceType>,
|
||||
pub old_en_passant_square: Option<Square>,
|
||||
pub old_castling_rights: u8,
|
||||
pub old_halfmove_clock: u8,
|
||||
}
|
||||
|
||||
impl UndoMove {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::mem;
|
||||
use crate::board::{Board, Color, PieceType, CASTLING_BK_FLAG, CASTLING_BQ_FLAG, CASTLING_WK_FLAG, CASTLING_WQ_FLAG};
|
||||
use crate::r#move::{Move, MOVE_FLAG_BK_CASTLE, MOVE_FLAG_BQ_CASTLE, MOVE_FLAG_PROMO_B, MOVE_FLAG_PROMO_B_CAP, MOVE_FLAG_PROMO_N, MOVE_FLAG_PROMO_N_CAP, MOVE_FLAG_PROMO_Q, MOVE_FLAG_PROMO_Q_CAP, MOVE_FLAG_PROMO_R, MOVE_FLAG_PROMO_R_CAP, MOVE_FLAG_WK_CASTLE, MOVE_FLAG_WQ_CASTLE, MOVE_FROM_MASK, MOVE_TO_MASK};
|
||||
use crate::square::Square;
|
||||
|
||||
impl Board {
|
||||
|
|
@ -234,4 +235,50 @@ impl Board {
|
|||
|
||||
fen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Move {
|
||||
/// Converts a square index (0-63) to algebraic notation (e.g., 0 -> "a1", 63 -> "h8").
|
||||
fn square_val_to_alg(val: u16) -> String {
|
||||
let file = (b'a' + (val % 8) as u8) as char;
|
||||
let rank = (b'1' + (val / 8) as u8) as char;
|
||||
format!("{}{}", file, rank)
|
||||
}
|
||||
|
||||
/// Converts the move to coordinate notation (e.g., "e2e4", "e7e8q", "e1g1").
|
||||
pub fn to_algebraic(&self) -> String {
|
||||
let flags = self.get_flags();
|
||||
|
||||
// Handle castling first. In this new format, the "to" square is
|
||||
// the *king's* destination square (g1/c1 or g8/c8).
|
||||
// Your old implementation reading the file is still fine.
|
||||
if (flags == MOVE_FLAG_WK_CASTLE) || (flags == MOVE_FLAG_BK_CASTLE) {
|
||||
return "O-O".to_string();
|
||||
}
|
||||
if (flags == MOVE_FLAG_WQ_CASTLE) || (flags == MOVE_FLAG_BQ_CASTLE) {
|
||||
return "O-O-O".to_string();
|
||||
}
|
||||
|
||||
let from_val = self.0 & MOVE_FROM_MASK;
|
||||
let to_val = (self.0 & MOVE_TO_MASK) >> 6;
|
||||
|
||||
let from_str = Self::square_val_to_alg(from_val);
|
||||
let to_str = Self::square_val_to_alg(to_val);
|
||||
|
||||
// Check if it's any promotion type (1xxx)
|
||||
if (flags & 0b1000_0000_0000_0000) != 0 {
|
||||
let promo_char = match flags {
|
||||
MOVE_FLAG_PROMO_N | MOVE_FLAG_PROMO_N_CAP => 'n',
|
||||
MOVE_FLAG_PROMO_B | MOVE_FLAG_PROMO_B_CAP => 'b',
|
||||
MOVE_FLAG_PROMO_R | MOVE_FLAG_PROMO_R_CAP => 'r',
|
||||
MOVE_FLAG_PROMO_Q | MOVE_FLAG_PROMO_Q_CAP => 'q',
|
||||
_ => '?', // Should not happen
|
||||
};
|
||||
format!("{}{}{}", from_str, to_str, promo_char)
|
||||
} else {
|
||||
// This covers Quiet, DoublePawn, Capture, EnPassant
|
||||
format!("{}{}", from_str, to_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
108
tests/make_undo.rs
Normal file
108
tests/make_undo.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use chess_engine::board::Board;
|
||||
use chess_engine::movegen::generate_pseudo_legal_moves;
|
||||
use chess_engine::r#move::MoveList;
|
||||
|
||||
/// Helper function to run the make/undo integrity check for any given board state.
|
||||
/// This avoids code duplication in all the specific test cases.
|
||||
fn assert_make_undo_integrity(board: &mut Board) {
|
||||
let mut list = MoveList::new();
|
||||
generate_pseudo_legal_moves(&board, &mut list);
|
||||
|
||||
// If no moves are generated (e.g., stalemate/checkmate), the test passes.
|
||||
if list.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for mv in list.iter() {
|
||||
let board_before_make = board.to_fen();
|
||||
let undo_obj = board.make_move(*mv);
|
||||
let board_after_make = board.to_fen();
|
||||
|
||||
// Ensure the board actually changed
|
||||
assert_ne!(
|
||||
board_before_make, board_after_make,
|
||||
"Board did not change after make_move for move: {:?}", mv
|
||||
);
|
||||
|
||||
board.undo_move(undo_obj);
|
||||
let board_after_undo = board.to_fen();
|
||||
|
||||
// Ensure the board is perfectly restored
|
||||
assert_eq!(
|
||||
board_before_make, board_after_undo,
|
||||
"Board state mismatch after undo_move for move: {:?}", mv
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_undo_standard() {
|
||||
let fen_standard = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
||||
let mut board = Board::from_fen(fen_standard);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests a complex, mid-game position with many interactions.
|
||||
/// This is the famous "Kiwipete" FEN used for perft testing.
|
||||
fn test_make_undo_kiwipete() {
|
||||
let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
|
||||
let mut board = Board::from_fen(fen);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests the en passant capture for White (e.g., e5xd6).
|
||||
/// This ensures the black pawn on d5 is correctly removed and restored.
|
||||
fn test_make_undo_en_passant_white() {
|
||||
// Position after 1. e4 e6 2. e5 d5
|
||||
let fen = "rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPP2PPP/RNBQKBNR w KQkq d6 0 3";
|
||||
let mut board = Board::from_fen(fen);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests the en passant capture for Black (e.g., d5xe6).
|
||||
/// This ensures the white pawn on e5 is correctly removed and restored.
|
||||
fn test_make_undo_en_passant_black() {
|
||||
// Position after 1. d4 c5 2. d5 e5
|
||||
let fen = "rnbqkbnr/pp1p1ppp/8/2pPp3/8/8/PPP1PPPP/RNBQKBNR b KQkq e6 0 3";
|
||||
let mut board = Board::from_fen(fen);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests White's kingside (O-O) and queenside (O-O-O) castling.
|
||||
/// Ensures both rook and king moves are correctly undone.
|
||||
fn test_make_undo_castling_white() {
|
||||
let fen = "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1";
|
||||
let mut board = Board::from_fen(fen);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests Black's kingside (O-O) and queenside (O-O-O) castling.
|
||||
/// Ensures both rook and king moves are correctly undone.
|
||||
fn test_make_undo_castling_black() {
|
||||
let fen = "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R b KQkq - 0 1";
|
||||
let mut board = Board::from_fen(fen);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests white pawn promotions (quiet and capture) to Q, R, B, N.
|
||||
fn test_make_undo_promotions_white() {
|
||||
// White pawn on c7, black rooks on b8 and d8 to test capture-promotions
|
||||
let fen = "1r1rkb1r/2Ppppb1/8/8/8/8/1PPPPPP1/RNBQKBNR w KQk - 0 1";
|
||||
let mut board = Board::from_fen(fen);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests black pawn promotions (quiet and capture) to q, r, b, n.
|
||||
fn test_make_undo_promotions_black() {
|
||||
// Black pawn on g2, white rooks on f1 and h1 to test capture-promotions
|
||||
let fen = "RNBQKBNR/1PPPPP2/8/8/8/8/6p1/R4RK1 b Qkq - 0 1";
|
||||
let mut board = Board::from_fen(fen);
|
||||
assert_make_undo_integrity(&mut board);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue