From 8aa5dacfc8cacd1cd80a07618504d528ef779d1b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:51:58 -0800 Subject: [PATCH 01/14] Initial implementation of CheckingPieces and Position::checking_pieces() --- position/src/check.rs | 29 +++++++++++++++++++++++ position/src/lib.rs | 1 + position/src/position/position.rs | 38 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 position/src/check.rs diff --git a/position/src/check.rs b/position/src/check.rs new file mode 100644 index 0000000..d9d5172 --- /dev/null +++ b/position/src/check.rs @@ -0,0 +1,29 @@ +// Eryn Wells + +use chessfriend_bitboard::BitBoard; + +pub struct CheckingPieces { + pawn: BitBoard, + knight: BitBoard, + bishop: BitBoard, + rook: BitBoard, + queen: BitBoard, +} + +impl CheckingPieces { + pub(crate) fn new( + pawn: BitBoard, + knight: BitBoard, + bishop: BitBoard, + rook: BitBoard, + queen: BitBoard, + ) -> CheckingPieces { + CheckingPieces { + pawn, + knight, + bishop, + rook, + queen, + } + } +} diff --git a/position/src/lib.rs b/position/src/lib.rs index 9c58540..dde3e5d 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -2,6 +2,7 @@ pub mod fen; +mod check; mod display; mod r#move; mod move_generator; diff --git a/position/src/position/position.rs b/position/src/position/position.rs index f2b6de6..7e211b4 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -249,6 +249,44 @@ impl Position { .next() .unwrap() } + + pub(crate) fn checking_pieces(&self) -> CheckingPieces { + let opponent = self.color_to_move.other(); + let king_square = self.king_square(self.color_to_move); + + let checking_pawns = { + // The current player's pawn attack moves *from* this square are the + // same as the pawn moves for the opposing player attacking this square. + let pawn_moves_to_king_square = BitBoard::pawn_attacks(king_square, self.color_to_move); + let opposing_pawn = Piece::pawn(opponent); + let opposing_pawns = self.pieces.bitboard_for_piece(&opposing_pawn); + + pawn_moves_to_king_square & opposing_pawns + }; + + macro_rules! checking_piece { + ($moves_bb_fn:path, $piece_fn:path) => {{ + let moves_from_opposing_square = $moves_bb_fn(king_square); + let piece = $piece_fn(opponent); + let opposing_pieces = self.pieces.bitboard_for_piece(&piece); + + moves_from_opposing_square & opposing_pieces + }}; + } + + let checking_knights = checking_piece!(BitBoard::knight_moves, Piece::knight); + let checking_bishops = checking_piece!(BitBoard::bishop_moves, Piece::bishop); + let checking_rooks = checking_piece!(BitBoard::rook_moves, Piece::rook); + let checking_queens = checking_piece!(BitBoard::queen_moves, Piece::queen); + + CheckingPieces::new( + checking_pawns, + checking_knights, + checking_bishops, + checking_rooks, + checking_queens, + ) + } } // crate::position methods From cac13b4bc753f0bdbc8a4a5a7be3e4c5aae88cbc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 30 Jan 2024 08:41:23 -0800 Subject: [PATCH 02/14] Cargo.lock changes --- Cargo.lock | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ebe93a..6e580bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,16 +56,40 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" -[[package]] -name = "board" -version = "0.1.0" - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chessfriend_bitboard" +version = "0.1.0" +dependencies = [ + "chessfriend_core", +] + +[[package]] +name = "chessfriend_core" +version = "0.1.0" + +[[package]] +name = "chessfriend_move_generator" +version = "0.1.0" +dependencies = [ + "chessfriend_bitboard", + "chessfriend_core", + "chessfriend_position", +] + +[[package]] +name = "chessfriend_position" +version = "0.1.0" +dependencies = [ + "chessfriend_bitboard", + "chessfriend_core", +] + [[package]] name = "clap" version = "4.4.18" @@ -121,10 +145,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "core" -version = "0.1.0" - [[package]] name = "endian-type" version = "0.1.2" @@ -151,7 +171,8 @@ checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" name = "explorer" version = "0.1.0" dependencies = [ - "board", + "chessfriend_core", + "chessfriend_position", "clap", "rustyline", "shlex", From 722a90b860afb5ad69da32670db2399c39566246 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:42:19 -0800 Subject: [PATCH 03/14] [position] Implement a SliderRayToSquare trait This trait declares ray_to_square() which should return a BitBoard representing a ray from a Square to another Square. The ray should include the target Square. PlacedPiece implements this trait. --- bitboard/src/bitboard.rs | 30 ++++++++++- bitboard/src/library.rs | 4 +- position/src/sight.rs | 114 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 2f9740d..4904c3e 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -39,7 +39,7 @@ impl BitBoard { library::FILES[*file as usize] } - pub fn ray(sq: Square, dir: Direction) -> BitBoard { + pub fn ray(sq: Square, dir: Direction) -> &'static BitBoard { library::library().ray(sq, dir) } @@ -107,6 +107,24 @@ impl BitBoard { pub fn occupied_squares_trailing(&self) -> impl Iterator { LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } + + pub fn first_occupied_square(&self) -> Option { + let leading_zeros = self.0.leading_zeros() as u8; + if leading_zeros < Square::NUM as u8 { + unsafe { Some(Square::from_index(Square::NUM as u8 - leading_zeros - 1)) } + } else { + None + } + } + + pub fn first_occupied_square_trailing(&self) -> Option { + let trailing_zeros = self.0.trailing_zeros() as u8; + if trailing_zeros < Square::NUM as u8 { + unsafe { Some(Square::from_index(trailing_zeros)) } + } else { + None + } + } } impl Default for BitBoard { @@ -377,4 +395,14 @@ mod tests { assert_eq!(BitBoard::from(Square::A1), BitBoard(0b1)); assert_eq!(BitBoard::from(Square::H8), BitBoard(1 << 63)); } + + #[test] + fn first_occupied_squares() { + let bb = bitboard![A8, E1]; + assert_eq!(bb.first_occupied_square(), Some(Square::A8)); + assert_eq!(bb.first_occupied_square_trailing(), Some(Square::E1)); + + let bb = bitboard![D6, E7, F8]; + assert_eq!(bb.first_occupied_square_trailing(), Some(Square::D6)); + } } diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 41951c7..4615caa 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -220,8 +220,8 @@ impl MoveLibrary { ray } - pub(super) fn ray(&self, sq: Square, dir: Direction) -> BitBoard { - self.rays[sq as usize][dir as usize] + pub(super) fn ray(&self, sq: Square, dir: Direction) -> &BitBoard { + &self.rays[sq as usize][dir as usize] } pub(super) fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard { diff --git a/position/src/sight.rs b/position/src/sight.rs index 76005a2..6e87d95 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -26,6 +26,10 @@ pub(crate) trait SightExt { fn king_sight(&self, pieces: &PieceBitBoards) -> BitBoard; } +pub(crate) trait SliderRayToSquareExt { + fn ray_to_square(&self, square: Square) -> Option; +} + impl SightExt for PlacedPiece { fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard { match self.shape() { @@ -183,8 +187,80 @@ impl SightExt for PlacedPiece { } } +impl SliderRayToSquareExt for PlacedPiece { + fn ray_to_square(&self, target: Square) -> Option { + macro_rules! ray { + ($square:expr, $direction:ident) => { + ( + BitBoard::ray($square, Direction::$direction), + Direction::$direction, + ) + }; + } + + let square = self.square(); + let target_bitboard: BitBoard = target.into(); + + let ray_and_direction = match self.shape() { + Shape::Bishop => [ + ray!(square, NorthEast), + ray!(square, SouthEast), + ray!(square, SouthWest), + ray!(square, NorthWest), + ] + .into_iter() + .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + Shape::Rook => [ + ray!(square, North), + ray!(square, East), + ray!(square, South), + ray!(square, West), + ] + .into_iter() + .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + Shape::Queen => [ + ray!(square, North), + ray!(square, NorthEast), + ray!(square, East), + ray!(square, SouthEast), + ray!(square, South), + ray!(square, SouthWest), + ray!(square, West), + ray!(square, NorthWest), + ] + .into_iter() + .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), + _ => None, + }; + + if let Some((ray, direction)) = ray_and_direction { + let first_occupied_square = match direction { + Direction::East + | Direction::NorthWest + | Direction::NorthEast + | Direction::North => ray.first_occupied_square_trailing(), + Direction::West + | Direction::SouthWest + | Direction::SouthEast + | Direction::South => ray.first_occupied_square(), + }; + + if let Some(occupied_square) = first_occupied_square { + let remainder = BitBoard::ray(target, direction); + return Some(ray & !remainder); + } + } + + None + } +} + #[cfg(test)] mod tests { + use super::*; + use chessfriend_bitboard::bitboard; + use chessfriend_core::{piece, Square}; + macro_rules! sight_test { ($test_name:ident, $position:expr, $piece:expr, $bitboard:expr) => { #[test] @@ -201,6 +277,12 @@ mod tests { }; } + #[test] + fn pawns_and_knights_cannot_make_rays() { + assert_eq!(piece!(White Pawn on F7).ray_to_square(Square::E8), None); + assert_eq!(piece!(White Knight on F6).ray_to_square(Square::E8), None); + } + mod pawn { use crate::test_position; use chessfriend_bitboard::{bitboard, BitBoard}; @@ -273,20 +355,25 @@ mod tests { } mod bishop { - use chessfriend_bitboard::bitboard; - use chessfriend_core::piece; + use super::*; sight_test!( c2_bishop, piece!(Black Bishop on C2), bitboard!(D1, B3, A4, B1, D3, E4, F5, G6, H7) ); + + #[test] + fn ray_to_square() { + let generated_ray = piece!(White Bishop on C5).ray_to_square(Square::E7); + let expected_ray = bitboard![D6, E7]; + assert_eq!(generated_ray, Some(expected_ray)); + } } mod rook { + use super::*; use crate::test_position; - use chessfriend_bitboard::bitboard; - use chessfriend_core::piece; sight_test!( g3_rook, @@ -304,6 +391,25 @@ mod tests { piece!(White Rook on E4), bitboard!(A4, B4, C4, D4, F4, G4, H4, E2, E3, E5, E6, E7) ); + + #[test] + fn ray_to_square() { + let generated_ray = piece!(White Rook on C2).ray_to_square(Square::C6); + let expected_ray = bitboard![C3, C4, C5, C6]; + assert_eq!(generated_ray, Some(expected_ray)); + + let generated_ray = piece!(White Rook on D2).ray_to_square(Square::H2); + let expected_ray = bitboard![E2, F2, G2, H2]; + assert_eq!(generated_ray, Some(expected_ray)); + + let generated_ray = piece!(White Rook on G6).ray_to_square(Square::B6); + let expected_ray = bitboard![B6, C6, D6, E6, F6]; + assert_eq!(generated_ray, Some(expected_ray)); + + let generated_ray = piece!(White Rook on A6).ray_to_square(Square::A3); + let expected_ray = bitboard![A5, A4, A3]; + assert_eq!(generated_ray, Some(expected_ray)); + } } mod king { From f1a05f33c96a25ec8328467245bdfff5de33de0c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:43:03 -0800 Subject: [PATCH 04/14] [position] Factor out the update_moves_with_ray! as ray_in_direction! This macro is implemented in every sight method. Factor it out, rename it, and use it in all these macros. --- position/src/sight.rs | 91 ++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 61 deletions(-) diff --git a/position/src/sight.rs b/position/src/sight.rs index 6e87d95..27cb53d 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -4,6 +4,20 @@ use crate::position::piece_sets::PieceBitBoards; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; +macro_rules! ray_in_direction { + ($square:expr, $blockers:expr, $direction:ident, $occupied_squares:tt) => {{ + let ray = BitBoard::ray($square, Direction::$direction); + if let Some(first_occupied_square) = BitBoard::$occupied_squares(&(ray & $blockers)).next() + { + let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); + let attack_ray = ray & !remainder; + attack_ray + } else { + *ray + } + }}; +} + pub(crate) trait SightExt { fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; @@ -92,25 +106,10 @@ impl SightExt for PlacedPiece { let blockers = pieces.all_pieces(); - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - sight |= attack_ray; - } else { - sight |= ray; - } - }; - } - - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); + sight |= ray_in_direction!(square, blockers, NorthEast, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, NorthWest, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, SouthEast, occupied_squares); + sight |= ray_in_direction!(square, blockers, SouthWest, occupied_squares); let friendly_pieces = pieces.all_pieces_of_color(self.color()); sight & !friendly_pieces @@ -123,25 +122,10 @@ impl SightExt for PlacedPiece { let blockers = pieces.all_pieces(); - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - sight |= attack_ray; - } else { - sight |= ray; - } - }; - } - - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(West, occupied_squares); + sight |= ray_in_direction!(square, blockers, North, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, East, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, South, occupied_squares); + sight |= ray_in_direction!(square, blockers, West, occupied_squares); let friendly_pieces = pieces.all_pieces_of_color(self.color()); sight & !friendly_pieces @@ -154,29 +138,14 @@ impl SightExt for PlacedPiece { let blockers = pieces.all_pieces(); - macro_rules! update_moves_with_ray { - ($direction:ident, $occupied_squares:tt) => { - let ray = BitBoard::ray(square, Direction::$direction); - if let Some(first_occupied_square) = - BitBoard::$occupied_squares(&(ray & blockers)).next() - { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - sight |= attack_ray; - } else { - sight |= ray; - } - }; - } - - update_moves_with_ray!(NorthWest, occupied_squares_trailing); - update_moves_with_ray!(North, occupied_squares_trailing); - update_moves_with_ray!(NorthEast, occupied_squares_trailing); - update_moves_with_ray!(East, occupied_squares_trailing); - update_moves_with_ray!(SouthEast, occupied_squares); - update_moves_with_ray!(South, occupied_squares); - update_moves_with_ray!(SouthWest, occupied_squares); - update_moves_with_ray!(West, occupied_squares); + sight |= ray_in_direction!(square, blockers, NorthWest, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, North, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, NorthEast, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, East, occupied_squares_trailing); + sight |= ray_in_direction!(square, blockers, SouthEast, occupied_squares); + sight |= ray_in_direction!(square, blockers, South, occupied_squares); + sight |= ray_in_direction!(square, blockers, SouthWest, occupied_squares); + sight |= ray_in_direction!(square, blockers, West, occupied_squares); let friendly_pieces = pieces.all_pieces_of_color(self.color()); sight & !friendly_pieces From 7b97060ba205f76c1a1166380349b59fa0b3126a Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:43:49 -0800 Subject: [PATCH 05/14] [position] The KingMoveGenerator doesn't use push or capture masks Mark these arguments with an _ so the linter stops complaining about it. --- position/src/move_generator/king.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index 7941bf3..b2d0d50 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -20,8 +20,8 @@ impl MoveGeneratorInternal for KingMoveGenerator { fn move_set_for_piece( position: &Position, placed_piece: PlacedPiece, - capture_mask: BitBoard, - push_mask: BitBoard, + _capture_mask: BitBoard, + _push_mask: BitBoard, ) -> MoveSet { let piece = placed_piece.piece(); let color = piece.color(); From deea23352bcbb6c4bad1bd9ecd678aa3a3e96c5d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:44:25 -0800 Subject: [PATCH 06/14] [bitboard] Implement BitBoard::population_count() Uses u64::count_ones() to return the population count of the BitBoard. --- bitboard/src/bitboard.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 4904c3e..b8a56ef 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -80,6 +80,20 @@ impl BitBoard { !(self & square_bitboard).is_empty() } + /// The number of 1 bits in the BitBoard. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// assert_eq!(BitBoard::EMPTY.population_count(), 0); + /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); + /// assert_eq!(BitBoard::FULL.population_count(), 64); + /// ``` + pub fn population_count(&self) -> u32 { + self.0.count_ones() + } + pub fn set_square(&mut self, sq: Square) { let sq_bb: BitBoard = sq.into(); *self |= sq_bb From ca861df9c4d1d73b3e4a7e35375fcaa52270236f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:44:53 -0800 Subject: [PATCH 07/14] [bitboard] Use TrailingBitScanner in occupied_squares_trailing This was an oversight. --- bitboard/src/bitboard.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index b8a56ef..4ed4719 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::library; -use crate::LeadingBitScanner; +use crate::{LeadingBitScanner, TrailingBitScanner}; use chessfriend_core::{Color, Direction, Square}; use std::fmt; use std::ops::Not; @@ -119,7 +119,7 @@ impl BitBoard { /// Return an Iterator over the occupied squares, starting from the trailing /// (least-significant bit) end of the field. pub fn occupied_squares_trailing(&self) -> impl Iterator { - LeadingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) + TrailingBitScanner::new(self.0).map(|idx| unsafe { Square::from_index(idx as u8) }) } pub fn first_occupied_square(&self) -> Option { From 032fabe072bb85c2fd6fd1a7c027d72939f81e5f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 1 Feb 2024 08:45:10 -0800 Subject: [PATCH 08/14] [bitboard] Return BitBoard::EMPTY from BitBoard's Default impl --- bitboard/src/bitboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 4ed4719..2453cce 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -143,7 +143,7 @@ impl BitBoard { impl Default for BitBoard { fn default() -> Self { - BitBoard::empty() + BitBoard::EMPTY } } From f09376f5dc6de86eb2c525606011393a04729378 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:25:59 -0800 Subject: [PATCH 09/14] [position] Make the checking_piece! macro handle specifying the path to the piece constructor --- position/src/position/position.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 7e211b4..75b4b79 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -265,19 +265,19 @@ impl Position { }; macro_rules! checking_piece { - ($moves_bb_fn:path, $piece_fn:path) => {{ + ($moves_bb_fn:path, $piece_fn:ident) => {{ let moves_from_opposing_square = $moves_bb_fn(king_square); - let piece = $piece_fn(opponent); + let piece = Piece::$piece_fn(opponent); let opposing_pieces = self.pieces.bitboard_for_piece(&piece); moves_from_opposing_square & opposing_pieces }}; } - let checking_knights = checking_piece!(BitBoard::knight_moves, Piece::knight); - let checking_bishops = checking_piece!(BitBoard::bishop_moves, Piece::bishop); - let checking_rooks = checking_piece!(BitBoard::rook_moves, Piece::rook); - let checking_queens = checking_piece!(BitBoard::queen_moves, Piece::queen); + let checking_knights = checking_piece!(BitBoard::knight_moves, knight); + let checking_bishops = checking_piece!(BitBoard::bishop_moves, bishop); + let checking_rooks = checking_piece!(BitBoard::rook_moves, rook); + let checking_queens = checking_piece!(BitBoard::queen_moves, queen); CheckingPieces::new( checking_pawns, From c8faad799e64af616539b698ad8ddb300039a062 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:28:12 -0800 Subject: [PATCH 10/14] [position] Clean up Position's construction scheme Position::default specifies the defaults for all field. Then, ::new() and ::starting() can use ..Default::default() in their implementations to avoid having to specify empty values for all the internal structures. --- position/src/position/position.rs | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 75b4b79..774fc26 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -26,16 +26,7 @@ pub struct Position { impl Position { pub fn empty() -> Position { - Position { - color_to_move: Color::White, - flags: Default::default(), - pieces: PieceBitBoards::default(), - en_passant_square: None, - sight: [OnceCell::new(), OnceCell::new()], - moves: OnceCell::new(), - half_move_counter: 0, - full_move_number: 1, - } + Default::default() } /// Return a starting position. @@ -60,13 +51,8 @@ impl Position { Self { color_to_move: Color::White, - flags: Flags::default(), pieces: PieceBitBoards::new([white_pieces, black_pieces]), - en_passant_square: None, - sight: [OnceCell::new(), OnceCell::new()], - moves: OnceCell::new(), - half_move_counter: 0, - full_move_number: 1, + ..Default::default() } } @@ -340,7 +326,16 @@ impl Position { impl Default for Position { fn default() -> Self { - Self::empty() + Self { + color_to_move: Color::White, + flags: Flags::default(), + pieces: PieceBitBoards::default(), + en_passant_square: None, + sight: [OnceCell::new(), OnceCell::new()], + moves: OnceCell::new(), + half_move_counter: 0, + full_move_number: 1, + } } } From ac07a8d6cfa6bca72f9581abe460a964197f4015 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:29:43 -0800 Subject: [PATCH 11/14] [position] Implement SliderRayToSquareExt on Shape instead of PlacedPiece Add an origin square argument to its one method ::ray_to_square(). Pushing this implementation down a layer means I don't have to care about the color of the piece. --- position/src/sight.rs | 72 +++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/position/src/sight.rs b/position/src/sight.rs index 27cb53d..056ae45 100644 --- a/position/src/sight.rs +++ b/position/src/sight.rs @@ -41,7 +41,7 @@ pub(crate) trait SightExt { } pub(crate) trait SliderRayToSquareExt { - fn ray_to_square(&self, square: Square) -> Option; + fn ray_to_square(&self, origin: Square, target: Square) -> Option; } impl SightExt for PlacedPiece { @@ -156,8 +156,8 @@ impl SightExt for PlacedPiece { } } -impl SliderRayToSquareExt for PlacedPiece { - fn ray_to_square(&self, target: Square) -> Option { +impl SliderRayToSquareExt for Shape { + fn ray_to_square(&self, origin: Square, target: Square) -> Option { macro_rules! ray { ($square:expr, $direction:ident) => { ( @@ -167,35 +167,34 @@ impl SliderRayToSquareExt for PlacedPiece { }; } - let square = self.square(); let target_bitboard: BitBoard = target.into(); - let ray_and_direction = match self.shape() { + let ray_and_direction = match self { Shape::Bishop => [ - ray!(square, NorthEast), - ray!(square, SouthEast), - ray!(square, SouthWest), - ray!(square, NorthWest), + ray!(origin, NorthEast), + ray!(origin, SouthEast), + ray!(origin, SouthWest), + ray!(origin, NorthWest), ] .into_iter() .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), Shape::Rook => [ - ray!(square, North), - ray!(square, East), - ray!(square, South), - ray!(square, West), + ray!(origin, North), + ray!(origin, East), + ray!(origin, South), + ray!(origin, West), ] .into_iter() .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), Shape::Queen => [ - ray!(square, North), - ray!(square, NorthEast), - ray!(square, East), - ray!(square, SouthEast), - ray!(square, South), - ray!(square, SouthWest), - ray!(square, West), - ray!(square, NorthWest), + ray!(origin, North), + ray!(origin, NorthEast), + ray!(origin, East), + ray!(origin, SouthEast), + ray!(origin, South), + ray!(origin, SouthWest), + ray!(origin, West), + ray!(origin, NorthWest), ] .into_iter() .find(|(&ray, _)| !(target_bitboard & ray).is_empty()), @@ -203,21 +202,8 @@ impl SliderRayToSquareExt for PlacedPiece { }; if let Some((ray, direction)) = ray_and_direction { - let first_occupied_square = match direction { - Direction::East - | Direction::NorthWest - | Direction::NorthEast - | Direction::North => ray.first_occupied_square_trailing(), - Direction::West - | Direction::SouthWest - | Direction::SouthEast - | Direction::South => ray.first_occupied_square(), - }; - - if let Some(occupied_square) = first_occupied_square { - let remainder = BitBoard::ray(target, direction); - return Some(ray & !remainder); - } + let remainder = BitBoard::ray(target, direction); + return Some(ray & !remainder); } None @@ -248,8 +234,8 @@ mod tests { #[test] fn pawns_and_knights_cannot_make_rays() { - assert_eq!(piece!(White Pawn on F7).ray_to_square(Square::E8), None); - assert_eq!(piece!(White Knight on F6).ray_to_square(Square::E8), None); + assert_eq!(Shape::Pawn.ray_to_square(Square::F7, Square::E8), None); + assert_eq!(Shape::Knight.ray_to_square(Square::F6, Square::E8), None); } mod pawn { @@ -334,7 +320,7 @@ mod tests { #[test] fn ray_to_square() { - let generated_ray = piece!(White Bishop on C5).ray_to_square(Square::E7); + let generated_ray = Shape::Bishop.ray_to_square(Square::C5, Square::E7); let expected_ray = bitboard![D6, E7]; assert_eq!(generated_ray, Some(expected_ray)); } @@ -363,19 +349,19 @@ mod tests { #[test] fn ray_to_square() { - let generated_ray = piece!(White Rook on C2).ray_to_square(Square::C6); + let generated_ray = Shape::Rook.ray_to_square(Square::C2, Square::C6); let expected_ray = bitboard![C3, C4, C5, C6]; assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = piece!(White Rook on D2).ray_to_square(Square::H2); + let generated_ray = Shape::Rook.ray_to_square(Square::D2, Square::H2); let expected_ray = bitboard![E2, F2, G2, H2]; assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = piece!(White Rook on G6).ray_to_square(Square::B6); + let generated_ray = Shape::Rook.ray_to_square(Square::G6, Square::B6); let expected_ray = bitboard![B6, C6, D6, E6, F6]; assert_eq!(generated_ray, Some(expected_ray)); - let generated_ray = piece!(White Rook on A6).ray_to_square(Square::A3); + let generated_ray = Shape::Rook.ray_to_square(Square::A6, Square::A3); let expected_ray = bitboard![A5, A4, A3]; assert_eq!(generated_ray, Some(expected_ray)); } From 31903cb514cce5d890bd0ae682569bc9ab53d13f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 07:53:50 -0800 Subject: [PATCH 12/14] [position] Calculate push and capture masks on CheckingPieces --- position/src/check.rs | 88 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/position/src/check.rs b/position/src/check.rs index d9d5172..df30749 100644 --- a/position/src/check.rs +++ b/position/src/check.rs @@ -1,13 +1,12 @@ // Eryn Wells use chessfriend_bitboard::BitBoard; +use chessfriend_core::{Color, Direction, Shape, Square}; + +use crate::sight::SliderRayToSquareExt; pub struct CheckingPieces { - pawn: BitBoard, - knight: BitBoard, - bishop: BitBoard, - rook: BitBoard, - queen: BitBoard, + bitboards: [BitBoard; 5], } impl CheckingPieces { @@ -19,11 +18,80 @@ impl CheckingPieces { queen: BitBoard, ) -> CheckingPieces { CheckingPieces { - pawn, - knight, - bishop, - rook, - queen, + bitboards: [pawn, knight, bishop, rook, queen], } } + + pub fn count(&self) -> u32 { + self.bitboards.iter().map(|b| b.population_count()).sum() + } + + /// A BitBoard representing the set of pieces that must be captured to + /// resolve check. + pub fn capture_mask(&self) -> BitBoard { + if self.count() == 0 { + BitBoard::FULL + } else { + self.bitboards + .iter() + .fold(BitBoard::EMPTY, std::ops::BitOr::bitor) + } + } + + /// A BitBoard representing the set of squares to which a player can move a + /// piece to block a checking piece. + pub fn push_mask(&self, king: &BitBoard) -> BitBoard { + let target = king.first_occupied_square().unwrap(); + + macro_rules! push_mask_for_shape { + ($push_mask:expr, $shape:ident, $king:expr) => {{ + let checking_pieces = self.bitboard_for_shape(Shape::$shape); + if !checking_pieces.is_empty() { + if let Some(checking_ray) = checking_pieces + .occupied_squares() + .flat_map(|sq| Shape::$shape.ray_to_square(sq, target).into_iter()) + .find(|bb| !(bb & $king).is_empty()) + { + $push_mask |= checking_ray & !$king + } + } + }}; + } + + let mut push_mask = BitBoard::EMPTY; + + push_mask_for_shape!(push_mask, Bishop, king); + push_mask_for_shape!(push_mask, Rook, king); + push_mask_for_shape!(push_mask, Queen, king); + + push_mask + } + + fn bitboard_for_shape(&self, shape: Shape) -> &BitBoard { + &self.bitboards[shape as usize] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chessfriend_bitboard::{bitboard, BitBoard}; + + /// This is a test position from [this execellent blog post][1] about how to + /// efficiently generate legal chess moves. + /// + /// [1]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ + #[test] + fn rook_push_mask() { + let checks = CheckingPieces::new( + BitBoard::EMPTY, + BitBoard::EMPTY, + BitBoard::EMPTY, + bitboard![E5], + BitBoard::EMPTY, + ); + + let push_mask = checks.push_mask(&bitboard![E8]); + assert_eq!(push_mask, bitboard![E6, E7]); + } } From 986758d0110de8c00d985f21452bf926ce4281e9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 08:03:51 -0800 Subject: [PATCH 13/14] [position] For non-King move generators, only generate moves if the push and captures masks aren't empty Always generate King moves. --- position/src/move_generator.rs | 19 ++++++++++++++----- position/src/move_generator/bishop.rs | 6 +++--- position/src/move_generator/king.rs | 8 ++++---- position/src/move_generator/knight.rs | 8 ++++---- position/src/move_generator/pawn.rs | 8 ++++---- position/src/move_generator/queen.rs | 6 +++--- position/src/move_generator/rook.rs | 6 +++--- 7 files changed, 35 insertions(+), 26 deletions(-) diff --git a/position/src/move_generator.rs b/position/src/move_generator.rs index 7e193c1..7f7d481 100644 --- a/position/src/move_generator.rs +++ b/position/src/move_generator.rs @@ -51,10 +51,15 @@ macro_rules! move_generator_declaration { capture_mask: chessfriend_bitboard::BitBoard, push_mask: chessfriend_bitboard::BitBoard, ) -> $name { - $name { - color, - move_sets: Self::move_sets(position, color, capture_mask, push_mask), - } + let move_sets = if Self::shape() == chessfriend_core::Shape::King + || (!capture_mask.is_empty() && !push_mask.is_empty()) + { + Self::move_sets(position, color, capture_mask, push_mask) + } else { + std::collections::BTreeMap::new() + }; + + $name { color, move_sets } } } }; @@ -84,7 +89,11 @@ macro_rules! move_generator_declaration { pub(self) use move_generator_declaration; trait MoveGeneratorInternal { - fn piece(color: Color) -> Piece; + fn shape() -> Shape; + + fn piece(color: Color) -> Piece { + Piece::new(color, Self::shape()) + } fn move_sets( position: &Position, diff --git a/position/src/move_generator/bishop.rs b/position/src/move_generator/bishop.rs index 5b72f78..2fae36f 100644 --- a/position/src/move_generator/bishop.rs +++ b/position/src/move_generator/bishop.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; +use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::bishop(color) + fn shape() -> Shape { + Shape::Bishop } fn move_set_for_piece( diff --git a/position/src/move_generator/king.rs b/position/src/move_generator/king.rs index b2d0d50..c18692a 100644 --- a/position/src/move_generator/king.rs +++ b/position/src/move_generator/king.rs @@ -6,15 +6,15 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::{r#move::Castle, Position}; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece}; +use chessfriend_core::{PlacedPiece, Shape}; move_generator_declaration!(KingMoveGenerator, struct); move_generator_declaration!(KingMoveGenerator, new); move_generator_declaration!(KingMoveGenerator, getters); impl MoveGeneratorInternal for KingMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::king(color) + fn shape() -> Shape { + Shape::King } fn move_set_for_piece( @@ -59,7 +59,7 @@ mod tests { use super::*; use crate::{assert_move_list, position, test_position, Move, MoveBuilder, PositionBuilder}; use chessfriend_bitboard::bitboard; - use chessfriend_core::{piece, Square}; + use chessfriend_core::{piece, Color, Square}; use std::collections::HashSet; #[test] diff --git a/position/src/move_generator/knight.rs b/position/src/move_generator/knight.rs index 66ee8ba..b4dc1bc 100644 --- a/position/src/move_generator/knight.rs +++ b/position/src/move_generator/knight.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece}; +use chessfriend_core::{PlacedPiece, Shape}; move_generator_declaration!(KnightMoveGenerator); impl MoveGeneratorInternal for KnightMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::knight(color) + fn shape() -> Shape { + Shape::Knight } fn move_set_for_piece( @@ -35,7 +35,7 @@ impl MoveGeneratorInternal for KnightMoveGenerator { mod tests { use super::*; use crate::{position, Move, MoveBuilder}; - use chessfriend_core::{piece, Square}; + use chessfriend_core::{piece, Color, Square}; use std::collections::HashSet; #[test] diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index f68baf4..c8b2e6c 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -3,7 +3,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, PlacedPiece, Rank, Square}; +use chessfriend_core::{Piece, PlacedPiece, Rank, Shape, Square}; #[derive(Debug)] struct MoveIterator(usize, usize); @@ -11,8 +11,8 @@ struct MoveIterator(usize, usize); move_generator_declaration!(PawnMoveGenerator); impl MoveGeneratorInternal for PawnMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::pawn(color) + fn shape() -> Shape { + Shape::Pawn } fn move_set_for_piece( @@ -63,7 +63,7 @@ impl PawnMoveGenerator { mod tests { use super::*; use crate::{assert_move_list, position::DiagramFormatter, test_position, Move, MoveBuilder}; - use chessfriend_core::{piece, Square}; + use chessfriend_core::{piece, Color, Square}; use std::collections::HashSet; #[test] diff --git a/position/src/move_generator/queen.rs b/position/src/move_generator/queen.rs index ad913fe..dc41769 100644 --- a/position/src/move_generator/queen.rs +++ b/position/src/move_generator/queen.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; +use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::queen(color) + fn shape() -> Shape { + Shape::Queen } fn move_set_for_piece( diff --git a/position/src/move_generator/rook.rs b/position/src/move_generator/rook.rs index b440273..18129c8 100644 --- a/position/src/move_generator/rook.rs +++ b/position/src/move_generator/rook.rs @@ -3,13 +3,13 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece}; +use chessfriend_core::{Direction, PlacedPiece, Shape}; move_generator_declaration!(ClassicalMoveGenerator); impl MoveGeneratorInternal for ClassicalMoveGenerator { - fn piece(color: Color) -> Piece { - Piece::rook(color) + fn shape() -> Shape { + Shape::Rook } fn move_set_for_piece( From 942c758e151450576d98f362ad29a7075974c8d5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 2 Feb 2024 08:05:37 -0800 Subject: [PATCH 14/14] [position] Calculate legal moves based on whether the king is in check Use CheckingPieces to determine which and how many pieces check the king. - If there are no checks, proceed with move generation as normal. - If there is one checking piece, calculate push and capture masks and use those to generate legal moves. - If there are more than one checking pieces, the only legal moves are king moves. Indicate this by setting the push and capture masks to EMPTY. --- position/src/position/position.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/position/src/position/position.rs b/position/src/position/position.rs index 774fc26..f0f8414 100644 --- a/position/src/position/position.rs +++ b/position/src/position/position.rs @@ -2,11 +2,11 @@ use super::{flags::Flags, piece_sets::PieceBitBoards, Pieces}; use crate::{ - move_generator::{MoveSet, Moves}, + check::{self, CheckingPieces}, + move_generator::Moves, position::DiagramFormatter, r#move::Castle, sight::SightExt, - Move, }; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; @@ -121,8 +121,22 @@ impl Position { } pub fn moves(&self) -> &Moves { - self.moves - .get_or_init(|| Moves::new(self, self.color_to_move, BitBoard::FULL, BitBoard::FULL)) + self.moves.get_or_init(|| { + let checking_pieces = self.checking_pieces(); + match checking_pieces.count() { + // Normal, unrestricted move generation + 0 => Moves::new(self, self.color_to_move, BitBoard::FULL, BitBoard::FULL), + 1 => { + // Calculate push and capture masks for checking piece. Moves are restricted to those that intersect those masks. + let capture_mask = checking_pieces.capture_mask(); + let push_mask = + checking_pieces.push_mask(self.king_bitboard(self.color_to_move)); + Moves::new(self, self.color_to_move, capture_mask, push_mask) + } + // With more than one checking piece, the only legal moves are king moves. + _ => Moves::new(self, self.color_to_move, BitBoard::EMPTY, BitBoard::EMPTY), + } + }) } /// Return a BitBoard representing the set of squares containing a piece.