From 89a9588e69e00ffb405e8e32f70259e569b7ec2f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 1 Jul 2025 13:02:35 -0700 Subject: [PATCH 01/20] [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/20] [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/20] [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/20] [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/20] [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/20] [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/20] [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/20] [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/20] [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/20] [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); } } From 45183c910ca95609b1e318cd7359fe22d600a1cf Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:08:25 -0700 Subject: [PATCH 11/20] [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 484fcf342e4e85d4734b1343f4b955ffa72a6227 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:15 -0700 Subject: [PATCH 12/20] [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 b3ff8dec49ee2e8cddd1718da3d8801bccbd9910 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:55 -0700 Subject: [PATCH 13/20] [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 0cedc7c..9edbbb5 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -70,7 +70,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 b50560692594f5c1b290634c7f72422ca329ad20 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:11:52 -0700 Subject: [PATCH 14/20] [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 9edbbb5..77126ba 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -75,13 +75,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 146e4d34d3b9daf89a1347fe6ef6ee617b12216b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:12:34 -0700 Subject: [PATCH 15/20] [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 b6d27356accafd3dcbedf7f3ca87ad6d0ca7beb9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:19:09 -0700 Subject: [PATCH 16/20] [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 3a0541a2c310474a6c6c91dd0dbb1fa156ed8e67 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:20:04 -0700 Subject: [PATCH 17/20] [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 3d73760146461a4d4b80289ef6e8ce06102d5464 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 16:14:34 -0700 Subject: [PATCH 18/20] [bitboard, board] Remove BitBoard::empty() and BitBoard::full() These have been deprecated for a while. Clean up the remaining uses and remove the methods from BitBoard. --- bitboard/src/bitboard.rs | 18 ++++-------------- bitboard/src/library.rs | 18 +++++++++--------- board/src/movement.rs | 20 ++++++++++---------- board/src/sight.rs | 2 +- 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index ccee9bd..35ce927 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -46,16 +46,6 @@ impl BitBoard { pub const EMPTY: BitBoard = BitBoard(u64::MIN); pub const FULL: BitBoard = BitBoard(u64::MAX); - #[deprecated(note = "Use BitBoard::EMPTY instead")] - pub const fn empty() -> BitBoard { - Self::EMPTY - } - - #[deprecated(note = "Use BitBoard::FULL instead")] - pub const fn full() -> BitBoard { - Self::FULL - } - pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) } @@ -109,7 +99,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(BitBoard::empty().is_empty()); + /// assert!(BitBoard::EMPTY.is_empty()); /// assert!(!BitBoard::full().is_empty()); /// assert!(!BitBoard::new(0b1000).is_empty()); /// ``` @@ -125,7 +115,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::empty().is_populated()); + /// assert!(!BitBoard::EMPTY.is_populated()); /// assert!(BitBoard::full().is_populated()); /// assert!(BitBoard::new(0b1).is_populated()); /// ``` @@ -564,8 +554,8 @@ mod tests { let b = bitboard![B5 G7 H3]; assert_eq!(a ^ b, bitboard![B5 C5 H3]); - assert_eq!(a ^ BitBoard::empty(), a); - assert_eq!(BitBoard::empty() ^ BitBoard::empty(), BitBoard::empty()); + assert_eq!(a ^ BitBoard::EMPTY, a); + assert_eq!(BitBoard::EMPTY ^ BitBoard::EMPTY, BitBoard::EMPTY); } #[test] diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 6a60392..3ea670c 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -110,14 +110,14 @@ pub(super) struct MoveLibrary { impl MoveLibrary { const fn new() -> MoveLibrary { MoveLibrary { - rays: [[BitBoard::empty(); Direction::NUM]; Square::NUM], - pawn_attacks: [[BitBoard::empty(); Square::NUM]; Color::NUM], - pawn_pushes: [[BitBoard::empty(); Square::NUM]; Color::NUM], - knight_moves: [BitBoard::empty(); Square::NUM], - bishop_moves: [BitBoard::empty(); Square::NUM], - rook_moves: [BitBoard::empty(); Square::NUM], - queen_moves: [BitBoard::empty(); Square::NUM], - king_moves: [BitBoard::empty(); Square::NUM], + rays: [[BitBoard::EMPTY; Direction::NUM]; Square::NUM], + pawn_attacks: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], + pawn_pushes: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], + knight_moves: [BitBoard::EMPTY; Square::NUM], + bishop_moves: [BitBoard::EMPTY; Square::NUM], + rook_moves: [BitBoard::EMPTY; Square::NUM], + queen_moves: [BitBoard::EMPTY; Square::NUM], + king_moves: [BitBoard::EMPTY; Square::NUM], } } @@ -238,7 +238,7 @@ impl MoveLibrary { } fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { - let mut ray = BitBoard::empty(); + let mut ray = BitBoard::EMPTY; let mut iter = shift(&sq); while !iter.is_empty() { diff --git a/board/src/movement.rs b/board/src/movement.rs index 2935eee..3ebf44c 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -41,7 +41,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::KingSide, color); parameters.target.king.into() } else { - BitBoard::empty() + BitBoard::EMPTY }; let queenside_target_square = if board @@ -51,7 +51,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::QueenSide, color); parameters.target.king.into() } else { - BitBoard::empty() + BitBoard::EMPTY }; self.sight(square, board) | kingside_target_square | queenside_target_square @@ -99,11 +99,11 @@ mod tests { #[test] fn white_pushes_empty_board() { assert_eq!( - pawn_pushes(Square::E4.into(), Color::White, BitBoard::empty()), + pawn_pushes(Square::E4.into(), Color::White, BitBoard::EMPTY), bitboard![E5] ); assert_eq!( - pawn_pushes(Square::E2.into(), Color::White, BitBoard::empty()), + pawn_pushes(Square::E2.into(), Color::White, BitBoard::EMPTY), bitboard![E3 E4] ); } @@ -111,11 +111,11 @@ mod tests { #[test] fn black_pawn_empty_board() { assert_eq!( - pawn_pushes(Square::A4.into(), Color::Black, BitBoard::empty()), + pawn_pushes(Square::A4.into(), Color::Black, BitBoard::EMPTY), bitboard![A3] ); assert_eq!( - pawn_pushes(Square::B7.into(), Color::Black, BitBoard::empty()), + pawn_pushes(Square::B7.into(), Color::Black, BitBoard::EMPTY), bitboard![B6 B5] ); } @@ -124,7 +124,7 @@ mod tests { fn white_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]), - BitBoard::empty() + BitBoard::EMPTY ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]), @@ -132,7 +132,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]), - BitBoard::empty() + BitBoard::EMPTY ); } @@ -140,7 +140,7 @@ mod tests { fn black_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]), - BitBoard::empty() + BitBoard::EMPTY ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]), @@ -148,7 +148,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]), - BitBoard::empty() + BitBoard::EMPTY ); } } diff --git a/board/src/sight.rs b/board/src/sight.rs index b8e366e..e682cb0 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -305,7 +305,7 @@ mod tests { let piece = piece!(White Pawn); let sight = piece.sight(Square::E4, &pos); - assert_eq!(sight, BitBoard::empty()); + assert_eq!(sight, BitBoard::EMPTY); } #[test] From 182bf8112669b156faa27f63e87384e1cfff9df5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 16:15:09 -0700 Subject: [PATCH 19/20] [board] Fix a counter underflow in the piece set During perft runs, the PieceSet counter would occasionally underflow, causing the whole program to crash. This is because, when building a Board from a list of bitboards, Counts::increment() was only being called once, even when the bitboard had more than one piece in it. Fix the bug by incrementing during the loop that sets up the mailbox. Additionally, refactor the increment() and decrement() methods to be a little more succinct. --- board/src/piece_sets.rs | 3 +-- board/src/piece_sets/counts.rs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index de43caa..52af054 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -51,11 +51,10 @@ impl PieceSet { color_occupancy[color_index] |= bitboard; shape_occupancy[shape_index] |= bitboard; - counts.increment(color, shape); - for square in bitboard.occupied_squares(&IterationDirection::default()) { let piece = Piece::new(color, shape); mailbox.set(piece, square); + counts.increment(color, shape); } } } diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs index effbbe0..7d3cade 100644 --- a/board/src/piece_sets/counts.rs +++ b/board/src/piece_sets/counts.rs @@ -17,20 +17,21 @@ impl Counts { const SQUARE_NUM: u8 = Square::NUM as u8; let updated_value = self.0[color as usize][shape as usize] + 1; - if updated_value <= SQUARE_NUM { - self.0[color as usize][shape as usize] = updated_value; - } else { - unreachable!("piece count for {color} {shape} overflowed"); + if updated_value > SQUARE_NUM { + let shape_name = shape.name(); + panic!("piece count for {color} {shape_name} overflowed"); } + + self.0[color as usize][shape as usize] = updated_value; } pub fn decrement(&mut self, color: Color, shape: Shape) { let count = self.0[color as usize][shape as usize]; - if let Some(updated_count) = count.checked_sub(1) { - self.0[color as usize][shape as usize] = updated_count; - } else { - unreachable!("piece count for {color} {shape} underflowed"); - } + let updated_count = count.checked_sub(1).unwrap_or_else(|| { + let shape_name = shape.name(); + panic!("piece count for {color} {shape_name} should not underflow"); + }); + self.0[color as usize][shape as usize] = updated_count; } #[cfg(test)] From dae5179947f9f7001e1a47725723d9ba0eb205d0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 17:06:07 -0700 Subject: [PATCH 20/20] Add a README --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ca6cb2 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +ChessFriend +=========== + +A chess engine written in Rust. + +The project is divided into crates for major components of the engine. These +crates are collected in a Cargo workspace. All crates have the `chessfriend_` +naming prefix. The directory structure omits this prefix, and I also frequently +do when referring to them. + + + +## Engine Crates + +The engine is divided into several crates, each providing vital types and +functionality. + + + +### `core` + +A collection of types for representing core concepts in a chess game and the +engine. Types like `Color` (player or piece color), `Shape` (the shape of a +piece: knight, etc), `Piece` (a piece of a particular color and shape), and +`Score` (for scoring a board position) live here. + + + +### `bitboard` + +Implements an efficient BitBoard type. Bitboards use a 64-bit bit field to mark a +square on a board as occupied or free. + + + +### `board` + +Implements a `Board` type that represents a moment-in-time board position. FEN +parsing and production lives here. + + + +### `moves` + +The `Move` type lives here, along with routines for encoding moves in efficient +forms, parsing moves from algebraic notation, and recording moves in a game +context. Additionally, the move generators for each shape of piece are here. + + + +### `position` + +Exports the `Position` type, representing a board position within the context of +a game. As such, it also provides a move list, and methods to make and unmake +moves. + + + +## Support Crates + +These crates are for debugging and testing. + + + +### `explorer` + +This crate implements a small command-line application for "exploring" board +positions. I meant for this program to be a debugging utility so that I could +examine bitboards and other board structures live. It has grown over time to +also support more aspects of interacting with the engine. So you can also use it +to play a game! + + + +### `perft` + +A small Perft utility that executes perft to a given depth from some starting +position. + + + + + +## Building + +Build the engine in the usual Rusty way. + +```sh +$ cargo build +``` + + + + + +## Testing + +Test in the usual Rusty way. + +```sh +$ cargo test +``` + +The engine has a fairly comprehensive unit test suite, as well as a decent pile +of integration tests. + + + + + +## Authors + +This engine is built entirely by me, Eryn Wells.