diff --git a/README.md b/README.md deleted file mode 100644 index 5ca6cb2..0000000 --- a/README.md +++ /dev/null @@ -1,113 +0,0 @@ -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. diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 35ce927..1897de4 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -46,6 +46,16 @@ 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) } @@ -99,7 +109,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()); /// ``` @@ -115,7 +125,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()); /// ``` @@ -150,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 { @@ -201,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()); /// ``` @@ -223,38 +233,6 @@ 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) @@ -265,24 +243,6 @@ 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() - } - } - } - - /// 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 { @@ -554,8 +514,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 3ea670c..6a60392 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/castle.rs b/board/src/castle.rs index 4ba9a4b..5acdaaf 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) { + if !self.has_castling_right_unwrapped(color, wing.into()) { return Err(CastleEvaluationError::NoRights { color, wing }); } diff --git a/board/src/movement.rs b/board/src/movement.rs index 3ebf44c..2935eee 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/piece_sets.rs b/board/src/piece_sets.rs index 52af054..de43caa 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -51,10 +51,11 @@ 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 7d3cade..effbbe0 100644 --- a/board/src/piece_sets/counts.rs +++ b/board/src/piece_sets/counts.rs @@ -17,21 +17,20 @@ 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 { - let shape_name = shape.name(); - panic!("piece count for {color} {shape_name} overflowed"); + if updated_value <= SQUARE_NUM { + self.0[color as usize][shape as usize] = updated_value; + } else { + unreachable!("piece count for {color} {shape} 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]; - 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; + 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"); + } } #[cfg(test)] diff --git a/board/src/sight.rs b/board/src/sight.rs index e682cb0..8e5cbc6 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -138,6 +138,20 @@ 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; @@ -161,27 +175,15 @@ 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; - 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); + #[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); sight } @@ -190,10 +192,11 @@ fn rook_sight(info: &SightInfo) -> BitBoard { let rook = info.square; let occupancy = info.occupancy; - 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); + #[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); sight } @@ -202,14 +205,15 @@ fn queen_sight(info: &SightInfo) -> BitBoard { let queen = info.square; let occupancy = info.occupancy; - 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); + #[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); sight } @@ -305,7 +309,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] diff --git a/core/src/score.rs b/core/src/score.rs index 3528861..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)] @@ -23,7 +24,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); @@ -31,13 +32,31 @@ impl Score { /// The maximum possible value of a score. pub const MAX: Score = Score(Value::MAX); - pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0; + const CENTIPAWNS_PER_POINT: f32 = 100.0; #[must_use] pub const fn new(value: Value) -> Self { 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 { diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 77126ba..0cedc7c 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -70,21 +70,18 @@ impl Shape { } #[must_use] - pub const fn is_promotable(&self) -> bool { + pub fn is_promotable(&self) -> bool { matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen) } #[must_use] - pub const fn score(self) -> Score { - #[allow(clippy::cast_possible_truncation)] - const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32; - + pub fn score(self) -> Score { match self { - 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), + 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), } } }