From 89a9588e69e00ffb405e8e32f70259e569b7ec2f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 1 Jul 2025 13:02:35 -0700 Subject: [PATCH 01/10] [core] Implement Slider::ray_direction() This method returns iterators over the directions each kind of slider can move. --- core/src/shapes.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 0cedc7c..0fa3ab0 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -1,10 +1,9 @@ // Eryn Wells +use crate::{Direction, score::Score}; use std::{array, fmt, slice, str::FromStr}; use thiserror::Error; -use crate::score::Score; - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { Pawn = 0, @@ -93,6 +92,54 @@ pub enum Slider { Queen, } +impl Slider { + pub const NUM: usize = 3; + pub const ALL: [Self; Self::NUM] = [Slider::Bishop, Slider::Rook, Slider::Queen]; + + /// Return the set of directions a slider can move. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_core::{Direction, Slider}; + /// + /// assert_eq!( + /// Slider::Bishop.ray_directions().collect::>(), + /// vec![ + /// Direction::NorthWest, + /// Direction::NorthEast, + /// Direction::SouthEast, + /// Direction::SouthWest + /// ] + /// ); + /// ``` + /// + #[must_use] + pub fn ray_directions(self) -> Box> { + match self { + Slider::Bishop => Box::new( + [ + Direction::NorthWest, + Direction::NorthEast, + Direction::SouthEast, + Direction::SouthWest, + ] + .into_iter(), + ), + Slider::Rook => Box::new( + [ + Direction::North, + Direction::East, + Direction::South, + Direction::West, + ] + .into_iter(), + ), + Slider::Queen => Box::new(Direction::ALL.into_iter()), + } + } +} + impl From for Shape { fn from(value: Slider) -> Self { match value { From 21a2a37e1f843684aa589c809db23df441c01edb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 2 Jul 2025 18:30:13 -0700 Subject: [PATCH 02/10] [board] Implement Board::is_king_attacked_from_square Calculate whether the active color's king is attacked from the provided Square. --- board/src/check.rs | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/board/src/check.rs b/board/src/check.rs index 6d8ceba..e905498 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -2,19 +2,34 @@ use crate::Board; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece}; +use chessfriend_core::{Color, Piece, Square}; impl Board { /// Return whether the active color is in check. #[must_use] pub fn is_in_check(&self) -> bool { let color = self.active_color(); - let king = self.king_bitboard(color); + let king = self.king_bitboard_active(); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() } - fn king_bitboard(&self, color: Color) -> BitBoard { + + fn is_king_attacked_from_square(&self, square: Square) -> bool { + let active_king = self.king_bitboard_active(); + let sight_from_square = self.sight_piece(square); + (active_king & sight_from_square).is_populated() + } + + fn king_bitboard(&self, color: Option) -> BitBoard { + self.king_bitboard_unwrapped(self.unwrap_color(color)) + } + + fn king_bitboard_active(&self) -> BitBoard { + self.king_bitboard_unwrapped(self.active_color()) + } + + fn king_bitboard_unwrapped(&self, color: Color) -> BitBoard { self.find_pieces(Piece::king(color)) } } @@ -22,7 +37,7 @@ impl Board { #[cfg(test)] mod tests { use crate::test_board; - use chessfriend_core::Color; + use chessfriend_core::Square; #[test] fn active_color_is_in_check() { @@ -43,4 +58,17 @@ mod tests { assert!(!board.is_in_check()); } + + #[test] + fn king_is_attacked_from_square() { + let board = test_board!( + White King on D3, + Black Rook on H3, + Black Queen on F4 + ); + + assert!(board.is_king_attacked_from_square(Square::H3)); + assert!(!board.is_king_attacked_from_square(Square::F4)); + assert!(!board.is_king_attacked_from_square(Square::A3)); + } } From 0a2dd7e09d21590c989cc5f34f486a40285ff31d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:08:25 -0700 Subject: [PATCH 03/10] [bitboard] Replace some references to BitBoard::full() and BitBoard::empty() with the const values Two doc tests reference the methods instead of the const variables. Update them. --- bitboard/src/bitboard.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index f896e57..a16297d 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -160,9 +160,9 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert_eq!(BitBoard::empty().population_count(), 0); + /// assert_eq!(BitBoard::EMPTY.population_count(), 0); /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); - /// assert_eq!(BitBoard::full().population_count(), 64); + /// assert_eq!(BitBoard::FULL.population_count(), 64); /// ``` #[must_use] pub const fn population_count(&self) -> u32 { @@ -211,8 +211,8 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::empty().is_single_square(), "Empty bitboards represent no squares"); - /// assert!(!BitBoard::full().is_single_square(), "Full bitboards represent all the squares"); + /// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares"); + /// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares"); /// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares"); /// assert!(BitBoard::new(0b10000000000000).is_single_square()); /// ``` From b4536ffbe3f531105747f02ca72e363894914e9d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:15 -0700 Subject: [PATCH 04/10] [board] Remove a useless .into() call Clippy pointed this out to me. This .into() call serves no purpose. --- board/src/castle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/castle.rs b/board/src/castle.rs index 5acdaaf..4ba9a4b 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.has_castling_right_unwrapped(color, wing.into()) { + if !self.has_castling_right_unwrapped(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } From 7d3fd4b20a75ffbd4cbfc521515c976dbbfadeee Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:55 -0700 Subject: [PATCH 05/10] [core] Make Shape::is_promotable() const --- core/src/shapes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 0fa3ab0..7bc0a0b 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -69,7 +69,7 @@ impl Shape { } #[must_use] - pub fn is_promotable(&self) -> bool { + pub const fn is_promotable(&self) -> bool { matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen) } From f658903b5471ce07ad8d2206ed95381df1673151 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:11:52 -0700 Subject: [PATCH 06/10] [core] Export Score::CENTIPAWNS_PER_POINT to the crate This constant is a conversion factor of points to the internal fixed point unit of centipawns. Points are more familiar to people because pawns are worth 1 pt. Calculate the scores of the various piece shapes with this constant. --- core/src/score.rs | 2 +- core/src/shapes.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/src/score.rs b/core/src/score.rs index 2dfd7c9..44d0478 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -31,7 +31,7 @@ impl Score { /// The maximum possible value of a score. pub const MAX: Score = Score(Value::MAX); - const CENTIPAWNS_PER_POINT: f32 = 100.0; + pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0; #[must_use] pub const fn new(value: Value) -> Self { diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 7bc0a0b..ba3a57b 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -74,13 +74,16 @@ impl Shape { } #[must_use] - pub fn score(self) -> Score { + pub const fn score(self) -> Score { + #[allow(clippy::cast_possible_truncation)] + const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32; + match self { - Shape::Pawn => Score::new(100), - Shape::Knight | Shape::Bishop => Score::new(300), - Shape::Rook => Score::new(500), - Shape::Queen => Score::new(900), - Shape::King => Score::new(20000), + Shape::Pawn => Score::new(CP_PER_PT), + Shape::Knight | Shape::Bishop => Score::new(3 * CP_PER_PT), + Shape::Rook => Score::new(5 * CP_PER_PT), + Shape::Queen => Score::new(9 * CP_PER_PT), + Shape::King => Score::new(200 * CP_PER_PT), } } } From 6e8c0d346692103c395ffac712a003ed3d29bff0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:12:34 -0700 Subject: [PATCH 07/10] [core] Fix an incorrect assertion in the Score doc test Negate with - instead of with !. --- core/src/score.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/score.rs b/core/src/score.rs index 44d0478..3528861 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -23,7 +23,7 @@ impl Score { /// /// ``` /// use chessfriend_core::score::Score; - /// assert_eq!(!Score::MIN, Score::MAX); + /// assert_eq!(-Score::MIN, Score::MAX); /// ``` /// pub const MIN: Score = Score(Value::MIN + 1); From d3c94356bd9ca0e5e26e9c37eb2486037b248300 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:19:09 -0700 Subject: [PATCH 08/10] [bitboard] Implement BitBoard::occupied_squares_direction Iterate a BitBoard in a direction (from leading or trailing edge) based on a board direction. --- bitboard/src/bitboard.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index a16297d..6eb63eb 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -233,6 +233,38 @@ impl BitBoard { } } + /// Iterate through the occupied squares in a direction specified by a + /// compass direction. This method is mose useful for bitboards of slider + /// rays so that iteration proceeds in order along the ray's direction. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// use chessfriend_core::{Direction, Square}; + /// + /// let ray = BitBoard::ray(Square::E4, Direction::North); + /// assert_eq!( + /// ray.occupied_squares_direction(Direction::North).collect::>(), + /// vec![Square::E5, Square::E6, Square::E7, Square::E8] + /// ); + /// ``` + /// + #[must_use] + pub fn occupied_squares_direction( + &self, + direction: Direction, + ) -> Box> { + match direction { + Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => { + Box::new(self.occupied_squares_trailing()) + } + Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => { + Box::new(self.occupied_squares_leading()) + } + } + } + #[must_use] pub fn occupied_squares_leading(&self) -> LeadingBitScanner { LeadingBitScanner::new(self.0) From 5444d8ea3a58842c880db1a79a84f2d9fda700e2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:20:04 -0700 Subject: [PATCH 09/10] [bitboard] Add a doc comment to BitBoard::first_occupied_square --- bitboard/src/bitboard.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 6eb63eb..ccee9bd 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -287,6 +287,12 @@ impl BitBoard { } } + /// Get the first occupied square in the given direction. + /// + /// ## To-Do + /// + /// - Take `direction` by value instead of reference + /// #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { From 1c94f157e671e07522843b27216c722d7c21f1fe Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:26:41 -0700 Subject: [PATCH 10/10] [board] First pass at computing checks Calculate checks using a method described on a blog post I read.[1] This requires knowing some details about the move that was just made: the origin and target squares. The piece moved is also passed to the method, though it could look it up. First look for a direct check, where the piece that just moved is now attacking the king. Second, look for revealed checks, where the moved piece reveals a ray from a sliding piece that attacks the king. This check information is collected into a CheckingPieces struct that describes for the newly active player how they can get out of check. The push mask is a BitBoard of the set of squares the player can move to in order to block the check. The capture mask is the set of pieces that must be captured to resolve the check. And the king moves BitBoard shows the squares the king can move to, accounting for the opposing player's sight. [1]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ --- board/src/check.rs | 255 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 240 insertions(+), 15 deletions(-) diff --git a/board/src/check.rs b/board/src/check.rs index e905498..ca7eca2 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -2,42 +2,241 @@ use crate::Board; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Square}; +use chessfriend_core::{Piece, Shape, Slider, Square}; +use std::{convert::Into, ops::BitOr}; impl Board { /// Return whether the active color is in check. #[must_use] pub fn is_in_check(&self) -> bool { let color = self.active_color(); - let king = self.king_bitboard_active(); + let king = self.kings(self.active_color()); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() } + /// Calculate checks on the board. + /// + /// ## Panics + /// + /// If the board has multiple kings for any color, this method will panic. + /// + #[must_use] + pub fn calculate_checks( + &self, + piece_moved: &Piece, + last_ply_origin: Square, + last_ply_target: Square, + ) -> CheckingPieces { + let active_king = self.kings(self.active_color()); - fn is_king_attacked_from_square(&self, square: Square) -> bool { - let active_king = self.king_bitboard_active(); + let active_king_square: Square = active_king + .try_into() + .expect("active color has more than one king on the board"); + + let mut checks: Vec = vec![]; + + // Calculate a direct check, where the piece that just moved attacks the + // king. + + if let Ok(sight) = self.is_king_attacked_from_square(last_ply_target) { + checks.push(CheckInfo { + square: last_ply_target, + piece: *piece_moved, + sight, + }); + } + + // Look for revealed checks, where moving a piece opens up an attacking + // ray from a slider to the king. + + let inactive_color = self.active_color().other(); + + for slider in Slider::ALL { + let shape: Shape = slider.into(); + let piece = Piece::new(inactive_color, shape); + + for square in self.find_pieces(piece).occupied_squares_trailing() { + if let Some(check) = + self.calculate_revealed_check(slider, square, active_king, last_ply_origin) + { + checks.push(check); + } + } + } + + let king_moves = BitBoard::king_moves(active_king_square); + // TODO: Bitwise OR with the sight mask of the opposing color. + + CheckingPieces::new(&checks, active_king_square, king_moves) + } + + fn is_king_attacked_from_square(&self, square: Square) -> Result { + let active_king = self.kings(self.active_color()); let sight_from_square = self.sight_piece(square); - (active_king & sight_from_square).is_populated() + if (active_king & sight_from_square).is_populated() { + Ok(sight_from_square) + } else { + Err(()) + } } - fn king_bitboard(&self, color: Option) -> BitBoard { - self.king_bitboard_unwrapped(self.unwrap_color(color)) + fn calculate_revealed_check( + &self, + slider: Slider, + square: Square, + active_king: BitBoard, + last_ply_origin: Square, + ) -> Option { + println!( + "Examining {} on {square} for checks", + Into::::into(slider).name() + ); + + let origin: BitBoard = last_ply_origin.into(); + + let sight = self.sight_piece(square); + let piece_is_attacking_origin = (sight & origin).is_populated(); + if !piece_is_attacking_origin { + println!("does not attack move origin"); + return None; + } + + let origin_and_king = active_king | origin; + println!("origin and king\n{origin_and_king}"); + + for direction in slider.ray_directions() { + let ray = BitBoard::ray(square, direction); + println!("ray\n{ray}"); + + let ray_attacks = ray & origin_and_king; + println!("ray attacks\n{ray_attacks}"); + + // When a check by a slider is revealed by moving another piece, the + // slider will first attack the square the piece was moved from + // (a.k.a. the origin), and also attack the king square. The + // attacked king square must immediately follow the attacked origin + // square, and the attacked origin square must be the first attacked + // square in the ray. + + let mut occupied_squares = ray_attacks.occupied_squares_direction(direction); + if let (Some(first_square), Some(second_square)) = + (occupied_squares.next(), occupied_squares.next()) + { + if first_square != last_ply_origin { + continue; + } + + if Into::::into(second_square) != active_king { + continue; + } + + println!("found check ray\n{ray}"); + + return Some(CheckInfo { + square, + piece: Piece::new(self.active_color(), slider.into()), + sight: ray, + }); + } + } + + None + } +} + +struct CheckInfo { + /// The square the checking piece is on in the current position. + square: Square, + + /// The piece checking the king. + piece: Piece, + + /// The complete sight or direct ray that attacks the king. + sight: BitBoard, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CheckingPieces { + /// Number of checking pieces + count: usize, + + /// A [`BitBoard`] representing the set of pieces to which the color in + /// check can move a piece to block the check. This bitboard includes + /// squares along sight rays of opposing slider pieces. + push_mask: BitBoard, + + /// A [`BitBoard`] representing the set of pieces that can be captured to + /// resolve the check. + capture_mask: BitBoard, + + /// A [`BitBoard`] representing the moves the king can make to get out of + /// check. + king_moves: BitBoard, +} + +impl CheckingPieces { + fn new(checks: &[CheckInfo], king: Square, king_moves: BitBoard) -> Self { + if checks.is_empty() { + return Self { + count: 0, + push_mask: BitBoard::FULL, + capture_mask: BitBoard::EMPTY, + king_moves, + }; + } + + let count = checks.len(); + let push_mask = CheckingPieces::calculate_push_mask(king, checks); + let capture_mask = CheckingPieces::calculate_capture_mask(checks); + + Self { + count, + push_mask, + capture_mask, + king_moves, + } } - fn king_bitboard_active(&self) -> BitBoard { - self.king_bitboard_unwrapped(self.active_color()) + pub fn is_empty(&self) -> bool { + self.count == 0 } - fn king_bitboard_unwrapped(&self, color: Color) -> BitBoard { - self.find_pieces(Piece::king(color)) + /// The number of checking pieces. + pub fn len(&self) -> usize { + self.count + } + + /// A [`BitBoard`] representing the set of pieces that must be captured to + /// resolve check. + fn calculate_capture_mask(checks: &[CheckInfo]) -> BitBoard { + if checks.is_empty() { + BitBoard::FULL + } else { + checks + .iter() + .map(|c| Into::::into(c.square)) + .fold(BitBoard::EMPTY, BitOr::bitor) + } + } + + /// A [`BitBoard`] representing the set of squares to which a player can + /// move a piece to block a checking piece. + fn calculate_push_mask(king: Square, checks: &[CheckInfo]) -> BitBoard { + let king_moves = BitBoard::king_moves(king); + let push_mask = checks + .iter() + .map(|c| c.sight) + .fold(BitBoard::EMPTY, BitOr::bitor); + + king_moves & !push_mask } } #[cfg(test)] mod tests { use crate::test_board; - use chessfriend_core::Square; + use chessfriend_core::{Square, piece}; #[test] fn active_color_is_in_check() { @@ -67,8 +266,34 @@ mod tests { Black Queen on F4 ); - assert!(board.is_king_attacked_from_square(Square::H3)); - assert!(!board.is_king_attacked_from_square(Square::F4)); - assert!(!board.is_king_attacked_from_square(Square::A3)); + assert!(board.is_king_attacked_from_square(Square::H3).is_ok()); + assert!(board.is_king_attacked_from_square(Square::F4).is_err()); + assert!(board.is_king_attacked_from_square(Square::A3).is_err()); + } + + #[test] + fn calculate_two_checks() { + let board = test_board!( + White King on D3, + Black Rook on H3, + Black Queen on F5 + ); + + let checks = board.calculate_checks(&piece!(Black Queen), Square::F3, Square::F5); + + assert!(!checks.is_empty()); + assert_eq!(checks.len(), 2); + } + + #[test] + fn calculate_en_passant_revealed_check() { + let board = test_board!(White, [ + White King on D4, + Black Pawn on E3, + Black Rook on H4 + ], E4); + + let checks = board.calculate_checks(&piece!(Black Pawn), Square::F4, Square::E3); + assert_eq!(checks.len(), 1); } }