chessfriend/chessfriend/src/uci.rs
2025-06-17 16:23:50 -07:00

148 lines
3.5 KiB
Rust

// 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),
}