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;