use std::default::Default;
use std::time::Duration;
use std::thread::{spawn, sleep};
use std::io;
use std::io::{Write, BufWriter, BufRead, ErrorKind};
use std::sync::mpsc::{channel, TryRecvError};
use regex::Regex;
enum UciCommand {
SetOption { name: String, value: String },
IsReady,
UciNewGame,
Position { fen: String, moves: String },
Go(GoParams),
Stop,
PonderHit,
Quit,
}
#[derive(Default)]
pub struct GoParams {
pub searchmoves: Vec<String>,
pub ponder: bool,
pub wtime: Option<u64>,
pub btime: Option<u64>,
pub winc: Option<u64>,
pub binc: Option<u64>,
pub movestogo: Option<u64>,
pub depth: Option<u64>,
pub nodes: Option<u64>,
pub mate: Option<u64>,
pub movetime: Option<u64>,
pub infinite: bool,
}
pub struct InfoItem {
pub info_type: String,
pub data: String,
}
pub enum EngineReply {
Info(Vec<InfoItem>),
BestMove {
best_move: String,
ponder_move: Option<String>,
},
}
#[derive(Clone, Debug)]
pub enum OptionDescription {
Check { default: bool },
Spin { min: i32, max: i32, default: i32 },
Combo { list: Vec<String>, default: String },
String { default: String },
Button,
}
pub trait SetOption {
fn options() -> Vec<(&'static str, OptionDescription)>
where Self: Sized
{
vec![]
}
#[allow(unused_variables)]
fn set_option(name: &str, value: &str) where Self: Sized {}
}
pub trait UciEngine {
fn name() -> &'static str;
fn author() -> &'static str;
fn options() -> Vec<(&'static str, OptionDescription)>;
fn new(tt_size_mb: Option<usize>) -> Self;
fn set_option(&mut self, name: &str, value: &str);
fn new_game(&mut self);
fn position(&mut self, fen: &str, moves: &mut Iterator<Item = &str>);
fn go(&mut self, params: &GoParams);
fn stop(&mut self);
fn ponder_hit(&mut self);
fn wait_for_reply(&mut self, duration: Duration) -> Option<EngineReply>;
fn exit(&mut self);
}
pub fn run_engine<E: UciEngine>() -> io::Result<()> {
let mut server = try!(Server::<E>::wait_for_hanshake());
server.serve()
}
struct Server<E: UciEngine> {
engine: Option<E>,
}
impl<E: UciEngine> Server<E> {
pub fn wait_for_hanshake() -> io::Result<Self> {
lazy_static! {
static ref RE: Regex = Regex::new(r"\buci(?:\s|$)").unwrap();
}
let stdin = io::stdin();
let mut reader = stdin.lock();
let mut writer = BufWriter::new(io::stdout());
let mut line = String::new();
if try!(reader.read_line(&mut line)) == 0 {
return Err(io::Error::new(ErrorKind::UnexpectedEof, "EOF"));
}
if !RE.is_match(line.as_str()) {
return Err(io::Error::new(ErrorKind::Other, "unrecognized protocol"));
}
try!(write!(writer, "id name {}\n", E::name()));
try!(write!(writer, "id author {}\n", E::author()));
for (name, description) in E::options() {
try!(write!(writer,
"option name {} type {}\n",
name,
match description {
OptionDescription::Check { default } => {
format!("check default {}", default)
}
OptionDescription::Spin { default, min, max } => {
format!("spin default {} min {} max {}", default, min, max)
}
OptionDescription::Combo { default, list } => {
format!("combo default {}{}",
default,
list.into_iter()
.fold(String::new(), |mut acc, x| {
acc.push_str(" var ");
acc.push_str(x.as_str());
acc
}))
}
OptionDescription::String { default } => {
format!("string default {}", default)
}
OptionDescription::Button => "button".to_string(),
}));
}
try!(write!(writer, "uciok\n"));
try!(writer.flush());
Ok(Server { engine: None })
}
pub fn serve(&mut self) -> io::Result<()> {
let mut writer = BufWriter::new(io::stdout());
let (tx, rx) = channel();
let read_thread = spawn(move || -> io::Result<()> {
let stdin = io::stdin();
let mut reader = stdin.lock();
let mut line = String::new();
loop {
if let Ok(cmd) = match try!(reader.read_line(&mut line)) {
0 => return Err(io::Error::new(ErrorKind::UnexpectedEof, "EOF")),
_ => parse_uci_command(line.as_str()),
} {
if let UciCommand::Quit = cmd {
return Ok(());
}
tx.send(cmd).unwrap();
}
line.clear();
}
});
'mainloop: loop {
'read_commands: while let Some(cmd) = match rx.try_recv() {
Ok(cmd) => Some(cmd),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => break 'mainloop,
} {
let engine = if let Some(ref mut e) = self.engine {
e
} else {
if let UciCommand::SetOption {
ref name,
ref value,
} = cmd {
if name == "Hash" {
let hash_size_mb = value.parse::<usize>().ok();
self.engine = Some(E::new(hash_size_mb));
continue 'read_commands;
}
}
self.engine = Some(E::new(None));
self.engine.as_mut().unwrap()
};
match cmd {
UciCommand::IsReady => {
try!(write!(writer, "readyok\n"));
try!(writer.flush());
}
UciCommand::SetOption { name, value } => {
engine.set_option(name.as_str(), value.as_str());
}
UciCommand::Position { fen, moves } => {
engine.position(fen.as_str(), &mut moves.split_whitespace());
}
UciCommand::Stop => {
engine.stop();
break 'read_commands;
}
UciCommand::UciNewGame => {
engine.new_game();
}
UciCommand::PonderHit => {
engine.ponder_hit();
}
UciCommand::Go(params) => {
engine.go(¶ms);
}
UciCommand::Quit => unreachable!(),
}
}
if let Some(ref mut engine) = self.engine {
let mut reply_count = 0;
while let Some(reply) = engine.wait_for_reply(Duration::from_millis(25)) {
reply_count += 1;
match reply {
EngineReply::BestMove {
best_move,
ponder_move,
} => {
try!(write!(writer,
"bestmove {}{}",
best_move,
match ponder_move {
None => "\n".to_string(),
Some(m) => format!(" ponder {}\n", m),
}))
}
EngineReply::Info(infos) => {
if infos.len() > 0 {
try!(write!(writer, "info"));
for InfoItem { info_type, data } in infos {
try!(write!(writer, " {} {}", info_type, data));
}
try!(write!(writer, "\n"));
}
}
}
if reply_count >= 40 {
break;
}
}
try!(writer.flush());
} else {
sleep(Duration::from_millis(25));
}
}
if let Some(ref mut engine) = self.engine {
engine.exit();
}
read_thread.join().unwrap()
}
}
struct ParseError;
fn parse_uci_command(s: &str) -> Result<UciCommand, ParseError> {
lazy_static! {
static ref RE: Regex = Regex::new(
format!(r"\b({})\s*(?:\s(.*)|$)",
"setoption|isready|ucinewgame|\
position|go|stop|ponderhit|quit",
).as_str()
).unwrap();
}
if let Some(captures) = RE.captures(s) {
let command_str = captures.get(1).unwrap().as_str();
let params_str = captures.get(2).map_or("", |m| m.as_str());
match command_str {
"stop" => Ok(UciCommand::Stop),
"quit" => Ok(UciCommand::Quit),
"isready" => Ok(UciCommand::IsReady),
"ponderhit" => Ok(UciCommand::PonderHit),
"ucinewgame" => Ok(UciCommand::UciNewGame),
"setoption" => parse_setoption_params(params_str),
"position" => parse_position_params(params_str),
"go" => parse_go_params(params_str),
_ => Err(ParseError),
}
} else {
Err(ParseError)
}
}
fn parse_setoption_params(s: &str) -> Result<UciCommand, ParseError> {
lazy_static! {
static ref RE: Regex = Regex::new(
r"^name\s+(\S.*?)(?:\s+value\s+(.*?))?\s*$").unwrap();
}
if let Some(captures) = RE.captures(s) {
Ok(UciCommand::SetOption {
name: captures.get(1).unwrap().as_str().to_string(),
value: captures.get(2).map_or("", |m| m.as_str()).to_string(),
})
} else {
Err(ParseError)
}
}
fn parse_position_params(s: &str) -> Result<UciCommand, ParseError> {
const STARTPOS: &'static str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1";
lazy_static! {
static ref RE: Regex = Regex::new(
format!(
r"^(?:fen\s+(?P<fen>{})|startpos)(?:\s+moves(?P<moves>{}))?\s*$",
r"[1-8KQRBNPkqrbnp/]+\s+[wb]\s+(?:[KQkq]{1,4}|-)\s+(?:[a-h][1-8]|-)\s+\d+\s+\d+",
r"(?:\s+[a-h][1-8][a-h][1-8][qrbn]?)*", ).as_str()
).unwrap();
}
if let Some(captures) = RE.captures(s) {
Ok(UciCommand::Position {
fen: if let Some(fen) = captures.name("fen") {
fen.as_str().to_string()
} else {
STARTPOS.to_string()
},
moves: captures
.name("moves")
.map_or("", |m| m.as_str())
.to_string(),
})
} else {
Err(ParseError)
}
}
fn parse_go_params(s: &str) -> Result<UciCommand, ParseError> {
lazy_static! {
static ref RE: Regex = Regex::new(
format!(
r"\b(?P<keyword>{})(?:\s+(?P<number>\d+)|(?P<moves>{}))?(?:\s+|$)",
"wtime|btime|winc|binc|movestogo|depth|\
nodes|mate|movetime|ponder|infinite|searchmoves",
r"(?:\s+[a-h][1-8][a-h][1-8][qrbn]?)+", ).as_str()
).unwrap();
}
let mut params = GoParams::default();
for captures in RE.captures_iter(s) {
let keyword = captures.name("keyword").unwrap();
match keyword.as_str() {
"searchmoves" => {
if let Some(moves) = captures.name("moves") {
params.searchmoves = moves
.as_str()
.split_whitespace()
.map(|x| x.to_string())
.collect();
}
}
"infinite" => {
params.infinite = true;
}
"ponder" => {
params.ponder = true;
}
_ => {
if let Some(number) = captures.name("number") {
let field = match keyword.as_str() {
"wtime" => &mut params.wtime,
"btime" => &mut params.btime,
"winc" => &mut params.winc,
"binc" => &mut params.binc,
"movestogo" => &mut params.movestogo,
"depth" => &mut params.depth,
"nodes" => &mut params.nodes,
"mate" => &mut params.mate,
"movetime" => &mut params.movetime,
_ => panic!("invalid keyword"),
};
*field = number.as_str().parse::<u64>().ok();
}
}
}
}
Ok(UciCommand::Go(params))
}
#[cfg(test)]
mod tests {
#[test]
fn parse_go_params() {
use super::{parse_go_params, UciCommand};
let params = [" wtime22000 ",
" wtime 22000 ",
"wtime 22000",
"wtime 99999999999999998888888888999999999999999999",
"wtime 22000",
"searchmoves e2e4 c7c8q ",
"searchmoves e2e4 c7c8q,ponder ",
"searchmoves aabb",
"infinite wtime 22000",
"wtime 22000 infinite btime 11000",
"wtime fdfee / 22000 infinite btime 11000 fdfds",
"wtime 22000 infinite btime 11000 ponder",
"searchmoves"];
for (i, s) in params.iter().enumerate() {
if let Some(UciCommand::Go(p)) = parse_go_params(s).ok() {
match i {
0 => {
assert_eq!(p.wtime, None);
}
1 => {
assert_eq!(p.wtime, Some(22000));
assert_eq!(p.ponder, false);
}
2 => {
assert_eq!(p.wtime, Some(22000));
}
3 => {
assert_eq!(p.wtime, None);
}
4 => {
assert_eq!(p.infinite, false);
}
5 => {
assert_eq!(p.searchmoves, vec!["e2e4".to_string(), "c7c8q".to_string()]);
}
6 => {
assert_eq!(p.searchmoves, vec!["e2e4".to_string()]);
}
7 => {
assert!(p.searchmoves.is_empty());
}
8 => {
assert_eq!(p.wtime, Some(22000));
assert_eq!(p.infinite, true);
}
9 => {
assert_eq!(p.infinite, true);
assert_eq!(p.wtime, Some(22000));
assert_eq!(p.btime, Some(11000));
}
10 => {
assert_eq!(p.infinite, true);
assert_eq!(p.wtime, None);
assert_eq!(p.btime, Some(11000));
}
11 => {
assert_eq!(p.infinite, true);
assert_eq!(p.wtime, Some(22000));
assert_eq!(p.btime, Some(11000));
assert_eq!(p.ponder, true);
assert!(p.searchmoves.is_empty());
}
12 => {
assert!(p.searchmoves.is_empty());
}
_ => (),
}
} else {
panic!("unsuccessful parsing: {}", s);
}
}
}
#[test]
fn parse_setoption_params() {
use super::{parse_setoption_params, UciCommand};
let params = ["name xxx value yyy ",
"name xxx value yyy",
"name xxx value ",
"name xxx "];
for (i, s) in params.iter().enumerate() {
if let Some(UciCommand::SetOption { name, value }) = parse_setoption_params(s).ok() {
match i {
0 => {
assert_eq!(name, "xxx");
assert_eq!(value, "yyy");
}
1 => {
assert_eq!(name, "xxx");
assert_eq!(value, "yyy");
}
2 => {
assert_eq!(name, "xxx");
assert_eq!(value, "");
}
3 => {
assert_eq!(name, "xxx");
assert_eq!(value, "");
}
_ => (),
}
} else {
panic!("unsuccessful parsing: {}", s);
}
}
assert!(parse_setoption_params("name ").is_err());
assert!(parse_setoption_params("namexxx ").is_err());
}
#[test]
fn parse_position_params() {
use super::{parse_position_params, UciCommand};
let params = ["startpos ",
"startpos ",
"startpos moves ",
"startpos moves e2e4 d2d4 ",
"fen 8/8/8/8/8/8/8/k6K w KQk e6 0 1 moves e2e4",
"fen 8/8/8/8/8/8/8/k6K w - - 0 1 moves e2e4",
"fen 8/8/8/8/8/8/8/k6K w - - 0 1 moves e2e4",
"fen 8/8/8/8/8/8/8/k6K w - - 0 1 moves",
"fen 8/8/8/8/8/8/8/k6K w - - 0 1 "];
for (i, s) in params.iter().enumerate() {
if let Some(UciCommand::Position { fen, moves }) = parse_position_params(s).ok() {
match i {
0 => {
assert_eq!(fen,
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1");
assert_eq!(moves.len(), 0);
}
1 => {
assert_eq!(fen,
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1");
assert_eq!(moves.len(), 0);
}
2 => {
assert_eq!(fen,
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1");
assert_eq!(moves.len(), 0);
}
3 => {
assert_eq!(fen,
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk - 0 1");
assert_eq!(moves.split_whitespace().count(), 2);
}
4 => {
assert_eq!(moves.split_whitespace().count(), 1);
}
5 => {
assert_eq!(fen, "8/8/8/8/8/8/8/k6K w - - 0 1".to_string());
assert_eq!(moves.split_whitespace().count(), 1);
}
6 => {
assert_eq!(fen, "8/8/8/8/8/8/8/k6K w - - 0 1".to_string());
assert_eq!(moves.split_whitespace().count(), 1);
}
7 => {
assert_eq!(fen, "8/8/8/8/8/8/8/k6K w - - 0 1".to_string());
assert_eq!(moves.len(), 0);
}
8 => {
assert_eq!(fen, "8/8/8/8/8/8/8/k6K w - - 0 1".to_string());
assert_eq!(moves.len(), 0);
}
_ => (),
}
} else {
panic!("unsuccessful parsing: {}", s);
}
}
}
#[test]
fn parse_uci_command() {
use super::{parse_uci_command, UciCommand};
assert!(match parse_uci_command("isready").ok().unwrap() {
UciCommand::IsReady => true,
_ => false,
});
assert!(match parse_uci_command(" isready ").ok().unwrap() {
UciCommand::IsReady => true,
_ => false,
});
assert!(match parse_uci_command("isready ").ok().unwrap() {
UciCommand::IsReady => true,
_ => false,
});
assert!(match parse_uci_command("isready xxx").ok().unwrap() {
UciCommand::IsReady => true,
_ => false,
});
assert!(match parse_uci_command("ponderhit ").ok().unwrap() {
UciCommand::PonderHit => true,
_ => false,
});
assert!(match parse_uci_command(" foo quit ").ok().unwrap() {
UciCommand::Quit => true,
_ => false,
});
assert!(match parse_uci_command(" stop ").ok().unwrap() {
UciCommand::Stop => true,
_ => false,
});
assert!(match parse_uci_command("ucinewgame").ok().unwrap() {
UciCommand::UciNewGame => true,
_ => false,
});
assert!(match parse_uci_command("position startpos").ok().unwrap() {
UciCommand::Position { .. } => true,
_ => false,
});
assert!(match parse_uci_command("position fen k7/8/8/8/8/8/8/7K w - - 0 1")
.ok()
.unwrap() {
UciCommand::Position { .. } => true,
_ => false,
});
assert!(match parse_uci_command("position fen k7/8/8/8/8/8/8/7K w - - 0 1 moves h1h2")
.ok()
.unwrap() {
UciCommand::Position { .. } => true,
_ => false,
});
assert!(parse_uci_command("position fen k7/8/8/8/8/8/8/7K w - - 0 1 moves h1h2 aabb")
.is_err());
assert!(match parse_uci_command("setoption name x value y")
.ok()
.unwrap() {
UciCommand::SetOption { .. } => true,
_ => false,
});
assert!(match parse_uci_command("go infinite").ok().unwrap() {
UciCommand::Go(_) => true,
_ => false,
});
}
}