diff --git a/src/board.rs b/src/board.rs index 1d7b4f3..e0af250 100644 --- a/src/board.rs +++ b/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); } + } + } } \ No newline at end of file diff --git a/src/display.rs b/src/display.rs index 34aea07..8a09861 100644 --- a/src/display.rs +++ b/src/display.rs @@ -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::>().join(" ")) diff --git a/src/move.rs b/src/move.rs index 04b1d7c..38f64c7 100644 --- a/src/move.rs +++ b/src/move.rs @@ -105,11 +105,11 @@ impl MoveList { } pub struct UndoMove { - mv: Move, - captured_piece: Option, - old_en_passant_square: Option, - old_castling_rights: u8, - old_halfmove_clock: u8, + pub mv: Move, + pub captured_piece: Option, + pub old_en_passant_square: Option, + pub old_castling_rights: u8, + pub old_halfmove_clock: u8, } impl UndoMove { diff --git a/src/parsing.rs b/src/parsing.rs index 55352bf..96c3c67 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -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) + } + } } \ No newline at end of file diff --git a/tests/make_undo.rs b/tests/make_undo.rs new file mode 100644 index 0000000..6ba2806 --- /dev/null +++ b/tests/make_undo.rs @@ -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); +} \ No newline at end of file