diff --git a/Cargo.lock b/Cargo.lock index 936cbaa..337e104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,8 +71,6 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chessfriend" version = "0.1.0" -dependencies = [ -] [[package]] name = "chessfriend_bitboard" diff --git a/Cargo.toml b/Cargo.toml index 49b9a15..37c14db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,7 @@ members = [ "position", ] resolver = "3" + +[profile.release-debug] +inherits = "release" +debug = true 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 b9e4c1c..35ce927 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -43,16 +43,8 @@ macro_rules! moves_getter { } impl BitBoard { - 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 EMPTY: BitBoard = BitBoard(u64::MIN); + pub const FULL: BitBoard = BitBoard(u64::MAX); pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) @@ -107,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()); /// ``` @@ -123,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()); /// ``` @@ -158,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 { @@ -209,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()); /// ``` @@ -231,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) @@ -241,6 +265,24 @@ 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 { @@ -512,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/lib.rs b/bitboard/src/lib.rs index 798e51b..12a25ed 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 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/board.rs b/board/src/board.rs index 666488e..338d4aa 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -1,13 +1,13 @@ // Eryn Wells use crate::{ - PieceSet, castle, + CastleRights, PieceSet, display::DiagramFormatter, - piece_sets::{PlacePieceError, PlacePieceStrategy}, + piece_sets::{Counter, PlacePieceError, PlacePieceStrategy}, zobrist::{ZobristHash, ZobristState}, }; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Shape, Square, Wing}; +use chessfriend_core::{Color, Piece, Shape, Square}; 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: castle::Rights, + castling_rights: CastleRights, en_passant_target: Option, pub half_move_clock: HalfMoveClock, pub full_move_number: FullMoveClock, @@ -92,59 +92,27 @@ impl Board { impl Board { #[must_use] - pub fn castling_rights(&self) -> castle::Rights { - self.castling_rights + pub fn castling_rights(&self) -> &CastleRights { + &self.castling_rights } - pub fn set_castling_rights(&mut self, rights: castle::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) { if rights == self.castling_rights { return; } let old_rights = self.castling_rights; self.castling_rights = rights; + self.update_zobrist_hash_castling_rights(old_rights); } - #[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) { + pub(crate) fn update_zobrist_hash_castling_rights(&mut self, old_rights: CastleRights) { let new_rights = self.castling_rights; if old_rights == new_rights { return; @@ -154,6 +122,18 @@ 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 { @@ -239,6 +219,11 @@ 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 f8f9c24..4ba9a4b 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::Rights; +pub use rights::{CastleRightsOption, Rights}; use crate::{Board, CastleParameters}; -use chessfriend_core::{Color, Piece, Square, Wing}; +use chessfriend_core::{Color, 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.color_has_castling_right_unwrapped(color, wing) { + if !self.has_castling_right_unwrapped(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } @@ -76,17 +76,60 @@ impl Board { Ok(parameters) } +} - 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()) +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_rook(&self, square: Square) -> Option { - let active_color = self.active_color(); - self.get_piece(square) - .filter(|piece| piece.color == active_color && piece.is_rook()) + #[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); } } @@ -104,8 +147,8 @@ mod tests { White Rook on H1 ); - assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); - assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); } #[test] diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs index b65461b..2c0a961 100644 --- a/board/src/castle/rights.rs +++ b/board/src/castle/rights.rs @@ -1,6 +1,14 @@ +// 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); @@ -12,16 +20,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 color_has_right(self, color: Color, wing: Wing) -> bool { - (self.0 & (1 << Self::flag_offset(color, wing))) != 0 + pub fn get(self, color: Color, option: CastleRightsOption) -> bool { + (self.0 & Self::flags(color, option)) != 0 } - pub fn grant(&mut self, color: Color, wing: Wing) { - self.0 |= 1 << Self::flag_offset(color, wing); + pub fn grant(&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)); + pub fn revoke(&mut self, color: Color, option: CastleRightsOption) { + self.0 &= !Self::flags(color, option); } /// Revoke castling rights for all colors and all sides of the board. @@ -31,14 +39,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 { - fn flag_offset(color: Color, wing: Wing) -> usize { - ((color as usize) << 1) + wing as usize + const fn flags(color: Color, option: CastleRightsOption) -> u8 { + option.as_flags() << (color as u8 * 2) } } @@ -54,36 +62,55 @@ 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::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); + 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); } #[test] fn default_rights() { let mut rights = Rights::default(); - 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)); + 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.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.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)); + 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())); } } diff --git a/board/src/check.rs b/board/src/check.rs index ba9f06d..6d8ceba 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -5,18 +5,10 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece}; impl Board { + /// Return whether the active color is in check. #[must_use] - 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 { + pub fn is_in_check(&self) -> bool { + let color = self.active_color(); let king = self.king_bitboard(color); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() @@ -39,7 +31,7 @@ mod tests { Black Rook on F3, ); - assert!(board.unwrapped_color_is_in_check(Color::White)); + assert!(board.is_in_check()); } #[test] @@ -49,6 +41,6 @@ mod tests { Black Rook on B4, ); - assert!(!board.unwrapped_color_is_in_check(Color::White)); + assert!(!board.is_in_check()); } } diff --git a/board/src/fen.rs b/board/src/fen.rs index 0da38dc..fe418a5 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -9,9 +9,10 @@ use thiserror::Error; #[macro_export] macro_rules! fen { - ($fen_string:literal) => { - Board::from_fen_str($fen_string) - }; + ($fen_string:literal) => {{ + use $crate::fen::FromFenStr; + $crate::Board::from_fen_str($fen_string) + }}; } #[derive(Clone, Debug, Error, Eq, PartialEq)] @@ -24,12 +25,16 @@ 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), } @@ -122,12 +127,12 @@ impl ToFenStr for Board { (Color::Black, Wing::KingSide), (Color::Black, Wing::QueenSide), ] - .map(|(color, castle)| { - if !self.color_has_castling_right_unwrapped(color, castle) { + .map(|(color, wing)| { + if !self.has_castling_right_unwrapped(color, wing) { return ""; } - match (color, castle) { + match (color, wing) { (Color::White, Wing::KingSide) => "K", (Color::White, Wing::QueenSide) => "Q", (Color::Black, Wing::KingSide) => "k", @@ -226,20 +231,23 @@ 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))?; - if castling_rights == "-" { - board.revoke_all_castling_rights(); - } else { + board.revoke_all_castling_rights(); + if castling_rights != "-" { for ch in castling_rights.chars() { - 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), - }; + let (color, wing) = + color_wing_from_char(ch).ok_or(FromFenStrError::InvalidValue)?; + board.grant_castling_rights_unwrapped(color, wing.into()); } } diff --git a/board/src/movement.rs b/board/src/movement.rs index 4fa4381..3ebf44c 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::{sight::Sight, Board}; +use crate::{Board, sight::Sight}; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; impl Board { - pub fn movement(&self, square: Square) -> BitBoard { + pub fn movement_piece(&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 063937d..52af054 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -1,8 +1,9 @@ // Eryn Wells +mod counts; mod mailbox; -use self::mailbox::Mailbox; +use self::{counts::Counts, mailbox::Mailbox}; use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, Shape, Square}; use std::{ @@ -11,6 +12,8 @@ use std::{ }; use thiserror::Error; +pub(crate) use counts::Counter; + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum PlacePieceStrategy { #[default] @@ -18,7 +21,7 @@ pub enum PlacePieceStrategy { PreserveExisting, } -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum PlacePieceError { #[error("cannot place piece on {square} with existing {piece}")] ExisitingPiece { piece: Piece, square: Square }, @@ -29,6 +32,7 @@ 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], } @@ -36,10 +40,11 @@ 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::iter().enumerate() { + for (color_index, color) in Color::into_iter().enumerate() { for (shape_index, shape) in Shape::into_iter().enumerate() { let bitboard = pieces[color_index][shape_index]; @@ -47,14 +52,16 @@ 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, } @@ -94,6 +101,10 @@ 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]; @@ -120,6 +131,7 @@ 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) @@ -127,8 +139,12 @@ impl PieceSet { pub(crate) fn remove(&mut self, square: Square) -> Option { if let Some(piece) = self.mailbox.get(square) { - self.color_occupancy[piece.color as usize].clear(square); - self.shape_occupancy[piece.shape as usize].clear(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.mailbox.remove(square); Some(piece) diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs new file mode 100644 index 0000000..7d3cade --- /dev/null +++ b/board/src/piece_sets/counts.rs @@ -0,0 +1,61 @@ +// 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 5d73561..74b32bc 100644 --- a/board/src/piece_sets/mailbox.rs +++ b/board/src/piece_sets/mailbox.rs @@ -7,10 +7,6 @@ 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] } @@ -46,7 +42,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::new(), |mut mailbox, (square, piece)| { + .fold(Self::default(), |mut mailbox, (square, piece)| { mailbox.set(piece, square); mailbox }) @@ -61,7 +57,7 @@ mod tests { #[test] fn iter_returns_all_pieces() { - let mut mailbox = Mailbox::new(); + let mut mailbox = Mailbox::default(); 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 81fb120..e682cb0 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -23,16 +23,31 @@ use std::ops::BitOr; impl Board { /// Compute sight of the piece on the given square. - pub fn sight(&self, square: Square) -> BitBoard { + pub fn sight_piece(&self, square: Square) -> BitBoard { if let Some(piece) = self.get_piece(square) { piece.sight(square, self) } else { - BitBoard::empty() + BitBoard::EMPTY } } - pub fn active_sight(&self) -> BitBoard { - self.friendly_sight(self.active_color()) + /// 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) } /// A [`BitBoard`] of all squares the given color can see. @@ -40,8 +55,8 @@ impl Board { // TODO: Probably want to implement a caching layer here. self.friendly_occupancy(color) .occupied_squares(&IterationDirection::default()) - .map(|square| self.sight(square)) - .fold(BitBoard::empty(), BitOr::bitor) + .map(|square| self.sight_piece(square)) + .fold(BitBoard::EMPTY, BitOr::bitor) } pub fn active_color_opposing_sight(&self) -> BitBoard { @@ -60,7 +75,7 @@ impl Board { Some(self.friendly_sight(c)) } }) - .fold(BitBoard::empty(), BitOr::bitor) + .fold(BitBoard::EMPTY, BitOr::bitor) } } @@ -123,20 +138,6 @@ struct SightInfo { friendly_occupancy: BitBoard, } -macro_rules! ray_in_direction { - ($square:expr, $blockers:expr, $direction:ident, $first_occupied_square:tt) => {{ - let ray = BitBoard::ray($square, Direction::$direction); - let ray_blockers = ray & $blockers; - if let Some(first_occupied_square) = ray_blockers.$first_occupied_square() { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - attack_ray - } else { - ray - } - }}; -} - /// Compute sight of a white pawn. fn white_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard { let possible_squares = !info.friendly_occupancy | en_passant_square; @@ -160,15 +161,27 @@ fn knight_sight(info: &SightInfo) -> BitBoard { BitBoard::knight_moves(info.square) } +fn ray_in_direction(square: Square, blockers: BitBoard, direction: Direction) -> BitBoard { + let ray = BitBoard::ray(square, direction); + + let ray_blockers = ray & blockers; + if let Some(first_occupied_square) = ray_blockers.first_occupied_square_direction(direction) { + let remainder = BitBoard::ray(first_occupied_square, direction); + let attack_ray = ray & !remainder; + attack_ray + } else { + ray + } +} + fn bishop_sight(info: &SightInfo) -> BitBoard { let bishop = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(bishop, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(bishop, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, NorthWest, first_occupied_square_trailing); + let sight = ray_in_direction(bishop, occupancy, Direction::NorthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthWest) + | ray_in_direction(bishop, occupancy, Direction::NorthWest); sight } @@ -177,11 +190,10 @@ fn rook_sight(info: &SightInfo) -> BitBoard { let rook = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(rook, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(rook, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(rook, occupancy, Direction::North) + | ray_in_direction(rook, occupancy, Direction::East) + | ray_in_direction(rook, occupancy, Direction::South) + | ray_in_direction(rook, occupancy, Direction::West); sight } @@ -190,15 +202,14 @@ fn queen_sight(info: &SightInfo) -> BitBoard { let queen = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(queen, occupancy, NorthWest, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(queen, occupancy, Direction::NorthWest) + | ray_in_direction(queen, occupancy, Direction::North) + | ray_in_direction(queen, occupancy, Direction::NorthEast) + | ray_in_direction(queen, occupancy, Direction::East) + | ray_in_direction(queen, occupancy, Direction::SouthEast) + | ray_in_direction(queen, occupancy, Direction::South) + | ray_in_direction(queen, occupancy, Direction::SouthWest) + | ray_in_direction(queen, occupancy, Direction::West); sight } @@ -244,7 +255,7 @@ mod tests { White King on E4, ); - let sight = pos.active_sight(); + let sight = pos.sight_active(); assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]); } @@ -267,8 +278,8 @@ mod tests { mod pawn { use crate::{sight::Sight, test_board}; - use chessfriend_bitboard::{bitboard, BitBoard}; - use chessfriend_core::{piece, Square}; + use chessfriend_bitboard::{BitBoard, bitboard}; + use chessfriend_core::{Square, piece}; sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]); @@ -294,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/colors.rs b/core/src/colors.rs index 5cf633b..e1255b7 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,7 @@ // Eryn Wells -use crate::Direction; +use crate::{Direction, score}; +use std::fmt; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -56,10 +57,18 @@ impl Color { Color::Black => "black", } } + + #[must_use] + pub const fn score_factor(self) -> score::Value { + match self { + Color::White => 1, + Color::Black => -1, + } + } } -impl std::fmt::Display for Color { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 99a252e..308fc3e 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -115,7 +115,9 @@ 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 { @@ -262,7 +264,7 @@ impl Square { #[must_use] pub unsafe fn from_index_unchecked(x: u8) -> Square { debug_assert!((x as usize) < Self::NUM); - Self::try_from(x).unwrap_unchecked() + Self::ALL[x as usize] } #[inline] diff --git a/core/src/lib.rs b/core/src/lib.rs index 0638410..f298a81 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,6 +4,7 @@ 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 new file mode 100644 index 0000000..3528861 --- /dev/null +++ b/core/src/score.rs @@ -0,0 +1,126 @@ +// 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 2c6b7e9..77126ba 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -3,6 +3,8 @@ 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, @@ -68,8 +70,22 @@ impl Shape { } #[must_use] - pub fn is_promotable(&self) -> bool { - Self::PROMOTABLE_SHAPES.contains(self) + 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), + } } } diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 77ef939..cfaf102 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,14 +1,11 @@ // Eryn Wells -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_board::{Board, ZobristState, fen::FromFenStr}; +use chessfriend_core::{Color, Piece, Shape, Square, Wing, random::RandomNumberGenerator}; +use chessfriend_moves::{GeneratedMove, ValidateMove, algebraic::AlgebraicMoveComponents}; use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr}; -use clap::{Arg, Command}; -use rustyline::DefaultEditor; -use rustyline::error::ReadlineError; +use clap::{Arg, Command, value_parser}; +use rustyline::{DefaultEditor, error::ReadlineError}; use std::sync::Arc; use thiserror::Error; @@ -45,6 +42,7 @@ 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)) @@ -53,8 +51,7 @@ fn command_line() -> Command { ) .subcommand( Command::new("make") - .arg(Arg::new("from").required(true)) - .arg(Arg::new("to").required(true)) + .arg(Arg::new("move").required(true)) .alias("m") .about("Make a move"), ) @@ -68,7 +65,7 @@ fn command_line() -> Command { ) .subcommand( Command::new("sight") - .arg(Arg::new("square").required(true)) + .arg(Arg::new("square").required(false)) .about("Show sight of a piece on a square"), ) .subcommand( @@ -81,6 +78,14 @@ 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")) @@ -107,6 +112,12 @@ 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 { @@ -116,6 +127,7 @@ 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)) => { @@ -126,9 +138,8 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { println!("{}", state.position.to_fen_str()?); result.should_print_position = false; } - Some(("make", _matches)) => { - unimplemented!() - } + Some(("make", matches)) => result = do_make_command(state, matches)?, + Some(("perft", matches)) => result = do_perft_command(state, matches)?, Some(("place", matches)) => { let color = matches .get_one::("color") @@ -152,12 +163,12 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { .place_piece(piece, square, PlacePieceStrategy::default())?; } Some(("sight", matches)) => { - let square = matches - .get_one::("square") - .ok_or(CommandHandlingError::MissingArgument("square"))?; - let square = square.parse::()?; - - let sight = state.position.sight(square); + 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 display = state.position.display().highlight(sight); println!("\n{display}"); @@ -175,6 +186,34 @@ 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") @@ -191,6 +230,26 @@ 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, @@ -272,7 +331,7 @@ fn do_movement_command( .get_one::("square") .ok_or(CommandHandlingError::MissingArgument("square"))?; - let movement = state.position.movement(square); + let movement = state.position.movement_piece(square); let display = state.position.display().highlight(movement); println!("\n{display}"); @@ -282,6 +341,25 @@ 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}"); @@ -312,7 +390,7 @@ fn main() -> Result<(), String> { loop { if should_print_position { println!("{}", &state.position); - println!("{} to move.", state.position.board.active_color()); + println!("{} to move.", state.position.active_color()); } let readline = editor.readline("\n? "); diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 7b8fd33..bcbca0a 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -3,7 +3,8 @@ use crate::{Move, MoveRecord}; use chessfriend_board::{ Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy, - castle::CastleEvaluationError, movement::Movement, + castle::{CastleEvaluationError, CastleRightsOption}, + movement::Movement, }; use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing}; use thiserror::Error; @@ -17,7 +18,7 @@ pub enum ValidateMove { Yes, } -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum MakeMoveError { #[error("no piece on {0}")] NoPiece(Square), @@ -251,7 +252,7 @@ impl MakeMoveInternal for T { // original board state is preserved. let record = MoveRecord::new(board, ply, None); - board.revoke_castling_right_unwrapped(active_color, wing); + board.revoke_castling_rights_active(wing.into()); self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance); @@ -307,21 +308,23 @@ impl MakeMoveInternal for T { Shape::Rook => { let origin = ply.origin_square(); - if board.color_has_castling_right(None, Wing::KingSide) { + if board.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_right(None, Wing::KingSide); + board.revoke_castling_rights(None, Wing::KingSide.into()); } } let queenside_parameters = CastleParameters::get(board.active_color(), Wing::QueenSide); if origin == queenside_parameters.origin.rook { - board.revoke_castling_right(None, Wing::QueenSide); + board.revoke_castling_rights(None, Wing::QueenSide.into()); } } - Shape::King => board.revoke_all_castling_rights(), + Shape::King => { + board.revoke_castling_rights(None, CastleRightsOption::All); + } _ => {} } @@ -559,7 +562,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.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide)); Ok(()) } @@ -579,7 +582,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.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide)); Ok(()) } diff --git a/moves/src/record.rs b/moves/src/record.rs index 47a2c3f..24472b8 100644 --- a/moves/src/record.rs +++ b/moves/src/record.rs @@ -1,7 +1,7 @@ // Eryn Wells use crate::Move; -use chessfriend_board::{board::HalfMoveClock, Board, CastleRights}; +use chessfriend_board::{Board, CastleRights, board::HalfMoveClock}; 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 8a477c6..6833608 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(Debug, Error, Eq, PartialEq)] +#[derive(Clone, 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.color_has_castling_right_unwrapped(Color::White, Wing::KingSide)); + assert!(!board.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.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide)); + assert!(!board.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 70630ae..d1e7f77 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -1,9 +1,13 @@ -use chessfriend_position::{Position, fen::FromFenStr, perft::Perft}; +use chessfriend_position::{ + Position, + fen::{FromFenStr, ToFenStr}, +}; 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")] @@ -14,17 +18,18 @@ 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) }; - let nodes_searched = position.perft(depth); + println!("fen \"{}\"", position.to_fen_str().unwrap()); + println!("depth {depth}"); - println!("nodes {nodes_searched}"); + let counters = position.perft(depth); + + println!("\n{counters}"); Ok(()) } diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs new file mode 100644 index 0000000..8496760 --- /dev/null +++ b/position/src/evaluation.rs @@ -0,0 +1,65 @@ +// 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 d0a1ec5..7ccee47 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,11 +1,12 @@ // Eryn Wells +mod evaluation; mod position; #[macro_use] mod macros; -pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; +pub use chessfriend_board::{PlacePieceError, PlacePieceStrategy, fen}; pub use chessfriend_moves::{GeneratedMove, ValidateMove}; pub use position::Position; diff --git a/position/src/perft.rs b/position/src/perft.rs index 57af81a..683cb67 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -1,45 +1,70 @@ // Eryn Wells use crate::{GeneratedMove, Position, ValidateMove}; +use std::fmt; -pub trait Perft { - fn perft(&mut self, depth: usize) -> u64; +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PerftCounters { + nodes: u64, } -impl Perft for Position { - fn perft(&mut self, depth: usize) -> u64 { - self.perft_recursive(depth, depth) +impl Position { + pub fn perft(&mut self, depth: usize) -> PerftCounters { + self.perft_recursive(0, depth) } } impl Position { - fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> u64 { - if depth == 0 { - return 1; - } + fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> PerftCounters { + let mut counters = PerftCounters::default(); - let mut total_nodes_counted = 0u64; + if depth == max_depth { + counters.count_node(); + return counters; + } let legal_moves: Vec = self.all_legal_moves(None).collect(); - for ply in legal_moves { - let ply = ply.ply(); + for generated_ply in legal_moves { + let ply = generated_ply.ply(); - let _has_seen_position = self + let has_seen_position = self .make_move(ply, ValidateMove::No) .expect("unable to make generated move"); - let nodes_counted = self.perft_recursive(depth - 1, depth); - - total_nodes_counted += nodes_counted; + let recursive_counters = if has_seen_position { + let mut counters = PerftCounters::default(); + counters.count_node(); + counters + } else { + self.perft_recursive(depth + 1, max_depth) + }; self.unmake_last_move().expect("unable to unmake last move"); - if depth == max_depth { - println!(" {ply} {nodes_counted}"); + counters.fold(&recursive_counters); + + if depth == 0 { + println!(" {ply}: {}", recursive_counters.nodes); } } - total_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) } } diff --git a/position/src/position.rs b/position/src/position.rs index 8787134..1763875 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::{ - display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, - ZobristState, + Board, PlacePieceError, PlacePieceStrategy, ZobristState, display::DiagramFormatter, + fen::ToFenStr, }; 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 board: Board, + pub(crate) board: Board, pub(crate) moves: Vec, pub(crate) captures: CapturesList, @@ -48,6 +48,16 @@ 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 { @@ -76,12 +86,14 @@ impl Position { } impl Position { - pub fn sight(&self, square: Square) -> BitBoard { - self.board.sight(square) + /// Calculate sight of a piece on the provided [`Square`]. + pub fn sight_piece(&self, square: Square) -> BitBoard { + self.board.sight_piece(square) } - pub fn movement(&self, square: Square) -> BitBoard { - self.board.movement(square) + /// Calculate movement of a piece on the provided [`Square`]. + pub fn movement_piece(&self, square: Square) -> BitBoard { + self.board.movement_piece(square) } } @@ -117,7 +129,7 @@ impl Position { ); }); - let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move)); + let move_is_legal = !test_board.is_in_check(); test_board.unmake_move(&record).unwrap_or_else(|err| { panic!( @@ -153,8 +165,8 @@ impl Position { } impl Position { - pub fn active_sight(&self) -> BitBoard { - self.board.active_sight() + pub fn sight_active(&self) -> BitBoard { + self.board.sight_active() } /// A [`BitBoard`] of all squares the given color can see. @@ -275,7 +287,7 @@ impl Position { } let target_bitboard: BitBoard = target.into(); - if !(self.movement(origin) & target_bitboard).is_populated() { + if !(self.movement_piece(origin) & target_bitboard).is_populated() { return None; } @@ -344,7 +356,7 @@ impl fmt::Display for Position { #[cfg(test)] mod tests { use super::*; - use crate::{test_position, Position}; + use crate::{Position, test_position}; use chessfriend_core::piece; #[test] diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs index eadcf4d..06ccc45 100644 --- a/position/tests/peterellisjones.rs +++ b/position/tests/peterellisjones.rs @@ -8,7 +8,7 @@ use chessfriend_core::Color; use chessfriend_moves::{ - assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move, + Move, assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, }; 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.active_color_is_in_check()); + assert!(pos.board().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.active_color_is_in_check()); + assert!(pos.board().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.active_color_is_in_check()); + assert!(!pos.board().is_in_check()); let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect(); diff --git a/rustfmt.toml b/rustfmt.toml index be6f4bf..b377055 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,5 @@ +style_edition = "2024" + imports_layout = "HorizontalVertical" group_imports = "StdExternalCrate"