Spawn a separate thread to receive UCI messages via mpsc channel, simplify output to println

This commit is contained in:
stefiosif
2025-03-01 23:54:51 +02:00
parent 7accc28aba
commit 91345848c6
3 changed files with 38 additions and 94 deletions

View File

@@ -1,7 +1,9 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
io::{BufRead, Write}, io::{self, BufRead},
str::SplitWhitespace, str::SplitWhitespace,
sync::mpsc,
thread,
}; };
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
@@ -51,12 +53,6 @@ pub enum Response {
Info(String), Info(String),
} }
pub fn write_response(handle_out: &mut impl Write, response: &Response) -> anyhow::Result<()> {
writeln!(handle_out, "{response}").map_err(|e| anyhow!(e))?;
handle_out.flush().map_err(|e| anyhow!(e))?;
Ok(())
}
use std::fmt; use std::fmt;
impl fmt::Display for Response { impl fmt::Display for Response {
@@ -220,16 +216,24 @@ fn parse_next<T: std::str::FromStr>(iter: &mut SplitWhitespace, val: &str) -> an
.and_then(|v| v.parse::<T>().map_err(|_| anyhow!("Invalid {val}"))) .and_then(|v| v.parse::<T>().map_err(|_| anyhow!("Invalid {val}")))
} }
pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> anyhow::Result<()> { pub fn uci_loop() -> anyhow::Result<()> {
let mut params = UciParameters::new(); let mut params = UciParameters::new();
let (uci_sender, uci_receiver) = mpsc::channel();
for line in input.lines() { thread::spawn(move || {
let input = io::stdin().lock();
for line in input.lines() {
uci_sender.send(line).expect("Failed to send line");
}
});
while let Ok(line) = uci_receiver.recv() {
let line_str = line.unwrap_or_else(|_| "quit".to_string()); let line_str = line.unwrap_or_else(|_| "quit".to_string());
let mut parts = line_str.split_whitespace(); let mut parts = line_str.split_whitespace();
let command = parse_command(&mut parts)?; let command = parse_command(&mut parts)?;
match command { match command {
Command::Uci => write_response(&mut output, &Response::UciOk)?, Command::Uci => println!("{}", Response::UciOk),
Command::IsReady => write_response(&mut output, &Response::ReadyOk)?, Command::IsReady => println!("{}", Response::ReadyOk),
Command::UciNewGame => { Command::UciNewGame => {
if let Some(game) = params.game.as_mut() { if let Some(game) = params.game.as_mut() {
game.tt = TranspositionTable::new(); game.tt = TranspositionTable::new();
@@ -243,17 +247,16 @@ pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> anyhow::Result
Command::Go => { Command::Go => {
if let Some(game) = params.game.as_mut() { if let Some(game) = params.game.as_mut() {
match uci_go(&mut parts, game) { match uci_go(&mut parts, game) {
Ok(best_move) => write_response( Ok(best_move) => {
&mut output, println!("{}", Response::BestMove(best_move.parse_into_str()))
&Response::BestMove(best_move.parse_into_str()), }
), Err(e) => println!("{}", Response::Info(e.to_string())),
Err(e) => write_response(&mut output, &Response::Info(e.to_string())), }
}?;
} else { } else {
write_response( println!(
&mut output, "{}",
&Response::Info("Failed to unwrap from UciParameter".to_string()), Response::Info("Failed to unwrap from UciParameter".to_string())
)?; )
}; };
} }
Command::Stop => break, Command::Stop => break,
@@ -267,8 +270,6 @@ pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> anyhow::Result
}; };
} }
}; };
output.flush().map_err(|e| anyhow!(e))?
} }
Ok(()) Ok(())
@@ -276,8 +277,6 @@ pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> anyhow::Result
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io::Cursor;
use crate::{ use crate::{
board::{fen::from_fen, square::Square}, board::{fen::from_fen, square::Square},
interface::uci::{parse_command, Command}, interface::uci::{parse_command, Command},
@@ -285,7 +284,6 @@ mod tests {
}; };
use super::uci_go; use super::uci_go;
use super::uci_loop;
use super::uci_position; use super::uci_position;
const FEN: [&str; 2] = [ const FEN: [&str; 2] = [
@@ -326,28 +324,4 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn test_uci_loop() -> anyhow::Result<()> {
init_attacks();
let commands = "uci\n\
ucinewgame\n\
position fen 8/8/8/8/8/4q1k1/8/5K2 b - - 0 1\n\
go\n\
quit";
let input = Cursor::new(commands);
let mut output: Vec<_> = vec![];
uci_loop(input, &mut output)?;
let expected_response = "id name zeal\n\
id author stefiosif\n\
uciok\n\
bestmove e3f2\n";
let actual_response = String::from_utf8(output).expect("Invalid UTF-8 in output");
assert_eq!(expected_response, actual_response);
Ok(())
}
} }

View File

@@ -1,5 +1,3 @@
use std::io;
mod board; mod board;
mod evaluation; mod evaluation;
mod interface; mod interface;
@@ -28,9 +26,7 @@ fn main() {
match Args::parse().mode { match Args::parse().mode {
Some(Mode::Perft) => search::perft::perftree_script(), Some(Mode::Perft) => search::perft::perftree_script(),
Some(Mode::Uci) | None => { Some(Mode::Uci) | None => {
let input = io::stdin().lock(); uci::uci_loop().unwrap_or_else(|e| println!("{e}"));
let output = io::stdout().lock();
uci::uci_loop(input, output).unwrap_or_else(|e| println!("{e}"));
} }
} }
} }

View File

@@ -1,9 +1,7 @@
use std::io;
use crate::{ use crate::{
board::game::Game, board::game::Game,
evaluation::{MAX_SCORE, MIN_SCORE}, evaluation::{MAX_SCORE, MIN_SCORE},
interface::uci::{write_response, Response}, interface::uci::Response,
movegen::r#move::Move, movegen::r#move::Move,
}; };
@@ -19,10 +17,7 @@ pub fn iterative_deepening(
for depth in 1..=max_depth { for depth in 1..=max_depth {
if time.exceed_soft_limit() { if time.exceed_soft_limit() {
write_response( println!("Soft limit exceeded in negamax");
&mut io::stdout(),
&Response::Info("Soft limit exceeded in negamax".to_string()),
)?;
return Ok(best_move); return Ok(best_move);
} }
@@ -44,7 +39,7 @@ pub fn iterative_deepening(
let score = match score { let score = match score {
Ok(score) => score, Ok(score) => score,
Err(ref e) => { Err(ref e) => {
write_response(&mut io::stdout(), &Response::Info(format!("{e}")))?; println!("Error: {}", Response::Info(e.to_string()));
break_from_aw = false; break_from_aw = false;
break; break;
} }
@@ -68,46 +63,25 @@ pub fn iterative_deepening(
} }
if let Err(e) = score { if let Err(e) = score {
write_response(&mut io::stdout(), &Response::Info(format!("{e}")))?; println!("Error: {}", Response::Info(e.to_string()));
break; break;
} }
best_move = game.tt.lookup(game.hash).and_then(|entry| entry.mv); best_move = game.tt.lookup(game.hash).and_then(|entry| entry.mv);
write_response( println!(
&mut io::stdout(), "info depth {} ms {} nodes {} nps {} eval {} bestmove {}",
&log_depth_results( depth,
depth, time.instant.elapsed().as_millis(),
time.instant.elapsed().as_secs(), nodes,
nodes, time.nps(nodes),
time.nps(nodes), score?,
score?, best_move.expect("No best move found")
best_move, );
),
)?;
} }
Ok(best_move) Ok(best_move)
} }
fn log_depth_results(
depth: u8,
seconds: u64,
nodes: u64,
nps: u64,
best_score: i16,
best_move: Option<Move>,
) -> Response {
Response::Info(format!(
"info depth {} seconds {} nodes {} nps {} eval {} bestmove {}",
depth,
seconds,
nodes,
nps,
best_score,
best_move.expect("No best move found")
))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{