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. diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index f896e57..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()); /// ``` @@ -160,9 +150,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 +201,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()); /// ``` @@ -233,6 +223,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) @@ -255,6 +277,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 { @@ -526,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/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 }); } 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/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)] 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] diff --git a/core/src/score.rs b/core/src/score.rs index 2dfd7c9..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); @@ -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 0cedc7c..77126ba 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -70,18 +70,21 @@ 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) } #[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), } } }