use std::{ collections::HashMap, io::{self, BufRead}, str::SplitWhitespace, sync::mpsc, thread, }; use anyhow::{anyhow, bail}; use crate::{ board::{ board::{Board, Color}, game::Game, }, movegen::r#move::Move, search::{ iterative_deepening, time::TimeInfo, transposition_table::TranspositionTable, INC, MAX_DEPTH, TIME, }, }; #[derive(PartialEq, Eq, Debug)] pub enum Command { Uci, IsReady, UciNewGame, Position, Go, Stop, Quit, SetOption, } fn parse_command(parts: &mut SplitWhitespace) -> anyhow::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("stop") => Ok(Command::Stop), Some("quit") => Ok(Command::Quit), Some("setoption") => Ok(Command::SetOption), _ => bail!("Unrecognised command"), } } pub enum Response { UciOk, ReadyOk, BestMove(String), Info(String), } 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 zeal\nid author stefiosif\nuciok"), Self::ReadyOk => write!(f, "readyok"), Self::BestMove(best_move) => write!(f, "bestmove {best_move}"), Self::Info(info) => write!(f, "{info}"), } } } #[derive(Debug)] struct UciParameters { depth: Option, game: Option, wtime: Option, btime: Option, winc: Option, binc: Option, options: HashMap, } impl UciParameters { fn new() -> Self { Self { depth: None, game: None, wtime: None, btime: None, winc: None, binc: None, options: HashMap::new(), } } fn add_move_time(&mut self, movetime: u128) { self.wtime = Some(movetime); self.btime = Some(movetime); } fn add_depth(&mut self, depth: u8) { self.depth = Some(depth); } fn add_wtime(&mut self, wtime: u128) { self.wtime = Some(wtime); } fn add_btime(&mut self, btime: u128) { self.btime = Some(btime); } fn add_winc(&mut self, winc: u128) { self.winc = Some(winc); } fn add_binc(&mut self, binc: u128) { self.binc = Some(binc); } fn add_game(&mut self, game: Game) { self.game = Some(game); } fn add_option(&mut self, name: String, value: String) { self.options.insert(name, value); } } impl Default for UciParameters { fn default() -> Self { Self::new() } } pub fn uci_position(position: &mut SplitWhitespace) -> anyhow::Result { let state = position .next() .ok_or_else(|| anyhow!("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::from_fen(&fen).map_err(|e| anyhow!("Failed to parse FEN: {fen}, {e}"))? } _ => bail!("Expected startpos or fen"), }; if Some("moves") != position.next() { return Ok(game); } for mv_str in position { let mv = Move::parse_from_str(&game, mv_str) .map_err(|e| anyhow!("Failed to parse move: {e}"))?; game.make_move(&mv); } Ok(game) } pub fn uci_go(go_iter: &mut SplitWhitespace, game: &mut Game) -> anyhow::Result { let mut params = UciParameters::new(); while let Some(subcommand) = go_iter.next() { match subcommand { "wtime" => params.add_wtime(parse_next(go_iter, "wtime")?), "btime" => params.add_btime(parse_next(go_iter, "btime")?), "winc" => params.add_winc(parse_next(go_iter, "winc")?), "binc" => params.add_binc(parse_next(go_iter, "binc")?), "depth" => params.add_depth(parse_next(go_iter, "depth")?), "movetime" => params.add_move_time(parse_next(go_iter, "movetime")?), _ => (), } } let (time, inc) = match game.current_player() { Color::White => (params.wtime.unwrap_or(TIME), params.winc.unwrap_or(INC)), Color::Black => (params.btime.unwrap_or(TIME), params.binc.unwrap_or(INC)), }; iterative_deepening::iterative_deepening( game, params.depth.unwrap_or(MAX_DEPTH), &TimeInfo::new(std::time::Instant::now(), time, inc), )? .ok_or_else(|| anyhow!("No stored best move found")) } fn uci_option(option_iter: &mut SplitWhitespace, game: &mut Game) -> anyhow::Result<()> { let mut params = UciParameters::new(); //TODO: Implement the rest of the options and refactor while let Some(subcommand) = option_iter.next() { if subcommand == "name" { let name: String = parse_next(option_iter, "name")?; option_iter.next(); let value: String = parse_next(option_iter, "value")?; params.add_option(name, value); } } if let Some(name) = params.options.get("name") { if name == "Hash" { let value = params.options.get("value").expect("Expected key value"); let size = value .parse::() .map_err(|_| anyhow!("Invalid value for Hash"))?; game.tt = TranspositionTable::with_capacity(size); } } Ok(()) } fn parse_next(iter: &mut SplitWhitespace, val: &str) -> anyhow::Result { iter.next() .ok_or_else(|| anyhow!("Expected {val}")) .and_then(|v| v.parse::().map_err(|_| anyhow!("Invalid {val}"))) } pub fn uci_loop() -> anyhow::Result<()> { let mut params = UciParameters::new(); let (uci_sender, uci_receiver) = mpsc::channel(); 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 mut parts = line_str.split_whitespace(); let command = parse_command(&mut parts)?; match command { Command::Uci => println!("{}", Response::UciOk), Command::IsReady => println!("{}", Response::ReadyOk), Command::UciNewGame => { if let Some(game) = params.game.as_mut() { game.tt = TranspositionTable::new(); game.board = Board::startpos(); } else { let game = Game::new(); params.add_game(game); } } Command::Position => params.add_game(uci_position(&mut parts)?), Command::Go => { if let Some(game) = params.game.as_mut() { match uci_go(&mut parts, game) { Ok(best_move) => { println!("{}", Response::BestMove(best_move.parse_into_str())) } Err(e) => println!("{}", Response::Info(e.to_string())), } } else { println!( "{}", Response::Info("Failed to unwrap from UciParameter".to_string()) ) }; } Command::Stop => break, Command::Quit => break, Command::SetOption => { if let Some(game) = params.game.as_mut() { uci_option(&mut parts, game)?; } else { let game = Game::new(); params.add_game(game); }; } }; } Ok(()) } #[cfg(test)] mod tests { use crate::{ board::{fen::from_fen, square::Square}, interface::uci::{parse_command, Command}, movegen::{attack_generator::init_attacks, r#move::Move}, }; use super::uci_go; 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() -> anyhow::Result<()> { 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() -> anyhow::Result<()> { init_attacks(); let mut game = from_fen(FEN_MATE_IN_1).unwrap(); let command_go = "go depth 2"; 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(()) } }