Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
90ee3e416f WIP 2025-06-17 16:23:50 -07:00
7 changed files with 344 additions and 2 deletions

6
Cargo.lock generated
View file

@ -72,6 +72,12 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
name = "chessfriend"
version = "0.1.0"
dependencies = [
"chessfriend_core",
"chessfriend_moves",
"chessfriend_position",
"clap",
"shlex",
"thiserror",
]
[[package]]

View file

@ -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"

View file

@ -0,0 +1,87 @@
// Eryn Wells <eryn@erynwells.me>
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<ZobristState>,
/// 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<usize>);
impl Threading {
#[must_use]
pub fn new(n: NonZero<usize>) -> Self {
Self(n)
}
#[must_use]
pub fn new_with_available_parallelism() -> Self {
const ONE: NonZero<usize> = NonZero::new(1).unwrap();
Self(std::thread::available_parallelism().unwrap_or(ONE))
}
}
impl Default for Threading {
fn default() -> Self {
Self::new_with_available_parallelism()
}
}

View file

@ -1,2 +1,19 @@
// Eryn Wells <eryn@erynwells.me>
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};
}

View file

@ -0,0 +1,77 @@
// Eryn Wells <eryn@erynwells.me>
use std::{
num::NonZero,
panic,
sync::{Arc, Mutex, mpsc},
thread,
};
pub(crate) trait Job: FnOnce() + Send + 'static {}
pub(crate) struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Box<dyn Job>>>,
}
impl ThreadPool {
pub fn new(threads_count: NonZero<usize>) -> 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<Mutex<mpsc::Receiver<Box<dyn Job>>>>) -> Self {
// TODO: A note from the Rust Programming Language
//
// Note: If the operating system cant create a thread because there
// arent 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 simplicitys sake, this behavior is fine,
// but in a production thread pool implementation, youd 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),
}
}
}

148
chessfriend/src/uci.rs Normal file
View file

@ -0,0 +1,148 @@
// Eryn Wells <eryn@erynwells.me>
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<Vec<Response>, 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<Response> = 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),
}

View file

@ -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;