From 3d5bbb8d2cca9f5f5c95aad63c1522b7184d62be Mon Sep 17 00:00:00 2001 From: dece Date: Tue, 23 Jun 2020 00:15:25 +0200 Subject: [PATCH] rules: rework illegal moves detection (WIP) --- src/analysis.rs | 6 ++-- src/board.rs | 86 +++++++++++++++++++++++++++++++++++++------------ src/engine.rs | 10 +++--- src/movement.rs | 48 +++++++++++++-------------- src/node.rs | 6 ++-- src/rules.rs | 6 ++-- src/stats.rs | 8 ++--- 7 files changed, 108 insertions(+), 62 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index e65b7f5..0c512b4 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -103,7 +103,7 @@ impl Analyzer { if self.debug { self.log(format!("Analyzing node:\n{}", &self.node)); - let moves = self.node.get_player_legal_moves(); + let moves = self.node.get_player_moves(); self.log(format!("Legal moves: {}", Move::list_to_uci_string(&moves))); self.log(format!("Move time: {}", self.time_limit)); } @@ -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, false); + let moves = rules::get_player_moves(&self.node.board, &self.node.game_state); let m = if moves.len() > 0 { Some(moves[0].clone()) } else { None }; self.report_best_move(m); } @@ -185,7 +185,7 @@ impl Analyzer { } // Get negamax for playable moves. - let moves = node.get_player_legal_moves(); + let moves = node.get_player_moves(); let mut alpha = alpha; let mut best_score = MIN_F32; let mut best_move = None; diff --git a/src/board.rs b/src/board.rs index d4da242..e95cdae 100644 --- a/src/board.rs +++ b/src/board.rs @@ -163,6 +163,21 @@ pub struct Board { pub pieces: [Bitboard; 6], } +/// A direction to move (file and rank). +pub type Direction = (i8, i8); +/// Direction in which bishops moves. +pub const BISHOP_DIRS: [Direction; 4] = [ + (1, 1), (1, -1), (-1, -1), (-1, 1) +]; +/// Direction in which rooks moves. +pub const ROOK_DIRS: [Direction; 4] = [ + (1, 0), (0, 1), (-1, 0), (0, -1) +]; +/// Direction in which queens moves. +pub const QUEEN_DIRS: [Direction; 8] = [ + (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1) +]; + // Factories. impl Board { /// Generate the board of a new game. @@ -311,14 +326,15 @@ impl Board { None } - /// Get capture rays for all pieces of `color`. + /// Get all rays for all pieces of `color`. /// /// This function is used to find illegal moves for opposite color. /// /// This add move rays of all piece types, pawns being a special /// case: their diagonal capture are all added even though no enemy - /// piece is on the target square. - pub fn get_rays(&self, color: Color) -> Bitboard { + /// piece is on the target square. Rays include simple moves, + /// captures and friendly pieces being protected. + pub fn get_full_rays(&self, color: Color) -> Bitboard { let mut ray_bb = 0; let color_bb = self.by_color(color); for square in 0..NUM_SQUARES { @@ -327,11 +343,11 @@ impl Board { } ray_bb |= match self.get_piece_on(square) { PAWN => self.get_pawn_protections(square, color), - BISHOP => self.get_bishop_rays(square, color), - KNIGHT => self.get_knight_rays(square, color), - ROOK => self.get_rook_rays(square, color), - QUEEN => self.get_queen_rays(square, color), - KING => self.get_king_rays(square, color), + BISHOP => self.get_bishop_full_rays(square, color), + KNIGHT => self.get_knight_full_rays(square), + ROOK => self.get_rook_full_rays(square, color), + QUEEN => self.get_queen_full_rays(square, color), + KING => self.get_king_full_rays(square), _ => { panic!("No piece on square {} but color {} bit is set.", square, color) } }; } @@ -373,19 +389,32 @@ impl Board { /// Get bishop rays: moves and captures bitboard. pub fn get_bishop_rays(&self, square: Square, color: Color) -> Bitboard { - self.get_blockable_rays(square, color, &[(1, 1), (1, -1), (-1, -1), (-1, 1)]) + self.get_blockable_rays(square, color, &BISHOP_DIRS, false) + } + + /// Get all bishop rays: moves, captures and protections bitboard. + pub fn get_bishop_full_rays(&self, square: Square, color: Color) -> Bitboard { + self.get_blockable_rays(square, color, &BISHOP_DIRS, true) } /// Get rook rays: moves and captures bitboard. pub fn get_rook_rays(&self, square: Square, color: Color) -> Bitboard { - self.get_blockable_rays(square, color, &[(1, 0), (0, 1), (-1, 0), (0, -1)]) + self.get_blockable_rays(square, color, &ROOK_DIRS, false) + } + + /// Get all rook rays: moves, captures and protections bitboard. + pub fn get_rook_full_rays(&self, square: Square, color: Color) -> Bitboard { + self.get_blockable_rays(square, color, &ROOK_DIRS, true) } /// Get queen rays: moves and captures bitboard. pub fn get_queen_rays(&self, square: Square, color: Color) -> Bitboard { - self.get_blockable_rays( - square, color, &[(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] - ) + self.get_blockable_rays(square, color, &QUEEN_DIRS, false) + } + + /// Get all queen rays: moves, captures and protections bitboard. + pub fn get_queen_full_rays(&self, square: Square, color: Color) -> Bitboard { + self.get_blockable_rays(square, color, &QUEEN_DIRS, true) } /// Get rays for piece that can move how far they want. @@ -393,14 +422,18 @@ impl Board { /// Used for bishops, rooks and queens. A ray bitboard is the /// combination of squares either empty or occupied by an enemy /// piece they can reach. - fn get_blockable_rays(&self, + /// + /// If `protection` is true, consider friend pieces in rays as well. + fn get_blockable_rays( + &self, square: Square, color: Color, - directions: &[(i8, i8)] + directions: &[(i8, i8)], + include_protections: bool ) -> Bitboard { let mut rays_bb: Bitboard = 0; let color_bb = self.by_color(color); - let enemy_bb = self.by_color(opposite(color)); + let combined_bb = self.combined(); for dir in directions { let mut ray_f = sq_file(square); let mut ray_r = sq_rank(square); @@ -411,11 +444,11 @@ impl Board { break } let bp = bit_pos(sq(ray_f, ray_r)); - if color_bb & bp != 0 { + if !include_protections && color_bb & bp != 0 { break } rays_bb |= bp; - if enemy_bb & bp != 0 { + if combined_bb & bp != 0 { break } } @@ -428,11 +461,21 @@ impl Board { KNIGHT_RAYS[square as usize] & !self.by_color(color) } + /// Get all knight rays: moves, captures and protections bitboard. + pub fn get_knight_full_rays(&self, square: Square) -> Bitboard { + KNIGHT_RAYS[square as usize] + } + /// Get king rays: moves and captures bitboard. pub fn get_king_rays(&self, square: Square, color: Color) -> Bitboard { KING_RAYS[square as usize] & !self.by_color(color) } + /// Get all king rays: moves, captures and protections bitboard. + pub fn get_king_full_rays(&self, square: Square) -> Bitboard { + KING_RAYS[square as usize] + } + /// Debug only: write a text view of the board to stderr. #[allow(dead_code)] // For tests only. pub(crate) fn draw(&self) { @@ -589,10 +632,11 @@ mod tests { } #[test] - fn test_get_rays() { + fn test_get_full_rays() { let b = Board::new(); - assert_eq!(count_bits(b.get_rays(WHITE)), 8); - assert_eq!(count_bits(b.get_rays(BLACK)), 8); + // Third ranks protected, all pieces protected except rooks = 22 squares. + assert_eq!(count_bits(b.get_full_rays(WHITE)), 22); + assert_eq!(count_bits(b.get_full_rays(BLACK)), 22); } #[test] diff --git a/src/engine.rs b/src/engine.rs index 3485d61..5362615 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -130,7 +130,7 @@ impl Engine { Cmd::DrawBoard => { let mut s = vec!(); self.node.board.draw_to(&mut s); - let s = format!("Current board:\n{}", String::from_utf8_lossy(&s)); + let s = format!("{}", String::from_utf8_lossy(&s)); self.reply(Cmd::Log(s)); } _ => eprintln!("Not an engine input command: {:?}", cmd), @@ -162,10 +162,10 @@ impl Engine { // Castling. for c in fen.castling.chars() { match c { - 'K' => self.node.game_state.castling |= castling::CASTLING_WH_K, - 'Q' => self.node.game_state.castling |= castling::CASTLING_WH_Q, - 'k' => self.node.game_state.castling |= castling::CASTLING_BL_K, - 'q' => self.node.game_state.castling |= castling::CASTLING_BL_Q, + 'K' => self.node.game_state.castling |= castling::CASTLE_WH_K, + 'Q' => self.node.game_state.castling |= castling::CASTLE_WH_Q, + 'k' => self.node.game_state.castling |= castling::CASTLE_BL_K, + 'q' => self.node.game_state.castling |= castling::CASTLE_BL_Q, _ => {} } } diff --git a/src/movement.rs b/src/movement.rs index d38a87c..86cccfa 100644 --- a/src/movement.rs +++ b/src/movement.rs @@ -37,13 +37,13 @@ impl Move { /// Apply this move to `board` and `game_state`. pub fn apply_to(&self, board: &mut Board, game_state: &mut GameState) { // If a king moves, remove it from castling options. - if self.source == E1 { game_state.castling &= !CASTLING_WH_MASK; } - else if self.source == E8 { game_state.castling &= !CASTLING_BL_MASK; } + if self.source == E1 { game_state.castling &= !CASTLE_WH_MASK; } + else if self.source == E8 { game_state.castling &= !CASTLE_BL_MASK; } // Same for rooks. - if self.source == A1 || self.dest == A1 { game_state.castling &= !CASTLING_WH_Q; } - else if self.source == H1 || self.dest == H1 { game_state.castling &= !CASTLING_WH_K; } - else if self.source == A8 || self.dest == A8 { game_state.castling &= !CASTLING_BL_Q; } - else if self.source == H8 || self.dest == H8 { game_state.castling &= !CASTLING_BL_K; } + 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); @@ -56,10 +56,10 @@ impl Move { if piece == KING { if let Some(castle) = self.get_castle() { match castle { - CASTLING_WH_K => { board.move_square(E1, G1); board.move_square(H1, F1); } - CASTLING_WH_Q => { board.move_square(E1, C1); board.move_square(A1, D1); } - CASTLING_BL_K => { board.move_square(E8, G8); board.move_square(H8, F8); } - CASTLING_BL_Q => { board.move_square(E8, C8); board.move_square(A8, D8); } + 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 @@ -75,10 +75,10 @@ impl Move { /// Get the corresponding castling flag for this move. pub fn get_castle(&self) -> Option { match (self.source, self.dest) { - (E1, C1) => Some(CASTLING_WH_Q), - (E1, G1) => Some(CASTLING_WH_K), - (E8, C8) => Some(CASTLING_BL_Q), - (E8, G8) => Some(CASTLING_BL_K), + (E1, C1) => Some(CASTLE_WH_Q), + (E1, G1) => Some(CASTLE_WH_K), + (E8, C8) => Some(CASTLE_BL_Q), + (E8, G8) => Some(CASTLE_BL_K), _ => None, } } @@ -86,10 +86,10 @@ impl Move { /// Get the move for this castle. pub fn get_castle_move(castle: u8) -> Move { match castle { - CASTLING_WH_Q => Move::new(E1, C1), - CASTLING_WH_K => Move::new(E1, G1), - CASTLING_BL_Q => Move::new(E8, C8), - CASTLING_BL_K => Move::new(E8, G8), + CASTLE_WH_Q => Move::new(E1, C1), + CASTLE_WH_K => Move::new(E1, G1), + CASTLE_BL_Q => Move::new(E8, C8), + CASTLE_BL_K => Move::new(E8, G8), _ => panic!("Illegal castling requested: {:08b}", castle), } } @@ -164,7 +164,7 @@ mod tests { fn test_apply_to_castling() { let mut b = Board::new(); let mut gs = GameState::new(); - assert_eq!(gs.castling, CASTLING_MASK); + assert_eq!(gs.castling, CASTLE_MASK); // On a starting board, start by making place for all castles. b.clear_square(B1, WHITE, KNIGHT); @@ -185,7 +185,7 @@ mod tests { assert_eq!(b.get_piece_on(D1), ROOK); assert!(b.is_empty(A1)); assert!(b.is_empty(E1)); - assert_eq!(gs.castling, CASTLING_BL_MASK); + assert_eq!(gs.castling, CASTLE_BL_MASK); // Black king-side castling. Move::new(E8, G8).apply_to(&mut b, &mut gs); assert_eq!(b.get_color_on(G8), BLACK); @@ -200,10 +200,10 @@ mod tests { #[test] fn test_get_castle() { - assert_eq!(Move::new(E1, C1).get_castle(), Some(CASTLING_WH_Q)); - assert_eq!(Move::new(E1, G1).get_castle(), Some(CASTLING_WH_K)); - assert_eq!(Move::new(E8, C8).get_castle(), Some(CASTLING_BL_Q)); - assert_eq!(Move::new(E8, G8).get_castle(), Some(CASTLING_BL_K)); + assert_eq!(Move::new(E1, C1).get_castle(), Some(CASTLE_WH_Q)); + assert_eq!(Move::new(E1, G1).get_castle(), Some(CASTLE_WH_K)); + assert_eq!(Move::new(E8, C8).get_castle(), Some(CASTLE_BL_Q)); + assert_eq!(Move::new(E8, G8).get_castle(), Some(CASTLE_BL_K)); assert_eq!(Move::new(D2, D4).get_castle(), None); } diff --git a/src/node.rs b/src/node.rs index c500979..9acd85e 100644 --- a/src/node.rs +++ b/src/node.rs @@ -29,8 +29,8 @@ impl Node { } /// Return player moves from this node. - pub fn get_player_legal_moves(&self) -> Vec { - rules::get_player_moves(&self.board, &self.game_state, false) + pub fn get_player_moves(&self) -> Vec { + rules::get_player_moves(&self.board, &self.game_state) } /// Compute stats for both players for this node. @@ -54,6 +54,6 @@ impl fmt::Display for Node { let mut s = vec!(); self.board.draw_to(&mut s); let board_drawing = String::from_utf8_lossy(&s).to_string(); - write!(f, "* Board:\n{}\nGame state: {}", board_drawing, self.game_state) + write!(f, "{}{}", board_drawing, self.game_state) } } diff --git a/src/rules.rs b/src/rules.rs index 795bbe7..1785067 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -60,7 +60,7 @@ pub fn get_player_moves( board: &Board, game_state: &GameState, ) -> Vec { - let attacked_bb = board.get_rays(opposite(game_state.color)); + 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 { @@ -190,6 +190,8 @@ fn is_illegal( } 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 } @@ -255,7 +257,7 @@ mod tests { square: Square, color: Color ) -> Vec { - let attacked_bb = board.get_rays(opposite(game_state.color)); + let attacked_bb = board.get_full_rays(opposite(game_state.color)); get_piece_moves(board, game_state, square, color, attacked_bb) } diff --git a/src/stats.rs b/src/stats.rs index 09a199e..2b46671 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -1,7 +1,7 @@ //! Board statistics used for heuristics. use crate::board::*; -use crate::rules::{GameState, get_player_moves, POS_MIN, POS_MAX}; +use crate::rules::{GameState, get_player_moves}; /// Storage for board pieces stats. #[derive(Debug, Clone, PartialEq)] @@ -62,7 +62,7 @@ impl BoardStats { self.reset(); let color = game_state.color; // Compute mobility for all pieces. - self.mobility = get_player_moves(board, game_state, false).len() as i32; + self.mobility = get_player_moves(board, game_state).len() as i32; // Compute amount of each piece. for file in 0..8 { for rank in 0..8 { @@ -87,12 +87,12 @@ impl BoardStats { } // Check for isolated and backward pawns. - let (iso_on_prev_file, bw_on_prev_file) = if file > POS_MIN { + let (iso_on_prev_file, bw_on_prev_file) = if file > FILE_A { self.find_isolated_and_backward(pawn_bb, square, color, file - 1) } else { (true, true) }; - let (iso_on_next_file, bw_on_next_file) = if file < POS_MAX { + let (iso_on_next_file, bw_on_next_file) = if file < FILE_H { self.find_isolated_and_backward(pawn_bb, square, color, file + 1) } else { (true, true)