From 90ee3e416ffd6713e3e168fc1d7b1d8d68036c2c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:23:50 -0700 Subject: [PATCH 01/40] WIP --- Cargo.lock | 6 ++ chessfriend/Cargo.toml | 7 ++ chessfriend/src/chessfriend.rs | 87 +++++++++++++++++++ chessfriend/src/lib.rs | 17 ++++ chessfriend/src/threadpool.rs | 77 +++++++++++++++++ chessfriend/src/uci.rs | 148 +++++++++++++++++++++++++++++++++ position/src/lib.rs | 4 +- 7 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 chessfriend/src/chessfriend.rs create mode 100644 chessfriend/src/threadpool.rs create mode 100644 chessfriend/src/uci.rs diff --git a/Cargo.lock b/Cargo.lock index 936cbaa..0d1d26f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,12 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "chessfriend" version = "0.1.0" dependencies = [ + "chessfriend_core", + "chessfriend_moves", + "chessfriend_position", + "clap", + "shlex", + "thiserror", ] [[package]] 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/position/src/lib.rs b/position/src/lib.rs index d0a1ec5..8bd4d49 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -5,8 +5,8 @@ mod position; #[macro_use] mod macros; -pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; -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; From 801e15fd5aabfac29585f45ba653ea27551d06f7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:24:46 -0700 Subject: [PATCH 02/40] Add style_edition to rustfmt.toml Set style edition to 2024. --- rustfmt.toml | 2 ++ 1 file changed, 2 insertions(+) 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" From 076cdfe66f29876a1ad2c1ba01b3c64fd58565db Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:42:17 -0700 Subject: [PATCH 03/40] Remove empty dependencies list from Cargo.lock --- Cargo.lock | 2 -- 1 file changed, 2 deletions(-) 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" From f3b31d5514e738c2d16807547b8b9dae8c0cd0e2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:42:35 -0700 Subject: [PATCH 04/40] [perft] Print the fen string of the board position --- perft/src/main.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/perft/src/main.rs b/perft/src/main.rs index 70630ae..dece54c 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -1,4 +1,7 @@ -use chessfriend_position::{Position, fen::FromFenStr, perft::Perft}; +use chessfriend_position::{ + Position, + fen::{FromFenStr, ToFenStr}, +}; use clap::Parser; #[derive(Parser, Debug)] @@ -14,14 +17,15 @@ 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); println!("nodes {nodes_searched}"); From 6996cbeb15305e135dc9cd4d50428307d1397c0c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 17 Jun 2025 16:42:57 -0700 Subject: [PATCH 05/40] [position] Remove the Perft trait It wasn't serving a purpose. --- position/src/perft.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/position/src/perft.rs b/position/src/perft.rs index 57af81a..a9ae169 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -2,12 +2,8 @@ use crate::{GeneratedMove, Position, ValidateMove}; -pub trait Perft { - fn perft(&mut self, depth: usize) -> u64; -} - -impl Perft for Position { - fn perft(&mut self, depth: usize) -> u64 { +impl Position { + pub fn perft(&mut self, depth: usize) -> u64 { self.perft_recursive(depth, depth) } } From bf17017694e96d094b46abd6f3cd04fb6a585323 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:21:21 -0700 Subject: [PATCH 06/40] [explorer] Add several commands to help with debugging flags : Print flags for the current board position. This prints the castling rights and whether the player can castle (regardless of whether they have the right). make : Finally reimplement the make command. Change the format so it takes a move in the UCI long algebraic style. perft : Run perft to a given depth on the current board position. --- explorer/src/main.rs | 101 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 77ef939..b409cd0 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,12 +1,14 @@ // Eryn Wells -use chessfriend_board::ZobristState; +use chessfriend_board::castle::CastleEvaluationError; use chessfriend_board::{Board, fen::FromFenStr}; +use chessfriend_board::{CastleParameters, ZobristState}; use chessfriend_core::random::RandomNumberGenerator; -use chessfriend_core::{Color, Piece, Shape, Square}; -use chessfriend_moves::GeneratedMove; +use chessfriend_core::{Color, Piece, Shape, Square, Wing}; +use chessfriend_moves::algebraic::AlgebraicMoveComponents; +use chessfriend_moves::{GeneratedMove, ValidateMove}; use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr}; -use clap::{Arg, Command}; +use clap::{Arg, Command, value_parser}; use rustyline::DefaultEditor; use rustyline::error::ReadlineError; use std::sync::Arc; @@ -45,6 +47,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 +56,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"), ) @@ -81,6 +83,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 +117,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 +132,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 +143,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") @@ -175,6 +191,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.color_has_castling_right_unwrapped(color, wing); + 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 +235,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, @@ -282,6 +346,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 nodes_count = position.perft(depth); + + println!("nodes {nodes_count}"); + + 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}"); From c5cc0646efc30a6723ed2b4ce6a524e77c095f45 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:22:12 -0700 Subject: [PATCH 07/40] [perft] Add back the block on searching into seen positions Check if the board position has been seen and stop recursion if so. --- position/src/perft.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/position/src/perft.rs b/position/src/perft.rs index a9ae169..97b3c51 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -18,14 +18,18 @@ impl Position { 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); + let nodes_counted = if has_seen_position { + 1 + } else { + self.perft_recursive(depth - 1, max_depth) + }; total_nodes_counted += nodes_counted; From 9972ce94d09674c78817ebefe09f43c9ea5dd7da Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:25:41 -0700 Subject: [PATCH 08/40] [moves] Revoke castling rights only for the player that moved There was a bug in the code that revokes castling rights after a king move where it revoked the rights for *all* players, rather than just the current player. Fix it. --- moves/src/make_move.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 7b8fd33..0ddeb1e 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -321,7 +321,10 @@ impl MakeMoveInternal for T { board.revoke_castling_right(None, Wing::QueenSide); } } - Shape::King => board.revoke_all_castling_rights(), + Shape::King => { + board.revoke_castling_right(None, Wing::KingSide); + board.revoke_castling_right(None, Wing::QueenSide); + } _ => {} } From 933924d37a1d5fe9a01040ec77ffe4e8fd08e10c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 08:26:29 -0700 Subject: [PATCH 09/40] [board] When loading a FEN string, start with no castling rights The default value of the castle::Rights struct is with all rights granted. When loading a FEN string, start with none and add to it. --- board/src/fen.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/board/src/fen.rs b/board/src/fen.rs index 0da38dc..a125aca 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -229,9 +229,8 @@ impl FromFenStr for Board { 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), From 4ce7e89cdb0ad2508d901b547dbd8f7167b3caaa Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 18 Jun 2025 23:44:40 +0000 Subject: [PATCH 10/40] [board, explorer, moves] Clean up the castling rights API Reorganize castling rights API on Board into methods named according to conventions applied to other API. Board::has_castling_right Board::has_castling_right_active Board::has_castling_right_unwrapped These all check if a color has the right to castle on a particular side (wing) of the board. The first takes an Option, the latter two operate on bare Colors: the active color, or an explicit Color. Board::grant_castling_right Board::grant_castling_right_active Board::grant_castling_right_unwrapped Grant castling rights to a color. Color arguments follow the pattern above. Board::revoke_castling_right Board::revoke_castling_right_active Board::revoke_castling_right_unwrapped Revoke castling rights from a color. Color arguments follow the pattern above. The latter two groups of methods take a new CastleRightsOption type that specifies either a single Wing or All. Rework the implementation of CastleRights to take a CastleRightsOption. Update the unit tests and make sure everything builds. --- board/src/board.rs | 70 ++++++++++++-------------------- board/src/castle.rs | 69 ++++++++++++++++++++++++++------ board/src/castle/rights.rs | 81 +++++++++++++++++++++++++------------- board/src/fen.rs | 28 ++++++++----- explorer/src/main.rs | 15 +++---- moves/src/make_move.rs | 18 ++++----- moves/src/record.rs | 4 +- moves/src/unmake_move.rs | 16 ++++---- 8 files changed, 177 insertions(+), 124 deletions(-) diff --git a/board/src/board.rs b/board/src/board.rs index 666488e..6c56346 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}, 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 { diff --git a/board/src/castle.rs b/board/src/castle.rs index f8f9c24..5acdaaf 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.into()) { 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/fen.rs b/board/src/fen.rs index a125aca..65c468a 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -24,12 +24,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 +126,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,19 +230,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))?; 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/explorer/src/main.rs b/explorer/src/main.rs index b409cd0..4f98a63 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,16 +1,11 @@ // Eryn Wells -use chessfriend_board::castle::CastleEvaluationError; -use chessfriend_board::{Board, fen::FromFenStr}; -use chessfriend_board::{CastleParameters, ZobristState}; -use chessfriend_core::random::RandomNumberGenerator; -use chessfriend_core::{Color, Piece, Shape, Square, Wing}; -use chessfriend_moves::algebraic::AlgebraicMoveComponents; -use chessfriend_moves::{GeneratedMove, ValidateMove}; +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, value_parser}; -use rustyline::DefaultEditor; -use rustyline::error::ReadlineError; +use rustyline::{DefaultEditor, error::ReadlineError}; use std::sync::Arc; use thiserror::Error; @@ -202,7 +197,7 @@ fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandRe (Color::Black, Wing::KingSide), (Color::Black, Wing::QueenSide), ] { - let has_right = board.color_has_castling_right_unwrapped(color, wing); + 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 { diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index 0ddeb1e..f413e72 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; @@ -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,23 +308,22 @@ 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_castling_right(None, Wing::KingSide); - board.revoke_castling_right(None, Wing::QueenSide); + board.revoke_castling_rights(None, CastleRightsOption::All); } _ => {} } @@ -562,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(()) } @@ -582,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..69c43b7 100644 --- a/moves/src/unmake_move.rs +++ b/moves/src/unmake_move.rs @@ -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(()) From 0f5a538f0a8ba5b6e5b3a19ba40fca0a71566c11 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 19 Jun 2025 11:33:35 -0700 Subject: [PATCH 11/40] [explorer, perft, position] Move node count into a new PerftCounters struct --- explorer/src/main.rs | 4 +-- perft/src/main.rs | 5 ++-- position/src/perft.rs | 57 +++++++++++++++++++++++++++++++------------ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 4f98a63..0acb2a1 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -350,9 +350,9 @@ fn do_perft_command( .ok_or(CommandHandlingError::MissingArgument("depth"))?; let mut position = state.position.clone(); - let nodes_count = position.perft(depth); + let counters = position.perft(depth); - println!("nodes {nodes_count}"); + println!("{counters}"); Ok(CommandResult { should_continue: true, diff --git a/perft/src/main.rs b/perft/src/main.rs index dece54c..d1e7f77 100644 --- a/perft/src/main.rs +++ b/perft/src/main.rs @@ -7,6 +7,7 @@ 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")] @@ -26,9 +27,9 @@ fn main() -> anyhow::Result<()> { println!("fen \"{}\"", position.to_fen_str().unwrap()); println!("depth {depth}"); - let nodes_searched = position.perft(depth); + let counters = position.perft(depth); - println!("nodes {nodes_searched}"); + println!("\n{counters}"); Ok(()) } diff --git a/position/src/perft.rs b/position/src/perft.rs index 97b3c51..3269a04 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -1,20 +1,29 @@ // Eryn Wells +use chessfriend_moves::Move; + use crate::{GeneratedMove, Position, ValidateMove}; +use std::fmt; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PerftCounters { + nodes: u64, +} impl Position { - pub fn perft(&mut self, depth: usize) -> u64 { - self.perft_recursive(depth, depth) + 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(); @@ -25,21 +34,39 @@ impl Position { .make_move(ply, ValidateMove::No) .expect("unable to make generated move"); - let nodes_counted = if has_seen_position { - 1 + 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.perft_recursive(depth + 1, max_depth) }; - total_nodes_counted += nodes_counted; - 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) } } From 1d8a0dc3a4ffd2c5a20935af212b71eda9523773 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 19 Jun 2025 14:27:52 -0700 Subject: [PATCH 12/40] Add a release-debug profile This profile builds binaries for release, but includes debugging information. Useful for profiling! --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) 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 From 481ae70698c3ae15ce6827750a856237457d9d5b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 19 Jun 2025 14:32:07 -0700 Subject: [PATCH 13/40] [core] Directly index the array of Squares with a given index In Square::from_index_unchecked, instead of using TryFrom to convert the index to a square, just index directly into the Square::ALL array. This function is already marked unsafe. --- core/src/coordinates.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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] From 7f25548335cfa8e081948484e9990f7f5a1c7b60 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:23:57 -0700 Subject: [PATCH 14/40] [board, core, position] A simple static evaluation method for scoring positions Implement a new Evaluator struct that evaluates a Board and returns a score. This evaluation mechanism uses only a material balance function. It doesn't account for anything else. Supporting this, add a Counts struct to the internal piece set structure of a Board. This struct is responsible for keeping counts of how many pieces of each shape are on the board for each color. Export a count_piece() method on Board that returns a count of the number of pieces of a particular color and shape. Implement a newtype wrapper around i32 called Score that represents the score of a position in centipawns, i.e. hundredths of a pawn. Add piece values to the Shape enum. --- board/src/board.rs | 7 +++- board/src/fen.rs | 5 ++- board/src/piece_sets.rs | 27 ++++++++++--- board/src/piece_sets/counts.rs | 60 ++++++++++++++++++++++++++++ core/src/colors.rs | 10 ++++- core/src/lib.rs | 1 + core/src/score.rs | 71 ++++++++++++++++++++++++++++++++++ core/src/shapes.rs | 13 +++++++ position/src/evaluation.rs | 62 +++++++++++++++++++++++++++++ position/src/lib.rs | 3 +- 10 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 board/src/piece_sets/counts.rs create mode 100644 core/src/score.rs create mode 100644 position/src/evaluation.rs diff --git a/board/src/board.rs b/board/src/board.rs index 6c56346..338d4aa 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -3,7 +3,7 @@ use crate::{ CastleRights, PieceSet, display::DiagramFormatter, - piece_sets::{PlacePieceError, PlacePieceStrategy}, + piece_sets::{Counter, PlacePieceError, PlacePieceStrategy}, zobrist::{ZobristHash, ZobristState}, }; use chessfriend_bitboard::BitBoard; @@ -219,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/fen.rs b/board/src/fen.rs index 65c468a..a44b735 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) => { + ($fen_string:literal) => {{ + use $crate::fen::FromFenStr; Board::from_fen_str($fen_string) - }; + }}; } #[derive(Clone, Debug, Error, Eq, PartialEq)] diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 063937d..316c1d8 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] @@ -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,18 +40,21 @@ 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]; color_occupancy[color_index] |= bitboard; shape_occupancy[shape_index] |= bitboard; + counts.increment(color, shape); + for square in bitboard.occupied_squares(&IterationDirection::default()) { - let piece = Piece::new(*color, shape); + let piece = Piece::new(color, shape); mailbox.set(piece, square); } } @@ -55,6 +62,7 @@ impl PieceSet { Self { mailbox, + counts, color_occupancy, shape_occupancy, } @@ -94,6 +102,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 +132,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 +140,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..effbbe0 --- /dev/null +++ b/board/src/piece_sets/counts.rs @@ -0,0 +1,60 @@ +// 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 { + self.0[color as usize][shape as usize] = updated_value; + } else { + unreachable!("piece count for {color} {shape} overflowed"); + } + } + + pub fn decrement(&mut self, color: Color, shape: Shape) { + let count = self.0[color as usize][shape as usize]; + if let Some(updated_count) = count.checked_sub(1) { + self.0[color as usize][shape as usize] = updated_count; + } else { + unreachable!("piece count for {color} {shape} underflowed"); + } + } + + #[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/core/src/colors.rs b/core/src/colors.rs index 5cf633b..ccf62a7 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::Direction; +use crate::{Direction, score::ScoreInner}; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -56,6 +56,14 @@ impl Color { Color::Black => "black", } } + + #[must_use] + pub const fn score_factor(self) -> ScoreInner { + match self { + Color::White => 1, + Color::Black => -1, + } + } } impl std::fmt::Display for Color { 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..eee49f6 --- /dev/null +++ b/core/src/score.rs @@ -0,0 +1,71 @@ +// Eryn Wells + +use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; + +pub(crate) type ScoreInner = i32; + +/// A score for a position in centipawns. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Score(ScoreInner); + +impl Score { + #[must_use] + pub const fn zero() -> Self { + Self(0) + } + + #[must_use] + pub const fn new(value: ScoreInner) -> Self { + Self(value) + } +} + +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 = Score; + + fn mul(self, rhs: ScoreInner) -> Self::Output { + Score(self.0 * rhs) + } +} + +impl Mul for ScoreInner { + type Output = Score; + + fn mul(self, rhs: Score) -> Self::Output { + Score(self * rhs.0) + } +} + +impl From for Score { + fn from(value: ScoreInner) -> Self { + Score(value) + } +} diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 2c6b7e9..8184f1d 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, @@ -71,6 +73,17 @@ impl Shape { pub fn is_promotable(&self) -> bool { Self::PROMOTABLE_SHAPES.contains(self) } + + #[must_use] + pub fn score(self) -> Score { + match self { + Shape::Pawn => Score::new(100), + Shape::Knight | Shape::Bishop => Score::new(300), + Shape::Rook => Score::new(500), + Shape::Queen => Score::new(900), + Shape::King => Score::new(20000), + } + } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs new file mode 100644 index 0000000..883398b --- /dev/null +++ b/position/src/evaluation.rs @@ -0,0 +1,62 @@ +// Eryn Wells + +use chessfriend_board::Board; +use chessfriend_core::{Color, Piece, Shape, score::Score}; + +struct Evaluator; + +impl Evaluator { + pub fn evaluate_symmetric_unwrapped(board: &Board, color: Color) -> Score { + let material_balance = Self::material_balance(board, color); + + let to_move_factor = color.score_factor(); + + to_move_factor * material_balance + } + + /// 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) = ( + board.count_piece(&Piece::new(color, shape)) as i32, + board.count_piece(&Piece::new(other_color, shape)) as i32, + ); + + 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 board = Board::starting(None); + assert_eq!( + Evaluator::evaluate_symmetric_unwrapped(&board, Color::White), + Evaluator::evaluate_symmetric_unwrapped(&board, 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; From a91bb8c9838ca9eabe4ec407a041ebfb52e441a9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:24:16 -0700 Subject: [PATCH 15/40] [board] Remove the unused Mailbox::new method Just use Mailbox::default(). --- board/src/piece_sets/mailbox.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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); From abaf277fb48fdf5aa4452fcac07a5d5ac0001e85 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:25:10 -0700 Subject: [PATCH 16/40] [core] Use the matches! macro to calculate the value of Shape::is_promotable I learned about this macro a little while ago and it's better than writing out a match block by hand, and also doesn't require static or const data, like the previous implementation did. --- core/src/shapes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 8184f1d..0cedc7c 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -71,7 +71,7 @@ impl Shape { #[must_use] pub fn is_promotable(&self) -> bool { - Self::PROMOTABLE_SHAPES.contains(self) + matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen) } #[must_use] From 4ae1fd62b7bf59bea637388f2cfb5efa838957d7 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 20 Jun 2025 14:25:27 -0700 Subject: [PATCH 17/40] [perft] Remove an unused Move import This was causing a warning. --- position/src/perft.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/position/src/perft.rs b/position/src/perft.rs index 3269a04..683cb67 100644 --- a/position/src/perft.rs +++ b/position/src/perft.rs @@ -1,7 +1,5 @@ // Eryn Wells -use chessfriend_moves::Move; - use crate::{GeneratedMove, Position, ValidateMove}; use std::fmt; From f84319272cb61cc759178bd930ffbcb22e54e58e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:07:26 -0700 Subject: [PATCH 18/40] [explorer, position] Make Position.board private to the crate Export a Position::board() method that returns a reference to the internal Board. --- explorer/src/main.rs | 4 ++-- position/src/position.rs | 7 ++++++- position/tests/peterellisjones.rs | 8 ++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 0acb2a1..b6af7a6 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -187,7 +187,7 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result { } fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult { - let board = &state.position.board; + let board = state.position.board(); println!("Castling:"); @@ -390,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/position/src/position.rs b/position/src/position.rs index 8787134..d34ce4e 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -24,7 +24,7 @@ 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,11 @@ impl Position { ..Default::default() } } + + #[must_use] + pub fn board(&self) -> &Board { + &self.board + } } impl Position { diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs index eadcf4d..ea90a2f 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().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.active_color_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.active_color_is_in_check()); + assert!(!pos.board().active_color_is_in_check()); let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect(); From 4b96db230d269c5fddc6d115435f4abcd8bcffb1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:08:04 -0700 Subject: [PATCH 19/40] [board, moves] Derive Clone on several error types - PlacePieceError - MakeMoveError - UnmakeMoveError --- board/src/piece_sets.rs | 2 +- moves/src/make_move.rs | 2 +- moves/src/unmake_move.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 316c1d8..de43caa 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -21,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 }, diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs index f413e72..bcbca0a 100644 --- a/moves/src/make_move.rs +++ b/moves/src/make_move.rs @@ -18,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), diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs index 69c43b7..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, From 54d9c3838dd7da7204fbf14c98e98438fbc3a835 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:08:32 -0700 Subject: [PATCH 20/40] [position] Export Position::active_color() Passes through to the Board method. --- position/src/position.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/position/src/position.rs b/position/src/position.rs index d34ce4e..e318374 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -53,6 +53,11 @@ impl Position { pub fn board(&self) -> &Board { &self.board } + + #[must_use] + pub fn active_color(&self) -> Color { + self.board.active_color() + } } impl Position { From 9f2dc3fa7606d53b051a223c01160398b8ac899f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 21 Jun 2025 21:09:01 -0700 Subject: [PATCH 21/40] [position] Update import ordering in position.rs --- position/src/position.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/position/src/position.rs b/position/src/position.rs index e318374..5a3ae69 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -6,18 +6,18 @@ 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}; @@ -354,7 +354,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] From 80ac8ea036093307ae51277db1d96427f3883545 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 15:18:49 -0700 Subject: [PATCH 22/40] [core] Import std::fmt and remove std:: from Display symbol spelling --- core/src/colors.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index ccf62a7..53e2c1e 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,7 @@ // Eryn Wells use crate::{Direction, score::ScoreInner}; +use std::fmt; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -66,8 +67,8 @@ impl Color { } } -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, "{}", From 4e80cc36ca9234830c3f6f47a64a0f3382168386 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 15:20:31 -0700 Subject: [PATCH 23/40] [core] Implement Display for Score --- core/src/score.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/src/score.rs b/core/src/score.rs index eee49f6..a7f24cd 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -1,6 +1,9 @@ // Eryn Wells -use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; +use std::{ + fmt, + ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}, +}; pub(crate) type ScoreInner = i32; @@ -69,3 +72,16 @@ impl From for Score { 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") + } + } +} From 1ae6d5df4887a4b9979be0cab41eeba0a23d913c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 20:00:04 -0700 Subject: [PATCH 24/40] =?UTF-8?q?[core,=20position]=20Rename=20the=20type?= =?UTF-8?q?=20of=20Score's=20inner=20value=20=E2=86=92=20Value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/colors.rs | 4 +-- core/src/score.rs | 65 ++++++++++++++++++++++++++++++-------- position/src/evaluation.rs | 6 ++-- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/core/src/colors.rs b/core/src/colors.rs index 53e2c1e..e1255b7 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::{Direction, score::ScoreInner}; +use crate::{Direction, score}; use std::fmt; use thiserror::Error; @@ -59,7 +59,7 @@ impl Color { } #[must_use] - pub const fn score_factor(self) -> ScoreInner { + pub const fn score_factor(self) -> score::Value { match self { Color::White => 1, Color::Black => -1, diff --git a/core/src/score.rs b/core/src/score.rs index a7f24cd..2dfd7c9 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -5,22 +5,53 @@ use std::{ ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}, }; -pub(crate) type ScoreInner = i32; +pub(crate) type Value = i32; /// A score for a position in centipawns. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Score(ScoreInner); +pub struct Score(Value); impl Score { - #[must_use] - pub const fn zero() -> Self { - Self(0) - } + 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); + + const CENTIPAWNS_PER_POINT: f32 = 100.0; #[must_use] - pub const fn new(value: ScoreInner) -> Self { + 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 { @@ -51,15 +82,15 @@ impl SubAssign for Score { } } -impl Mul for Score { - type Output = Score; +impl Mul for Score { + type Output = Self; - fn mul(self, rhs: ScoreInner) -> Self::Output { + fn mul(self, rhs: Value) -> Self::Output { Score(self.0 * rhs) } } -impl Mul for ScoreInner { +impl Mul for Value { type Output = Score; fn mul(self, rhs: Score) -> Self::Output { @@ -67,8 +98,16 @@ impl Mul for ScoreInner { } } -impl From for Score { - fn from(value: ScoreInner) -> Self { +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) } } diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs index 883398b..b776777 100644 --- a/position/src/evaluation.rs +++ b/position/src/evaluation.rs @@ -19,10 +19,10 @@ impl Evaluator { fn material_balance(board: &Board, color: Color) -> Score { let other_color = color.other(); - Shape::into_iter().fold(Score::zero(), |acc, shape| { + Shape::into_iter().fold(Score::ZERO, |acc, shape| { let (active_pieces, other_pieces) = ( - board.count_piece(&Piece::new(color, shape)) as i32, - board.count_piece(&Piece::new(other_color, shape)) as i32, + 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); From 74c0e4144f875dd0291a7ff189a0b7f3ae3b767c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 24 Jun 2025 20:04:41 -0700 Subject: [PATCH 25/40] [position] Remove the to_move_factor from symmetric evaluation Just use material balance. --- position/src/evaluation.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs index b776777..8496760 100644 --- a/position/src/evaluation.rs +++ b/position/src/evaluation.rs @@ -1,17 +1,20 @@ // 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(board: &Board, color: Color) -> Score { + pub fn evaluate_symmetric_unwrapped(position: &Position, color: Color) -> Score { + let board = &position.board; + let material_balance = Self::material_balance(board, color); - let to_move_factor = color.score_factor(); + let score = material_balance; - to_move_factor * material_balance + score } /// Evaluate a board using the symmetric evaluation algorithm defined by @@ -53,10 +56,10 @@ mod tests { #[test] fn starting_position_is_even() { - let board = Board::starting(None); + let position = Position::new(Board::starting(None)); assert_eq!( - Evaluator::evaluate_symmetric_unwrapped(&board, Color::White), - Evaluator::evaluate_symmetric_unwrapped(&board, Color::Black) + Evaluator::evaluate_symmetric_unwrapped(&position, Color::White), + Evaluator::evaluate_symmetric_unwrapped(&position, Color::Black) ); } } From 8db533cb5298e49d01cd7b599da02bcfb4e7cd95 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 27 Jun 2025 08:44:56 -0700 Subject: [PATCH 26/40] [board] Use $crate in the fen! macro so you don't have to import Board to get one back --- board/src/fen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/fen.rs b/board/src/fen.rs index a44b735..fe418a5 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -11,7 +11,7 @@ use thiserror::Error; macro_rules! fen { ($fen_string:literal) => {{ use $crate::fen::FromFenStr; - Board::from_fen_str($fen_string) + $crate::Board::from_fen_str($fen_string) }}; } From e7fd65672d432c92ba8c4c3abe883b3d7d0e8550 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 29 Jun 2025 09:18:44 -0700 Subject: [PATCH 27/40] [bitboard, board] Make BitBoard::EMPTY and BitBoard::FULL public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deprecate the methods. I think I'm undoing a change I made earlier. 🙃 --- bitboard/src/bitboard.rs | 6 ++++-- bitboard/src/lib.rs | 2 +- board/src/movement.rs | 2 +- board/src/sight.rs | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index b9e4c1c..1897de4 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -43,13 +43,15 @@ macro_rules! moves_getter { } impl BitBoard { - const EMPTY: BitBoard = BitBoard(u64::MIN); - const FULL: BitBoard = BitBoard(u64::MAX); + pub const EMPTY: BitBoard = BitBoard(u64::MIN); + pub const FULL: BitBoard = BitBoard(u64::MAX); + #[deprecated(note = "Use BitBoard::EMPTY instead")] pub const fn empty() -> BitBoard { Self::EMPTY } + #[deprecated(note = "Use BitBoard::FULL instead")] pub const fn full() -> BitBoard { Self::FULL } 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/board/src/movement.rs b/board/src/movement.rs index 4fa4381..8cb8be9 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -13,7 +13,7 @@ impl Board { if let Some(piece) = self.get_piece(square) { piece.movement(square, self) } else { - BitBoard::empty() + BitBoard::EMPTY } } } diff --git a/board/src/sight.rs b/board/src/sight.rs index 81fb120..f6378b2 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -27,7 +27,7 @@ impl Board { if let Some(piece) = self.get_piece(square) { piece.sight(square, self) } else { - BitBoard::empty() + BitBoard::EMPTY } } @@ -41,7 +41,7 @@ impl Board { self.friendly_occupancy(color) .occupied_squares(&IterationDirection::default()) .map(|square| self.sight(square)) - .fold(BitBoard::empty(), BitOr::bitor) + .fold(BitBoard::EMPTY, BitOr::bitor) } pub fn active_color_opposing_sight(&self) -> BitBoard { @@ -60,7 +60,7 @@ impl Board { Some(self.friendly_sight(c)) } }) - .fold(BitBoard::empty(), BitOr::bitor) + .fold(BitBoard::EMPTY, BitOr::bitor) } } From a30553503fcb9818975e21f830cab34d8bd494e5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 29 Jun 2025 09:23:20 -0700 Subject: [PATCH 28/40] [board, explorer, position] Clean up naming of sight and movement methods These methods have a prefix, either `sight` or `movement`, and then follow the conventions for other "trio" clusters where there's an un-suffixed method that takes an Option, a _active method that uses the active color, and a _unwrapped method that takes a bare Color. --- board/src/movement.rs | 6 +++--- board/src/sight.rs | 29 ++++++++++++++++++++++------- explorer/src/main.rs | 16 ++++++++-------- position/src/position.rs | 16 +++++++++------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/board/src/movement.rs b/board/src/movement.rs index 8cb8be9..2935eee 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -4,12 +4,12 @@ //! 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 { @@ -93,7 +93,7 @@ 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] diff --git a/board/src/sight.rs b/board/src/sight.rs index f6378b2..8e5cbc6 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -23,7 +23,7 @@ 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 { @@ -31,8 +31,23 @@ impl Board { } } - 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,7 +55,7 @@ impl Board { // TODO: Probably want to implement a caching layer here. self.friendly_occupancy(color) .occupied_squares(&IterationDirection::default()) - .map(|square| self.sight(square)) + .map(|square| self.sight_piece(square)) .fold(BitBoard::EMPTY, BitOr::bitor) } @@ -244,7 +259,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 +282,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]); diff --git a/explorer/src/main.rs b/explorer/src/main.rs index b6af7a6..cfaf102 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -65,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( @@ -163,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}"); @@ -331,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}"); diff --git a/position/src/position.rs b/position/src/position.rs index 5a3ae69..d2a82f3 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -86,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) } } @@ -163,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. @@ -285,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; } From e3d17219ad4648871d12b44cb3bc302094f38a00 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 29 Jun 2025 09:25:08 -0700 Subject: [PATCH 29/40] [board, position] Simplify check methods Only one check-testing method, Board::is_in_check(), that tests if the current active player is in check. It doesn't make sense to test if the non-active player is in check. --- board/src/check.rs | 18 +++++------------- position/src/position.rs | 2 +- position/tests/peterellisjones.rs | 6 +++--- 3 files changed, 9 insertions(+), 17 deletions(-) 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/position/src/position.rs b/position/src/position.rs index d2a82f3..1763875 100644 --- a/position/src/position.rs +++ b/position/src/position.rs @@ -129,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!( diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs index ea90a2f..06ccc45 100644 --- a/position/tests/peterellisjones.rs +++ b/position/tests/peterellisjones.rs @@ -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(); From a904e4a5bb2163c3bcfcb25a520878cbfc189365 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 30 Jun 2025 15:37:35 -0700 Subject: [PATCH 30/40] [bitboard, board] Replace ray_in_direction! macro with a function This is simpler than writing a macro, at the expense of some overhead for calling a function. But the Rust compiler might inline it anyway! To support this change, implement BitBoard::first_occupied_square_direction, which iterates a bitboard in a direction (i.e. leading or trailing) depending on the core::Direction value passed to it. --- bitboard/src/bitboard.rs | 12 ++++++++ board/src/sight.rs | 62 +++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 1897de4..f896e57 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -243,6 +243,18 @@ impl BitBoard { TrailingBitScanner::new(self.0) } + #[must_use] + pub fn first_occupied_square_direction(&self, direction: Direction) -> Option { + match direction { + Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => { + self.first_occupied_square_trailing() + } + Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => { + self.first_occupied_square_leading() + } + } + } + #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { diff --git a/board/src/sight.rs b/board/src/sight.rs index 8e5cbc6..b8e366e 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -138,20 +138,6 @@ struct SightInfo { friendly_occupancy: BitBoard, } -macro_rules! ray_in_direction { - ($square:expr, $blockers:expr, $direction:ident, $first_occupied_square:tt) => {{ - let ray = BitBoard::ray($square, Direction::$direction); - let ray_blockers = ray & $blockers; - if let Some(first_occupied_square) = ray_blockers.$first_occupied_square() { - let remainder = BitBoard::ray(first_occupied_square, Direction::$direction); - let attack_ray = ray & !remainder; - attack_ray - } else { - ray - } - }}; -} - /// Compute sight of a white pawn. fn white_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard { let possible_squares = !info.friendly_occupancy | en_passant_square; @@ -175,15 +161,27 @@ fn knight_sight(info: &SightInfo) -> BitBoard { BitBoard::knight_moves(info.square) } +fn ray_in_direction(square: Square, blockers: BitBoard, direction: Direction) -> BitBoard { + let ray = BitBoard::ray(square, direction); + + let ray_blockers = ray & blockers; + if let Some(first_occupied_square) = ray_blockers.first_occupied_square_direction(direction) { + let remainder = BitBoard::ray(first_occupied_square, direction); + let attack_ray = ray & !remainder; + attack_ray + } else { + ray + } +} + fn bishop_sight(info: &SightInfo) -> BitBoard { let bishop = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(bishop, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(bishop, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(bishop, occupancy, NorthWest, first_occupied_square_trailing); + let sight = ray_in_direction(bishop, occupancy, Direction::NorthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthEast) + | ray_in_direction(bishop, occupancy, Direction::SouthWest) + | ray_in_direction(bishop, occupancy, Direction::NorthWest); sight } @@ -192,11 +190,10 @@ fn rook_sight(info: &SightInfo) -> BitBoard { let rook = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(rook, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(rook, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(rook, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(rook, occupancy, Direction::North) + | ray_in_direction(rook, occupancy, Direction::East) + | ray_in_direction(rook, occupancy, Direction::South) + | ray_in_direction(rook, occupancy, Direction::West); sight } @@ -205,15 +202,14 @@ fn queen_sight(info: &SightInfo) -> BitBoard { let queen = info.square; let occupancy = info.occupancy; - #[rustfmt::skip] - let sight = ray_in_direction!(queen, occupancy, NorthWest, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, North, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, NorthEast, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, East, first_occupied_square_trailing) - | ray_in_direction!(queen, occupancy, SouthEast, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, South, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, SouthWest, first_occupied_square_leading) - | ray_in_direction!(queen, occupancy, West, first_occupied_square_leading); + let sight = ray_in_direction(queen, occupancy, Direction::NorthWest) + | ray_in_direction(queen, occupancy, Direction::North) + | ray_in_direction(queen, occupancy, Direction::NorthEast) + | ray_in_direction(queen, occupancy, Direction::East) + | ray_in_direction(queen, occupancy, Direction::SouthEast) + | ray_in_direction(queen, occupancy, Direction::South) + | ray_in_direction(queen, occupancy, Direction::SouthWest) + | ray_in_direction(queen, occupancy, Direction::West); sight } From 45183c910ca95609b1e318cd7359fe22d600a1cf Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:08:25 -0700 Subject: [PATCH 31/40] [bitboard] Replace some references to BitBoard::full() and BitBoard::empty() with the const values Two doc tests reference the methods instead of the const variables. Update them. --- bitboard/src/bitboard.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index f896e57..a16297d 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -160,9 +160,9 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert_eq!(BitBoard::empty().population_count(), 0); + /// assert_eq!(BitBoard::EMPTY.population_count(), 0); /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); - /// assert_eq!(BitBoard::full().population_count(), 64); + /// assert_eq!(BitBoard::FULL.population_count(), 64); /// ``` #[must_use] pub const fn population_count(&self) -> u32 { @@ -211,8 +211,8 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::empty().is_single_square(), "Empty bitboards represent no squares"); - /// assert!(!BitBoard::full().is_single_square(), "Full bitboards represent all the squares"); + /// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares"); + /// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares"); /// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares"); /// assert!(BitBoard::new(0b10000000000000).is_single_square()); /// ``` From 484fcf342e4e85d4734b1343f4b955ffa72a6227 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:15 -0700 Subject: [PATCH 32/40] [board] Remove a useless .into() call Clippy pointed this out to me. This .into() call serves no purpose. --- board/src/castle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board/src/castle.rs b/board/src/castle.rs index 5acdaaf..4ba9a4b 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.has_castling_right_unwrapped(color, wing.into()) { + if !self.has_castling_right_unwrapped(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } From b3ff8dec49ee2e8cddd1718da3d8801bccbd9910 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:09:55 -0700 Subject: [PATCH 33/40] [core] Make Shape::is_promotable() const --- core/src/shapes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 0cedc7c..9edbbb5 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -70,7 +70,7 @@ impl Shape { } #[must_use] - pub fn is_promotable(&self) -> bool { + pub const fn is_promotable(&self) -> bool { matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen) } From b50560692594f5c1b290634c7f72422ca329ad20 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:11:52 -0700 Subject: [PATCH 34/40] [core] Export Score::CENTIPAWNS_PER_POINT to the crate This constant is a conversion factor of points to the internal fixed point unit of centipawns. Points are more familiar to people because pawns are worth 1 pt. Calculate the scores of the various piece shapes with this constant. --- core/src/score.rs | 2 +- core/src/shapes.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/src/score.rs b/core/src/score.rs index 2dfd7c9..44d0478 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -31,7 +31,7 @@ impl Score { /// The maximum possible value of a score. pub const MAX: Score = Score(Value::MAX); - const CENTIPAWNS_PER_POINT: f32 = 100.0; + pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0; #[must_use] pub const fn new(value: Value) -> Self { diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 9edbbb5..77126ba 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -75,13 +75,16 @@ impl Shape { } #[must_use] - pub fn score(self) -> Score { + pub const fn score(self) -> Score { + #[allow(clippy::cast_possible_truncation)] + const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32; + match self { - Shape::Pawn => Score::new(100), - Shape::Knight | Shape::Bishop => Score::new(300), - Shape::Rook => Score::new(500), - Shape::Queen => Score::new(900), - Shape::King => Score::new(20000), + Shape::Pawn => Score::new(CP_PER_PT), + Shape::Knight | Shape::Bishop => Score::new(3 * CP_PER_PT), + Shape::Rook => Score::new(5 * CP_PER_PT), + Shape::Queen => Score::new(9 * CP_PER_PT), + Shape::King => Score::new(200 * CP_PER_PT), } } } From 146e4d34d3b9daf89a1347fe6ef6ee617b12216b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 17:12:34 -0700 Subject: [PATCH 35/40] [core] Fix an incorrect assertion in the Score doc test Negate with - instead of with !. --- core/src/score.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/score.rs b/core/src/score.rs index 44d0478..3528861 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -23,7 +23,7 @@ impl Score { /// /// ``` /// use chessfriend_core::score::Score; - /// assert_eq!(!Score::MIN, Score::MAX); + /// assert_eq!(-Score::MIN, Score::MAX); /// ``` /// pub const MIN: Score = Score(Value::MIN + 1); From b6d27356accafd3dcbedf7f3ca87ad6d0ca7beb9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:19:09 -0700 Subject: [PATCH 36/40] [bitboard] Implement BitBoard::occupied_squares_direction Iterate a BitBoard in a direction (from leading or trailing edge) based on a board direction. --- bitboard/src/bitboard.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index a16297d..6eb63eb 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -233,6 +233,38 @@ impl BitBoard { } } + /// Iterate through the occupied squares in a direction specified by a + /// compass direction. This method is mose useful for bitboards of slider + /// rays so that iteration proceeds in order along the ray's direction. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// use chessfriend_core::{Direction, Square}; + /// + /// let ray = BitBoard::ray(Square::E4, Direction::North); + /// assert_eq!( + /// ray.occupied_squares_direction(Direction::North).collect::>(), + /// vec![Square::E5, Square::E6, Square::E7, Square::E8] + /// ); + /// ``` + /// + #[must_use] + pub fn occupied_squares_direction( + &self, + direction: Direction, + ) -> Box> { + match direction { + Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => { + Box::new(self.occupied_squares_trailing()) + } + Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => { + Box::new(self.occupied_squares_leading()) + } + } + } + #[must_use] pub fn occupied_squares_leading(&self) -> LeadingBitScanner { LeadingBitScanner::new(self.0) From 3a0541a2c310474a6c6c91dd0dbb1fa156ed8e67 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 12 Jul 2025 20:20:04 -0700 Subject: [PATCH 37/40] [bitboard] Add a doc comment to BitBoard::first_occupied_square --- bitboard/src/bitboard.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 6eb63eb..ccee9bd 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -287,6 +287,12 @@ impl BitBoard { } } + /// Get the first occupied square in the given direction. + /// + /// ## To-Do + /// + /// - Take `direction` by value instead of reference + /// #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { From 3d73760146461a4d4b80289ef6e8ce06102d5464 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 16:14:34 -0700 Subject: [PATCH 38/40] [bitboard, board] Remove BitBoard::empty() and BitBoard::full() These have been deprecated for a while. Clean up the remaining uses and remove the methods from BitBoard. --- bitboard/src/bitboard.rs | 18 ++++-------------- bitboard/src/library.rs | 18 +++++++++--------- board/src/movement.rs | 20 ++++++++++---------- board/src/sight.rs | 2 +- 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index ccee9bd..35ce927 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -46,16 +46,6 @@ impl BitBoard { pub const EMPTY: BitBoard = BitBoard(u64::MIN); pub const FULL: BitBoard = BitBoard(u64::MAX); - #[deprecated(note = "Use BitBoard::EMPTY instead")] - pub const fn empty() -> BitBoard { - Self::EMPTY - } - - #[deprecated(note = "Use BitBoard::FULL instead")] - pub const fn full() -> BitBoard { - Self::FULL - } - pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) } @@ -109,7 +99,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(BitBoard::empty().is_empty()); + /// assert!(BitBoard::EMPTY.is_empty()); /// assert!(!BitBoard::full().is_empty()); /// assert!(!BitBoard::new(0b1000).is_empty()); /// ``` @@ -125,7 +115,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::empty().is_populated()); + /// assert!(!BitBoard::EMPTY.is_populated()); /// assert!(BitBoard::full().is_populated()); /// assert!(BitBoard::new(0b1).is_populated()); /// ``` @@ -564,8 +554,8 @@ mod tests { let b = bitboard![B5 G7 H3]; assert_eq!(a ^ b, bitboard![B5 C5 H3]); - assert_eq!(a ^ BitBoard::empty(), a); - assert_eq!(BitBoard::empty() ^ BitBoard::empty(), BitBoard::empty()); + assert_eq!(a ^ BitBoard::EMPTY, a); + assert_eq!(BitBoard::EMPTY ^ BitBoard::EMPTY, BitBoard::EMPTY); } #[test] diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 6a60392..3ea670c 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -110,14 +110,14 @@ pub(super) struct MoveLibrary { impl MoveLibrary { const fn new() -> MoveLibrary { MoveLibrary { - rays: [[BitBoard::empty(); Direction::NUM]; Square::NUM], - pawn_attacks: [[BitBoard::empty(); Square::NUM]; Color::NUM], - pawn_pushes: [[BitBoard::empty(); Square::NUM]; Color::NUM], - knight_moves: [BitBoard::empty(); Square::NUM], - bishop_moves: [BitBoard::empty(); Square::NUM], - rook_moves: [BitBoard::empty(); Square::NUM], - queen_moves: [BitBoard::empty(); Square::NUM], - king_moves: [BitBoard::empty(); Square::NUM], + rays: [[BitBoard::EMPTY; Direction::NUM]; Square::NUM], + pawn_attacks: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], + pawn_pushes: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], + knight_moves: [BitBoard::EMPTY; Square::NUM], + bishop_moves: [BitBoard::EMPTY; Square::NUM], + rook_moves: [BitBoard::EMPTY; Square::NUM], + queen_moves: [BitBoard::EMPTY; Square::NUM], + king_moves: [BitBoard::EMPTY; Square::NUM], } } @@ -238,7 +238,7 @@ impl MoveLibrary { } fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { - let mut ray = BitBoard::empty(); + let mut ray = BitBoard::EMPTY; let mut iter = shift(&sq); while !iter.is_empty() { diff --git a/board/src/movement.rs b/board/src/movement.rs index 2935eee..3ebf44c 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -41,7 +41,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::KingSide, color); parameters.target.king.into() } else { - BitBoard::empty() + BitBoard::EMPTY }; let queenside_target_square = if board @@ -51,7 +51,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::QueenSide, color); parameters.target.king.into() } else { - BitBoard::empty() + BitBoard::EMPTY }; self.sight(square, board) | kingside_target_square | queenside_target_square @@ -99,11 +99,11 @@ mod tests { #[test] fn white_pushes_empty_board() { assert_eq!( - pawn_pushes(Square::E4.into(), Color::White, BitBoard::empty()), + pawn_pushes(Square::E4.into(), Color::White, BitBoard::EMPTY), bitboard![E5] ); assert_eq!( - pawn_pushes(Square::E2.into(), Color::White, BitBoard::empty()), + pawn_pushes(Square::E2.into(), Color::White, BitBoard::EMPTY), bitboard![E3 E4] ); } @@ -111,11 +111,11 @@ mod tests { #[test] fn black_pawn_empty_board() { assert_eq!( - pawn_pushes(Square::A4.into(), Color::Black, BitBoard::empty()), + pawn_pushes(Square::A4.into(), Color::Black, BitBoard::EMPTY), bitboard![A3] ); assert_eq!( - pawn_pushes(Square::B7.into(), Color::Black, BitBoard::empty()), + pawn_pushes(Square::B7.into(), Color::Black, BitBoard::EMPTY), bitboard![B6 B5] ); } @@ -124,7 +124,7 @@ mod tests { fn white_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]), - BitBoard::empty() + BitBoard::EMPTY ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]), @@ -132,7 +132,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]), - BitBoard::empty() + BitBoard::EMPTY ); } @@ -140,7 +140,7 @@ mod tests { fn black_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]), - BitBoard::empty() + BitBoard::EMPTY ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]), @@ -148,7 +148,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]), - BitBoard::empty() + BitBoard::EMPTY ); } } diff --git a/board/src/sight.rs b/board/src/sight.rs index b8e366e..e682cb0 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -305,7 +305,7 @@ mod tests { let piece = piece!(White Pawn); let sight = piece.sight(Square::E4, &pos); - assert_eq!(sight, BitBoard::empty()); + assert_eq!(sight, BitBoard::EMPTY); } #[test] From 182bf8112669b156faa27f63e87384e1cfff9df5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 16:15:09 -0700 Subject: [PATCH 39/40] [board] Fix a counter underflow in the piece set During perft runs, the PieceSet counter would occasionally underflow, causing the whole program to crash. This is because, when building a Board from a list of bitboards, Counts::increment() was only being called once, even when the bitboard had more than one piece in it. Fix the bug by incrementing during the loop that sets up the mailbox. Additionally, refactor the increment() and decrement() methods to be a little more succinct. --- board/src/piece_sets.rs | 3 +-- board/src/piece_sets/counts.rs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index de43caa..52af054 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -51,11 +51,10 @@ impl PieceSet { color_occupancy[color_index] |= bitboard; shape_occupancy[shape_index] |= bitboard; - counts.increment(color, shape); - for square in bitboard.occupied_squares(&IterationDirection::default()) { let piece = Piece::new(color, shape); mailbox.set(piece, square); + counts.increment(color, shape); } } } diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs index effbbe0..7d3cade 100644 --- a/board/src/piece_sets/counts.rs +++ b/board/src/piece_sets/counts.rs @@ -17,20 +17,21 @@ impl Counts { const SQUARE_NUM: u8 = Square::NUM as u8; let updated_value = self.0[color as usize][shape as usize] + 1; - if updated_value <= SQUARE_NUM { - self.0[color as usize][shape as usize] = updated_value; - } else { - unreachable!("piece count for {color} {shape} overflowed"); + if updated_value > SQUARE_NUM { + let shape_name = shape.name(); + panic!("piece count for {color} {shape_name} overflowed"); } + + self.0[color as usize][shape as usize] = updated_value; } pub fn decrement(&mut self, color: Color, shape: Shape) { let count = self.0[color as usize][shape as usize]; - if let Some(updated_count) = count.checked_sub(1) { - self.0[color as usize][shape as usize] = updated_count; - } else { - unreachable!("piece count for {color} {shape} underflowed"); - } + let updated_count = count.checked_sub(1).unwrap_or_else(|| { + let shape_name = shape.name(); + panic!("piece count for {color} {shape_name} should not underflow"); + }); + self.0[color as usize][shape as usize] = updated_count; } #[cfg(test)] From dae5179947f9f7001e1a47725723d9ba0eb205d0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 15 Aug 2025 17:06:07 -0700 Subject: [PATCH 40/40] Add a README --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ca6cb2 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +ChessFriend +=========== + +A chess engine written in Rust. + +The project is divided into crates for major components of the engine. These +crates are collected in a Cargo workspace. All crates have the `chessfriend_` +naming prefix. The directory structure omits this prefix, and I also frequently +do when referring to them. + + + +## Engine Crates + +The engine is divided into several crates, each providing vital types and +functionality. + + + +### `core` + +A collection of types for representing core concepts in a chess game and the +engine. Types like `Color` (player or piece color), `Shape` (the shape of a +piece: knight, etc), `Piece` (a piece of a particular color and shape), and +`Score` (for scoring a board position) live here. + + + +### `bitboard` + +Implements an efficient BitBoard type. Bitboards use a 64-bit bit field to mark a +square on a board as occupied or free. + + + +### `board` + +Implements a `Board` type that represents a moment-in-time board position. FEN +parsing and production lives here. + + + +### `moves` + +The `Move` type lives here, along with routines for encoding moves in efficient +forms, parsing moves from algebraic notation, and recording moves in a game +context. Additionally, the move generators for each shape of piece are here. + + + +### `position` + +Exports the `Position` type, representing a board position within the context of +a game. As such, it also provides a move list, and methods to make and unmake +moves. + + + +## Support Crates + +These crates are for debugging and testing. + + + +### `explorer` + +This crate implements a small command-line application for "exploring" board +positions. I meant for this program to be a debugging utility so that I could +examine bitboards and other board structures live. It has grown over time to +also support more aspects of interacting with the engine. So you can also use it +to play a game! + + + +### `perft` + +A small Perft utility that executes perft to a given depth from some starting +position. + + + + + +## Building + +Build the engine in the usual Rusty way. + +```sh +$ cargo build +``` + + + + + +## Testing + +Test in the usual Rusty way. + +```sh +$ cargo test +``` + +The engine has a fairly comprehensive unit test suite, as well as a decent pile +of integration tests. + + + + + +## Authors + +This engine is built entirely by me, Eryn Wells.