use std::{ io::{BufRead, Write}, str::SplitWhitespace, }; use crate::{ board::game::Game, evaluation::{MAX_SCORE, MIN_SCORE}, movegen::r#move::Move, search, }; #[derive(PartialEq, Eq, Debug)] pub enum Command { Uci, IsReady, UciNewGame, Position, Go, Quit, } fn parse_command(parts: &mut SplitWhitespace) -> Result { match parts.next() { Some("uci") => Ok(Command::Uci), Some("isready") => Ok(Command::IsReady), Some("ucinewgame") => Ok(Command::UciNewGame), Some("position") => Ok(Command::Position), Some("go") => Ok(Command::Go), Some("quit") => Ok(Command::Quit), _ => Err("Unrecognised command".to_string()), } } pub enum Response { UciOk, ReadyOk, BestMove(String), Info(String), } fn write_response(handle_out: &mut impl Write, response: &Response) -> Result<(), String> { writeln!(handle_out, "{response}").map_err(|e| e.to_string())?; handle_out.flush().map_err(|e| e.to_string())?; Ok(()) } use std::fmt; impl fmt::Display for Response { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UciOk => write!(f, "id name ippos\nid author stefiosif\nuciok"), Self::ReadyOk => write!(f, "readyok"), Self::BestMove(best_move) => write!(f, "bestmove {best_move}"), Self::Info(info) => write!(f, "{info}"), } } } struct UciParameters { movetime: Option, depth: Option, game: Option, } impl UciParameters { const fn new() -> Self { Self { movetime: None, depth: None, game: None, } } fn add_movetime(&mut self, movetime: usize) { self.movetime = Some(movetime); } fn add_depth(&mut self, depth: u8) { self.depth = Some(depth); } fn add_game(&mut self, game: Game) { self.game = Some(game); } } impl Default for UciParameters { fn default() -> Self { Self::new() } } pub fn uci_position(position: &mut SplitWhitespace) -> Result { let state = position.next().ok_or("Expected startpos or fen")?; let mut game = match state { "startpos" => Game::new(), "fen" => { let fen_parts: Vec<&str> = position.take_while(|&part| part != "moves").collect(); let fen = fen_parts.join(" "); Game::new_from_fen(&fen)? } _ => return Err("Expected startpos or fen".to_string()), }; if Some("moves") != position.next() { return Ok(game); } for mv_str in position { let mv = Move::parse_from_str(mv_str)?; game.board.make_move(&mv); } Ok(game) } const MAX_DEPTH: u8 = 5; pub fn uci_go(go: &mut SplitWhitespace, game: &mut Game) -> Result { let mut params = UciParameters::new(); while let Some(subcommand) = go.next() { match subcommand { "depth" => { let depth_str = go.next().ok_or("Expected depth value")?; let depth = depth_str.parse::().map_err(|_| "Invalid depth value")?; params.add_depth(depth); } "movetime" => { let movetime_str = go.next().ok_or("Expected movetime value")?; let movetime = movetime_str .parse::() .map_err(|_| "Invalid movetime value")?; params.add_movetime(movetime); } _ => (), } } Ok(search::negamax::negamax( game, MIN_SCORE, MAX_SCORE, params.depth.unwrap_or(MAX_DEPTH), 0, ) .0 .expect("No move selected")) } pub fn uci_loop(input: R, mut output: W) -> Result<(), String> { let mut params = UciParameters::new(); for line in input.lines() { let line_str = line.unwrap_or_else(|_| "quit".to_string()); let mut parts = line_str.split_whitespace(); let command = parse_command(&mut parts)?; let response = match command { Command::Uci => Response::UciOk, Command::IsReady => Response::ReadyOk, Command::UciNewGame => Response::Info("Clear cache".to_string()), Command::Position => { params.add_game(uci_position(&mut parts)?); Response::Info("Initialized position".to_string()) } Command::Go => { if let Some(ref mut game) = params.game { let best_move = uci_go(&mut parts, game)?; Response::BestMove(best_move.parse_into_str()) } else { Response::Info("Going?".to_string()) } } Command::Quit => break, }; write_response(&mut output, &response)?; output.flush().map_err(|e| e.to_string())?; } Ok(()) } #[cfg(test)] mod tests { use std::io::Cursor; use crate::{ board::{fen::from_fen, square::Square}, interface::uci::{parse_command, Command}, movegen::{attack::init_attacks, r#move::Move}, }; use super::uci_go; use super::uci_loop; use super::uci_position; const FEN: [&str; 2] = [ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "r1bqkbnr/pppppppp/2n4B/8/3P4/8/PPP1PPPP/RN1QKBNR b KQkq - 2 2", ]; #[test] fn test_uci_position() -> Result<(), String> { let command_position = "position startpos"; let mut parts = command_position.split_whitespace(); let command = parse_command(&mut parts)?; assert_eq!(command, Command::Position); assert_eq!(from_fen(FEN[0]).unwrap(), uci_position(&mut parts)?); let command_position_moves = "position startpos moves d2d4 b8c6 c1h6"; let mut parts = command_position_moves.split_whitespace(); let command = parse_command(&mut parts)?; assert_eq!(command, Command::Position); assert_eq!(from_fen(FEN[1]).unwrap(), uci_position(&mut parts)?); Ok(()) } const FEN_MATE_IN_1: &str = "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1"; #[test] fn test_uci_go() -> Result<(), String> { init_attacks(); let mut game = from_fen(FEN_MATE_IN_1)?; let command_go = "go depth 4"; let mut parts = command_go.split_whitespace(); let response = uci_go(&mut parts, &mut game)?; assert_eq!(Move::new(Square::E3, Square::F2), response); Ok(()) } #[test] fn test_uci_loop() -> Result<(), String> { init_attacks(); let commands = b"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 ippos\n\ id author stefiosif\n\ uciok\n\ Clear cache\n\ Initialized position\n\ bestmove e3f2\n"; let actual_response = String::from_utf8(output).expect("Invalid UTF-8 in output"); assert_eq!(expected_response, actual_response); Ok(()) } }