From d1950def002f7b25e6447d7be9b1522d7c7722ef Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 30 Jun 2025 15:32:38 -0700 Subject: [PATCH 01/12] [core] Add conversions between Score and f32 These conversions assume the float value you want is a point value where 1 point equals 1 pawn. --- core/src/score.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/core/src/score.rs b/core/src/score.rs index 2dfd7c9..c7f4f2e 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -6,6 +6,7 @@ use std::{ }; pub(crate) type Value = i32; +pub(crate) type FloatValue = f32; /// A score for a position in centipawns. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -38,6 +39,24 @@ impl Score { Self(value) } + /// Create a [`Score`] from a floating point value. This method assumes the + /// value is a point value where 1 point = 1 pawn. The floating point value + /// will be truncated as part of this conversion. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_core::score::Score; + /// assert_eq!(Score::from_float(3.1415926), Score(314)); + /// assert_ne!(Score::from_float(2.7182818).to_float(), 2.7182818); + /// ``` + /// + #[must_use] + pub const fn from_float(value: FloatValue) -> Self { + #[allow(clippy::cast_possible_truncation)] + Self((value * Self::CENTIPAWNS_PER_POINT) as Value) + } + /// Returns `true` if this [`Score`] is zero. /// /// ## Examples @@ -52,6 +71,14 @@ impl Score { pub const fn is_zero(&self) -> bool { self.0 == 0 } + + /// Return a floating point value in points where 1 point = 1 pawn. This + /// conversion loses precision. + #[must_use] + #[allow(clippy::cast_precision_loss)] + pub const fn to_float(&self) -> FloatValue { + self.0 as f32 / Self::CENTIPAWNS_PER_POINT + } } impl Add for Score { From a904e4a5bb2163c3bcfcb25a520878cbfc189365 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 30 Jun 2025 15:37:35 -0700 Subject: [PATCH 02/12] [bitboard, board] Replace ray_in_direction! macro with a function This is simpler than writing a macro, at the expense of some overhead for calling a function. But the Rust compiler might inline it anyway! To support this change, implement BitBoard::first_occupied_square_direction, which iterates a bitboard in a direction (i.e. leading or trailing) depending on the core::Direction value passed to it. --- bitboard/src/bitboard.rs | 12 ++++++++ board/src/sight.rs | 62 +++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 1897de4..f896e57 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -243,6 +243,18 @@ impl BitBoard { TrailingBitScanner::new(self.0) } + #[must_use] + pub fn first_occupied_square_direction(&self, direction: Direction) -> Option { + match direction { + Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => { + self.first_occupied_square_trailing() + } + Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => { + self.first_occupied_square_leading() + } + } + } + #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { diff --git a/board/src/sight.rs b/board/src/sight.rs index 8e5cbc6..b8e366e 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -138,20 +138,6 @@ struct SightInfo { friendly_occupancy: BitBoard, } -macro_rules! ray_in_direction { - ($square:expr, $blockers:expr, $direction:ident, $first_occupied_square:tt) => {{ - let ray = BitBoard::ray($square, Direction::$direction); - let ray_blockers = ray & $blockers; - if let Some(first_occupied_square) = ray_blockers.$first_occupied_square() { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - attack_ray - } else { - ray - } - }}; -} - /// Compute sight of a white pawn. fn white_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard { let possible_squares = !info.friendly_occupancy | en_passant_square; @@ -175,15 +161,27 @@ fn knight_sight(info: &SightInfo) -> BitBoard { BitBoard::knight_moves(info.square) } +fn ray_in_direction(square: Square, blockers: BitBoard, direction: Direction) -> BitBoard { + let ray = BitBoard::ray(square, direction); + + let ray_blockers = ray & blockers; + if let Some(first_occupied_square) = ray_blockers.first_occupied_square_direction(direction) { + let remainder = BitBoard::ray(first_occupied_square, direction); + let attack_ray = ray & !remainder; + attack_ray + } else { + ray + } +} + fn bishop_sight(info: &SightInfo) -> BitBoard { let bishop = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(bishop, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(bishop, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, NorthWest, first_occupied_square_trailing); + let sight = ray_in_direction(bishop, occupancy, Direction::NorthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthWest) + | ray_in_direction(bishop, occupancy, Direction::NorthWest); sight } @@ -192,11 +190,10 @@ fn rook_sight(info: &SightInfo) -> BitBoard { let rook = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(rook, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(rook, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(rook, occupancy, Direction::North) + | ray_in_direction(rook, occupancy, Direction::East) + | ray_in_direction(rook, occupancy, Direction::South) + | ray_in_direction(rook, occupancy, Direction::West); sight } @@ -205,15 +202,14 @@ fn queen_sight(info: &SightInfo) -> BitBoard { let queen = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(queen, occupancy, NorthWest, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(queen, occupancy, Direction::NorthWest) + | ray_in_direction(queen, occupancy, Direction::North) + | ray_in_direction(queen, occupancy, Direction::NorthEast) + | ray_in_direction(queen, occupancy, Direction::East) + | ray_in_direction(queen, occupancy, Direction::SouthEast) + | ray_in_direction(queen, occupancy, Direction::South) + | ray_in_direction(queen, occupancy, Direction::SouthWest) + | ray_in_direction(queen, occupancy, Direction::West); sight } From 45183c910ca95609b1e318cd7359fe22d600a1cf Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:08:25 -0700 Subject: [PATCH 03/12] [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 04/12] [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 05/12] [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 06/12] [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 07/12] [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 08/12] [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 09/12] [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 10/12] [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 11/12] [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 12/12] 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.