diff --git a/chess_uci/src/lib.rs b/chess_uci/src/lib.rs index 3760a8a..74abb48 100644 --- a/chess_uci/src/lib.rs +++ b/chess_uci/src/lib.rs @@ -3,15 +3,16 @@ /// Manage UCI messages and link into chess component pub mod uci_command; +use crate::uci_command::*; use log::{info, warn}; use std::io::*; -use std::time::Duration; use std::result::Result; -use crate::uci_command::*; +use std::time::Duration; -use chess::{Game, Board, ChessMove}; +use chess::{Board, ChessMove, Game}; -const DEFAULT_TIME: (Option, Option) = (Some(Duration::from_secs(120 * 60)), None); +const DEFAULT_TIME: (Option, Option) = + (Some(Duration::from_secs(120 * 60)), None); /// Structure used to manage a chess game with a uci engine. /// @@ -49,28 +50,30 @@ pub struct UciEngine { } pub enum UciOption { - Ponder{value: bool}, - UCIElo{value: Option}, + Ponder { value: bool }, + UCIElo { value: Option }, } #[derive(Clone, Copy)] -pub enum Player{Human{elo: Option}, Machine{elo: Option}} +pub enum Player { + Human { elo: Option }, + Machine { elo: Option }, +} pub enum GameOption { - WhiteTotalTime{value: Option}, - BlackTotalTime{value: Option}, - WhiteIncrement{value: Option}, - BlackIncrement{value: Option}, - WhitePlayer{value: Player}, - BlackPlayer{value: Player}, + WhiteTotalTime { value: Option }, + BlackTotalTime { value: Option }, + WhiteIncrement { value: Option }, + BlackIncrement { value: Option }, + WhitePlayer { value: Player }, + BlackPlayer { value: Player }, } -impl UciEngine { - +impl UciEngine { /// Create new game manager /// /// Requires line by line input and output streams to communicate with uci engine pub fn new(source: Option, destination: Option) -> UciEngine { - UciEngine::{ + UciEngine:: { source: source.map(|x| BufReader::new(x)), destination, id: Id::default(), @@ -78,25 +81,49 @@ impl UciEngine { initial: Board::default(), game: Game::new(), time: [DEFAULT_TIME; 2], - player: [Player::Human{elo: None}; 2], + player: [Player::Human { elo: None }; 2], } } pub fn game_option(&mut self, option: GameOption) -> GameOption { let old_value: GameOption; match option { - GameOption::WhiteTotalTime{value} => {old_value = GameOption::WhiteTotalTime { value: self.time[Color::White.to_index()].0 }; - self.time[Color::White.to_index()].0 = value}, - GameOption::BlackTotalTime{value} => {old_value = GameOption::BlackTotalTime { value: self.time[Color::Black.to_index()].0 }; - self.time[Color::Black.to_index()].0 = value}, - GameOption::WhiteIncrement{value} => {old_value = GameOption::WhiteIncrement { value: self.time[Color::White.to_index()].1 }; - self.time[Color::White.to_index()].1 = value}, - GameOption::BlackIncrement{value} => {old_value = GameOption::BlackIncrement { value: self.time[Color::Black.to_index()].1 }; - self.time[Color::Black.to_index()].1 = value}, - GameOption::WhitePlayer{value} => {old_value = GameOption::WhitePlayer { value: self.player[Color::White.to_index()] }; - self.player[Color::White.to_index()] = value}, - GameOption::BlackPlayer{value} => {old_value = GameOption::WhitePlayer { value: self.player[Color::Black.to_index()] }; - self.player[Color::Black.to_index()] = value}, + GameOption::WhiteTotalTime { value } => { + old_value = GameOption::WhiteTotalTime { + value: self.time[Color::White.to_index()].0, + }; + self.time[Color::White.to_index()].0 = value + } + GameOption::BlackTotalTime { value } => { + old_value = GameOption::BlackTotalTime { + value: self.time[Color::Black.to_index()].0, + }; + self.time[Color::Black.to_index()].0 = value + } + GameOption::WhiteIncrement { value } => { + old_value = GameOption::WhiteIncrement { + value: self.time[Color::White.to_index()].1, + }; + self.time[Color::White.to_index()].1 = value + } + GameOption::BlackIncrement { value } => { + old_value = GameOption::BlackIncrement { + value: self.time[Color::Black.to_index()].1, + }; + self.time[Color::Black.to_index()].1 = value + } + GameOption::WhitePlayer { value } => { + old_value = GameOption::WhitePlayer { + value: self.player[Color::White.to_index()], + }; + self.player[Color::White.to_index()] = value + } + GameOption::BlackPlayer { value } => { + old_value = GameOption::WhitePlayer { + value: self.player[Color::Black.to_index()], + }; + self.player[Color::Black.to_index()] = value + } } old_value @@ -105,7 +132,7 @@ impl UciEngine { /// Launch uci engine initialisation /// /// Retrieve data from uci engine (until uciok command from engine) - pub fn init(&mut self){ + pub fn init(&mut self) { self.push(GuiCommand::Uci); // Consume commands until uciok messages @@ -140,16 +167,17 @@ impl UciEngine { /// uci.make_move(ChessMove::from_str("e2e4").expect("error converting e2e4")); /// ``` pub fn make_move(&mut self, chess_move: ChessMove) -> bool { - if self.game.make_move(chess_move) { - self.push(GuiCommand::Position { position: Some(self.initial), moves: self.game.actions().to_vec() }); + if self.game.make_move(chess_move) { + self.push(GuiCommand::Position { + position: Some(self.initial), + moves: self.game.actions().to_vec(), + }); true } else { false } - } - fn terminate(&mut self, reason: &str) { self.uciok = false; info!("UCI termination: {}", reason); @@ -165,13 +193,17 @@ impl UciEngine { self.id.author() } - fn update(&mut self, id: &Id){ - if self.is_uciok() {warn!("Engine info should not be updated now (uciok)");} + fn update(&mut self, id: &Id) { + if self.is_uciok() { + warn!("Engine info should not be updated now (uciok)"); + } self.id.update(id); } - fn update_from_str(&mut self, name: Option<&str>, author: Option<&str>){ - if self.is_uciok() {warn!("Engine info should not be updated now (uciok)");} + fn update_from_str(&mut self, name: Option<&str>, author: Option<&str>) { + if self.is_uciok() { + warn!("Engine info should not be updated now (uciok)"); + } self.id.update_from_str(name, author); } @@ -180,17 +212,20 @@ impl UciEngine { /// if `EngineCommand::Id`: update name or authorship of the engine /// if `EngineCommand::UciOk`: end engine initialization phase pub fn exec(&mut self, command: &str) -> Result { - let uci_command = parse (&mut command.to_string()); - match uci_command.clone() { - Ok(EngineCommand::Id{id}) => self.update(&id), + let uci_command = parse(&mut command.to_string()); + match uci_command.clone() { + Ok(EngineCommand::Id { id }) => self.update(&id), Ok(EngineCommand::UciOk) => self.uciok(), - Ok(EngineCommand::Opt { options: _ }) => {}, - Ok(EngineCommand::Info { infos: _ }) | - Ok(EngineCommand::BestMove { best_move: _, ponder: _ }) | - Ok(EngineCommand::ReadyOk) - => {}, - Ok(EngineCommand::Registration)| - Ok(EngineCommand::CopyProtection)=> {unimplemented!("command not implemented")}, + Ok(EngineCommand::Opt { options: _ }) => {} + Ok(EngineCommand::Info { infos: _ }) + | Ok(EngineCommand::BestMove { + best_move: _, + ponder: _, + }) + | Ok(EngineCommand::ReadyOk) => {} + Ok(EngineCommand::Registration) | Ok(EngineCommand::CopyProtection) => { + unimplemented!("command not implemented") + } Err(_) => warn!("Not a command"), }; uci_command @@ -198,16 +233,21 @@ impl UciEngine { /// Retrieve a line from the uci engine input stream and parse it pub fn pull(&mut self) -> Option { - if let Some(source) = &mut self.source { let mut command = String::new(); match source.read_line(&mut command) { - Ok(0) => {self.terminate("Chess engine closed connection."); None}, + Ok(0) => { + self.terminate("Chess engine closed connection."); + None + } Ok(_) => { info!("← {}", command); self.exec(&command).ok() - }, - Err(reason) => {warn!("Unable to read from engine: {reason}"); None}, + } + Err(reason) => { + warn!("Unable to read from engine: {reason}"); + None + } } } else { info!("❌ No connected uci engine"); @@ -220,9 +260,18 @@ impl UciEngine { if let Some(source) = &mut self.source { let mut data = String::new(); match source.read_line(&mut data) { - Ok(0) => {self.terminate("Chess engine closed connection."); None}, - Ok(_) => {info!("↜ {}", data); Some(data.clone())}, - Err(reason) => {warn!("Unable to read from engine: {reason}"); None}, + Ok(0) => { + self.terminate("Chess engine closed connection."); + None + } + Ok(_) => { + info!("↜ {}", data); + Some(data.clone()) + } + Err(reason) => { + warn!("Unable to read from engine: {reason}"); + None + } } } else { info!("❌ No connected uci engine"); @@ -234,7 +283,7 @@ impl UciEngine { pub fn push(&mut self, command: GuiCommand) { if let Some(destination) = &mut self.destination { let command_str = command.to_string(); - match destination.write(command_str.as_bytes()){ + match destination.write(command_str.as_bytes()) { Ok(n) if n == command_str.len() => info!("→ gui: {command_str}"), Ok(n) => warn!("⚠ gui: {command_str} truncated at {n}"), Err(reason) => warn!("Unable to send command {command_str}: {reason}"), @@ -245,7 +294,7 @@ impl UciEngine { } /// Push string (not a game manager integrated command) to the Uci engine output stream - pub fn push_raw(&mut self, data: &str){ + pub fn push_raw(&mut self, data: &str) { if let Some(destination) = &mut self.destination { match destination.write(data.as_bytes()) { Ok(n) if n == data.len() => info!("↝ raw: {data}"), @@ -258,8 +307,8 @@ impl UciEngine { } } -impl UciEngine { - fn uciok(&mut self){ +impl UciEngine { + fn uciok(&mut self) { self.uciok = true; } @@ -270,42 +319,59 @@ impl UciEngine { pub fn human_play(&mut self, chess_move: ChessMove) -> Result { match self.player[self.side_to_move().to_index()] { - Player::Human{..} => { + Player::Human { .. } => { if self.make_move(chess_move) { Ok(chess_move) } else { Err("Invalid move for human player") } - }, - Player::Machine {..} => Err("Not a human to play for current color.") + } + Player::Machine { .. } => Err("Not a human to play for current color."), } } pub fn go(&mut self) -> Result<(), &'static str> { match self.player[self.side_to_move().to_index()] { - Player::Human{..} => Err("Not a machine to play for current color."), - Player::Machine {elo} => - { + Player::Human { .. } => Err("Not a machine to play for current color."), + Player::Machine { elo } => { if self.is_uciok() { if let Some(elo) = elo { - self.push(GuiCommand::SetOption {option: Opt::UCILimitStrength{value: true}}); - self.push(GuiCommand::SetOption {option: Opt::UCIElo{value: elo}}); + self.push(GuiCommand::SetOption { + option: Opt::UCILimitStrength { value: true }, + }); + self.push(GuiCommand::SetOption { + option: Opt::UCIElo { value: elo }, + }); } let (wtime, wincr) = self.time[Color::White.to_index()]; let (btime, bincr) = self.time[Color::Black.to_index()]; - self.push(GuiCommand::Go{wtime, wincr, btime, bincr}); + self.push(GuiCommand::Go { + wtime, + wincr, + btime, + bincr, + }); Ok(()) } else { - Err("UCI engine not ready") + Err("UCI engine not ready") } } } } + pub fn stop(&mut self) -> Result<(), &'static str> { + match self.player[self.side_to_move().to_index()] { + Player::Human { .. } => Err("Not a machine to play for current color."), + Player::Machine { .. } => { + self.push(GuiCommand::Stop); + Ok(()) + } + } + } } -use chess::{Square, Piece, Color}; -impl UciEngine { +use chess::{Color, Piece, Square}; +impl UciEngine { pub fn piece_on(&self, square: Square) -> Option { self.game.current_position().piece_on(square) } diff --git a/chess_uci/src/uci_command.rs b/chess_uci/src/uci_command.rs index 098a914..79762b8 100644 --- a/chess_uci/src/uci_command.rs +++ b/chess_uci/src/uci_command.rs @@ -1,20 +1,31 @@ use itertools::join; -use std::{time::Duration, str::FromStr}; use std::fmt; +use std::{str::FromStr, time::Duration}; -use chess::{ChessMove, Board, Action}; +use chess::{Action, Board, ChessMove}; #[derive(Clone, Default)] -pub struct Id {name: Option, author: Option} +pub struct Id { + name: Option, + author: Option, +} impl Id { - pub fn update(&mut self, id: &Id){ - if let Some(name) = &id.name {self.name = Some(name.clone());} - if let Some(author) = &id.author {self.author = Some(author.clone());} + pub fn update(&mut self, id: &Id) { + if let Some(name) = &id.name { + self.name = Some(name.clone()); + } + if let Some(author) = &id.author { + self.author = Some(author.clone()); + } } - pub fn update_from_str(&mut self, name: Option<&str>, author: Option<&str>){ - if let Some(name) = name {self.name = Some(name.to_string());} - if let Some(author) = author {self.author = Some(author.to_string());} + pub fn update_from_str(&mut self, name: Option<&str>, author: Option<&str>) { + if let Some(name) = name { + self.name = Some(name.to_string()); + } + if let Some(author) = author { + self.author = Some(author.to_string()); + } } pub fn name(&self) -> Option { @@ -28,10 +39,10 @@ impl Id { #[derive(Debug, Clone)] pub struct Info {} #[derive(Debug, Clone)] -pub enum Opt{ - UCILimitStrength{value: bool}, - UCIElo{value: u32}, - SlowMover{value: u32}, +pub enum Opt { + UCILimitStrength { value: bool }, + UCIElo { value: u32 }, + SlowMover { value: u32 }, } impl Opt { pub fn name(&self) -> &str { @@ -43,7 +54,7 @@ impl Opt { } } impl fmt::Display for Opt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Opt::UCILimitStrength { value } => write!(f, "{value}"), Opt::UCIElo { value } => write!(f, "{value}"), @@ -54,91 +65,160 @@ impl fmt::Display for Opt { #[derive(Clone)] pub enum EngineCommand { - Id{id: Id}, + Id { + id: Id, + }, UciOk, ReadyOk, - BestMove{best_move: ChessMove, ponder: Option}, + BestMove { + best_move: ChessMove, + ponder: Option, + }, CopyProtection, // unimplemented - Registration, // unimplemented - Info{infos: Vec}, - Opt{options: Vec}, + Registration, // unimplemented + Info { + infos: Vec, + }, + Opt { + options: Vec, + }, } - #[derive(Debug)] pub enum GuiCommand { Uci, - Debug{mode: bool}, + Debug { + mode: bool, + }, IsReady, - SetOption{option: Opt}, + SetOption { + option: Opt, + }, Register, // unimplemented UciNewGame, - Position{position: Option, moves: Vec}, - Go{wtime: Option, wincr: Option, - btime: Option, bincr: Option}, + Position { + position: Option, + moves: Vec, + }, + Go { + wtime: Option, + wincr: Option, + btime: Option, + bincr: Option, + }, Stop, PonderHit, Quit, } -pub fn parse(message: &mut str) -> Result { +pub fn parse(message: &mut str) -> Result { let mut message_iter = message.split_whitespace(); match message_iter.next() { None => Err("No command provided"), - Some("id") => - match message_iter.collect::>().as_slice() { - [] => Err("Empty id command"), - ["name"] => Err("Empty id name command"), - ["author"] => Err("Empty id author command"), - ["name", tail @ ..] => Ok(EngineCommand::Id{id: { let mut id = Id::default(); id.update_from_str(Some(&join(tail, " ")), None); id}}), - ["author", tail @ ..] => Ok(EngineCommand::Id{id: { let mut id = Id::default(); id.update_from_str(None, Some(&join(tail, " "))); id}}), - _ => Err("Invalid id subcommand") - }, + Some("id") => match message_iter.collect::>().as_slice() { + [] => Err("Empty id command"), + ["name"] => Err("Empty id name command"), + ["author"] => Err("Empty id author command"), + ["name", tail @ ..] => Ok(EngineCommand::Id { + id: { + let mut id = Id::default(); + id.update_from_str(Some(&join(tail, " ")), None); + id + }, + }), + ["author", tail @ ..] => Ok(EngineCommand::Id { + id: { + let mut id = Id::default(); + id.update_from_str(None, Some(&join(tail, " "))); + id + }, + }), + _ => Err("Invalid id subcommand"), + }, Some("uciok") => Ok(EngineCommand::UciOk), Some("readyok") => unimplemented!(), - Some("bestmove") => - match message_iter.collect::>().as_slice() { - [] => Err("Empty bestmove command"), - [chessmove] => Ok(EngineCommand::BestMove { best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"), ponder: None }), - [_, "ponder"] => Err("Empty ponder in bestmove command"), - [chessmove, "ponder", chess_ponder] => Ok(EngineCommand::BestMove { - best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"), - ponder: Some(ChessMove::from_str(chess_ponder).expect("chessmove ponder is invalid")) }), - _ => Err("Invalid chessmove subcommand") + Some("bestmove") => match message_iter.collect::>().as_slice() { + [] => Err("Empty bestmove command"), + [chessmove] => Ok(EngineCommand::BestMove { + best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"), + ponder: None, + }), + [_, "ponder"] => Err("Empty ponder in bestmove command"), + [chessmove, "ponder", chess_ponder] => Ok(EngineCommand::BestMove { + best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"), + ponder: Some( + ChessMove::from_str(chess_ponder).expect("chessmove ponder is invalid"), + ), + }), + _ => Err("Invalid chessmove subcommand"), }, Some("copyprotection") => unimplemented!(), Some("registration") => unimplemented!(), Some("info") => Ok(EngineCommand::Info { infos: Vec::new() }), //todo!("parse info lines") - Some("option") => Ok(EngineCommand::Opt { options: Vec::new() }), // todo!("Parse options lines") + Some("option") => Ok(EngineCommand::Opt { + options: Vec::new(), + }), // todo!("Parse options lines") Some(_) => Err("Unknown command provided"), } } impl fmt::Display for GuiCommand { - - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { GuiCommand::Uci => writeln!(f, "uci"), GuiCommand::UciNewGame => writeln!(f, "ucinewgame"), GuiCommand::Position { position, moves } => { - writeln!(f, "position {} moves {}", - match position { None => "startpos".to_string(), - Some(board) if *board == Board::default() => "startpos".to_string(), - Some(board) =>"fen ".to_string() + &board.to_string()}, - join(moves.iter().map(|x| if let Action::MakeMove(chessmove) = x - {" ".to_string() + &chessmove.to_string()} else {"".to_string()}), "") + writeln!( + f, + "position {} moves {}", + match position { + None => "startpos".to_string(), + Some(board) if *board == Board::default() => "startpos".to_string(), + Some(board) => "fen ".to_string() + &board.to_string(), + }, + join( + moves + .iter() + .map(|x| if let Action::MakeMove(chessmove) = x { + " ".to_string() + &chessmove.to_string() + } else { + "".to_string() + }), + "" + ) ) } GuiCommand::SetOption { option } => { - writeln!(f, "setoption name {} value {}", option.name(), option) + writeln!(f, "setoption name {} value {}", option.name(), option) } - GuiCommand::Go { wtime, wincr, btime, bincr } => { - let wtime = if let Some(wtime) = wtime {format!(" wtime {}", wtime.as_millis())} else {"".to_string()}; - let btime = if let Some(btime) = btime {format!(" btime {}", btime.as_millis())} else {"".to_string()}; - let wincr = if let Some(wincr) = wincr {format!(" wincr {}", wincr.as_millis())} else {"".to_string()}; - let bincr = if let Some(bincr) = bincr {format!(" bincr {}", bincr.as_millis())} else {"".to_string()}; + GuiCommand::Go { + wtime, + wincr, + btime, + bincr, + } => { + let wtime = if let Some(wtime) = wtime { + format!(" wtime {}", wtime.as_millis()) + } else { + "".to_string() + }; + let btime = if let Some(btime) = btime { + format!(" btime {}", btime.as_millis()) + } else { + "".to_string() + }; + let wincr = if let Some(wincr) = wincr { + format!(" wincr {}", wincr.as_millis()) + } else { + "".to_string() + }; + let bincr = if let Some(bincr) = bincr { + format!(" bincr {}", bincr.as_millis()) + } else { + "".to_string() + }; writeln!(f, "go{}{}{}{}", wtime, btime, wincr, bincr) } @@ -146,9 +226,9 @@ impl fmt::Display for GuiCommand { GuiCommand::PonderHit => writeln!(f, "ponderhit"), GuiCommand::Quit => writeln!(f, "quit"), - GuiCommand::Register|GuiCommand::Debug { .. }|GuiCommand::IsReady - => unimplemented!("{:?}", self), + GuiCommand::Register | GuiCommand::Debug { .. } | GuiCommand::IsReady => { + unimplemented!("{:?}", self) + } } - } } diff --git a/chess_uci_demo/src/main.rs b/chess_uci_demo/src/main.rs index d87e8ce..2376677 100644 --- a/chess_uci_demo/src/main.rs +++ b/chess_uci_demo/src/main.rs @@ -2,13 +2,13 @@ extern crate chess; extern crate chess_uci; use chess::ChessMove; -use chess_uci::*; use chess_uci::uci_command::EngineCommand; +use chess_uci::*; use std::process::{Command, Stdio}; use std::str::FromStr; -pub fn main(){ +pub fn main() { env_logger::init(); println!("Launching hardcoded stockfish program (should be in PATH)"); @@ -17,37 +17,58 @@ pub fn main(){ let process = match Command::new("stockfish") .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .spawn() { - Err(why) => panic!("couldn't spawn stockfish: {}", why), - Ok(process) => process, - }; + .spawn() + { + Err(why) => panic!("couldn't spawn stockfish: {}", why), + Ok(process) => process, + }; - let (sf_in,sf_out) = (process.stdin.expect("Program stdin"), process.stdout.expect("Program stdout")); + let (sf_in, sf_out) = ( + process.stdin.expect("Program stdin"), + process.stdout.expect("Program stdout"), + ); let mut uci = UciEngine::new(Some(sf_out), Some(sf_in)); - uci.game_option(GameOption::WhitePlayer { value: Player::Human { elo: None } }); - uci.game_option(GameOption::BlackPlayer { value: Player::Machine { elo: Some(1500) } }); + uci.game_option(GameOption::WhitePlayer { + value: Player::Human { elo: None }, + }); + uci.game_option(GameOption::BlackPlayer { + value: Player::Machine { elo: Some(1500) }, + }); uci.init(); - println!("Engine: {} \nby: {}", - if let Some(name) = uci.name() {name} else {"Not defined".to_string()}, - if let Some(author) = uci.author() {author} else {"Not defined".to_string()}); - uci.push(uci_command::GuiCommand::SetOption { option: uci_command::Opt::SlowMover { value: 11 }}); + println!( + "Engine: {} \nby: {}", + if let Some(name) = uci.name() { + name + } else { + "Not defined".to_string() + }, + if let Some(author) = uci.author() { + author + } else { + "Not defined".to_string() + } + ); + uci.push(uci_command::GuiCommand::SetOption { + option: uci_command::Opt::SlowMover { value: 11 }, + }); uci.new_game(); uci.push_raw("d\n"); - uci.human_play(ChessMove::from_str("e2e4").expect("error converting e2e4")).expect("can not make human move"); + uci.human_play(ChessMove::from_str("e2e4").expect("error converting e2e4")) + .expect("can not make human move"); uci.push_raw("d\n"); uci.go().expect("can not make engine move"); - + loop { match uci.pull() { Some(EngineCommand::BestMove { best_move, .. }) => { uci.make_move(best_move); - break - }, + break; + } Some(EngineCommand::Info { .. }) => continue, _ => continue, }