movement: thoroughly test unmake with castling

This commit is contained in:
dece 2020-06-25 00:15:04 +02:00
parent 47cc483d9e
commit 53a96f8787
3 changed files with 194 additions and 52 deletions

View file

@ -42,48 +42,78 @@ 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.
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; }
// Same for rooks.
}
}
// 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) {
// 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); }
@ -92,16 +122,25 @@ impl Move {
CASTLE_BL_Q => { board.move_square(C8, E8); board.move_square(D8, A8); }
_ => { panic!("Invalid castle.") }
}
} else {
game_state.color = opposite(game_state.color);
return
}
}
// 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);
}
}
game_state.castling = self.old_castles;
// 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]

View file

@ -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();

View file

@ -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,