From 53a96f878708242ddcc18cb8030d3c2993476923 Mon Sep 17 00:00:00 2001 From: dece Date: Thu, 25 Jun 2020 00:15:04 +0200 Subject: [PATCH] movement: thoroughly test unmake with castling --- src/movement.rs | 154 +++++++++++++++++++++++++++++++++--------------- src/rules.rs | 87 ++++++++++++++++++++++++++- src/uci.rs | 5 +- 3 files changed, 194 insertions(+), 52 deletions(-) diff --git a/src/movement.rs b/src/movement.rs index 5d24478..669a89d 100644 --- a/src/movement.rs +++ b/src/movement.rs @@ -42,66 +42,105 @@ impl Move { } /// Apply this move to `board` and `game_state`. + /// + /// Set automatic queen promotion for pawns, register captured + /// pieces and castle options. pub fn apply_to(&mut self, board: &mut Board, game_state: &mut GameState) { + // Save current castling options to unmake later. self.old_castles = game_state.castling; - // If a king moves, remove it from castling options. - if self.source == E1 { game_state.castling &= !CASTLE_WH_MASK; } - else if self.source == E8 { game_state.castling &= !CASTLE_BL_MASK; } - // Same for rooks. + + let piece = board.get_piece_on(self.source); + // Handle king castling. + if piece == KING { + if let Some(castle) = self.get_castle() { + match castle { + CASTLE_WH_K => { + board.move_square(E1, G1); + board.move_square(H1, F1); + game_state.castling &= !CASTLE_WH_MASK; + } + CASTLE_WH_Q => { + board.move_square(E1, C1); + board.move_square(A1, D1); + game_state.castling &= !CASTLE_WH_MASK; + } + CASTLE_BL_K => { + board.move_square(E8, G8); + board.move_square(H8, F8); + game_state.castling &= !CASTLE_BL_MASK; + } + CASTLE_BL_Q => { + board.move_square(E8, C8); + board.move_square(A8, D8); + game_state.castling &= !CASTLE_BL_MASK; + } + _ => { panic!("Invalid castle.") } + } + game_state.color = opposite(game_state.color); + return + } else { + // If the king moved from starting square, remove it from castling options. + if self.source == E1 { game_state.castling &= !CASTLE_WH_MASK; } + else if self.source == E8 { game_state.castling &= !CASTLE_BL_MASK; } + } + } + // Record captured piece if any. + if !board.is_empty(self.dest) { + self.capture = Some(board.get_piece_on(self.dest)); + } + + // Move the piece. + board.move_square(self.source, self.dest); + + // Apply promotion if any. + if let Some(piece) = self.promotion { + board.set_piece(self.dest, PAWN, piece); + } + + // If a rook moved, remove the castle side. if self.source == A1 || self.dest == A1 { game_state.castling &= !CASTLE_WH_Q; } else if self.source == H1 || self.dest == H1 { game_state.castling &= !CASTLE_WH_K; } else if self.source == A8 || self.dest == A8 { game_state.castling &= !CASTLE_BL_Q; } else if self.source == H8 || self.dest == H8 { game_state.castling &= !CASTLE_BL_K; } - // Update board and game state. - self.apply_to_board(board); - game_state.color = opposite(game_state.color); - } - /// Apply the move into `board`. - pub fn apply_to_board(&mut self, board: &mut Board) { - let piece = board.get_piece_on(self.source); - // If a king is castling, apply special move. - if piece == KING { - if let Some(castle) = self.get_castle() { - match castle { - CASTLE_WH_K => { board.move_square(E1, G1); board.move_square(H1, F1); } - CASTLE_WH_Q => { board.move_square(E1, C1); board.move_square(A1, D1); } - CASTLE_BL_K => { board.move_square(E8, G8); board.move_square(H8, F8); } - CASTLE_BL_Q => { board.move_square(E8, C8); board.move_square(A8, D8); } - _ => { panic!("Invalid castle.") } - } - return - } - } - if !board.is_empty(self.dest) { - self.capture = Some(board.get_piece_on(self.dest)); - } - board.move_square(self.source, self.dest); - if let Some(piece) = self.promotion { - board.set_piece(self.dest, PAWN, piece); - } + // Finally, switch to the opposing player in the game state. + game_state.color = opposite(game_state.color); } /// Unmake a move. pub fn unmake(&self, board: &mut Board, game_state: &mut GameState) { - if let Some(castle) = self.get_castle() { - match castle { - CASTLE_WH_K => { board.move_square(G1, E1); board.move_square(F1, H1); } - CASTLE_WH_Q => { board.move_square(C1, E1); board.move_square(D1, A1); } - CASTLE_BL_K => { board.move_square(G8, E8); board.move_square(F8, H8); } - CASTLE_BL_Q => { board.move_square(C8, E8); board.move_square(D8, A8); } - _ => { panic!("Invalid castle.") } - } - } else { - board.move_square(self.dest, self.source); - if let Some(piece) = self.promotion { - board.set_piece(self.source, piece, PAWN); - } - if let Some(piece) = self.capture { - board.set_square(self.dest, game_state.color, piece); + // Always restore previous castle options. + game_state.castling = self.old_castles; + // If the move is a castle, unmake it properly. + let piece = board.get_piece_on(self.dest); + if piece == KING { + if let Some(castle) = self.get_castle() { + match castle { + CASTLE_WH_K => { board.move_square(G1, E1); board.move_square(F1, H1); } + CASTLE_WH_Q => { board.move_square(C1, E1); board.move_square(D1, A1); } + CASTLE_BL_K => { board.move_square(G8, E8); board.move_square(F8, H8); } + CASTLE_BL_Q => { board.move_square(C8, E8); board.move_square(D8, A8); } + _ => { panic!("Invalid castle.") } + } + game_state.color = opposite(game_state.color); + return } } - game_state.castling = self.old_castles; + + // Move the piece back. + board.move_square(self.dest, self.source); + + // Cancel the promotion. + if let Some(piece) = self.promotion { + board.set_piece(self.source, piece, PAWN); + } + + // Restore captured piece. + if let Some(piece) = self.capture { + board.set_square(self.dest, game_state.color, piece); + } + + // And switch back to previous player. game_state.color = opposite(game_state.color); } @@ -178,13 +217,14 @@ mod tests { #[test] fn test_apply_to_board() { let mut b = Board::new_empty(); + let mut gs = GameState::new(); // Put 2 enemy knights on board. b.set_square(D4, WHITE, KNIGHT); b.set_square(F4, BLACK, KNIGHT); // Move white knight in a position attacked by black knight. let mut m = Move::new(D4, E6); - m.apply_to_board(&mut b); + m.apply_to(&mut b, &mut gs); assert!(b.is_empty(D4)); assert_eq!(b.get_color_on(E6), WHITE); assert_eq!(b.get_piece_on(E6), KNIGHT); @@ -192,7 +232,7 @@ mod tests { assert!(m.capture.is_none()); // Sack it with black knight let mut m = Move::new(F4, E6); - m.apply_to_board(&mut b); + m.apply_to(&mut b, &mut gs); assert_eq!(b.get_color_on(E6), BLACK); assert_eq!(b.get_piece_on(E6), KNIGHT); assert_eq!(count_bits(b.combined()), 1); @@ -242,6 +282,7 @@ mod tests { let mut b = Board::new_empty(); let mut gs = GameState::new(); + // On unmaking a move, the white pawn is back to its original square. b.set_square(D4, WHITE, PAWN); let mut m = Move::new(D4, D5); m.apply_to(&mut b, &mut gs); @@ -249,6 +290,21 @@ mod tests { assert!(b.is_empty(D5)); assert_eq!(b.get_color_on(D4), WHITE); assert_eq!(b.get_piece_on(D4), PAWN); + + // Castle options should be properly unmade. + b.set_square(E1, WHITE, KING); + b.set_square(H1, WHITE, ROOK); + let mut m = Move::new(E1, G1); + m.apply_to(&mut b, &mut gs); + assert!(!b.is_empty(G1)); + assert!(!b.is_empty(F1)); + assert_eq!(gs.castling, CASTLE_MASK ^ CASTLE_WH_MASK); + m.unmake(&mut b, &mut gs); + assert!(b.is_empty(G1)); + assert!(b.is_empty(F1)); + assert_eq!(b.get_piece_on(E1), KING); + assert_eq!(b.get_piece_on(H1), ROOK); + assert_eq!(gs.castling, CASTLE_MASK); } #[test] diff --git a/src/rules.rs b/src/rules.rs index bdd7f2e..32dcac3 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -416,10 +416,9 @@ mod tests { #[test] fn test_get_king_moves() { - let mut gs = GameState::new(); - // King can move 1 square in any direction. let mut b = Board::new_empty(); + let mut gs = GameState::new(); b.set_square(D4, WHITE, KING); assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 8); b.set_square(E5, WHITE, PAWN); @@ -427,6 +426,7 @@ mod tests { // If castling is available, other moves are possible: 5 moves + 2 castles. let mut b = Board::new_empty(); + let mut gs = GameState::new(); b.set_square(E1, WHITE, KING); b.set_square(A1, WHITE, ROOK); b.set_square(H1, WHITE, ROOK); @@ -440,6 +440,89 @@ mod tests { assert_eq!(get_piece_moves(&mut b, &mut gs, E8, BLACK).len(), 5 + 2); } + #[test] + fn test_get_king_castles() { + // Rule 1 (king/rook on initial rank) is not checked here. + let mut b = Board::new_empty(); + let mut gs = GameState::new(); + b.set_square(E1, WHITE, KING); + b.set_square(A1, WHITE, ROOK); + b.set_square(H1, WHITE, ROOK); + b.set_square(E8, BLACK, KING); + b.set_square(A8, BLACK, ROOK); + b.set_square(H8, BLACK, ROOK); + + // 2. Neither the king nor the chosen rook has previously moved. + gs.color = WHITE; + let moves = get_piece_moves(&mut b, &mut gs, E1, WHITE); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_WH_K))); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_WH_Q))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_BL_K))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_BL_Q))); + gs.color = BLACK; + let moves = get_piece_moves(&mut b, &mut gs, E8, BLACK); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_K))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_Q))); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_BL_K))); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_BL_Q))); + // Either the king moved and lost all castles... + gs.color = WHITE; + gs.castling = CASTLE_MASK ^ CASTLE_WH_MASK; + let moves = get_piece_moves(&mut b, &mut gs, E1, WHITE); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_K))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_Q))); + // Or a rook moves and the player loses only one castle. + gs.castling = CASTLE_MASK ^ CASTLE_WH_Q; + let moves = get_piece_moves(&mut b, &mut gs, E1, WHITE); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_WH_K))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_Q))); + + // 3. There are no pieces between the king and the chosen rook. + gs.castling = CASTLE_MASK; + b.set_square(C1, BLACK, KNIGHT); // Black knight blocking white queen-side castle. + let moves = get_piece_moves(&mut b, &mut gs, E1, WHITE); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_WH_K))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_Q))); + gs.color = BLACK; + b.set_square(G8, WHITE, KNIGHT); // White knight blocking black king-side castle. + let moves = get_piece_moves(&mut b, &mut gs, E8, BLACK); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_BL_K))); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_BL_Q))); + + b.clear_square(C1, BLACK, KNIGHT); + b.clear_square(G8, WHITE, KNIGHT); + + // 4. The king is not currently in check. + gs.color = WHITE; + b.set_square(F2, BLACK, BISHOP); // Check white king. + let moves = get_piece_moves(&mut b, &mut gs, E1, WHITE); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_K))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_Q))); + gs.color = BLACK; + b.set_square(H5, WHITE, BISHOP); // Check black king. + let moves = get_piece_moves(&mut b, &mut gs, E8, BLACK); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_BL_K))); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_BL_Q))); + + b.clear_square(F2, BLACK, BISHOP); + b.clear_square(H5, WHITE, BISHOP); + + // 5. The king does not pass through a square that is attacked by an enemy piece. + gs.color = WHITE; + b.set_square(F8, BLACK, QUEEN); // Black queen protects f-file against king-side castle. + let moves = get_piece_moves(&mut b, &mut gs, E1, WHITE); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_K))); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_WH_Q))); + + b.clear_square(F8, BLACK, QUEEN); + + // 6. The king does not end up in check. + b.set_square(G8, BLACK, QUEEN); + let moves = get_piece_moves(&mut b, &mut gs, E1, WHITE); + assert!(moves.iter().all(|m| m.get_castle() != Some(CASTLE_WH_K))); + assert!(moves.iter().any(|m| m.get_castle() == Some(CASTLE_WH_Q))); + } + #[test] fn test_is_illegal() { let mut b = Board::new_empty(); diff --git a/src/uci.rs b/src/uci.rs index f7f2cff..0dee8a8 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -298,6 +298,9 @@ impl Uci { // UCI command parsers /// Parse an UCI command. +/// +/// Handle main UCI commands: position, go, etc. The command "p" is an +/// alias to "position". fn parse_command(s: &str) -> UciCmd { if s.len() == 0 { return UciCmd::Unknown("Empty command.".to_string()); @@ -309,7 +312,7 @@ fn parse_command(s: &str) -> UciCmd { "isready" => UciCmd::IsReady, "ucinewgame" => UciCmd::UciNewGame, "stop" => UciCmd::Stop, - "position" => parse_position_command(&fields[1..]), + "position" | "p" => parse_position_command(&fields[1..]), "go" => parse_go_command(&fields[1..]), "quit" => UciCmd::Quit, "vatunode" => UciCmd::VatuNode,