use log::{debug, error, info, log_enabled, Level};
use envor::envor::env_true;
use std::collections::HashMap;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::sync::*;
use crate::analysis::*;
#[derive(Debug)]
pub enum PosSpec {
Startpos,
Fen,
No,
}
use PosSpec::*;
#[derive(Debug)]
pub struct GoJob {
uci_options: HashMap<String, String>,
pos_spec: PosSpec,
pos_fen: Option<String>,
pos_moves: Option<String>,
go_options: HashMap<String, String>,
custom_command: Option<String>,
ponder: bool,
ponderhit: bool,
pondermiss: bool,
rtx: Option<oneshot::Sender<GoResult>>,
}
#[derive(Debug)]
pub struct Timecontrol {
pub wtime: usize,
pub winc: usize,
pub btime: usize,
pub binc: usize,
}
impl Timecontrol {
pub fn default() -> Self {
Self {
wtime: 60000,
winc: 0,
btime: 60000,
binc: 0,
}
}
}
impl GoJob {
pub fn new() -> Self {
Self {
pos_spec: No,
pos_fen: None,
pos_moves: None,
uci_options: HashMap::new(),
go_options: HashMap::new(),
rtx: None,
custom_command: None,
ponder: false,
ponderhit: false,
pondermiss: false,
}
}
pub fn custom<T>(mut self, command: T) -> Self
where
T: core::fmt::Display,
{
self.custom_command = Some(format!("{}", command));
self
}
pub fn to_commands(&self) -> Vec<String> {
let mut commands: Vec<String> = vec![];
if self.ponderhit {
commands.push(format!("{}", "ponderhit"));
return commands;
}
if self.pondermiss {
commands.push(format!("{}", "stop"));
return commands;
}
if let Some(command) = &self.custom_command {
commands.push(format!("{}", command));
return commands;
}
for (key, value) in &self.uci_options {
commands.push(format!("setoption name {} value {}", key, value));
}
let mut pos_command_moves = "".to_string();
if let Some(pos_moves) = &self.pos_moves {
pos_command_moves = format!(" moves {}", pos_moves)
}
let pos_command: Option<String> = match self.pos_spec {
Startpos => Some(format!("position startpos{}", pos_command_moves)),
Fen => {
let fen = match &self.pos_fen {
Some(fen) => fen,
_ => "",
};
Some(format!("position fen {}{}", fen, pos_command_moves))
}
_ => None,
};
if let Some(pos_command) = pos_command {
commands.push(pos_command);
}
let mut go_command = "go".to_string();
for (key, value) in &self.go_options {
go_command = go_command + &format!(" {} {}", key, value);
}
if self.ponder {
go_command = go_command + &format!(" {}", "ponder");
}
commands.push(go_command);
commands
}
pub fn set_ponder(mut self, value: bool) -> Self {
self.ponder = value;
self
}
pub fn ponder(mut self) -> Self {
self.ponder = true;
self
}
pub fn ponderhit(mut self) -> Self {
self.ponderhit = true;
self
}
pub fn pondermiss(mut self) -> Self {
self.pondermiss = true;
self
}
pub fn pos_fen<T>(mut self, fen: T) -> Self
where
T: core::fmt::Display,
{
self.pos_spec = Fen;
self.pos_fen = Some(format!("{}", fen).to_string());
self
}
pub fn pos_startpos(mut self) -> Self {
self.pos_spec = Startpos;
self
}
pub fn pos_moves<T>(mut self, moves: T) -> Self
where
T: core::fmt::Display,
{
self.pos_moves = Some(format!("{}", moves));
self
}
pub fn uci_opt<K, V>(mut self, key: K, value: V) -> Self
where
K: core::fmt::Display,
V: core::fmt::Display,
{
self.uci_options
.insert(format!("{}", key), format!("{}", value));
self
}
pub fn go_opt<K, V>(mut self, key: K, value: V) -> Self
where
K: core::fmt::Display,
V: core::fmt::Display,
{
self.go_options
.insert(format!("{}", key), format!("{}", value));
self
}
pub fn tc(mut self, tc: Timecontrol) -> Self {
self.go_options
.insert("wtime".to_string(), format!("{}", tc.wtime));
self.go_options
.insert("winc".to_string(), format!("{}", tc.winc));
self.go_options
.insert("btime".to_string(), format!("{}", tc.btime));
self.go_options
.insert("binc".to_string(), format!("{}", tc.binc));
self
}
}
#[derive(Debug)]
pub struct GoResult {
pub bestmove: Option<String>,
pub ponder: Option<String>,
pub ai: AnalysisInfo,
}
pub struct UciEngine {
gtx: mpsc::UnboundedSender<GoJob>,
pub ai: std::sync::Arc<std::sync::Mutex<AnalysisInfo>>,
pub atx: std::sync::Arc<broadcast::Sender<AnalysisInfo>>,
}
impl UciEngine {
pub fn new<T>(path: T) -> std::sync::Arc<UciEngine>
where
T: core::fmt::Display,
{
let path = path.to_string();
let mut child = Command::new(path.as_str())
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.spawn()
.expect("failed to spawn engine");
let stdout = child
.stdout
.take()
.expect("child did not have a handle to stdout");
let stdin = child
.stdin
.take()
.expect("child did not have a handle to stdin");
let reader = BufReader::new(stdout).lines();
let (tx, rx) = mpsc::unbounded_channel::<String>();
tokio::spawn(async move {
let status = child
.wait()
.await
.expect("engine process encountered an error");
if log_enabled!(Level::Info) {
info!("engine process exit status : {}", status);
}
});
let ai = std::sync::Arc::new(std::sync::Mutex::new(AnalysisInfo::new()));
let ai_clone = ai.clone();
let (atx, _) = broadcast::channel::<AnalysisInfo>(20);
let atx = std::sync::Arc::new(atx);
let atx_clone = atx.clone();
tokio::spawn(async move {
let mut reader = reader;
let ai = ai_clone;
let atx = atx_clone;
let test_parse_info = env_true("TEST_PARSE_INFO");
let mut num_lines: usize = 0;
let mut ok_lines: usize = 0;
let mut failed_lines: usize = 0;
loop {
match reader.next_line().await {
Ok(line_opt) => {
if let Some(line) = line_opt {
num_lines += 1;
if log_enabled!(Level::Debug) {
debug!("uci engine out ( {} ) : {}", num_lines, line);
}
let mut is_bestmove = line.len() >= 8;
if is_bestmove {
is_bestmove = &line[0..8] == "bestmove";
}
{
let mut ai = ai.lock().unwrap();
let parse_result = ai.parse(line.to_owned());
if is_bestmove {
ai.done = true;
}
debug!("parse result {:?} , ai {:?}", parse_result, ai);
if parse_result.is_ok() {
ok_lines += 1;
let send_result = atx.send(*ai);
debug!("send ai result {:?}", send_result);
} else {
failed_lines += 1;
println!(
"parsing failed on {} with error {:?}",
line, parse_result
);
}
if test_parse_info {
println!(
"read {} , parsed ok {} , failed {}",
num_lines, ok_lines, failed_lines
);
}
}
if is_bestmove {
let send_result = tx.send(line);
if log_enabled!(Level::Debug) {
debug!("send bestmove result {:?}", send_result);
}
}
} else {
if log_enabled!(Level::Debug) {
debug!("engine returned empty line option");
}
break;
}
}
Err(err) => {
if log_enabled!(Level::Error) {
error!("engine read error {:?}", err);
}
break;
}
}
}
if log_enabled!(Level::Debug) {
debug!("engine read terminated");
}
});
let (gtx, grx) = mpsc::unbounded_channel::<GoJob>();
let ai_clone = ai.clone();
tokio::spawn(async move {
let mut stdin = stdin;
let mut grx = grx;
let mut rx = rx;
let ai = ai_clone;
while let Some(go_job) = grx.recv().await {
if log_enabled!(Level::Debug) {
debug!("received go job {:?}", go_job);
}
for command in go_job.to_commands() {
let command = format!("{}\n", command);
if log_enabled!(Level::Debug) {
debug!("issuing engine command : {}", command);
}
let write_result = stdin.write_all(command.as_bytes()).await;
if log_enabled!(Level::Debug) {
debug!("write result {:?}", write_result);
}
}
if go_job.custom_command.is_none() && (!go_job.ponder) {
{
let mut ai = ai.lock().unwrap();
*ai = AnalysisInfo::new();
}
let recv_result = rx.recv().await.unwrap();
if log_enabled!(Level::Debug) {
debug!("recv result {:?}", recv_result);
}
let parts: Vec<&str> = recv_result.split(" ").collect();
let send_ai: AnalysisInfo;
{
let ai = ai.lock().unwrap();
send_ai = *ai;
}
let mut go_result = GoResult {
bestmove: None,
ponder: None,
ai: send_ai,
};
if parts.len() > 1 {
go_result.bestmove = Some(parts[1].to_string());
}
if parts.len() > 3 {
go_result.ponder = Some(parts[3].to_string());
}
let send_result = go_job.rtx.unwrap().send(go_result);
if log_enabled!(Level::Debug) {
debug!("result of send go result {:?}", send_result);
}
}
}
});
if log_enabled!(Level::Info) {
info!("spawned uci engine : {}", path);
}
std::sync::Arc::new(UciEngine {
gtx: gtx,
ai: ai,
atx: atx,
})
}
pub fn get_ai(&self) -> AnalysisInfo {
let ai = self.ai.lock().unwrap();
*ai
}
pub fn go(&self, go_job: GoJob) -> oneshot::Receiver<GoResult> {
let mut go_job = go_job;
let (rtx, rrx): (oneshot::Sender<GoResult>, oneshot::Receiver<GoResult>) =
oneshot::channel();
go_job.rtx = Some(rtx);
let send_result = self.gtx.send(go_job);
if log_enabled!(Level::Debug) {
debug!("send go job result {:?}", send_result);
}
rrx
}
pub fn quit(&self) {
self.go(GoJob::new().custom("quit"));
}
}