rules: rework illegal moves detection

This commit is contained in:
dece 2020-06-24 22:19:09 +02:00
parent 54cfd43911
commit 47cc483d9e
6 changed files with 150 additions and 117 deletions

View file

@ -119,7 +119,7 @@ impl Analyzer {
} else {
// If no best move could be found, checkmate is unavoidable; send the first legal move.
self.log("Checkmate is unavoidable.".to_string());
let moves = rules::get_player_moves(&self.node.board, &self.node.game_state);
let moves = rules::get_player_moves(&mut self.node.board, &mut self.node.game_state);
let m = if moves.len() > 0 { Some(moves[0].clone()) } else { None };
self.report_best_move(m);
}

View file

@ -163,6 +163,7 @@ impl Engine {
_ => {}
};
// Castling.
self.node.game_state.castling = 0;
for c in fen.castling.chars() {
match c {
'K' => self.node.game_state.castling |= castling::CASTLE_WH_K,

View file

@ -95,10 +95,10 @@ impl Move {
} else {
board.move_square(self.dest, self.source);
if let Some(piece) = self.promotion {
board.set_piece(self.source, piece, PAWN)
board.set_piece(self.source, piece, PAWN);
}
if let Some(piece) = self.capture {
board.set_square(self.dest, opposite(game_state.color), piece);
board.set_square(self.dest, game_state.color, piece);
}
}
game_state.castling = self.old_castles;

View file

@ -33,13 +33,13 @@ impl Node {
}
/// Return player moves from this node.
pub fn get_player_moves(&self) -> Vec<Move> {
rules::get_player_moves(&self.board, &self.game_state)
pub fn get_player_moves(&mut self) -> Vec<Move> {
rules::get_player_moves(&mut self.board, &mut self.game_state)
}
/// Compute stats for both players for this node.
pub fn compute_stats(&self) -> (stats::BoardStats, stats::BoardStats) {
stats::BoardStats::new_from(&self.board, &self.game_state)
pub fn compute_stats(&mut self) -> (stats::BoardStats, stats::BoardStats) {
stats::BoardStats::new_from(&mut self.board, &mut self.game_state)
}
}

View file

@ -57,10 +57,9 @@ impl std::fmt::Display for GameState {
/// `pseudo_legal` as a collection of attacked squares instead of legal
/// move collection.
pub fn get_player_moves(
board: &Board,
game_state: &GameState,
board: &mut Board,
game_state: &mut GameState,
) -> Vec<Move> {
let attacked_bb = board.get_full_rays(opposite(game_state.color)); // FIXME remove, doesn't w
let mut moves = Vec::with_capacity(32);
for r in 0..8 {
for f in 0..8 {
@ -70,7 +69,7 @@ pub fn get_player_moves(
}
if board.get_color_on(square) == game_state.color {
moves.append(
&mut get_piece_moves(board, game_state, square, game_state.color, attacked_bb)
&mut get_piece_moves(board, game_state, square, game_state.color)
);
}
}
@ -85,11 +84,10 @@ pub fn get_player_moves(
/// board but that would require an additional lookup and this function
/// is always called in a context where the piece color is known.
fn get_piece_moves(
board: &Board,
game_state: &GameState,
board: &mut Board,
game_state: &mut GameState,
square: Square,
color: Color,
attacked_bb: Bitboard,
) -> Vec<Move> {
let piece = board.get_piece_on(square);
let mut moves = Vec::with_capacity(32);
@ -111,11 +109,10 @@ fn get_piece_moves(
square,
color,
piece,
attacked_bb,
&mut moves
);
if piece == KING && sq_rank(square) == CASTLE_RANK_BY_COLOR[color] {
get_king_castles(board, game_state, square, color, attacked_bb, &mut moves);
get_king_castles(board, game_state, square, color, &mut moves);
}
moves
}
@ -126,20 +123,19 @@ fn get_piece_moves(
/// legal move. Does not take castle into account. Pawns that reach
/// the last rank are promoted as queens.
fn get_moves_from_bb(
board: &Board,
game_state: &GameState,
board: &mut Board,
game_state: &mut GameState,
bitboard: Bitboard,
square: Square,
color: Color,
piece: Piece,
attacked_bb: Bitboard,
moves: &mut Vec<Move>
) {
for ray_square in 0..NUM_SQUARES {
if ray_square == square || bitboard & bit_pos(ray_square) == 0 {
continue
}
if let Some(mut m) = inspect_move(board, game_state, square, ray_square, attacked_bb) {
if let Some(mut m) = inspect_move(board, game_state, square, ray_square) {
// Automatic queen promotion for pawns moving to the opposite rank.
if
piece == PAWN
@ -161,16 +157,21 @@ fn get_moves_from_bb(
/// `ray_square` is either empty or an enemy piece, but not a friend
/// piece: they should have been filtered.
///
/// This function, in case a move is accepted, sets the `capture` field
/// if the target square hold a piece.
///
/// This function does not set promotions for pawns reaching last rank.
fn inspect_move(
board: &Board,
game_state: &GameState,
board: &mut Board,
game_state: &mut GameState,
square: Square,
ray_square: Square,
attacked_bb: Bitboard
) -> Option<Move> {
let m = Move::new(square, ray_square);
if !is_illegal(board, game_state, &m, attacked_bb) {
let mut m = Move::new(square, ray_square);
if !is_illegal(board, game_state, &mut m) {
if !board.is_empty(ray_square) {
m.capture = Some(board.get_piece_on(ray_square))
}
Some(m)
} else {
None
@ -179,20 +180,21 @@ fn inspect_move(
/// Check if a move is illegal.
fn is_illegal(
board: &Board,
game_state: &GameState,
m: &Move,
attacked_bb: Bitboard
board: &mut Board,
game_state: &mut GameState,
m: &mut Move,
) -> bool {
let color = game_state.color;
// A move is illegal if the king ends up in check.
let king_square = if board.get_piece_on(m.source) == KING {
m.dest
m.apply_to(board, game_state);
if let Some(king) = board.find_king(color) {
let attacked_bb = board.get_full_rays(opposite(color));
m.unmake(board, game_state);
attacked_bb & bit_pos(king) != 0
} else {
if let Some(king) = board.find_king(game_state.color) { king } else { return false }
};
// FIXME implement unmake move
let attacked_bb = board.get_full_rays(opposite(game_state.color));
attacked_bb & bit_pos(king_square) != 0
m.unmake(board, game_state);
false
}
}
/// Get possible castles.
@ -212,7 +214,6 @@ fn get_king_castles(
game_state: &GameState,
square: Square,
color: Color,
attacked_bb: Bitboard,
moves: &mut Vec<Move>
) {
let combined_bb = board.combined();
@ -230,6 +231,7 @@ fn get_king_castles(
if (game_state.castling & castle_color_mask & castle_side_mask) != 0 {
// Check that squares in the king's path are not attacked (R4, R5, R6).
let castle_legality_path = CASTLE_LEGALITY_PATHS[color][castle_side_id];
let attacked_bb = board.get_full_rays(opposite(game_state.color));
if attacked_bb & castle_legality_path != 0 {
continue
}
@ -250,156 +252,166 @@ fn get_king_castles(
mod tests {
use super::*;
/// Like `get_piece_moves` but generate attacked bitboard.
fn get_legal_piece_moves(
board: &Board,
game_state: &GameState,
square: Square,
color: Color
) -> Vec<Move> {
let attacked_bb = board.get_full_rays(opposite(game_state.color));
get_piece_moves(board, game_state, square, color, attacked_bb)
}
#[test]
fn test_get_player_moves() {
let b = Board::new();
let gs = GameState::new();
let mut b = Board::new();
let mut gs = GameState::new();
// At first move, white has 16 pawn moves and 4 knight moves.
let moves = get_player_moves(&b, &gs);
let moves = get_player_moves(&mut b, &mut gs);
assert_eq!(moves.len(), 20);
}
#[test]
fn test_get_pawn_moves() {
fn test_get_pawn_progress_moves() {
let mut b = Board::new_empty();
let gs = GameState::new();
let mut gs = GameState::new();
// Check that a pawn (here white queen's pawn) can move forward if the road is free.
b.set_square(D3, WHITE, PAWN);
let moves = get_legal_piece_moves(&b, &gs, D3, WHITE);
let moves = get_piece_moves(&mut b, &mut gs, D3, WHITE);
assert_eq!(moves.len(), 1);
assert!(moves.contains(&Move::new(D3, D4)));
assert!(moves.iter().any(|m| m.source == D3 && m.dest == D4));
// Check that a pawn (here white king's pawn) can move 2 square forward on first move.
b.set_square(E2, WHITE, PAWN);
let moves = get_legal_piece_moves(&b, &gs, E2, WHITE);
let moves = get_piece_moves(&mut b, &mut gs, E2, WHITE);
assert_eq!(moves.len(), 2);
assert!(moves.contains(&Move::new(E2, E3)));
assert!(moves.contains(&Move::new(E2, E4)));
assert!(moves.iter().any(|m| m.source == E2 && m.dest == E3));
assert!(moves.iter().any(|m| m.source == E2 && m.dest == E4));
// Check that a pawn cannot move forward if a piece is blocking its path.
// 1. black pawn 2 square forward; only 1 square forward available from start pos.
b.set_square(E4, BLACK, PAWN);
let moves = get_legal_piece_moves(&b, &gs, E2, WHITE);
let moves = get_piece_moves(&mut b, &mut gs, E2, WHITE);
assert_eq!(moves.len(), 1);
assert!(moves.contains(&Move::new(E2, E3)));
assert!(moves.iter().any(|m| m.source == E2 && m.dest == E3));
// 2. black pawn 1 square forward; no square available.
b.set_square(E3, BLACK, PAWN);
let moves = get_legal_piece_moves(&b, &gs, E2, WHITE);
let moves = get_piece_moves(&mut b, &mut gs, E2, WHITE);
assert_eq!(moves.len(), 0);
// 3. remove the e4 black pawn; the white pawn should not be able to jump above e3 pawn.
b.clear_square(E4, BLACK, PAWN);
let moves = get_legal_piece_moves(&b, &gs, E2, WHITE);
let moves = get_piece_moves(&mut b, &mut gs, E2, WHITE);
assert_eq!(moves.len(), 0);
}
#[test]
fn test_get_pawn_capture_moves() {
let mut b = Board::new_empty();
let mut gs = GameState::new();
// Check that a pawn can take a piece diagonally.
b.set_square(F3, BLACK, PAWN);
let moves = get_legal_piece_moves(&b, &gs, E2, WHITE);
assert_eq!(moves.len(), 1);
b.set_square(D3, BLACK, PAWN);
let moves = get_legal_piece_moves(&b, &gs, E2, WHITE);
b.set_square(E2, WHITE, PAWN);
let moves = get_piece_moves(&mut b, &mut gs, E2, WHITE);
assert_eq!(moves.len(), 2);
b.set_square(F3, BLACK, PAWN);
let moves = get_piece_moves(&mut b, &mut gs, E2, WHITE);
assert_eq!(moves.len(), 3);
assert!(moves.iter().any(|m| m.source == E2 && m.dest == F3));
b.set_square(D3, BLACK, PAWN);
let moves = get_piece_moves(&mut b, &mut gs, E2, WHITE);
assert_eq!(moves.len(), 4);
assert!(moves.iter().any(|m| m.source == E2 && m.dest == F3));
assert!(moves.iter().any(|m| m.source == E2 && m.dest == D3));
}
#[test]
fn test_get_pawn_promotion_moves() {
let mut b = Board::new_empty();
let mut gs = GameState::new();
// Check that a pawn moving to the last rank leads to queen promotion.
// 1. by simply moving forward.
b.set_square(A7, WHITE, PAWN);
let moves = get_legal_piece_moves(&b, &gs, A7, WHITE);
let moves = get_piece_moves(&mut b, &mut gs, A7, WHITE);
assert_eq!(moves.len(), 1);
assert!(moves.contains(&Move::new_promotion(A7, A8, QUEEN)));
let m = &moves[0];
assert_eq!(m.source, A7);
assert_eq!(m.dest, A8);
assert_eq!(m.promotion, Some(QUEEN));
}
#[test]
fn test_get_bishop_moves() {
let mut b = Board::new_empty();
let gs = GameState::new();
let mut gs = GameState::new();
// A bishop has maximum range when it's in a center square.
b.set_square(D4, WHITE, BISHOP);
let moves = get_legal_piece_moves(&b, &gs, D4, WHITE);
let moves = get_piece_moves(&mut b, &mut gs, D4, WHITE);
assert_eq!(moves.len(), 13);
// Going top-right.
assert!(moves.contains(&Move::new(D4, E5)));
assert!(moves.contains(&Move::new(D4, F6)));
assert!(moves.contains(&Move::new(D4, G7)));
assert!(moves.contains(&Move::new(D4, H8)));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == E5));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == F6));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == G7));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == H8));
// Going bottom-right.
assert!(moves.contains(&Move::new(D4, E3)));
assert!(moves.contains(&Move::new(D4, F2)));
assert!(moves.contains(&Move::new(D4, G1)));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == E3));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == F2));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == G1));
// Going bottom-left.
assert!(moves.contains(&Move::new(D4, C3)));
assert!(moves.contains(&Move::new(D4, B2)));
assert!(moves.contains(&Move::new(D4, A1)));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == C3));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == B2));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == A1));
// Going top-left.
assert!(moves.contains(&Move::new(D4, C5)));
assert!(moves.contains(&Move::new(D4, B6)));
assert!(moves.contains(&Move::new(D4, A7)));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == C5));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == B6));
assert!(moves.iter().any(|m| m.source == D4 && m.dest == A7));
// When blocking commit to one square with friendly piece, lose 2 moves.
b.set_square(B2, WHITE, PAWN);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 11);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 11);
// When blocking commit to one square with enemy piece, lose only 1 move.
b.set_square(B2, BLACK, PAWN);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 12);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 12);
}
#[test]
fn test_get_knight_moves() {
let mut b = Board::new_empty();
let gs = GameState::new();
let mut gs = GameState::new();
// A knight never has blocked commit; if it's in the center of the board, it can have up to
// 8 moves.
b.set_square(D4, WHITE, KNIGHT);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 8);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 8);
// If on a side if has only 4 moves.
b.set_square(A4, WHITE, KNIGHT);
assert_eq!(get_legal_piece_moves(&b, &gs, A4, WHITE).len(), 4);
assert_eq!(get_piece_moves(&mut b, &mut gs, A4, WHITE).len(), 4);
// And in a corner, only 2 moves.
b.set_square(A1, WHITE, KNIGHT);
assert_eq!(get_legal_piece_moves(&b, &gs, A1, WHITE).len(), 2);
assert_eq!(get_piece_moves(&mut b, &mut gs, A1, WHITE).len(), 2);
// Add 2 friendly pieces and it is totally blocked.
b.set_square(B3, WHITE, PAWN);
b.set_square(C2, WHITE, PAWN);
assert_eq!(get_legal_piece_moves(&b, &gs, A1, WHITE).len(), 0);
assert_eq!(get_piece_moves(&mut b, &mut gs, A1, WHITE).len(), 0);
}
#[test]
fn test_get_rook_moves() {
let mut b = Board::new_empty();
let gs = GameState::new();
let mut gs = GameState::new();
b.set_square(D4, WHITE, ROOK);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 14);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 14);
b.set_square(D6, BLACK, PAWN);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 12);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 12);
b.set_square(D6, WHITE, PAWN);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 11);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 11);
}
#[test]
fn test_get_queen_moves() {
let mut b = Board::new_empty();
let gs = GameState::new();
let mut gs = GameState::new();
b.set_square(D4, WHITE, QUEEN);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 14 + 13);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 14 + 13);
}
#[test]
@ -409,27 +421,47 @@ mod tests {
// King can move 1 square in any direction.
let mut b = Board::new_empty();
b.set_square(D4, WHITE, KING);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 8);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 8);
b.set_square(E5, WHITE, PAWN);
assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 7);
assert_eq!(get_piece_moves(&mut b, &mut gs, D4, WHITE).len(), 7);
// If castling is available, other moves are possible: 5 moves + 2 castles.
let mut b = Board::new_empty();
b.set_square(E1, WHITE, KING);
b.set_square(A1, WHITE, ROOK);
b.set_square(H1, WHITE, ROOK);
assert_eq!(get_legal_piece_moves(&b, &gs, E1, WHITE).len(), 5 + 2);
assert_eq!(get_piece_moves(&mut b, &mut gs, E1, WHITE).len(), 5 + 2);
// Castling works as well for black.
gs.color = BLACK;
b.set_square(E8, BLACK, KING);
b.set_square(A8, BLACK, ROOK);
b.set_square(H8, BLACK, ROOK);
assert_eq!(get_legal_piece_moves(&b, &gs, E8, BLACK).len(), 5 + 2);
assert_eq!(get_piece_moves(&mut b, &mut gs, E8, BLACK).len(), 5 + 2);
}
#[test]
fn test_filter_illegal_moves() {
fn test_is_illegal() {
let mut b = Board::new_empty();
let mut gs = GameState::new();
gs.castling = 0;
// Place white's king on first rank.
b.set_square(E1, WHITE, KING);
// Place black rook in second rank: king can only move left or right.
b.set_square(H2, BLACK, ROOK);
// Check that the king can't go to a rook controlled square.
assert!(is_illegal(&mut b, &mut gs, &mut Move::new(E1, E2)));
assert!(is_illegal(&mut b, &mut gs, &mut Move::new(E1, D2)));
assert!(is_illegal(&mut b, &mut gs, &mut Move::new(E1, F2)));
assert!(!is_illegal(&mut b, &mut gs, &mut Move::new(E1, D1)));
assert!(!is_illegal(&mut b, &mut gs, &mut Move::new(E1, F1)));
let all_wh_moves = get_piece_moves(&mut b, &mut gs, E1, WHITE);
assert_eq!(all_wh_moves.len(), 2);
}
#[test]
fn test_get_king_moves_legality() {
let mut b = Board::new_empty();
let mut gs = GameState::new();
@ -440,7 +472,7 @@ mod tests {
// No castling available.
gs.castling = 0;
// 5 moves in absolute but only 2 are legal.
let all_wh_moves = get_legal_piece_moves(&b, &gs, E1, WHITE);
let all_wh_moves = get_piece_moves(&mut b, &mut gs, E1, WHITE);
assert_eq!(all_wh_moves.len(), 2);
}
}

View file

@ -31,12 +31,12 @@ impl BoardStats {
///
/// The playing color will have its stats filled in the first
/// BoardStats object, its opponent in the second.
pub fn new_from(board: &Board, game_state: &GameState) -> (BoardStats, BoardStats) {
pub fn new_from(board: &mut Board, game_state: &mut GameState) -> (BoardStats, BoardStats) {
let mut stats = (BoardStats::new(), BoardStats::new());
let mut gs = game_state.clone();
stats.0.compute(board, &gs);
stats.0.compute(board, &mut gs);
gs.color = opposite(gs.color);
stats.1.compute(board, &gs);
stats.1.compute(board, &mut gs);
stats
}
@ -58,7 +58,7 @@ impl BoardStats {
///
/// Only the current playing side stats are created,
/// prepare the game_state accordingly.
pub fn compute(&mut self, board: &Board, game_state: &GameState) {
pub fn compute(&mut self, board: &mut Board, game_state: &mut GameState) {
self.reset();
let color = game_state.color;
// Compute mobility for all pieces.
@ -157,8 +157,8 @@ mod tests {
#[test]
fn test_compute_stats() {
// Check that initial stats are correct.
let b = Board::new();
let gs = GameState::new();
let mut b = Board::new();
let mut gs = GameState::new();
let initial_stats = BoardStats {
num_pawns: 8,
num_bishops: 2,
@ -171,7 +171,7 @@ mod tests {
num_isolated_pawns: 0,
mobility: 20,
};
let mut stats = BoardStats::new_from(&b, &gs);
let mut stats = BoardStats::new_from(&mut b, &mut gs);
assert!(stats.0 == stats.1);
assert!(stats.0 == initial_stats);
@ -179,15 +179,15 @@ mod tests {
let mut b = Board::new_empty();
b.set_square(D4, WHITE, PAWN);
b.set_square(D6, WHITE, PAWN);
stats.0.compute(&b, &gs);
stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 2);
// Add a pawn on another file, no changes expected.
b.set_square(E6, WHITE, PAWN);
stats.0.compute(&b, &gs);
stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 2);
// Add a pawn backward in the d-file: there are now 3 doubled pawns.
b.set_square(D2, WHITE, PAWN);
stats.0.compute(&b, &gs);
stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 3);
// Check that isolated and backward pawns are correctly counted.
@ -195,19 +195,19 @@ mod tests {
assert_eq!(stats.0.num_backward_pawns, 2); // A bit weird?
// Protect d4 pawn with a friend in e3: it is not isolated nor backward anymore.
b.set_square(E3, WHITE, PAWN);
stats.0.compute(&b, &gs);
stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 1);
// Add an adjacent friend to d2 pawn: no pawns are left isolated or backward.
b.set_square(C2, WHITE, PAWN);
stats.0.compute(&b, &gs);
stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 0);
// Add an isolated/backward white pawn in a far file.
b.set_square(A2, WHITE, PAWN);
stats.0.compute(&b, &gs);
stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 1);
assert_eq!(stats.0.num_backward_pawns, 1);
@ -218,7 +218,7 @@ mod tests {
b.set_square(D4, WHITE, PAWN);
b.set_square(E5, WHITE, PAWN);
b.set_square(E3, WHITE, PAWN);
stats.0.compute(&b, &gs);
stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 2);
assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 1);