diff --git a/README.md b/README.md index 1c6def8..d70b79e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ cp external/lichess-bot/config.yml.example /tmp/vatu-config/config.yml docker build -f res/docker/Dockerfile -t vatu . # Run with the config folder mounted at /config. docker run -v /tmp/vatu-config:/config -ti vatu +# In the container, use the following command: +python lichess-bot.py --config /config/config.yml ``` @@ -74,10 +76,11 @@ docker run -v /tmp/vatu-config:/config -ti vatu TODO ---- -- Support time constraints -- Proper unmake mechanism instead of allocating boards like there is no tomorrow -- Precompute some pieces moves, maybe -- Transposition table that does not actually slows search down -- Check Zobrist hashes for previous point -- Actual bitboard -- Multithreading (never) +- [X] Support time constraints +- [ ] Unmake mechanism instead of allocating nodes like there is no tomorrow +- [X] Precompute some pieces moves, maybe (done for knights) +- [ ] Transposition table that does not actually slows search down +- [ ] Check Zobrist hashes for previous point +- [X] Actual bitboard +- [ ] Some kind of move ordering could be great +- [ ] Multithreading (never) diff --git a/res/scripts/gen_knight_rays.py b/res/scripts/gen_knight_rays.py new file mode 100755 index 0000000..f30e6dd --- /dev/null +++ b/res/scripts/gen_knight_rays.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Pre-compute knight ray bitboards for each square.""" + +TEMPLATE = """\ +const KNIGHT_RAYS: [Bitboard; 64] = [ +{} +]; +""" + +DIRS = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)] + +def bit_pos(square): + return 1 << square + +def get_rays(): + rays = [] + for f in range(8): + for r in range(8): + bitboard = 0 + for dir_f, dir_r in DIRS: + ray_f = f + dir_f + ray_r = r + dir_r + if ray_f < 0 or ray_f > 7 or ray_r < 0 or ray_r > 7: + continue + bitboard |= bit_pos(ray_f * 8 + ray_r) + rays.append(" 0b{:064b},".format(bitboard)) + return rays + +print(TEMPLATE.format("\n".join(get_rays()))) diff --git a/src/analysis.rs b/src/analysis.rs index 1581d5f..e65b7f5 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_moves(true); + let moves = self.node.get_player_legal_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, true); + let moves = rules::get_player_moves(&self.node.board, &self.node.game_state, false); 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_moves(true); + let moves = node.get_player_legal_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 8968b9a..62263c0 100644 --- a/src/board.rs +++ b/src/board.rs @@ -137,6 +137,26 @@ pub fn sq_to_string(square: Square) -> String { /// Bitboard for color or piece bits. pub type Bitboard = u64; +pub const FILE_A: i8 = 0; +pub const FILE_B: i8 = 1; +pub const FILE_C: i8 = 2; +pub const FILE_D: i8 = 3; +pub const FILE_E: i8 = 4; +pub const FILE_F: i8 = 5; +pub const FILE_G: i8 = 6; +pub const FILE_H: i8 = 7; +pub const NUM_FILES: usize = 8; + +pub const RANK_1: i8 = 0; +pub const RANK_2: i8 = 1; +pub const RANK_3: i8 = 2; +pub const RANK_4: i8 = 3; +pub const RANK_5: i8 = 4; +pub const RANK_6: i8 = 5; +pub const RANK_7: i8 = 6; +pub const RANK_8: i8 = 7; +pub const NUM_RANKS: usize = 8; + pub const FILES: [Bitboard; 8] = [ 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111, 0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000, @@ -172,18 +192,99 @@ pub const fn after_on_file(file: i8, rank: i8) -> Bitboard { FILES[file as usize] & bits_after(file, rank) } -/// Get the bitboard of squares on lower ranks of the `square` file. -#[inline] -pub const fn before_on_square_file(square: Square) -> Bitboard { - before_on_file(sq_file(square), sq_rank(square)) +/// Debug only: count positive bits of the bitboard. +#[allow(dead_code)] +pub(crate) fn count_bits(bitboard: Bitboard) -> u8 { + let mut bitboard = bitboard; + let mut count = 0; + while bitboard > 0 { + count += bitboard & 1; + bitboard >>= 1; + } + count as u8 } -/// Get the bitboard of squares on upper ranks of the `square` file. -#[inline] -pub const fn after_on_square_file(square: Square) -> Bitboard { - after_on_file(sq_file(square), sq_rank(square)) +/// Debug only: pretty print a bitboard +#[allow(dead_code)] +pub(crate) fn draw_bits(bitboard: Bitboard, f: &mut dyn std::io::Write) { + for rank in (0..8).rev() { + let mut rank_str = String::with_capacity(8); + for file in 0..8 { + rank_str.push(if bitboard & bit_pos(sq(file, rank)) == 0 { '.' } else { '1' }); + } + writeln!(f, "{}", rank_str).unwrap(); + } } +// Generated by gen_knight_rays.py. +/// Pre-computed knight rays. +const KNIGHT_RAYS: [Bitboard; 64] = [ + 0b00000000_00000000_00000000_00000000_00000000_00000010_00000100_00000000, + 0b00000000_00000000_00000000_00000000_00000000_00000101_00001000_00000000, + 0b00000000_00000000_00000000_00000000_00000000_00001010_00010001_00000000, + 0b00000000_00000000_00000000_00000000_00000000_00010100_00100010_00000000, + 0b00000000_00000000_00000000_00000000_00000000_00101000_01000100_00000000, + 0b00000000_00000000_00000000_00000000_00000000_01010000_10001000_00000000, + 0b00000000_00000000_00000000_00000000_00000000_10100000_00010000_00000000, + 0b00000000_00000000_00000000_00000000_00000000_01000000_00100000_00000000, + 0b00000000_00000000_00000000_00000000_00000010_00000100_00000000_00000100, + 0b00000000_00000000_00000000_00000000_00000101_00001000_00000000_00001000, + 0b00000000_00000000_00000000_00000000_00001010_00010001_00000000_00010001, + 0b00000000_00000000_00000000_00000000_00010100_00100010_00000000_00100010, + 0b00000000_00000000_00000000_00000000_00101000_01000100_00000000_01000100, + 0b00000000_00000000_00000000_00000000_01010000_10001000_00000000_10001000, + 0b00000000_00000000_00000000_00000000_10100000_00010000_00000000_00010000, + 0b00000000_00000000_00000000_00000000_01000000_00100000_00000000_00100000, + 0b00000000_00000000_00000000_00000010_00000100_00000000_00000100_00000010, + 0b00000000_00000000_00000000_00000101_00001000_00000000_00001000_00000101, + 0b00000000_00000000_00000000_00001010_00010001_00000000_00010001_00001010, + 0b00000000_00000000_00000000_00010100_00100010_00000000_00100010_00010100, + 0b00000000_00000000_00000000_00101000_01000100_00000000_01000100_00101000, + 0b00000000_00000000_00000000_01010000_10001000_00000000_10001000_01010000, + 0b00000000_00000000_00000000_10100000_00010000_00000000_00010000_10100000, + 0b00000000_00000000_00000000_01000000_00100000_00000000_00100000_01000000, + 0b00000000_00000000_00000010_00000100_00000000_00000100_00000010_00000000, + 0b00000000_00000000_00000101_00001000_00000000_00001000_00000101_00000000, + 0b00000000_00000000_00001010_00010001_00000000_00010001_00001010_00000000, + 0b00000000_00000000_00010100_00100010_00000000_00100010_00010100_00000000, + 0b00000000_00000000_00101000_01000100_00000000_01000100_00101000_00000000, + 0b00000000_00000000_01010000_10001000_00000000_10001000_01010000_00000000, + 0b00000000_00000000_10100000_00010000_00000000_00010000_10100000_00000000, + 0b00000000_00000000_01000000_00100000_00000000_00100000_01000000_00000000, + 0b00000000_00000010_00000100_00000000_00000100_00000010_00000000_00000000, + 0b00000000_00000101_00001000_00000000_00001000_00000101_00000000_00000000, + 0b00000000_00001010_00010001_00000000_00010001_00001010_00000000_00000000, + 0b00000000_00010100_00100010_00000000_00100010_00010100_00000000_00000000, + 0b00000000_00101000_01000100_00000000_01000100_00101000_00000000_00000000, + 0b00000000_01010000_10001000_00000000_10001000_01010000_00000000_00000000, + 0b00000000_10100000_00010000_00000000_00010000_10100000_00000000_00000000, + 0b00000000_01000000_00100000_00000000_00100000_01000000_00000000_00000000, + 0b00000010_00000100_00000000_00000100_00000010_00000000_00000000_00000000, + 0b00000101_00001000_00000000_00001000_00000101_00000000_00000000_00000000, + 0b00001010_00010001_00000000_00010001_00001010_00000000_00000000_00000000, + 0b00010100_00100010_00000000_00100010_00010100_00000000_00000000_00000000, + 0b00101000_01000100_00000000_01000100_00101000_00000000_00000000_00000000, + 0b01010000_10001000_00000000_10001000_01010000_00000000_00000000_00000000, + 0b10100000_00010000_00000000_00010000_10100000_00000000_00000000_00000000, + 0b01000000_00100000_00000000_00100000_01000000_00000000_00000000_00000000, + 0b00000100_00000000_00000100_00000010_00000000_00000000_00000000_00000000, + 0b00001000_00000000_00001000_00000101_00000000_00000000_00000000_00000000, + 0b00010001_00000000_00010001_00001010_00000000_00000000_00000000_00000000, + 0b00100010_00000000_00100010_00010100_00000000_00000000_00000000_00000000, + 0b01000100_00000000_01000100_00101000_00000000_00000000_00000000_00000000, + 0b10001000_00000000_10001000_01010000_00000000_00000000_00000000_00000000, + 0b00010000_00000000_00010000_10100000_00000000_00000000_00000000_00000000, + 0b00100000_00000000_00100000_01000000_00000000_00000000_00000000_00000000, + 0b00000000_00000100_00000010_00000000_00000000_00000000_00000000_00000000, + 0b00000000_00001000_00000101_00000000_00000000_00000000_00000000_00000000, + 0b00000000_00010001_00001010_00000000_00000000_00000000_00000000_00000000, + 0b00000000_00100010_00010100_00000000_00000000_00000000_00000000_00000000, + 0b00000000_01000100_00101000_00000000_00000000_00000000_00000000_00000000, + 0b00000000_10001000_01010000_00000000_00000000_00000000_00000000_00000000, + 0b00000000_00010000_10100000_00000000_00000000_00000000_00000000_00000000, + 0b00000000_00100000_01000000_00000000_00000000_00000000_00000000_00000000, +]; + /// Board representation with color/piece bitboards. #[derive(Clone, PartialEq)] pub struct Board { @@ -245,9 +346,7 @@ impl Board { } board } -} -impl Board { /// Get combined white/black pieces bitboard. #[inline] pub fn combined(&self) -> Bitboard { @@ -330,16 +429,52 @@ impl Board { None } - /// Debug only: count number of pieces on board. - #[allow(dead_code)] // Currently used in tests only. - pub(crate) fn num_pieces(&self) -> u8 { - let mut cbb = self.combined(); - let mut count = 0; - while cbb > 0 { - count += cbb & 1; - cbb >>= 1; + 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)]) + } + + 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)]) + } + + 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)] + ) + } + + fn get_blockable_rays(&self, + square: Square, + color: Color, + directions: &[(i8, i8)] + ) -> Bitboard { + let mut rays_bb: Bitboard = 0; + let color_bb = self.by_color(color); + let enemy_bb = self.by_color(opposite(color)); + for dir in directions { + let mut ray_f = sq_file(square); + let mut ray_r = sq_rank(square); + loop { + ray_f += dir.0; + ray_r += dir.1; + if ray_f < 0 || ray_f > 7 || ray_r < 0 || ray_r > 7 { + break + } + let bp = bit_pos(sq(ray_f, ray_r)); + if color_bb & bp != 0 { + break + } + rays_bb |= bp; + if enemy_bb & bp != 0 { + break + } + } } - count as u8 + rays_bb + } + + pub fn get_knight_rays(&self, square: Square, color: Color) -> Bitboard { + KNIGHT_RAYS[square as usize] & !self.by_color(color) } /// Debug only: write a text view of the board. @@ -372,7 +507,7 @@ impl Board { } writeln!(f, "{} {}", rank + 1, rank_str).unwrap(); } - write!(f, " abcdefgh").unwrap(); + writeln!(f, " abcdefgh").unwrap(); } } @@ -410,35 +545,41 @@ mod tests { // Bitboard #[test] - fn test_before_on_square_file() { - // Only should the 4 lowest files for readability. - assert_eq!(before_on_square_file(A1), 0b00000000_00000000_00000000_00000000); - assert_eq!(before_on_square_file(A2), 0b00000000_00000000_00000000_00000001); - assert_eq!(before_on_square_file(A4), 0b00000000_00000000_00000000_00000111); - assert_eq!(before_on_square_file(A8), 0b00000000_00000000_00000000_01111111); - assert_eq!(before_on_square_file(B1), 0b00000000_00000000_00000000_00000000); - assert_eq!(before_on_square_file(C1), 0b00000000_00000000_00000000_00000000); - assert_eq!(before_on_square_file(C4), 0b00000000_00000111_00000000_00000000); - // 4 highest files. - assert_eq!(before_on_square_file(H4), 0b00000111_00000000_00000000_00000000 << 32); - assert_eq!(before_on_square_file(H7), 0b00111111_00000000_00000000_00000000 << 32); - assert_eq!(before_on_square_file(H8), 0b01111111_00000000_00000000_00000000 << 32); + fn test_count_bits() { + assert_eq!(count_bits(Board::new_empty().combined()), 0); + assert_eq!(count_bits(Board::new().combined()), 32); } #[test] - fn test_after_on_file() { - assert_eq!(after_on_square_file(A1), 0b00000000_00000000_00000000_11111110); - assert_eq!(after_on_square_file(A2), 0b00000000_00000000_00000000_11111100); - assert_eq!(after_on_square_file(A4), 0b00000000_00000000_00000000_11110000); - assert_eq!(after_on_square_file(A8), 0b00000000_00000000_00000000_00000000); - assert_eq!(after_on_square_file(B1), 0b00000000_00000000_11111110_00000000); - assert_eq!(after_on_square_file(C1), 0b00000000_11111110_00000000_00000000); - assert_eq!(after_on_square_file(C4), 0b00000000_11110000_00000000_00000000); - assert_eq!(after_on_square_file(C8), 0b00000000_00000000_00000000_00000000); + fn test_before_on_file() { + // Only should the 4 lowest files for readability. + assert_eq!(before_on_file(FILE_A, RANK_1), 0b00000000_00000000_00000000_00000000); + assert_eq!(before_on_file(FILE_A, RANK_2), 0b00000000_00000000_00000000_00000001); + assert_eq!(before_on_file(FILE_A, RANK_4), 0b00000000_00000000_00000000_00000111); + assert_eq!(before_on_file(FILE_A, RANK_8), 0b00000000_00000000_00000000_01111111); + assert_eq!(before_on_file(FILE_B, RANK_1), 0b00000000_00000000_00000000_00000000); + assert_eq!(before_on_file(FILE_C, RANK_1), 0b00000000_00000000_00000000_00000000); + assert_eq!(before_on_file(FILE_C, RANK_4), 0b00000000_00000111_00000000_00000000); // 4 highest files. - assert_eq!(after_on_square_file(H4), 0b11110000_00000000_00000000_00000000 << 32); - assert_eq!(after_on_square_file(H7), 0b10000000_00000000_00000000_00000000 << 32); - assert_eq!(after_on_square_file(H8), 0b00000000_00000000_00000000_00000000 << 32); + assert_eq!(before_on_file(FILE_H, RANK_4), 0b00000111_00000000_00000000_00000000 << 32); + assert_eq!(before_on_file(FILE_H, RANK_7), 0b00111111_00000000_00000000_00000000 << 32); + assert_eq!(before_on_file(FILE_H, RANK_8), 0b01111111_00000000_00000000_00000000 << 32); + } + + #[test] + fn test_after_on_square_file() { + assert_eq!(after_on_file(FILE_A, RANK_1), 0b00000000_00000000_00000000_11111110); + assert_eq!(after_on_file(FILE_A, RANK_2), 0b00000000_00000000_00000000_11111100); + assert_eq!(after_on_file(FILE_A, RANK_4), 0b00000000_00000000_00000000_11110000); + assert_eq!(after_on_file(FILE_A, RANK_8), 0b00000000_00000000_00000000_00000000); + assert_eq!(after_on_file(FILE_B, RANK_1), 0b00000000_00000000_11111110_00000000); + assert_eq!(after_on_file(FILE_C, RANK_1), 0b00000000_11111110_00000000_00000000); + assert_eq!(after_on_file(FILE_C, RANK_4), 0b00000000_11110000_00000000_00000000); + assert_eq!(after_on_file(FILE_C, RANK_8), 0b00000000_00000000_00000000_00000000); + // 4 highest files. + assert_eq!(after_on_file(FILE_H, RANK_4), 0b11110000_00000000_00000000_00000000 << 32); + assert_eq!(after_on_file(FILE_H, RANK_7), 0b10000000_00000000_00000000_00000000 << 32); + assert_eq!(after_on_file(FILE_H, RANK_8), 0b00000000_00000000_00000000_00000000 << 32); } // Board @@ -486,8 +627,95 @@ mod tests { } #[test] - fn test_num_pieces() { - assert_eq!(Board::new_empty().num_pieces(), 0); - assert_eq!(Board::new().num_pieces(), 32); + fn test_get_bishop_rays() { + let mut b = Board::new_empty(); + + // A bishop has maximum range when it's in a center square. + b.set_square(D4, WHITE, BISHOP); + let rays_bb = b.get_bishop_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 13); + // Going top-right. + assert!(rays_bb & bit_pos(E5) != 0); + assert!(rays_bb & bit_pos(F6) != 0); + assert!(rays_bb & bit_pos(G7) != 0); + assert!(rays_bb & bit_pos(H8) != 0); + // Going bottom-right. + assert!(rays_bb & bit_pos(E3) != 0); + assert!(rays_bb & bit_pos(F2) != 0); + assert!(rays_bb & bit_pos(G1) != 0); + // Going bottom-left. + assert!(rays_bb & bit_pos(C3) != 0); + assert!(rays_bb & bit_pos(B2) != 0); + assert!(rays_bb & bit_pos(A1) != 0); + // Going top-left. + assert!(rays_bb & bit_pos(C5) != 0); + assert!(rays_bb & bit_pos(B6) != 0); + assert!(rays_bb & bit_pos(A7) != 0); + + // When blocking commit to one square with friendly piece, lose 2 moves. + b.set_square(B2, WHITE, PAWN); + let rays_bb = b.get_bishop_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 11); + + // When blocking commit to one square with enemy piece, lose only 1 move. + b.set_square(B2, BLACK, PAWN); + let rays_bb = b.get_bishop_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 12); + } + + #[test] + fn test_get_knight_moves() { + let mut b = Board::new_empty(); + + // A knight is never blocked; if it's in the center of the board, + // it can have up to 8 moves. + b.set_square(D4, WHITE, KNIGHT); + let rays_bb = b.get_knight_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 8); + + // If on a side if has only 4 moves. + b.set_square(A4, WHITE, KNIGHT); + let rays_bb = b.get_knight_rays(A4, WHITE); + assert_eq!(count_bits(rays_bb), 4); + + // And in a corner, only 2 moves. + b.set_square(A1, WHITE, KNIGHT); + let rays_bb = b.get_knight_rays(A1, WHITE); + assert_eq!(count_bits(rays_bb), 2); + + // Add 2 friendly pieces and it is totally blocked. + b.set_square(B3, WHITE, PAWN); + b.set_square(C2, WHITE, PAWN); + let rays_bb = b.get_knight_rays(A1, WHITE); + assert_eq!(count_bits(rays_bb), 0); + + // If one of those pieces is an enemy, it can be taken. + b.set_square(B3, BLACK, PAWN); + let rays_bb = b.get_knight_rays(A1, WHITE); + assert_eq!(count_bits(rays_bb), 1); + } + + #[test] + fn test_get_rook_moves() { + let mut b = Board::new_empty(); + + b.set_square(D4, WHITE, ROOK); + let rays_bb = b.get_rook_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 14); + b.set_square(D6, BLACK, PAWN); + let rays_bb = b.get_rook_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 12); + b.set_square(D6, WHITE, PAWN); + let rays_bb = b.get_rook_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 11); + } + + #[test] + fn test_get_queen_moves() { + let mut b = Board::new_empty(); + + b.set_square(D4, WHITE, QUEEN); + let rays_bb = b.get_queen_rays(D4, WHITE); + assert_eq!(count_bits(rays_bb), 14 + 13); } } diff --git a/src/movement.rs b/src/movement.rs index 6ee6fd1..c2d6fdd 100644 --- a/src/movement.rs +++ b/src/movement.rs @@ -152,12 +152,12 @@ mod tests { assert!(b.is_empty(D4)); assert_eq!(b.get_color_on(E6), WHITE); assert_eq!(b.get_piece_on(E6), KNIGHT); - assert_eq!(b.num_pieces(), 2); + assert_eq!(count_bits(b.combined()), 2); // Sack it with black knight Move::new(F4, E6).apply_to_board(&mut b); assert_eq!(b.get_color_on(E6), BLACK); assert_eq!(b.get_piece_on(E6), KNIGHT); - assert_eq!(b.num_pieces(), 1); + assert_eq!(count_bits(b.combined()), 1); } #[test] diff --git a/src/node.rs b/src/node.rs index 9ea2960..08aa9fa 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_moves(&self, commit: bool) -> Vec { - rules::get_player_moves(&self.board, &self.game_state, commit) + pub fn get_player_legal_moves(&self) -> Vec { + rules::get_player_moves(&self.board, &self.game_state, false) } /// Compute stats for both players for this node. diff --git a/src/rules.rs b/src/rules.rs index 3d3449e..11627e6 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -53,17 +53,18 @@ impl std::fmt::Display for GameState { /// Get a list of moves for all pieces of the playing color. /// -/// If `commit` is false, do not check for illegal moves. This is used -/// to avoid endless recursion when checking if a P move is illegal, -/// as it needs to check all possible following enemy moves, e.g. to -/// see if P's king can be taken. Consider a call with true `commit` as -/// a collection of attacked squares instead of legal move collection. +/// If `pseudo_legal` is true, do not check for illegal moves. This is +/// used to avoid endless recursion when checking if a P move is +/// illegal, as it needs to check all possible following enemy moves, +/// e.g. to see if P's king can be taken. Consider a call with true +/// `pseudo_legal` as a collection of attacked squares instead of legal +/// move collection. pub fn get_player_moves( board: &Board, game_state: &GameState, - commit: bool, + pseudo_legal: bool, ) -> Vec { - let mut moves = Vec::with_capacity(256); + let mut moves = Vec::with_capacity(32); for r in 0..8 { for f in 0..8 { let square = sq(f, r); @@ -72,7 +73,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, commit) + &mut get_piece_moves(board, game_state, square, game_state.color, pseudo_legal) ); } } @@ -86,21 +87,21 @@ pub fn get_player_moves( /// of the piece on `square`; it could technically be found from the /// board but that would require an additional lookup and this function /// is always called in a context where the piece color is known. -pub fn get_piece_moves( +fn get_piece_moves( board: &Board, game_state: &GameState, square: Square, color: Color, - commit: bool, + pseudo_legal: bool, ) -> Vec { match board.get_piece_on(square) { - PAWN => get_pawn_moves(board, game_state, square, color, commit), - BISHOP => get_bishop_moves(board, game_state, square, color, commit), - KNIGHT => get_knight_moves(board, game_state, square, color, commit), - ROOK => get_rook_moves(board, game_state, square, color, commit), - QUEEN => get_queen_moves(board, game_state, square, color, commit), - KING => get_king_moves(board, game_state, square, color, commit), - _ => vec!(), + PAWN => get_pawn_moves(board, game_state, square, color, pseudo_legal), + BISHOP => get_bishop_moves(board, game_state, square, color, pseudo_legal), + KNIGHT => get_knight_moves(board, game_state, square, color, pseudo_legal), + ROOK => get_rook_moves(board, game_state, square, color, pseudo_legal), + QUEEN => get_queen_moves(board, game_state, square, color, pseudo_legal), + KING => get_king_moves(board, game_state, square, color, pseudo_legal), + _ => { panic!("No piece on square.") }, } } @@ -109,7 +110,7 @@ fn get_pawn_moves( game_state: &GameState, square: Square, color: Color, - commit: bool, + pseudo_legal: bool, ) -> Vec { let (f, r) = (sq_file(square), sq_rank(square)); let mut moves = vec!(); @@ -133,7 +134,7 @@ fn get_pawn_moves( if (dir > 0 && forward_r == POS_MAX) || (dir < 0 && forward_r == POS_MIN) { m.promotion = Some(QUEEN) } - if can_register(commit, board, game_state, &m) { + if pseudo_legal || !is_illegal(board, game_state, &m) { moves.push(m); } } @@ -145,7 +146,7 @@ fn get_pawn_moves( if !board.is_empty(diag) { let diag_color = board.get_color_on(diag); if let Some(m) = get_capture_move(color, square, diag_color, diag, true) { - if can_register(commit, board, game_state, &m) { + if pseudo_legal || !is_illegal(board, game_state, &m) { moves.push(m); } } @@ -158,7 +159,7 @@ fn get_pawn_moves( if !board.is_empty(diag) { let diag_color = board.get_color_on(diag); if let Some(m) = get_capture_move(color, square, diag_color, diag, true) { - if can_register(commit, board, game_state, &m) { + if pseudo_legal || !is_illegal(board, game_state, &m) { moves.push(m); } } @@ -175,7 +176,7 @@ fn get_bishop_moves( game_state: &GameState, square: Square, color: Color, - commit: bool, + pseudo_legal: bool, ) -> Vec { let (f, r) = (sq_file(square), sq_rank(square)); let mut views = [true; 4]; // Store diagonals where a piece blocks commit. @@ -199,7 +200,7 @@ fn get_bishop_moves( } let ray_square = sq(ray_f, ray_r); - match get_move_type(board, game_state, square, ray_square, color, commit) { + match get_move_type(board, game_state, square, ray_square, color, pseudo_legal) { MoveType::Simple(m) => { moves.push(m) } MoveType::Capture(m) => { moves.push(m); views[dir] = false; } MoveType::CantTakeFriend => { views[dir] = false; } @@ -215,7 +216,7 @@ fn get_knight_moves( game_state: &GameState, square: Square, color: Color, - commit: bool, + pseudo_legal: bool, ) -> Vec { let (f, r) = (sq_file(square), sq_rank(square)); let mut moves = Vec::with_capacity(8); @@ -230,7 +231,7 @@ fn get_knight_moves( } let ray_square = sq(ray_f, ray_r); - match get_move_type(board, game_state, square, ray_square, color, commit) { + match get_move_type(board, game_state, square, ray_square, color, pseudo_legal) { MoveType::Simple(m) | MoveType::Capture(m) => moves.push(m), _ => {} } @@ -243,7 +244,7 @@ fn get_rook_moves( game_state: &GameState, square: Square, color: Color, - commit: bool, + pseudo_legal: bool, ) -> Vec { let (f, r) = (sq_file(square), sq_rank(square)); let mut moves = Vec::with_capacity(8); @@ -266,7 +267,7 @@ fn get_rook_moves( } let ray_square = sq(ray_f, ray_r); - match get_move_type(board, game_state, square, ray_square, color, commit) { + match get_move_type(board, game_state, square, ray_square, color, pseudo_legal) { MoveType::Simple(m) => { moves.push(m) } MoveType::Capture(m) => { moves.push(m); views[dir] = false; } MoveType::CantTakeFriend => { views[dir] = false; } @@ -282,12 +283,12 @@ fn get_queen_moves( game_state: &GameState, square: Square, color: Color, - commit: bool, + pseudo_legal: bool, ) -> Vec { let mut moves = Vec::with_capacity(16); // Easy way to get queen moves, but may be a bit quicker if everything was rewritten here. - moves.append(&mut get_bishop_moves(board, game_state, square, color, commit)); - moves.append(&mut get_rook_moves(board, game_state, square, color, commit)); + moves.append(&mut get_bishop_moves(board, game_state, square, color, pseudo_legal)); + moves.append(&mut get_rook_moves(board, game_state, square, color, pseudo_legal)); moves } @@ -296,7 +297,7 @@ fn get_king_moves( game_state: &GameState, square: Square, color: Color, - commit: bool, + pseudo_legal: bool, ) -> Vec { let (f, r) = (sq_file(square), sq_rank(square)); let mut moves = Vec::with_capacity(8); @@ -311,14 +312,14 @@ fn get_king_moves( } let ray_square = sq(ray_f, ray_r); - match get_move_type(board, game_state, square, ray_square, color, commit) { + match get_move_type(board, game_state, square, ray_square, color, pseudo_legal) { MoveType::Simple(m) | MoveType::Capture(m) => moves.push(m), _ => {} } } - // Stop here for uncommitted moves. - if !commit { + // Stop here for pseudo legal moves as castling is not considered along with them. + if pseudo_legal { return moves } @@ -370,7 +371,7 @@ fn get_king_moves( } let castle = castling_side_mask & castling_color_mask; let m = Move::get_castle_move(castle); - if can_register(commit, board, game_state, &m) { + if pseudo_legal || !is_illegal(board, game_state, &m) { moves.push(m); } } @@ -390,7 +391,7 @@ fn get_move_type( ) -> MoveType { if board.is_empty(ray_square) { let m = Move::new(square, ray_square); - if can_register(commit, board, game_state, &m) { + if is_legal(commit, board, game_state, &m) { MoveType::Simple(m) } else { MoveType::CantRegister @@ -398,7 +399,7 @@ fn get_move_type( } else { let ray_color = board.get_color_on(ray_square); if let Some(m) = get_capture_move(color, square, ray_color, ray_square, false) { - if can_register(commit, board, game_state, &m) { + if is_legal(commit, board, game_state, &m) { MoveType::Capture(m) } else { MoveType::CantRegister @@ -420,15 +421,15 @@ enum MoveType { CantTakeFriend, } -/// Return true if `commit` is false, or the move is not illegal, +/// Return true if `pseudo_legal` is true, or the move is not illegal, /// /// Committing a move means that it can be safely played afterwards. /// Sometimes it is not what is needed to accept a move in a collection /// of moves, e.g. when simply checking if some moves would make a /// previous move illegal. #[inline] -fn can_register(commit: bool, board: &Board, game_state: &GameState, m: &Move) -> bool { - !commit || !is_illegal(board, game_state, m) +fn is_legal(pseudo_legal: bool, board: &Board, game_state: &GameState, m: &Move) -> bool { + pseudo_legal || !is_illegal(board, game_state, m) } /// Return a move from `square1` to `square2` if colors are opposite. @@ -486,7 +487,7 @@ fn is_attacked(board: &Board, game_state: &GameState, square: Square) -> bool { let mut enemy_game_state = game_state.clone(); enemy_game_state.color = opposite(game_state.color); // Do not attempt to commit moves, just check for attacked squares. - let enemy_moves = get_player_moves(board, &enemy_game_state, false); + let enemy_moves = get_player_moves(board, &enemy_game_state, true); for m in enemy_moves.iter() { if square == m.dest { return true @@ -505,7 +506,7 @@ mod tests { let gs = GameState::new(); // At first move, white has 16 pawn moves and 4 knight moves. - let moves = get_player_moves(&b, &gs, true); + let moves = get_player_moves(&b, &gs, false); assert_eq!(moves.len(), 20); } @@ -516,12 +517,12 @@ mod tests { // 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_piece_moves(&b, &gs, D3, WHITE, true); + let moves = get_piece_moves(&b, &gs, D3, WHITE, false); assert!(moves.len() == 1 && moves.contains(&Move::new(D3, 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_piece_moves(&b, &gs, E2, WHITE, true); + let moves = get_piece_moves(&b, &gs, E2, WHITE, false); assert_eq!(moves.len(), 2); assert!(moves.contains(&Move::new(E2, E3))); assert!(moves.contains(&Move::new(E2, E4))); @@ -529,23 +530,23 @@ mod tests { // 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_piece_moves(&b, &gs, E2, WHITE, true); + let moves = get_piece_moves(&b, &gs, E2, WHITE, false); assert!(moves.len() == 1 && moves.contains(&Move::new(E2, E3))); // 2. black pawn 1 square forward; no square available. b.set_square(E3, BLACK, PAWN); - let moves = get_piece_moves(&b, &gs, E2, WHITE, true); + let moves = get_piece_moves(&b, &gs, E2, WHITE, false); 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); - let moves = get_piece_moves(&b, &gs, E2, WHITE, true); + let moves = get_piece_moves(&b, &gs, E2, WHITE, false); assert_eq!(moves.len(), 0); // Check that a pawn can take a piece diagonally. b.set_square(F3, BLACK, PAWN); - let moves = get_piece_moves(&b, &gs, E2, WHITE, true); + let moves = get_piece_moves(&b, &gs, E2, WHITE, false); assert!(moves.len() == 1 && moves.contains(&Move::new(E2, F3))); b.set_square(D3, BLACK, PAWN); - let moves = get_piece_moves(&b, &gs, E2, WHITE, true); + let moves = get_piece_moves(&b, &gs, E2, WHITE, false); assert_eq!(moves.len(), 2); assert!(moves.contains( &Move::new(E2, F3) )); assert!(moves.contains( &Move::new(E2, D3) )); @@ -553,7 +554,7 @@ mod tests { // 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_piece_moves(&b, &gs, A7, WHITE, true); + let moves = get_piece_moves(&b, &gs, A7, WHITE, false); assert!(moves.len() == 1 && moves.contains(&Move::new_promotion(A7, A8, QUEEN))); } @@ -564,7 +565,7 @@ mod tests { // A bishop has maximum range when it's in a center square. b.set_square(D4, WHITE, BISHOP); - let moves = get_piece_moves(&b, &gs, D4, WHITE, true); + let moves = get_piece_moves(&b, &gs, D4, WHITE, false); assert_eq!(moves.len(), 13); // Going top-right. assert!(moves.contains(&Move::new(D4, E5))); @@ -586,11 +587,11 @@ mod tests { // When blocking commit to one square with friendly piece, lose 2 moves. b.set_square(B2, WHITE, PAWN); - assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 11); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 11); // When blocking commit to one square with enemy piece, lose only 1 move. b.set_square(B2, BLACK, PAWN); - assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 12); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 12); } #[test] @@ -601,20 +602,20 @@ mod tests { // 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_piece_moves(&b, &gs, D4, WHITE, true).len(), 8); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 8); // If on a side if has only 4 moves. b.set_square(A4, WHITE, KNIGHT); - assert_eq!(get_piece_moves(&b, &gs, A4, WHITE, true).len(), 4); + assert_eq!(get_piece_moves(&b, &gs, A4, WHITE, false).len(), 4); // And in a corner, only 2 moves. b.set_square(A1, WHITE, KNIGHT); - assert_eq!(get_piece_moves(&b, &gs, A1, WHITE, true).len(), 2); + assert_eq!(get_piece_moves(&b, &gs, A1, WHITE, false).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_piece_moves(&b, &gs, A1, WHITE, true).len(), 0); + assert_eq!(get_piece_moves(&b, &gs, A1, WHITE, false).len(), 0); } #[test] @@ -623,11 +624,11 @@ mod tests { let gs = GameState::new(); b.set_square(D4, WHITE, ROOK); - assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 14); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 14); b.set_square(D6, BLACK, PAWN); - assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 12); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 12); b.set_square(D6, WHITE, PAWN); - assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 11); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 11); } #[test] @@ -636,7 +637,7 @@ mod tests { let gs = GameState::new(); b.set_square(D4, WHITE, QUEEN); - assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 14 + 13); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 14 + 13); } #[test] @@ -646,23 +647,23 @@ 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_piece_moves(&b, &gs, D4, WHITE, true).len(), 8); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).len(), 8); b.set_square(E5, WHITE, PAWN); - assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 7); + assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, false).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_piece_moves(&b, &gs, E1, WHITE, true).len(), 5 + 2); + assert_eq!(get_piece_moves(&b, &gs, E1, WHITE, false).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_piece_moves(&b, &gs, E8, BLACK, true).len(), 5 + 2); + assert_eq!(get_piece_moves(&b, &gs, E8, BLACK, false).len(), 5 + 2); } #[test] @@ -677,7 +678,7 @@ mod tests { // No castling available. gs.castling = 0; // 5 moves in absolute but only 2 are legal. - let all_wh_moves = get_piece_moves(&b, &gs, E1, WHITE, true); + let all_wh_moves = get_piece_moves(&b, &gs, E1, WHITE, false); assert_eq!(all_wh_moves.len(), 2); } diff --git a/src/stats.rs b/src/stats.rs index 2d06437..09a199e 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -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, true).len() as i32; + self.mobility = get_player_moves(board, game_state, false).len() as i32; // Compute amount of each piece. for file in 0..8 { for rank in 0..8 { @@ -172,8 +172,6 @@ mod tests { mobility: 20, }; let mut stats = BoardStats::new_from(&b, &gs); - eprintln!("{}", stats.0); - eprintln!("{}", stats.1); assert!(stats.0 == stats.1); assert!(stats.0 == initial_stats);