diff --git a/Cargo.lock b/Cargo.lock index 337e104..0d1d26f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chessfriend" version = "0.1.0" +dependencies = [ + "chessfriend_core", + "chessfriend_moves", + "chessfriend_position", + "clap", + "shlex", + "thiserror", +] [[package]] name = "chessfriend_bitboard" diff --git a/Cargo.toml b/Cargo.toml index 37c14db..49b9a15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,3 @@ members = [ "position", ] resolver = "3" - -[profile.release-debug] -inherits = "release" -debug = true 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..b9e4c1c 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -43,8 +43,16 @@ macro_rules! moves_getter { } impl BitBoard { - pub const EMPTY: BitBoard = BitBoard(u64::MIN); - pub const FULL: BitBoard = BitBoard(u64::MAX); + const EMPTY: BitBoard = BitBoard(u64::MIN); + const FULL: BitBoard = BitBoard(u64::MAX); + + pub const fn empty() -> BitBoard { + Self::EMPTY + } + + pub const fn full() -> BitBoard { + Self::FULL + } pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) @@ -99,7 +107,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 +123,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 +158,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 +209,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 +231,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 +241,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 +512,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/lib.rs b/bitboard/src/lib.rs index 12a25ed..798e51b 100644 --- a/bitboard/src/lib.rs +++ b/bitboard/src/lib.rs @@ -14,7 +14,7 @@ pub use direction::IterationDirection; macro_rules! bitboard { ($($sq:ident)* $(,)?) => { { - let mut bitboard = $crate::BitBoard::EMPTY; + let mut bitboard = $crate::BitBoard::empty(); $(bitboard.set(chessfriend_core::Square::$sq);)* bitboard } 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/board.rs b/board/src/board.rs index 338d4aa..666488e 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,13 +1,13 @@ // Eryn Wells use crate::{ - CastleRights, PieceSet, + PieceSet, castle, display::DiagramFormatter, - piece_sets::{Counter, PlacePieceError, PlacePieceStrategy}, + piece_sets::{PlacePieceError, PlacePieceStrategy}, zobrist::{ZobristHash, ZobristState}, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Shape, Square}; +use chessfriend_core::{Color, Piece, Shape, Square, Wing}; use std::sync::Arc; pub type HalfMoveClock = u32; @@ -17,7 +17,7 @@ pub type FullMoveClock = u32; pub struct Board { active_color: Color, pieces: PieceSet, - castling_rights: CastleRights, + castling_rights: castle::Rights, en_passant_target: Option, pub half_move_clock: HalfMoveClock, pub full_move_number: FullMoveClock, @@ -92,27 +92,59 @@ impl Board { impl Board { #[must_use] - pub fn castling_rights(&self) -> &CastleRights { - &self.castling_rights + pub fn castling_rights(&self) -> castle::Rights { + self.castling_rights } - pub(crate) fn castling_rights_mut(&mut self) -> &mut CastleRights { - &mut self.castling_rights - } - - /// Replace castling rights with new rights wholesale. - pub fn set_castling_rights(&mut self, rights: CastleRights) { + pub fn set_castling_rights(&mut self, rights: castle::Rights) { if rights == self.castling_rights { return; } let old_rights = self.castling_rights; self.castling_rights = rights; - self.update_zobrist_hash_castling_rights(old_rights); } - pub(crate) fn update_zobrist_hash_castling_rights(&mut self, old_rights: CastleRights) { + #[must_use] + pub fn active_color_has_castling_right(&self, wing: Wing) -> bool { + self.color_has_castling_right_unwrapped(self.active_color, wing) + } + + #[must_use] + pub fn color_has_castling_right(&self, color: Option, wing: Wing) -> bool { + self.color_has_castling_right_unwrapped(self.unwrap_color(color), wing) + } + + #[must_use] + pub fn color_has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool { + self.castling_rights.color_has_right(color, wing) + } + + pub fn grant_castling_right(&mut self, color: Color, wing: Wing) { + let old_rights = self.castling_rights; + self.castling_rights.grant(color, wing); + self.update_zobrist_hash_castling_rights(old_rights); + } + + pub fn revoke_all_castling_rights(&mut self) { + let old_rights = self.castling_rights; + self.castling_rights.revoke_all(); + self.update_zobrist_hash_castling_rights(old_rights); + } + + pub fn revoke_castling_right(&mut self, color: Option, wing: Wing) { + let color = self.unwrap_color(color); + self.revoke_castling_right_unwrapped(color, wing); + } + + pub fn revoke_castling_right_unwrapped(&mut self, color: Color, wing: Wing) { + let old_rights = self.castling_rights; + self.castling_rights.revoke(color, wing); + self.update_zobrist_hash_castling_rights(old_rights); + } + + fn update_zobrist_hash_castling_rights(&mut self, old_rights: castle::Rights) { let new_rights = self.castling_rights; if old_rights == new_rights { return; @@ -122,18 +154,6 @@ impl Board { zobrist.update_modifying_castling_rights(new_rights, old_rights); } } - - pub(crate) fn castling_king(&self, square: Square) -> Option { - let active_color = self.active_color(); - self.get_piece(square) - .filter(|piece| piece.color == active_color && piece.is_king()) - } - - pub(crate) fn castling_rook(&self, square: Square) -> Option { - let active_color = self.active_color(); - self.get_piece(square) - .filter(|piece| piece.color == active_color && piece.is_rook()) - } } impl Board { @@ -219,11 +239,6 @@ impl Board { removed_piece } - - #[must_use] - pub fn count_piece(&self, piece: &Piece) -> Counter { - self.pieces.count(piece) - } } impl Board { diff --git a/board/src/castle.rs b/board/src/castle.rs index 4ba9a4b..f8f9c24 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -4,10 +4,10 @@ mod parameters; mod rights; pub use parameters::Parameters; -pub use rights::{CastleRightsOption, Rights}; +pub use rights::Rights; use crate::{Board, CastleParameters}; -use chessfriend_core::{Color, Wing}; +use chessfriend_core::{Color, Piece, Square, Wing}; use thiserror::Error; #[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.has_castling_right_unwrapped(color, wing) { + if !self.color_has_castling_right_unwrapped(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } @@ -76,60 +76,17 @@ impl Board { Ok(parameters) } -} -impl Board { - #[must_use] - pub fn has_castling_right(&self, color: Option, wing: Wing) -> bool { - self.has_castling_right_unwrapped(self.unwrap_color(color), wing) + pub(crate) fn castling_king(&self, square: Square) -> Option { + let active_color = self.active_color(); + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_king()) } - #[must_use] - pub fn has_castling_right_active(&self, wing: Wing) -> bool { - self.has_castling_right_unwrapped(self.active_color(), wing) - } - - #[must_use] - pub fn has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool { - self.castling_rights().get(color, wing.into()) - } -} - -impl Board { - pub fn grant_castling_rights(&mut self, color: Option, rights: CastleRightsOption) { - let color = self.unwrap_color(color); - self.grant_castling_rights_unwrapped(color, rights); - } - - pub fn grant_castling_rights_active(&mut self, rights: CastleRightsOption) { - self.grant_castling_rights_unwrapped(self.active_color(), rights); - } - - pub fn grant_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) { - let old_rights = *self.castling_rights(); - self.castling_rights_mut().grant(color, rights); - self.update_zobrist_hash_castling_rights(old_rights); - } -} - -impl Board { - pub fn revoke_all_castling_rights(&mut self) { - self.castling_rights_mut().revoke_all(); - } - - pub fn revoke_castling_rights(&mut self, color: Option, rights: CastleRightsOption) { - let color = self.unwrap_color(color); - self.revoke_castling_rights_unwrapped(color, rights); - } - - pub fn revoke_castling_rights_active(&mut self, rights: CastleRightsOption) { - self.revoke_castling_rights_unwrapped(self.active_color(), rights); - } - - pub fn revoke_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) { - let old_rights = *self.castling_rights(); - self.castling_rights_mut().revoke(color, rights); - self.update_zobrist_hash_castling_rights(old_rights); + pub(crate) fn castling_rook(&self, square: Square) -> Option { + let active_color = self.active_color(); + self.get_piece(square) + .filter(|piece| piece.color == active_color && piece.is_rook()) } } @@ -147,8 +104,8 @@ mod tests { White Rook on H1 ); - assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); - assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); } #[test] diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs index 2c0a961..b65461b 100644 --- a/board/src/castle/rights.rs +++ b/board/src/castle/rights.rs @@ -1,14 +1,6 @@ -// Eryn Wells - use chessfriend_core::{Color, Wing}; use std::fmt; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CastleRightsOption { - Wing(Wing), - All, -} - #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Rights(u8); @@ -20,16 +12,16 @@ impl Rights { /// as long as they have not moved their king, or the rook on that side of /// the board. #[must_use] - pub fn get(self, color: Color, option: CastleRightsOption) -> bool { - (self.0 & Self::flags(color, option)) != 0 + pub fn color_has_right(self, color: Color, wing: Wing) -> bool { + (self.0 & (1 << Self::flag_offset(color, wing))) != 0 } - pub fn grant(&mut self, color: Color, option: CastleRightsOption) { - self.0 |= Self::flags(color, option); + pub fn grant(&mut self, color: Color, wing: Wing) { + self.0 |= 1 << Self::flag_offset(color, wing); } - pub fn revoke(&mut self, color: Color, option: CastleRightsOption) { - self.0 &= !Self::flags(color, option); + pub fn revoke(&mut self, color: Color, wing: Wing) { + self.0 &= !(1 << Self::flag_offset(color, wing)); } /// Revoke castling rights for all colors and all sides of the board. @@ -39,14 +31,14 @@ impl Rights { } impl Rights { - pub(crate) fn as_index(self) -> usize { + pub(crate) fn as_index(&self) -> usize { self.0 as usize } } impl Rights { - const fn flags(color: Color, option: CastleRightsOption) -> u8 { - option.as_flags() << (color as u8 * 2) + fn flag_offset(color: Color, wing: Wing) -> usize { + ((color as usize) << 1) + wing as usize } } @@ -62,55 +54,36 @@ impl Default for Rights { } } -impl CastleRightsOption { - #[must_use] - pub const fn as_flags(self) -> u8 { - match self { - Self::Wing(wing) => 1 << (wing as u8), - Self::All => (1 << Wing::KingSide as u8) | (1 << Wing::QueenSide as u8), - } - } -} - -impl From for CastleRightsOption { - fn from(value: Wing) -> Self { - Self::Wing(value) - } -} - #[cfg(test)] mod tests { use super::*; #[test] fn bitfield_offsets() { - assert_eq!(Rights::flags(Color::White, Wing::KingSide.into()), 1); - assert_eq!(Rights::flags(Color::White, Wing::QueenSide.into()), 1 << 1); - assert_eq!(Rights::flags(Color::Black, Wing::KingSide.into()), 1 << 2); - assert_eq!(Rights::flags(Color::Black, Wing::QueenSide.into()), 1 << 3); - - assert_eq!(Rights::flags(Color::White, CastleRightsOption::All), 0b11); - assert_eq!(Rights::flags(Color::Black, CastleRightsOption::All), 0b1100); + assert_eq!(Rights::flag_offset(Color::White, Wing::KingSide), 0); + assert_eq!(Rights::flag_offset(Color::White, Wing::QueenSide), 1); + assert_eq!(Rights::flag_offset(Color::Black, Wing::KingSide), 2); + assert_eq!(Rights::flag_offset(Color::Black, Wing::QueenSide), 3); } #[test] fn default_rights() { let mut rights = Rights::default(); - assert!(rights.get(Color::White, Wing::KingSide.into())); - assert!(rights.get(Color::White, Wing::QueenSide.into())); - assert!(rights.get(Color::Black, Wing::KingSide.into())); - assert!(rights.get(Color::Black, Wing::QueenSide.into())); + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(rights.color_has_right(Color::White, Wing::QueenSide)); + assert!(rights.color_has_right(Color::Black, Wing::KingSide)); + assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); - rights.revoke(Color::White, Wing::QueenSide.into()); - assert!(rights.get(Color::White, Wing::KingSide.into())); - assert!(!rights.get(Color::White, Wing::QueenSide.into())); - assert!(rights.get(Color::Black, Wing::KingSide.into())); - assert!(rights.get(Color::Black, Wing::QueenSide.into())); + rights.revoke(Color::White, Wing::QueenSide); + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(!rights.color_has_right(Color::White, Wing::QueenSide)); + assert!(rights.color_has_right(Color::Black, Wing::KingSide)); + assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); - rights.grant(Color::White, Wing::QueenSide.into()); - assert!(rights.get(Color::White, Wing::KingSide.into())); - assert!(rights.get(Color::White, Wing::QueenSide.into())); - assert!(rights.get(Color::Black, Wing::KingSide.into())); - assert!(rights.get(Color::Black, Wing::QueenSide.into())); + rights.grant(Color::White, Wing::QueenSide); + assert!(rights.color_has_right(Color::White, Wing::KingSide)); + assert!(rights.color_has_right(Color::White, Wing::QueenSide)); + assert!(rights.color_has_right(Color::Black, Wing::KingSide)); + assert!(rights.color_has_right(Color::Black, Wing::QueenSide)); } } diff --git a/board/src/check.rs b/board/src/check.rs index 6d8ceba..ba9f06d 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -5,10 +5,18 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece}; impl Board { - /// Return whether the active color is in check. #[must_use] - pub fn is_in_check(&self) -> bool { - let color = self.active_color(); + pub fn active_color_is_in_check(&self) -> bool { + self.unwrapped_color_is_in_check(self.active_color()) + } + + #[must_use] + pub fn color_is_in_check(&self, color: Option) -> bool { + self.unwrapped_color_is_in_check(self.unwrap_color(color)) + } + + #[must_use] + pub fn unwrapped_color_is_in_check(&self, color: Color) -> bool { let king = self.king_bitboard(color); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() @@ -31,7 +39,7 @@ mod tests { Black Rook on F3, ); - assert!(board.is_in_check()); + assert!(board.unwrapped_color_is_in_check(Color::White)); } #[test] @@ -41,6 +49,6 @@ mod tests { Black Rook on B4, ); - assert!(!board.is_in_check()); + assert!(!board.unwrapped_color_is_in_check(Color::White)); } } diff --git a/board/src/fen.rs b/board/src/fen.rs index fe418a5..0da38dc 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -9,10 +9,9 @@ use thiserror::Error; #[macro_export] macro_rules! fen { - ($fen_string:literal) => {{ - use $crate::fen::FromFenStr; - $crate::Board::from_fen_str($fen_string) - }}; + ($fen_string:literal) => { + Board::from_fen_str($fen_string) + }; } #[derive(Clone, Debug, Error, Eq, PartialEq)] @@ -25,16 +24,12 @@ pub enum ToFenStrError { pub enum FromFenStrError { #[error("missing {0} field")] MissingField(Field), - #[error("missing piece placement")] MissingPlacement, - #[error("invalid value")] InvalidValue, - #[error("{0}")] ParseIntError(#[from] std::num::ParseIntError), - #[error("{0}")] ParseSquareError(#[from] ParseSquareError), } @@ -127,12 +122,12 @@ impl ToFenStr for Board { (Color::Black, Wing::KingSide), (Color::Black, Wing::QueenSide), ] - .map(|(color, wing)| { - if !self.has_castling_right_unwrapped(color, wing) { + .map(|(color, castle)| { + if !self.color_has_castling_right_unwrapped(color, castle) { return ""; } - match (color, wing) { + match (color, castle) { (Color::White, Wing::KingSide) => "K", (Color::White, Wing::QueenSide) => "Q", (Color::Black, Wing::KingSide) => "k", @@ -231,23 +226,20 @@ impl FromFenStr for Board { )?; board.set_active_color(active_color); - let color_wing_from_char = |ch| match ch { - 'K' => Some((Color::White, Wing::KingSide)), - 'Q' => Some((Color::White, Wing::QueenSide)), - 'k' => Some((Color::Black, Wing::KingSide)), - 'q' => Some((Color::Black, Wing::QueenSide)), - _ => None, - }; - let castling_rights = fields .next() .ok_or(FromFenStrError::MissingField(Field::CastlingRights))?; - board.revoke_all_castling_rights(); - if castling_rights != "-" { + if castling_rights == "-" { + board.revoke_all_castling_rights(); + } else { for ch in castling_rights.chars() { - let (color, wing) = - color_wing_from_char(ch).ok_or(FromFenStrError::InvalidValue)?; - board.grant_castling_rights_unwrapped(color, wing.into()); + match ch { + 'K' => board.grant_castling_right(Color::White, Wing::KingSide), + 'Q' => board.grant_castling_right(Color::White, Wing::QueenSide), + 'k' => board.grant_castling_right(Color::Black, Wing::KingSide), + 'q' => board.grant_castling_right(Color::Black, Wing::QueenSide), + _ => return Err(FromFenStrError::InvalidValue), + }; } } diff --git a/board/src/movement.rs b/board/src/movement.rs index 3ebf44c..4fa4381 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -4,16 +4,16 @@ //! of squares a piece can move to. For all pieces except pawns, the Movement //! set is equal to the Sight set. -use crate::{Board, sight::Sight}; +use crate::{sight::Sight, Board}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; impl Board { - pub fn movement_piece(&self, square: Square) -> BitBoard { + pub fn movement(&self, square: Square) -> BitBoard { if let Some(piece) = self.get_piece(square) { piece.movement(square, self) } else { - BitBoard::EMPTY + BitBoard::empty() } } } @@ -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 @@ -93,17 +93,17 @@ fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard { #[cfg(test)] mod tests { use super::pawn_pushes; - use chessfriend_bitboard::{BitBoard, bitboard}; + use chessfriend_bitboard::{bitboard, BitBoard}; use chessfriend_core::{Color, Square}; #[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..063937d 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -1,9 +1,8 @@ // Eryn Wells -mod counts; mod mailbox; -use self::{counts::Counts, mailbox::Mailbox}; +use self::mailbox::Mailbox; use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, Shape, Square}; use std::{ @@ -12,8 +11,6 @@ use std::{ }; use thiserror::Error; -pub(crate) use counts::Counter; - #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum PlacePieceStrategy { #[default] @@ -21,7 +18,7 @@ pub enum PlacePieceStrategy { PreserveExisting, } -#[derive(Clone, Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error, Eq, PartialEq)] pub enum PlacePieceError { #[error("cannot place piece on {square} with existing {piece}")] ExisitingPiece { piece: Piece, square: Square }, @@ -32,7 +29,6 @@ pub enum PlacePieceError { #[derive(Clone, Debug, Default, Eq)] pub struct PieceSet { mailbox: Mailbox, - counts: Counts, color_occupancy: [BitBoard; Color::NUM], shape_occupancy: [BitBoard; Shape::NUM], } @@ -40,11 +36,10 @@ pub struct PieceSet { impl PieceSet { pub(crate) fn new(pieces: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self { let mut mailbox = Mailbox::default(); - let mut counts = Counts::default(); let mut color_occupancy: [BitBoard; Color::NUM] = Default::default(); let mut shape_occupancy: [BitBoard; Shape::NUM] = Default::default(); - for (color_index, color) in Color::into_iter().enumerate() { + for (color_index, color) in Color::iter().enumerate() { for (shape_index, shape) in Shape::into_iter().enumerate() { let bitboard = pieces[color_index][shape_index]; @@ -52,16 +47,14 @@ impl PieceSet { shape_occupancy[shape_index] |= bitboard; for square in bitboard.occupied_squares(&IterationDirection::default()) { - let piece = Piece::new(color, shape); + let piece = Piece::new(*color, shape); mailbox.set(piece, square); - counts.increment(color, shape); } } } Self { mailbox, - counts, color_occupancy, shape_occupancy, } @@ -101,10 +94,6 @@ impl PieceSet { self.mailbox.get(square) } - pub(crate) fn count(&self, piece: &Piece) -> Counter { - self.counts.get(piece.color, piece.shape) - } - // TODO: Rename this. Maybe get_all() is better? pub(crate) fn find_pieces(&self, piece: Piece) -> BitBoard { let color_occupancy = self.color_occupancy[piece.color as usize]; @@ -131,7 +120,6 @@ impl PieceSet { self.color_occupancy[color as usize].set(square); self.shape_occupancy[shape as usize].set(square); - self.counts.increment(color, shape); self.mailbox.set(piece, square); Ok(existing_piece) @@ -139,12 +127,8 @@ impl PieceSet { pub(crate) fn remove(&mut self, square: Square) -> Option { if let Some(piece) = self.mailbox.get(square) { - let color_index = piece.color as usize; - let shape_index = piece.shape as usize; - - self.color_occupancy[color_index].clear(square); - self.shape_occupancy[shape_index].clear(square); - self.counts.decrement(piece.color, piece.shape); + self.color_occupancy[piece.color as usize].clear(square); + self.shape_occupancy[piece.shape as usize].clear(square); self.mailbox.remove(square); Some(piece) diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs deleted file mode 100644 index 7d3cade..0000000 --- a/board/src/piece_sets/counts.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Eryn Wells - -use chessfriend_core::{Color, Shape, Square}; - -pub(crate) type Counter = u8; - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub(super) struct Counts([[Counter; Shape::NUM]; Color::NUM]); - -impl Counts { - pub fn get(&self, color: Color, shape: Shape) -> Counter { - self.0[color as usize][shape as usize] - } - - pub fn increment(&mut self, color: Color, shape: Shape) { - #[allow(clippy::cast_possible_truncation)] - 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"); - } - - 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; - } - - #[cfg(test)] - fn set(&mut self, color: Color, shape: Shape, value: u8) { - self.0[color as usize][shape as usize] = value; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[should_panic(expected = "underflowed")] - fn underflow() { - let mut counts = Counts::default(); - counts.decrement(Color::White, Shape::Queen); - } - - #[test] - #[should_panic(expected = "overflowed")] - fn overflow() { - let mut counts = Counts::default(); - counts.set(Color::White, Shape::Queen, 64); - counts.increment(Color::White, Shape::Queen); - } -} diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs index 74b32bc..5d73561 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -7,6 +7,10 @@ use std::iter::FromIterator; pub(crate) struct Mailbox([Option; Square::NUM]); impl Mailbox { + pub fn new() -> Self { + Self::default() + } + pub fn get(&self, square: Square) -> Option { self.0[square as usize] } @@ -42,7 +46,7 @@ impl From<[Option; Square::NUM]> for Mailbox { impl FromIterator<(Square, Piece)> for Mailbox { fn from_iter>(iter: T) -> Self { iter.into_iter() - .fold(Self::default(), |mut mailbox, (square, piece)| { + .fold(Self::new(), |mut mailbox, (square, piece)| { mailbox.set(piece, square); mailbox }) @@ -57,7 +61,7 @@ mod tests { #[test] fn iter_returns_all_pieces() { - let mut mailbox = Mailbox::default(); + let mut mailbox = Mailbox::new(); mailbox.set(piece!(White Queen), Square::C3); mailbox.set(piece!(White Rook), Square::H8); mailbox.set(piece!(Black Bishop), Square::E4); diff --git a/board/src/sight.rs b/board/src/sight.rs index e682cb0..81fb120 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -23,31 +23,16 @@ use std::ops::BitOr; impl Board { /// Compute sight of the piece on the given square. - pub fn sight_piece(&self, square: Square) -> BitBoard { + pub fn sight(&self, square: Square) -> BitBoard { if let Some(piece) = self.get_piece(square) { piece.sight(square, self) } else { - BitBoard::EMPTY + BitBoard::empty() } } - /// Calculate sight of all pieces of the given [`Color`]. If `color` is - /// `None`, calculate sight of the active color. - pub fn sight(&self, color: Option) -> BitBoard { - self.sight_unwrapped(self.unwrap_color(color)) - } - - /// Calculate sight of all pieces of the active color. - pub fn sight_active(&self) -> BitBoard { - self.sight_unwrapped(self.active_color()) - } - - /// Calculate sight of all pieces of the given [`Color`]. - pub fn sight_unwrapped(&self, color: Color) -> BitBoard { - self.friendly_occupancy(color) - .occupied_squares_leading() - .map(|square| self.sight_piece(square)) - .fold(BitBoard::EMPTY, BitOr::bitor) + pub fn active_sight(&self) -> BitBoard { + self.friendly_sight(self.active_color()) } /// A [`BitBoard`] of all squares the given color can see. @@ -55,8 +40,8 @@ impl Board { // TODO: Probably want to implement a caching layer here. self.friendly_occupancy(color) .occupied_squares(&IterationDirection::default()) - .map(|square| self.sight_piece(square)) - .fold(BitBoard::EMPTY, BitOr::bitor) + .map(|square| self.sight(square)) + .fold(BitBoard::empty(), BitOr::bitor) } pub fn active_color_opposing_sight(&self) -> BitBoard { @@ -75,7 +60,7 @@ impl Board { Some(self.friendly_sight(c)) } }) - .fold(BitBoard::EMPTY, BitOr::bitor) + .fold(BitBoard::empty(), BitOr::bitor) } } @@ -138,6 +123,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 +160,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 +177,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 +190,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 } @@ -255,7 +244,7 @@ mod tests { White King on E4, ); - let sight = pos.sight_active(); + let sight = pos.active_sight(); assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]); } @@ -278,8 +267,8 @@ mod tests { mod pawn { use crate::{sight::Sight, test_board}; - use chessfriend_bitboard::{BitBoard, bitboard}; - use chessfriend_core::{Square, piece}; + use chessfriend_bitboard::{bitboard, BitBoard}; + use chessfriend_core::{piece, Square}; sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]); @@ -305,7 +294,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/chessfriend/Cargo.toml b/chessfriend/Cargo.toml index 25bc79e..e696120 100644 --- a/chessfriend/Cargo.toml +++ b/chessfriend/Cargo.toml @@ -3,3 +3,10 @@ name = "chessfriend" version = "0.1.0" edition = "2024" +[dependencies] +chessfriend_core = { path = "../core" } +chessfriend_moves = { path = "../moves" } +chessfriend_position = { path = "../position" } +clap = { version = "4.4.12", features = ["derive"] } +shlex = "1.2.0" +thiserror = "2" diff --git a/chessfriend/src/chessfriend.rs b/chessfriend/src/chessfriend.rs new file mode 100644 index 0000000..127e05e --- /dev/null +++ b/chessfriend/src/chessfriend.rs @@ -0,0 +1,87 @@ +// Eryn Wells + +use crate::{ + core::{Piece, Square}, + position::{MakeMoveError, Move, ValidateMove}, + threadpool::ThreadPool, +}; +use chessfriend_core::random::RandomNumberGenerator; +use chessfriend_position::{ + PlacePieceError, PlacePieceStrategy, Position, ZobristState, fen::FromFenStr, +}; +use std::{num::NonZero, sync::Arc}; + +pub struct ChessFriend { + /// A pool of worker threads over which tasks may be distributed. + thread_pool: ThreadPool, + + zobrist_state: Arc, + + /// A global Position for the engine. + position: Position, +} + +impl ChessFriend { + #[must_use] + pub fn new(options: Options) -> Self { + let mut rng = RandomNumberGenerator::default(); + + let zobrist_state = Arc::new(ZobristState::new(&mut rng)); + + let position = match options.initial_position { + InitialPosition::Empty => Position::empty(Some(zobrist_state.clone())), + InitialPosition::Starting => Position::starting(Some(zobrist_state.clone())), + InitialPosition::Fen(fen) => { + let mut position = Position::from_fen_str(fen).unwrap_or_default(); + position.set_zobrist_state(zobrist_state.clone()); + position + } + }; + + let thread_pool = ThreadPool::new(options.threading.0); + + Self { + thread_pool, + zobrist_state, + position, + } + } +} + +impl ChessFriend {} + +#[derive(Clone, Copy, Default, Eq, PartialEq)] +pub struct Options<'a> { + initial_position: InitialPosition<'a>, + threading: Threading, +} + +#[derive(Clone, Copy, Default, Eq, PartialEq)] +pub enum InitialPosition<'a> { + Empty, + #[default] + Starting, + Fen(&'a str), +} + +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct Threading(NonZero); + +impl Threading { + #[must_use] + pub fn new(n: NonZero) -> Self { + Self(n) + } + + #[must_use] + pub fn new_with_available_parallelism() -> Self { + const ONE: NonZero = NonZero::new(1).unwrap(); + Self(std::thread::available_parallelism().unwrap_or(ONE)) + } +} + +impl Default for Threading { + fn default() -> Self { + Self::new_with_available_parallelism() + } +} diff --git a/chessfriend/src/lib.rs b/chessfriend/src/lib.rs index 4e01ba5..a73019b 100644 --- a/chessfriend/src/lib.rs +++ b/chessfriend/src/lib.rs @@ -1,2 +1,19 @@ // Eryn Wells +mod chessfriend; +mod threadpool; +mod uci; + +pub use crate::chessfriend::ChessFriend; + +pub mod options { + pub use crate::chessfriend::{InitialPosition, Options, Threading}; +} + +pub mod core { + pub use chessfriend_core::{Color, Piece, Shape, Square}; +} + +pub mod position { + pub use chessfriend_moves::{MakeMoveError, Move, ValidateMove}; +} diff --git a/chessfriend/src/threadpool.rs b/chessfriend/src/threadpool.rs new file mode 100644 index 0000000..00584c9 --- /dev/null +++ b/chessfriend/src/threadpool.rs @@ -0,0 +1,77 @@ +// Eryn Wells + +use std::{ + num::NonZero, + panic, + sync::{Arc, Mutex, mpsc}, + thread, +}; + +pub(crate) trait Job: FnOnce() + Send + 'static {} + +pub(crate) struct ThreadPool { + workers: Vec, + sender: Option>>, +} + +impl ThreadPool { + pub fn new(threads_count: NonZero) -> Self { + let (sender, receiver) = mpsc::channel(); + + let receiver = Arc::new(Mutex::new(receiver)); + let workers: Vec<_> = (0..threads_count.into()) + .map(|i| Worker::new(i, receiver.clone())) + .collect(); + + Self { + workers, + sender: Some(sender), + } + } +} + +impl Drop for ThreadPool { + fn drop(&mut self) { + drop(self.sender.take()); + self.workers.drain(..).for_each(Worker::join); + } +} + +struct Worker { + id: usize, + handle: thread::JoinHandle<()>, +} + +impl Worker { + fn new(id: usize, receiver: Arc>>>) -> Self { + // TODO: A note from the Rust Programming Language + // + // Note: If the operating system can’t create a thread because there + // aren’t enough system resources, thread::spawn will panic. That will + // cause our whole server to panic, even though the creation of some + // threads might succeed. For simplicity’s sake, this behavior is fine, + // but in a production thread pool implementation, you’d likely want to + // use std::thread::Builder and its spawn method that returns Result + // instead. + + let handle = thread::spawn(move || { + loop { + let job = { + let receiver = receiver.lock().unwrap(); + receiver.recv().unwrap() + }; + + job(); + } + }); + + Self { id, handle } + } + + fn join(self) { + match self.handle.join() { + Ok(()) => {} + Err(error) => panic::resume_unwind(error), + } + } +} diff --git a/chessfriend/src/uci.rs b/chessfriend/src/uci.rs new file mode 100644 index 0000000..64cc311 --- /dev/null +++ b/chessfriend/src/uci.rs @@ -0,0 +1,148 @@ +// Eryn Wells + +use clap::{Error as ClapError, Parser, Subcommand, ValueEnum}; +use std::{ + fmt::Display, + io::{BufRead, Write}, +}; +use thiserror::Error; + +#[derive(Parser, Debug)] +#[command(multicall = true)] +pub struct Uci { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Establish UCI (Universal Chess Interface) as the channel's exchange protocol. + Uci, + + /// Toggle debug state on or off. + Debug { state: DebugState }, + + /// Synchronize the engine with the client. Can also be used as a 'ping'. + IsReady, + + /// Stop calculating as soon as possible. + Stop, + + /// Stop all processing and quit the program. + Quit, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] +enum DebugState { + On, + Off, +} + +pub enum Response<'a> { + /// Declares one aspect of the engine's identity. + Id(IdValue<'a>), + + /// Declares that communicating in UCI is acceptable. + UciOk, + + /// Declares that the engine is ready to receive commands from the client. + ReadyOk, +} + +impl Display for Response<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Response::Id(value) => write!(f, "id {value}"), + Response::UciOk => write!(f, "uciok"), + Response::ReadyOk => write!(f, "readyok"), + } + } +} + +pub enum IdValue<'a> { + Name(&'a str), + Author(&'a str), +} + +impl Display for IdValue<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IdValue::Name(name) => write!(f, "name {name}"), + IdValue::Author(author) => write!(f, "author {author}"), + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("unable to parse command")] + LexError, + + #[error("{0}")] + ClapError(#[from] ClapError), +} + +impl Uci { + /// Respond to a command. + /// + /// ## Errors + /// + /// Returns an error if parsing the command string fails, otherwise returns an array of + /// responses. + /// + pub fn respond(line: &str) -> Result, Error> { + let arguments = shlex::split(line).ok_or(Error::LexError)?; + + let interface = Self::try_parse_from(arguments)?; + + match interface.command { + Command::Uci => { + const IDENTITIES: [Response; 2] = [ + Response::Id(IdValue::Name("ChessFriend")), + Response::Id(IdValue::Author("Eryn Wells")), + ]; + + let options: Vec = vec![]; + + Ok(IDENTITIES.into_iter().chain(options).collect()) + } + Command::Debug { state: _ } => Ok(vec![]), + Command::IsReady => Ok(vec![Response::ReadyOk]), + Command::Stop => Ok(vec![]), + Command::Quit => Ok(vec![]), + } + } +} + +pub struct UciInterface {} + +impl UciInterface { + pub fn read_until_quit( + &self, + input: impl BufRead, + output: &mut impl Write, + ) -> Result<(), UciInterfaceError> { + for line in input.lines() { + let line = line?; + + let responses = Uci::respond(line.as_str())?; + + // TODO: Dispatch command to background processing thread. + + for r in responses { + write!(output, "{r}")?; + } + } + + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum UciInterfaceError { + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + #[error("uci error: {0}")] + UciError(#[from] Error), +} diff --git a/core/src/colors.rs b/core/src/colors.rs index e1255b7..5cf633b 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,7 +1,6 @@ // Eryn Wells -use crate::{Direction, score}; -use std::fmt; +use crate::Direction; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -57,18 +56,10 @@ impl Color { Color::Black => "black", } } - - #[must_use] - pub const fn score_factor(self) -> score::Value { - match self { - Color::White => 1, - Color::Black => -1, - } - } } -impl fmt::Display for Color { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 308fc3e..99a252e 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -115,9 +115,7 @@ macro_rules! range_bound_struct { coordinate_enum!( Direction, - [ - North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest - ] + [North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest] ); impl Direction { @@ -264,7 +262,7 @@ impl Square { #[must_use] pub unsafe fn from_index_unchecked(x: u8) -> Square { debug_assert!((x as usize) < Self::NUM); - Self::ALL[x as usize] + Self::try_from(x).unwrap_unchecked() } #[inline] diff --git a/core/src/lib.rs b/core/src/lib.rs index f298a81..0638410 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,7 +4,6 @@ pub mod colors; pub mod coordinates; pub mod pieces; pub mod random; -pub mod score; pub mod shapes; mod macros; diff --git a/core/src/score.rs b/core/src/score.rs deleted file mode 100644 index 3528861..0000000 --- a/core/src/score.rs +++ /dev/null @@ -1,126 +0,0 @@ -// Eryn Wells - -use std::{ - fmt, - ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}, -}; - -pub(crate) type Value = i32; - -/// A score for a position in centipawns. -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Score(Value); - -impl Score { - pub const ZERO: Score = Score(0); - - /// The minimum possible value of a score. Notably, this is *not* the - /// minimum value for the inner integer value so negation works correctly. - /// This property is important during search, which relies on being able to - /// negate "infinity". - /// - /// ## Examples - /// - /// ``` - /// use chessfriend_core::score::Score; - /// assert_eq!(-Score::MIN, Score::MAX); - /// ``` - /// - pub const MIN: Score = Score(Value::MIN + 1); - - /// The maximum possible value of a score. - pub const MAX: Score = Score(Value::MAX); - - pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0; - - #[must_use] - pub const fn new(value: Value) -> Self { - Self(value) - } - - /// Returns `true` if this [`Score`] is zero. - /// - /// ## Examples - /// - /// ``` - /// use chessfriend_core::score::Score; - /// assert!(Score::ZERO.is_zero()); - /// assert!(Score::new(0).is_zero()); - /// ``` - /// - #[must_use] - pub const fn is_zero(&self) -> bool { - self.0 == 0 - } -} - -impl Add for Score { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Score(self.0 + rhs.0) - } -} - -impl AddAssign for Score { - fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0; - } -} - -impl Sub for Score { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Score(self.0 - rhs.0) - } -} - -impl SubAssign for Score { - fn sub_assign(&mut self, rhs: Self) { - self.0 -= rhs.0; - } -} - -impl Mul for Score { - type Output = Self; - - fn mul(self, rhs: Value) -> Self::Output { - Score(self.0 * rhs) - } -} - -impl Mul for Value { - type Output = Score; - - fn mul(self, rhs: Score) -> Self::Output { - Score(self * rhs.0) - } -} - -impl Neg for Score { - type Output = Self; - - fn neg(self) -> Self::Output { - Score(-self.0) - } -} - -impl From for Score { - fn from(value: Value) -> Self { - Score(value) - } -} - -impl fmt::Display for Score { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let value = self.0; - if *self == Self::MAX { - write!(f, "INF") - } else if *self == Self::MIN { - write!(f, "-INF") - } else { - write!(f, "{value}cp") - } - } -} diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 77126ba..2c6b7e9 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -3,8 +3,6 @@ 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, @@ -70,22 +68,8 @@ impl Shape { } #[must_use] - pub const 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; - - 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), - } + pub fn is_promotable(&self) -> bool { + Self::PROMOTABLE_SHAPES.contains(self) } } diff --git a/explorer/src/main.rs b/explorer/src/main.rs index cfaf102..77ef939 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,11 +1,14 @@ // Eryn Wells -use chessfriend_board::{Board, ZobristState, fen::FromFenStr}; -use chessfriend_core::{Color, Piece, Shape, Square, Wing, random::RandomNumberGenerator}; -use chessfriend_moves::{GeneratedMove, ValidateMove, algebraic::AlgebraicMoveComponents}; +use chessfriend_board::ZobristState; +use chessfriend_board::{Board, fen::FromFenStr}; +use chessfriend_core::random::RandomNumberGenerator; +use chessfriend_core::{Color, Piece, Shape, Square}; +use chessfriend_moves::GeneratedMove; use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr}; -use clap::{Arg, Command, value_parser}; -use rustyline::{DefaultEditor, error::ReadlineError}; +use clap::{Arg, Command}; +use rustyline::DefaultEditor; +use rustyline::error::ReadlineError; use std::sync::Arc; use thiserror::Error; @@ -42,7 +45,6 @@ fn command_line() -> Command { .subcommand_help_heading("COMMANDS") .help_template(PARSER_TEMPLATE) .subcommand(Command::new("fen").about("Print the current position as a FEN string")) - .subcommand(Command::new("flags").about("Print flags for the current position")) .subcommand( Command::new("load") .arg(Arg::new("fen").required(true)) @@ -51,7 +53,8 @@ fn command_line() -> Command { ) .subcommand( Command::new("make") - .arg(Arg::new("move").required(true)) + .arg(Arg::new("from").required(true)) + .arg(Arg::new("to").required(true)) .alias("m") .about("Make a move"), ) @@ -65,7 +68,7 @@ fn command_line() -> Command { ) .subcommand( Command::new("sight") - .arg(Arg::new("square").required(false)) + .arg(Arg::new("square").required(true)) .about("Show sight of a piece on a square"), ) .subcommand( @@ -78,14 +81,6 @@ fn command_line() -> Command { .arg(Arg::new("square").required(true)) .about("Show moves of a piece on a square"), ) - .subcommand( - Command::new("perft") - .arg(Arg::new("depth") - .required(true) - .value_parser(value_parser!(usize)) - ) - .about("Run Perft on the current position to the given depth") - ) .subcommand( Command::new("reset") .subcommand(Command::new("clear").about("Reset to a cleared board")) @@ -112,12 +107,6 @@ enum CommandHandlingError<'a> { #[error("no piece on {0}")] NoPiece(Square), - - #[error("{value:?} is not a valid value for {argument_name:?}")] - ValueError { - argument_name: &'static str, - value: String, - }, } fn respond(line: &str, state: &mut State) -> anyhow::Result { @@ -127,7 +116,6 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { let mut result = CommandResult::default(); match matches.subcommand() { - Some(("flags", matches)) => result = do_flags_command(state, matches), Some(("load", matches)) => result = do_load_command(state, matches)?, Some(("print", _matches)) => {} Some(("quit", _matches)) => { @@ -138,8 +126,9 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { println!("{}", state.position.to_fen_str()?); result.should_print_position = false; } - Some(("make", matches)) => result = do_make_command(state, matches)?, - Some(("perft", matches)) => result = do_perft_command(state, matches)?, + Some(("make", _matches)) => { + unimplemented!() + } Some(("place", matches)) => { let color = matches .get_one::("color") @@ -163,12 +152,12 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { .place_piece(piece, square, PlacePieceStrategy::default())?; } Some(("sight", matches)) => { - let sight = if let Some(square) = matches.get_one::("square") { - let square: Square = square.parse()?; - state.position.sight_piece(square) - } else { - state.position.sight_active() - }; + let square = matches + .get_one::("square") + .ok_or(CommandHandlingError::MissingArgument("square"))?; + let square = square.parse::()?; + + let sight = state.position.sight(square); let display = state.position.display().highlight(sight); println!("\n{display}"); @@ -186,34 +175,6 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { Ok(result) } -fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult { - let board = state.position.board(); - - println!("Castling:"); - - for (color, wing) in [ - (Color::White, Wing::KingSide), - (Color::White, Wing::QueenSide), - (Color::Black, Wing::KingSide), - (Color::Black, Wing::QueenSide), - ] { - let has_right = board.has_castling_right_unwrapped(color, wing.into()); - let can_castle = board.color_can_castle(wing, Some(color)); - - let can_castle_message = match can_castle { - Ok(_) => "ok".to_string(), - Err(error) => format!("{error}"), - }; - - println!(" {color} {wing}: {has_right}, {can_castle_message}"); - } - - CommandResult { - should_continue: true, - should_print_position: false, - } -} - fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result { let fen_string = matches .get_one::("fen") @@ -230,26 +191,6 @@ fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Res }) } -fn do_make_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result { - let move_string = matches - .get_one::("move") - .ok_or(CommandHandlingError::MissingArgument("move"))?; - - let algebraic_move: AlgebraicMoveComponents = move_string.parse()?; - - let encoded_move = state - .position - .move_from_algebraic_components(algebraic_move) - .ok_or(CommandHandlingError::ValueError { - argument_name: "move", - value: move_string.to_string(), - })?; - - state.position.make_move(encoded_move, ValidateMove::Yes)?; - - Ok(CommandResult::default()) -} - fn do_reset_command( state: &mut State, matches: &clap::ArgMatches, @@ -331,7 +272,7 @@ fn do_movement_command( .get_one::("square") .ok_or(CommandHandlingError::MissingArgument("square"))?; - let movement = state.position.movement_piece(square); + let movement = state.position.movement(square); let display = state.position.display().highlight(movement); println!("\n{display}"); @@ -341,25 +282,6 @@ fn do_movement_command( }) } -fn do_perft_command( - state: &mut State, - matches: &clap::ArgMatches, -) -> anyhow::Result { - let depth = *matches - .get_one::("depth") - .ok_or(CommandHandlingError::MissingArgument("depth"))?; - - let mut position = state.position.clone(); - let counters = position.perft(depth); - - println!("{counters}"); - - Ok(CommandResult { - should_continue: true, - should_print_position: false, - }) -} - fn do_zobrist_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult { if let Some(hash) = state.position.zobrist_hash() { println!("hash:{hash}"); @@ -390,7 +312,7 @@ fn main() -> Result<(), String> { loop { if should_print_position { println!("{}", &state.position); - println!("{} to move.", state.position.active_color()); + println!("{} to move.", state.position.board.active_color()); } let readline = editor.readline("\n? "); diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index bcbca0a..7b8fd33 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -3,8 +3,7 @@ use crate::{Move, MoveRecord}; use chessfriend_board::{ Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy, - castle::{CastleEvaluationError, CastleRightsOption}, - movement::Movement, + castle::CastleEvaluationError, movement::Movement, }; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; use thiserror::Error; @@ -18,7 +17,7 @@ pub enum ValidateMove { Yes, } -#[derive(Clone, Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error, Eq, PartialEq)] pub enum MakeMoveError { #[error("no piece on {0}")] NoPiece(Square), @@ -252,7 +251,7 @@ impl MakeMoveInternal for T { // original board state is preserved. let record = MoveRecord::new(board, ply, None); - board.revoke_castling_rights_active(wing.into()); + board.revoke_castling_right_unwrapped(active_color, wing); self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance); @@ -308,23 +307,21 @@ impl MakeMoveInternal for T { Shape::Rook => { let origin = ply.origin_square(); - if board.has_castling_right(None, Wing::KingSide) { + if board.color_has_castling_right(None, Wing::KingSide) { let kingside_parameters = CastleParameters::get(board.active_color(), Wing::KingSide); if origin == kingside_parameters.origin.rook { - board.revoke_castling_rights(None, Wing::KingSide.into()); + board.revoke_castling_right(None, Wing::KingSide); } } let queenside_parameters = CastleParameters::get(board.active_color(), Wing::QueenSide); if origin == queenside_parameters.origin.rook { - board.revoke_castling_rights(None, Wing::QueenSide.into()); + board.revoke_castling_right(None, Wing::QueenSide); } } - Shape::King => { - board.revoke_castling_rights(None, CastleRightsOption::All); - } + Shape::King => board.revoke_all_castling_rights(), _ => {} } @@ -562,7 +559,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); Ok(()) } @@ -582,7 +579,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); Ok(()) } diff --git a/moves/src/record.rs b/moves/src/record.rs index 24472b8..47a2c3f 100644 --- a/moves/src/record.rs +++ b/moves/src/record.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::Move; -use chessfriend_board::{Board, CastleRights, board::HalfMoveClock}; +use chessfriend_board::{board::HalfMoveClock, Board, CastleRights}; use chessfriend_core::{Color, Piece, Square}; /// A record of a move made on a board. This struct contains all the information @@ -35,7 +35,7 @@ impl MoveRecord { color: board.active_color(), ply, en_passant_target: board.en_passant_target(), - castling_rights: *board.castling_rights(), + castling_rights: board.castling_rights(), half_move_clock: board.half_move_clock, captured_piece: capture, } diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 6833608..8a477c6 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -7,7 +7,7 @@ use thiserror::Error; pub type UnmakeMoveResult = Result<(), UnmakeMoveError>; -#[derive(Clone, Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error, Eq, PartialEq)] pub enum UnmakeMoveError { #[error("no move to unmake")] NoMove, @@ -396,7 +396,7 @@ mod tests { White Rook on H1, ]; - let original_castling_rights = *board.castling_rights(); + let original_castling_rights = board.castling_rights(); let ply = Move::castle(Color::White, Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -406,7 +406,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), None); assert_eq!(board.get_piece(Square::G1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook))); - assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); board.unmake_move(&record)?; @@ -415,7 +415,7 @@ mod tests { assert_eq!(board.get_piece(Square::H1), Some(piece!(White Rook))); assert_eq!(board.get_piece(Square::G1), None); assert_eq!(board.get_piece(Square::F1), None); - assert_eq!(*board.castling_rights(), original_castling_rights); + assert_eq!(board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::White); Ok(()) @@ -428,7 +428,7 @@ mod tests { White Rook on A1, ]; - let original_castling_rights = *board.castling_rights(); + let original_castling_rights = board.castling_rights(); let ply = Move::castle(Color::White, Wing::QueenSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -438,7 +438,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), None); assert_eq!(board.get_piece(Square::C1), Some(piece!(White King))); assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook))); - assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); board.unmake_move(&record)?; @@ -447,7 +447,7 @@ mod tests { assert_eq!(board.get_piece(Square::A1), Some(piece!(White Rook))); assert_eq!(board.get_piece(Square::C1), None); assert_eq!(board.get_piece(Square::D1), None); - assert_eq!(*board.castling_rights(), original_castling_rights); + assert_eq!(board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::White); Ok(()) @@ -460,7 +460,7 @@ mod tests { Black Rook on H8, ]); - let original_castling_rights = *board.castling_rights(); + let original_castling_rights = board.castling_rights(); let ply = Move::castle(Color::Black, Wing::KingSide); let record = board.make_move(ply, ValidateMove::Yes)?; @@ -478,7 +478,7 @@ mod tests { assert_eq!(board.get_piece(Square::H8), Some(piece!(Black Rook))); assert_eq!(board.get_piece(Square::G8), None); assert_eq!(board.get_piece(Square::F8), None); - assert_eq!(*board.castling_rights(), original_castling_rights); + assert_eq!(board.castling_rights(), original_castling_rights); assert_eq!(board.active_color(), Color::Black); Ok(()) diff --git a/perft/src/main.rs b/perft/src/main.rs index d1e7f77..70630ae 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -1,13 +1,9 @@ -use chessfriend_position::{ - Position, - fen::{FromFenStr, ToFenStr}, -}; +use chessfriend_position::{Position, fen::FromFenStr, perft::Perft}; use clap::Parser; #[derive(Parser, Debug)] #[command(name = "Perft")] struct Arguments { - #[arg(long, short, value_name = "INT")] depth: usize, #[arg(long, short, value_name = "FEN")] @@ -18,18 +14,17 @@ fn main() -> anyhow::Result<()> { let args = Arguments::parse(); let depth = args.depth; + println!("depth {depth}"); + let mut position = if let Some(fen) = args.fen { Position::from_fen_str(&fen)? } else { Position::starting(None) }; - println!("fen \"{}\"", position.to_fen_str().unwrap()); - println!("depth {depth}"); + let nodes_searched = position.perft(depth); - let counters = position.perft(depth); - - println!("\n{counters}"); + println!("nodes {nodes_searched}"); Ok(()) } diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs deleted file mode 100644 index 8496760..0000000 --- a/position/src/evaluation.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Eryn Wells - -use crate::Position; -use chessfriend_board::Board; -use chessfriend_core::{Color, Piece, Shape, score::Score}; - -struct Evaluator; - -impl Evaluator { - pub fn evaluate_symmetric_unwrapped(position: &Position, color: Color) -> Score { - let board = &position.board; - - let material_balance = Self::material_balance(board, color); - - let score = material_balance; - - score - } - - /// Evaluate a board using the symmetric evaluation algorithm defined by - /// Claude Shannon. - fn material_balance(board: &Board, color: Color) -> Score { - let other_color = color.other(); - - Shape::into_iter().fold(Score::ZERO, |acc, shape| { - let (active_pieces, other_pieces) = ( - i32::from(board.count_piece(&Piece::new(color, shape))), - i32::from(board.count_piece(&Piece::new(other_color, shape))), - ); - - let factor = shape.score() * (active_pieces - other_pieces); - - acc + factor - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chessfriend_board::fen; - - #[test] - fn pawn_material_balance() -> Result<(), Box> { - let board = fen!("8/8/8/8/8/3P4/8/8 w - - 0 1")?; - assert_eq!( - Evaluator::material_balance(&board, Color::White), - 100i32.into() - ); - - let board = fen!("8/8/3p4/8/8/3P4/8/8 w - - 0 1")?; - assert_eq!(Evaluator::material_balance(&board, Color::White), 0.into()); - - Ok(()) - } - - #[test] - fn starting_position_is_even() { - let position = Position::new(Board::starting(None)); - assert_eq!( - Evaluator::evaluate_symmetric_unwrapped(&position, Color::White), - Evaluator::evaluate_symmetric_unwrapped(&position, Color::Black) - ); - } -} diff --git a/position/src/lib.rs b/position/src/lib.rs index 7ccee47..8bd4d49 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,13 +1,12 @@ // Eryn Wells -mod evaluation; mod position; #[macro_use] mod macros; -pub use chessfriend_board::{PlacePieceError, PlacePieceStrategy, fen}; -pub use chessfriend_moves::{GeneratedMove, ValidateMove}; +pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy, ZobristState}; +pub use chessfriend_moves::{GeneratedMove, Move, ValidateMove}; pub use position::Position; pub mod perft; diff --git a/position/src/perft.rs b/position/src/perft.rs index 683cb67..57af81a 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -1,70 +1,45 @@ // Eryn Wells use crate::{GeneratedMove, Position, ValidateMove}; -use std::fmt; -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct PerftCounters { - nodes: u64, +pub trait Perft { + fn perft(&mut self, depth: usize) -> u64; } -impl Position { - pub fn perft(&mut self, depth: usize) -> PerftCounters { - self.perft_recursive(0, depth) +impl Perft for Position { + fn perft(&mut self, depth: usize) -> u64 { + self.perft_recursive(depth, depth) } } impl Position { - fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> PerftCounters { - let mut counters = PerftCounters::default(); - - if depth == max_depth { - counters.count_node(); - return counters; + fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> u64 { + if depth == 0 { + return 1; } + let mut total_nodes_counted = 0u64; + let legal_moves: Vec = self.all_legal_moves(None).collect(); - for generated_ply in legal_moves { - let ply = generated_ply.ply(); + for ply in legal_moves { + let ply = ply.ply(); - let has_seen_position = self + let _has_seen_position = self .make_move(ply, ValidateMove::No) .expect("unable to make generated move"); - let recursive_counters = if has_seen_position { - let mut counters = PerftCounters::default(); - counters.count_node(); - counters - } else { - self.perft_recursive(depth + 1, max_depth) - }; + let nodes_counted = self.perft_recursive(depth - 1, depth); + + total_nodes_counted += nodes_counted; self.unmake_last_move().expect("unable to unmake last move"); - counters.fold(&recursive_counters); - - if depth == 0 { - println!(" {ply}: {}", recursive_counters.nodes); + if depth == max_depth { + println!(" {ply} {nodes_counted}"); } } - counters - } -} - -impl PerftCounters { - fn count_node(&mut self) { - self.nodes += 1; - } - fn fold(&mut self, results: &Self) { - self.nodes += results.nodes; - } -} - -impl fmt::Display for PerftCounters { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Perft Results")?; - write!(f, " Nodes: {}", self.nodes) + total_nodes_counted } } diff --git a/position/src/position.rs b/position/src/position.rs index 1763875..8787134 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -6,25 +6,25 @@ use crate::fen::{FromFenStr, FromFenStrError}; use captures::CapturesList; use chessfriend_bitboard::BitBoard; use chessfriend_board::{ - Board, PlacePieceError, PlacePieceStrategy, ZobristState, display::DiagramFormatter, - fen::ToFenStr, + display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, + ZobristState, }; use chessfriend_core::{Color, Piece, Shape, Square}; use chessfriend_moves::{ - GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, - UnmakeMoveResult, ValidateMove, algebraic::AlgebraicMoveComponents, generators::{ AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator, }, + GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError, + UnmakeMoveResult, ValidateMove, }; use std::{collections::HashSet, fmt, sync::Arc}; #[must_use] #[derive(Clone, Debug, Default, Eq)] pub struct Position { - pub(crate) board: Board, + pub board: Board, pub(crate) moves: Vec, pub(crate) captures: CapturesList, @@ -48,16 +48,6 @@ impl Position { ..Default::default() } } - - #[must_use] - pub fn board(&self) -> &Board { - &self.board - } - - #[must_use] - pub fn active_color(&self) -> Color { - self.board.active_color() - } } impl Position { @@ -86,14 +76,12 @@ impl Position { } impl Position { - /// Calculate sight of a piece on the provided [`Square`]. - pub fn sight_piece(&self, square: Square) -> BitBoard { - self.board.sight_piece(square) + pub fn sight(&self, square: Square) -> BitBoard { + self.board.sight(square) } - /// Calculate movement of a piece on the provided [`Square`]. - pub fn movement_piece(&self, square: Square) -> BitBoard { - self.board.movement_piece(square) + pub fn movement(&self, square: Square) -> BitBoard { + self.board.movement(square) } } @@ -129,7 +117,7 @@ impl Position { ); }); - let move_is_legal = !test_board.is_in_check(); + let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move)); test_board.unmake_move(&record).unwrap_or_else(|err| { panic!( @@ -165,8 +153,8 @@ impl Position { } impl Position { - pub fn sight_active(&self) -> BitBoard { - self.board.sight_active() + pub fn active_sight(&self) -> BitBoard { + self.board.active_sight() } /// A [`BitBoard`] of all squares the given color can see. @@ -287,7 +275,7 @@ impl Position { } let target_bitboard: BitBoard = target.into(); - if !(self.movement_piece(origin) & target_bitboard).is_populated() { + if !(self.movement(origin) & target_bitboard).is_populated() { return None; } @@ -356,7 +344,7 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { use super::*; - use crate::{Position, test_position}; + use crate::{test_position, Position}; use chessfriend_core::piece; #[test] diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs index 06ccc45..eadcf4d 100644 --- a/position/tests/peterellisjones.rs +++ b/position/tests/peterellisjones.rs @@ -8,7 +8,7 @@ use chessfriend_core::Color; use chessfriend_moves::{ - Move, assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, + assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move, }; use chessfriend_position::test_position; use std::collections::HashSet; @@ -107,7 +107,7 @@ fn en_passant_check_capture() { White Pawn on D4, ], D3); - assert!(pos.board().is_in_check()); + assert!(pos.board.active_color_is_in_check()); let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); @@ -123,7 +123,7 @@ fn en_passant_check_block() { White Queen on F1, ], D3); - assert!(pos.board().is_in_check()); + assert!(pos.board.active_color_is_in_check()); let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); @@ -139,7 +139,7 @@ fn pinned_pieces_rook_cannot_move_out_of_pin() { White King on C1, ]); - assert!(!pos.board().is_in_check()); + assert!(!pos.board.active_color_is_in_check()); let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect(); diff --git a/rustfmt.toml b/rustfmt.toml index b377055..be6f4bf 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,3 @@ -style_edition = "2024" - imports_layout = "HorizontalVertical" group_imports = "StdExternalCrate"