This commit is contained in:
Baptiste Fouques 2023-01-30 15:56:01 +01:00
parent b48f52c41c
commit 69cbe46f1d
3 changed files with 317 additions and 150 deletions

View File

@ -3,15 +3,16 @@
/// Manage UCI messages and link into chess component /// Manage UCI messages and link into chess component
pub mod uci_command; pub mod uci_command;
use crate::uci_command::*;
use log::{info, warn}; use log::{info, warn};
use std::io::*; use std::io::*;
use std::time::Duration;
use std::result::Result; 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<Duration>, Option<Duration>) = (Some(Duration::from_secs(120 * 60)), None); const DEFAULT_TIME: (Option<Duration>, Option<Duration>) =
(Some(Duration::from_secs(120 * 60)), None);
/// Structure used to manage a chess game with a uci engine. /// Structure used to manage a chess game with a uci engine.
/// ///
@ -54,7 +55,10 @@ pub enum UciOption {
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Player{Human{elo: Option<u32>}, Machine{elo: Option<u32>}} pub enum Player {
Human { elo: Option<u32> },
Machine { elo: Option<u32> },
}
pub enum GameOption { pub enum GameOption {
WhiteTotalTime { value: Option<Duration> }, WhiteTotalTime { value: Option<Duration> },
BlackTotalTime { value: Option<Duration> }, BlackTotalTime { value: Option<Duration> },
@ -65,7 +69,6 @@ pub enum GameOption {
} }
impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> { impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
/// Create new game manager /// Create new game manager
/// ///
/// Requires line by line input and output streams to communicate with uci engine /// Requires line by line input and output streams to communicate with uci engine
@ -85,18 +88,42 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
pub fn game_option(&mut self, option: GameOption) -> GameOption { pub fn game_option(&mut self, option: GameOption) -> GameOption {
let old_value: GameOption; let old_value: GameOption;
match option { match option {
GameOption::WhiteTotalTime{value} => {old_value = GameOption::WhiteTotalTime { value: self.time[Color::White.to_index()].0 }; GameOption::WhiteTotalTime { value } => {
self.time[Color::White.to_index()].0 = value}, old_value = GameOption::WhiteTotalTime {
GameOption::BlackTotalTime{value} => {old_value = GameOption::BlackTotalTime { value: self.time[Color::Black.to_index()].0 }; value: self.time[Color::White.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()].0 = value
self.time[Color::White.to_index()].1 = value}, }
GameOption::BlackIncrement{value} => {old_value = GameOption::BlackIncrement { value: self.time[Color::Black.to_index()].1 }; GameOption::BlackTotalTime { value } => {
self.time[Color::Black.to_index()].1 = value}, old_value = GameOption::BlackTotalTime {
GameOption::WhitePlayer{value} => {old_value = GameOption::WhitePlayer { value: self.player[Color::White.to_index()] }; value: self.time[Color::Black.to_index()].0,
self.player[Color::White.to_index()] = value}, };
GameOption::BlackPlayer{value} => {old_value = GameOption::WhitePlayer { value: self.player[Color::Black.to_index()] }; self.time[Color::Black.to_index()].0 = value
self.player[Color::Black.to_index()] = 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 old_value
@ -141,15 +168,16 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
/// ``` /// ```
pub fn make_move(&mut self, chess_move: ChessMove) -> bool { pub fn make_move(&mut self, chess_move: ChessMove) -> bool {
if self.game.make_move(chess_move) { if self.game.make_move(chess_move) {
self.push(GuiCommand::Position { position: Some(self.initial), moves: self.game.actions().to_vec() }); self.push(GuiCommand::Position {
position: Some(self.initial),
moves: self.game.actions().to_vec(),
});
true true
} else { } else {
false false
} }
} }
fn terminate(&mut self, reason: &str) { fn terminate(&mut self, reason: &str) {
self.uciok = false; self.uciok = false;
info!("UCI termination: {}", reason); info!("UCI termination: {}", reason);
@ -166,12 +194,16 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
} }
fn update(&mut self, id: &Id) { fn update(&mut self, id: &Id) {
if self.is_uciok() {warn!("Engine info should not be updated now (uciok)");} if self.is_uciok() {
warn!("Engine info should not be updated now (uciok)");
}
self.id.update(id); self.id.update(id);
} }
fn update_from_str(&mut self, name: Option<&str>, author: Option<&str>) { 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)");} if self.is_uciok() {
warn!("Engine info should not be updated now (uciok)");
}
self.id.update_from_str(name, author); self.id.update_from_str(name, author);
} }
@ -184,13 +216,16 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
match uci_command.clone() { match uci_command.clone() {
Ok(EngineCommand::Id { id }) => self.update(&id), Ok(EngineCommand::Id { id }) => self.update(&id),
Ok(EngineCommand::UciOk) => self.uciok(), Ok(EngineCommand::UciOk) => self.uciok(),
Ok(EngineCommand::Opt { options: _ }) => {}, Ok(EngineCommand::Opt { options: _ }) => {}
Ok(EngineCommand::Info { infos: _ }) | Ok(EngineCommand::Info { infos: _ })
Ok(EngineCommand::BestMove { best_move: _, ponder: _ }) | | Ok(EngineCommand::BestMove {
Ok(EngineCommand::ReadyOk) best_move: _,
=> {}, ponder: _,
Ok(EngineCommand::Registration)| })
Ok(EngineCommand::CopyProtection)=> {unimplemented!("command not implemented")}, | Ok(EngineCommand::ReadyOk) => {}
Ok(EngineCommand::Registration) | Ok(EngineCommand::CopyProtection) => {
unimplemented!("command not implemented")
}
Err(_) => warn!("Not a command"), Err(_) => warn!("Not a command"),
}; };
uci_command uci_command
@ -198,16 +233,21 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
/// Retrieve a line from the uci engine input stream and parse it /// Retrieve a line from the uci engine input stream and parse it
pub fn pull(&mut self) -> Option<EngineCommand> { pub fn pull(&mut self) -> Option<EngineCommand> {
if let Some(source) = &mut self.source { if let Some(source) = &mut self.source {
let mut command = String::new(); let mut command = String::new();
match source.read_line(&mut command) { 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(_) => { Ok(_) => {
info!("← {}", command); info!("← {}", command);
self.exec(&command).ok() 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 { } else {
info!("❌ No connected uci engine"); info!("❌ No connected uci engine");
@ -220,9 +260,18 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
if let Some(source) = &mut self.source { if let Some(source) = &mut self.source {
let mut data = String::new(); let mut data = String::new();
match source.read_line(&mut data) { match source.read_line(&mut data) {
Ok(0) => {self.terminate("Chess engine closed connection."); None}, Ok(0) => {
Ok(_) => {info!("↜ {}", data); Some(data.clone())}, self.terminate("Chess engine closed connection.");
Err(reason) => {warn!("Unable to read from engine: {reason}"); None}, None
}
Ok(_) => {
info!("↜ {}", data);
Some(data.clone())
}
Err(reason) => {
warn!("Unable to read from engine: {reason}");
None
}
} }
} else { } else {
info!("❌ No connected uci engine"); info!("❌ No connected uci engine");
@ -276,24 +325,32 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
} else { } else {
Err("Invalid move for human player") 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> { pub fn go(&mut self) -> Result<(), &'static str> {
match self.player[self.side_to_move().to_index()] { match self.player[self.side_to_move().to_index()] {
Player::Human { .. } => Err("Not a machine to play for current color."), Player::Human { .. } => Err("Not a machine to play for current color."),
Player::Machine {elo} => Player::Machine { elo } => {
{
if self.is_uciok() { if self.is_uciok() {
if let Some(elo) = elo { if let Some(elo) = elo {
self.push(GuiCommand::SetOption {option: Opt::UCILimitStrength{value: true}}); self.push(GuiCommand::SetOption {
self.push(GuiCommand::SetOption {option: Opt::UCIElo{value: elo}}); option: Opt::UCILimitStrength { value: true },
});
self.push(GuiCommand::SetOption {
option: Opt::UCIElo { value: elo },
});
} }
let (wtime, wincr) = self.time[Color::White.to_index()]; let (wtime, wincr) = self.time[Color::White.to_index()];
let (btime, bincr) = self.time[Color::Black.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(()) Ok(())
} else { } else {
@ -302,9 +359,18 @@ impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
} }
} }
} }
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}; use chess::{Color, Piece, Square};
impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> { impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
pub fn piece_on(&self, square: Square) -> Option<Piece> { pub fn piece_on(&self, square: Square) -> Option<Piece> {
self.game.current_position().piece_on(square) self.game.current_position().piece_on(square)

View File

@ -1,20 +1,31 @@
use itertools::join; use itertools::join;
use std::{time::Duration, str::FromStr};
use std::fmt; use std::fmt;
use std::{str::FromStr, time::Duration};
use chess::{ChessMove, Board, Action}; use chess::{Action, Board, ChessMove};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Id {name: Option<String>, author: Option<String>} pub struct Id {
name: Option<String>,
author: Option<String>,
}
impl Id { impl Id {
pub fn update(&mut self, id: &Id) { pub fn update(&mut self, id: &Id) {
if let Some(name) = &id.name {self.name = Some(name.clone());} if let Some(name) = &id.name {
if let Some(author) = &id.author {self.author = Some(author.clone());} 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>) { 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(name) = name {
if let Some(author) = author {self.author = Some(author.to_string());} self.name = Some(name.to_string());
}
if let Some(author) = author {
self.author = Some(author.to_string());
}
} }
pub fn name(&self) -> Option<String> { pub fn name(&self) -> Option<String> {
@ -54,28 +65,47 @@ impl fmt::Display for Opt {
#[derive(Clone)] #[derive(Clone)]
pub enum EngineCommand { pub enum EngineCommand {
Id{id: Id}, Id {
id: Id,
},
UciOk, UciOk,
ReadyOk, ReadyOk,
BestMove{best_move: ChessMove, ponder: Option<ChessMove>}, BestMove {
best_move: ChessMove,
ponder: Option<ChessMove>,
},
CopyProtection, // unimplemented CopyProtection, // unimplemented
Registration, // unimplemented Registration, // unimplemented
Info{infos: Vec<Info>}, Info {
Opt{options: Vec<Opt>}, infos: Vec<Info>,
},
Opt {
options: Vec<Opt>,
},
} }
#[derive(Debug)] #[derive(Debug)]
pub enum GuiCommand { pub enum GuiCommand {
Uci, Uci,
Debug{mode: bool}, Debug {
mode: bool,
},
IsReady, IsReady,
SetOption{option: Opt}, SetOption {
option: Opt,
},
Register, // unimplemented Register, // unimplemented
UciNewGame, UciNewGame,
Position{position: Option<Board>, moves: Vec<Action>}, Position {
Go{wtime: Option<Duration>, wincr: Option<Duration>, position: Option<Board>,
btime: Option<Duration>, bincr: Option<Duration>}, moves: Vec<Action>,
},
Go {
wtime: Option<Duration>,
wincr: Option<Duration>,
btime: Option<Duration>,
bincr: Option<Duration>,
},
Stop, Stop,
PonderHit, PonderHit,
Quit, Quit,
@ -87,58 +117,108 @@ pub fn parse(message: &mut str) -> Result<EngineCommand,&'static str> {
match message_iter.next() { match message_iter.next() {
None => Err("No command provided"), None => Err("No command provided"),
Some("id") => Some("id") => match message_iter.collect::<Vec<&str>>().as_slice() {
match message_iter.collect::<Vec<&str>>().as_slice() {
[] => Err("Empty id command"), [] => Err("Empty id command"),
["name"] => Err("Empty id name command"), ["name"] => Err("Empty id name command"),
["author"] => Err("Empty id author 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}}), ["name", tail @ ..] => Ok(EngineCommand::Id {
["author", tail @ ..] => Ok(EngineCommand::Id{id: { let mut id = Id::default(); id.update_from_str(None, Some(&join(tail, " "))); id}}), id: {
_ => Err("Invalid id subcommand") 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("uciok") => Ok(EngineCommand::UciOk),
Some("readyok") => unimplemented!(), Some("readyok") => unimplemented!(),
Some("bestmove") => Some("bestmove") => match message_iter.collect::<Vec<&str>>().as_slice() {
match message_iter.collect::<Vec<&str>>().as_slice() {
[] => Err("Empty bestmove command"), [] => Err("Empty bestmove command"),
[chessmove] => Ok(EngineCommand::BestMove { best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"), ponder: None }), [chessmove] => Ok(EngineCommand::BestMove {
best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"),
ponder: None,
}),
[_, "ponder"] => Err("Empty ponder in bestmove command"), [_, "ponder"] => Err("Empty ponder in bestmove command"),
[chessmove, "ponder", chess_ponder] => Ok(EngineCommand::BestMove { [chessmove, "ponder", chess_ponder] => Ok(EngineCommand::BestMove {
best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"), best_move: ChessMove::from_str(chessmove).expect("chessmove is invalid"),
ponder: Some(ChessMove::from_str(chess_ponder).expect("chessmove ponder is invalid")) }), ponder: Some(
_ => Err("Invalid chessmove subcommand") ChessMove::from_str(chess_ponder).expect("chessmove ponder is invalid"),
),
}),
_ => Err("Invalid chessmove subcommand"),
}, },
Some("copyprotection") => unimplemented!(), Some("copyprotection") => unimplemented!(),
Some("registration") => unimplemented!(), Some("registration") => unimplemented!(),
Some("info") => Ok(EngineCommand::Info { infos: Vec::new() }), //todo!("parse info lines") 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"), Some(_) => Err("Unknown command provided"),
} }
} }
impl fmt::Display for GuiCommand { 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 { match self {
GuiCommand::Uci => writeln!(f, "uci"), GuiCommand::Uci => writeln!(f, "uci"),
GuiCommand::UciNewGame => writeln!(f, "ucinewgame"), GuiCommand::UciNewGame => writeln!(f, "ucinewgame"),
GuiCommand::Position { position, moves } => { GuiCommand::Position { position, moves } => {
writeln!(f, "position {} moves {}", writeln!(
match position { None => "startpos".to_string(), f,
"position {} moves {}",
match position {
None => "startpos".to_string(),
Some(board) if *board == Board::default() => "startpos".to_string(), Some(board) if *board == Board::default() => "startpos".to_string(),
Some(board) =>"fen ".to_string() + &board.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()}), "") join(
moves
.iter()
.map(|x| if let Action::MakeMove(chessmove) = x {
" ".to_string() + &chessmove.to_string()
} else {
"".to_string()
}),
""
)
) )
} }
GuiCommand::SetOption { option } => { 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 } => { GuiCommand::Go {
let wtime = if let Some(wtime) = wtime {format!(" wtime {}", wtime.as_millis())} else {"".to_string()}; wtime,
let btime = if let Some(btime) = btime {format!(" btime {}", btime.as_millis())} else {"".to_string()}; wincr,
let wincr = if let Some(wincr) = wincr {format!(" wincr {}", wincr.as_millis())} else {"".to_string()}; btime,
let bincr = if let Some(bincr) = bincr {format!(" bincr {}", bincr.as_millis())} else {"".to_string()}; 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) writeln!(f, "go{}{}{}{}", wtime, btime, wincr, bincr)
} }
@ -146,9 +226,9 @@ impl fmt::Display for GuiCommand {
GuiCommand::PonderHit => writeln!(f, "ponderhit"), GuiCommand::PonderHit => writeln!(f, "ponderhit"),
GuiCommand::Quit => writeln!(f, "quit"), GuiCommand::Quit => writeln!(f, "quit"),
GuiCommand::Register|GuiCommand::Debug { .. }|GuiCommand::IsReady GuiCommand::Register | GuiCommand::Debug { .. } | GuiCommand::IsReady => {
=> unimplemented!("{:?}", self), unimplemented!("{:?}", self)
} }
}
} }
} }

View File

@ -2,8 +2,8 @@ extern crate chess;
extern crate chess_uci; extern crate chess_uci;
use chess::ChessMove; use chess::ChessMove;
use chess_uci::*;
use chess_uci::uci_command::EngineCommand; use chess_uci::uci_command::EngineCommand;
use chess_uci::*;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::str::FromStr; use std::str::FromStr;
@ -17,26 +17,47 @@ pub fn main(){
let process = match Command::new("stockfish") let process = match Command::new("stockfish")
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn() { .spawn()
{
Err(why) => panic!("couldn't spawn stockfish: {}", why), Err(why) => panic!("couldn't spawn stockfish: {}", why),
Ok(process) => process, 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)); 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::WhitePlayer {
uci.game_option(GameOption::BlackPlayer { value: Player::Machine { elo: Some(1500) } }); value: Player::Human { elo: None },
});
uci.game_option(GameOption::BlackPlayer {
value: Player::Machine { elo: Some(1500) },
});
uci.init(); uci.init();
println!("Engine: {} \nby: {}", println!(
if let Some(name) = uci.name() {name} else {"Not defined".to_string()}, "Engine: {} \nby: {}",
if let Some(author) = uci.author() {author} else {"Not defined".to_string()}); if let Some(name) = uci.name() {
uci.push(uci_command::GuiCommand::SetOption { option: uci_command::Opt::SlowMover { value: 11 }}); 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.new_game();
uci.push_raw("d\n"); 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.push_raw("d\n");
@ -46,8 +67,8 @@ pub fn main(){
match uci.pull() { match uci.pull() {
Some(EngineCommand::BestMove { best_move, .. }) => { Some(EngineCommand::BestMove { best_move, .. }) => {
uci.make_move(best_move); uci.make_move(best_move);
break break;
}, }
Some(EngineCommand::Info { .. }) => continue, Some(EngineCommand::Info { .. }) => continue,
_ => continue, _ => continue,
} }