#[allow(unused_imports)]
use alloc::string::ToString as _;
use alloc::{string::String, vec::Vec};
use core::{num::ParseIntError, time::Duration};
use nom::{
AsChar, IResult, Parser,
branch::alt,
bytes::complete::{tag, tag_no_case, take_until},
character::complete::{anychar, char, digit1, i8, i32, i64, space0, space1, u8, u16, u64},
combinator::{all_consuming, eof, map_res, opt, recognize, rest, success, verify},
multi::{count, many_m_n, many0, many1, separated_list0, separated_list1},
sequence::delimited,
};
use nom_permutation::permutation_opt;
use shakmaty::{
fen::{Fen, ParseFenError},
uci::{ParseUciMoveError, UciMove},
};
use crate::{
ProtectionState, UciInfo, UciInfoCurrLine, UciInfoScore, UciMessage, UciOptionConfig,
UciSearchControl, UciTimeControl,
};
fn ucimove(input: &str) -> IResult<&str, UciMove> {
let promotion = alt((char('q'), char('r'), char('n'), char('b')));
map_res(recognize((square, square, opt(promotion))), |m| {
let m = UciMove::from_ascii(m.as_bytes())?;
Ok::<UciMove, ParseUciMoveError>(m)
})
.parse(input)
}
fn rank(input: &str) -> IResult<&str, u8> {
verify(digit1.and_then(u8), |r| 1 <= *r && *r <= 8).parse(input)
}
fn square(input: &str) -> IResult<&str, &str> {
let file = verify(anychar, |f: &char| matches!(f, 'a'..='h' | 'A'..='H'));
recognize((file, rank)).parse(input)
}
fn fen(input: &str) -> IResult<&str, Fen> {
let fen_color = alt((tag_no_case("w"), tag_no_case("b")));
let fen_castling = alt((
recognize(many_m_n(
1,
4,
alt((char('k'), char('K'), char('q'), char('Q'))),
)),
recognize(char('-')),
));
let fen_en_passant = alt((recognize(square), recognize(char('-'))));
map_res(
recognize((
count((fen_rank, tag_no_case("/")), 7),
fen_rank,
space1,
fen_color,
space1,
fen_castling,
space1,
fen_en_passant,
space1,
fen_counter,
space1,
fen_counter,
)),
|f| {
let fen = Fen::from_ascii(f.as_bytes())?;
Ok::<Fen, ParseFenError>(fen)
},
)
.parse(input)
}
fn fen_rank(input: &str) -> IResult<&str, &str> {
let piece = verify(anychar, |p: &char| {
matches!(
p,
'k' | 'K' | 'q' | 'Q' | 'r' | 'R' | 'n' | 'N' | 'b' | 'B' | 'p' | 'P'
)
});
recognize(many_m_n(1, 8, alt((recognize(piece), recognize(rank))))).parse(input)
}
fn fen_counter(input: &str) -> IResult<&str, &str> {
recognize(many_m_n(1, 4, verify(anychar, |c| c.is_dec_digit()))).parse(input)
}
fn command_debug(input: &str) -> IResult<&str, bool> {
(
tag_no_case("debug"),
space1,
alt((
tag_no_case("on").map(|_| true),
tag_no_case("off").map(|_| false),
)),
)
.map(|(_, _, switch)| switch)
.parse(input)
}
fn command_setoption(input: &str) -> IResult<&str, (String, Option<String>)> {
(
tag_no_case("setoption"),
space1,
tag_no_case("name"),
space1,
alt((take_until(" value"), rest)).map(|v: &str| String::from(v.trim())),
opt((space1, tag_no_case("value"), rest)
.map(|(_, _, value): (&str, &str, &str)| String::from(value.trim()))),
)
.map(|(_, _, _, _, name, value)| (name, value))
.parse(input)
}
fn command_register(input: &str) -> IResult<&str, (bool, Option<String>, Option<String>)> {
alt((
(tag_no_case("register"), space1, tag_no_case("later")).map(|_| (true, None, None)),
(
tag_no_case("register"),
space1,
tag_no_case("name"),
space1,
take_until(" code").map(|v: &str| v.trim()),
space0,
tag_no_case("code"),
space1,
rest,
)
.map(|(_, _, _, _, name, _, _, _, code)| {
(false, Some(String::from(name)), Some(String::from(code)))
}),
))
.parse(input)
}
fn command_position(input: &str) -> IResult<&str, (bool, Option<Fen>, Vec<UciMove>)> {
(
tag_no_case("position"),
space1,
alt((
tag_no_case("startpos").map(|_| (true, None)),
(tag_no_case("fen"), space1, fen).map(|(_, _, fen)| (false, Some(fen))),
)),
opt((
space1,
tag_no_case("moves"),
space1,
separated_list1(space1, ucimove),
)
.map(|(_, _, _, m)| m)),
)
.map(|(_, _, (startpos, fen), moves)| (startpos, fen, moves.unwrap_or_default()))
.parse(input)
}
fn command_go(input: &str) -> IResult<&str, (Option<UciTimeControl>, Option<UciSearchControl>)> {
alt((
(tag_no_case("go"), space0, eof).map(|_| (None, None)),
(tag_no_case("go"), space1, tag_no_case("infinite"))
.map(|_| (Some(UciTimeControl::Infinite), None)),
(
tag_no_case("go"),
space1,
permutation_opt((
(space0, tag_no_case("ponder")).map(|_| true),
(space0, tag_no_case("movetime"), space1, u64).map(|(_, _, _, x)| x),
(
space0,
tag_no_case("searchmoves"),
space0,
separated_list0(space1, ucimove),
)
.map(|(_, _, _, m)| m),
(space0, tag_no_case("depth"), space1, u8).map(|(_, _, _, x)| x),
(space0, tag_no_case("mate"), space1, u8).map(|(_, _, _, x)| x),
(space0, tag_no_case("nodes"), space1, u64).map(|(_, _, _, x)| x),
(space0, tag_no_case("wtime"), space1, u64)
.map(|(_, _, _, x)| Duration::from_millis(x)),
(space0, tag_no_case("winc"), space1, u64)
.map(|(_, _, _, x)| Duration::from_millis(x)),
(space0, tag_no_case("btime"), space1, u64)
.map(|(_, _, _, x)| Duration::from_millis(x)),
(space0, tag_no_case("binc"), space1, u64)
.map(|(_, _, _, x)| Duration::from_millis(x)),
(space0, tag_no_case("movestogo"), space1, u8).map(|(_, _, _, x)| x),
)),
)
.map(
|(
_,
_,
(
ponder,
movetime,
search_moves,
depth,
mate,
nodes,
wtime,
winc,
btime,
binc,
mtg,
),
)| {
let mut time_control: Option<UciTimeControl> = None;
let mut search_control: Option<UciSearchControl> = None;
if search_moves.is_some()
|| depth.is_some()
|| mate.is_some()
|| nodes.is_some()
{
search_control = Some(UciSearchControl {
depth,
mate,
nodes,
search_moves: search_moves.unwrap_or_default(),
});
}
if ponder.is_some() {
time_control = Some(UciTimeControl::Ponder);
} else if let Some(mt) = movetime {
time_control = Some(UciTimeControl::MoveTime(Duration::from_millis(mt)));
} else if wtime.is_some()
|| winc.is_some()
|| btime.is_some()
|| binc.is_some()
|| mtg.is_some()
{
time_control = Some(UciTimeControl::TimeLeft {
white_time: wtime,
white_increment: winc,
black_time: btime,
black_increment: binc,
moves_to_go: mtg,
});
}
(time_control, search_control)
},
),
))
.parse(input)
}
#[allow(clippy::too_many_lines)]
pub(crate) fn parse(input: &str) -> IResult<&str, UciMessage> {
all_consuming(delimited(
space0,
alt((
(tag_no_case("uci"), space0, eof).map(|_| UciMessage::Uci),
command_debug.map(UciMessage::Debug),
command_setoption.map(|(name, value)| UciMessage::SetOption { name, value }),
tag_no_case("isready").map(|_| UciMessage::IsReady),
tag_no_case("ucinewgame").map(|_| UciMessage::UciNewGame),
tag_no_case("stop").map(|_| UciMessage::Stop),
tag_no_case("quit").map(|_| UciMessage::Quit),
command_register.map(|(later, name, code)| UciMessage::Register { later, name, code }),
command_position.map(|(startpos, fen, moves)| UciMessage::Position {
startpos,
fen,
moves,
}),
command_go.map(|(time_control, search_control)| UciMessage::Go {
time_control,
search_control,
}),
(
tag_no_case("id"),
space1,
alt((
(tag_no_case("author"), space1, rest)
.map(|(_, _, author)| (Some(String::from(author)), None)),
(tag_no_case("name"), space1, rest)
.map(|(_, _, name)| (None, Some(String::from(name)))),
)),
)
.map(|(_, _, (author, name))| UciMessage::Id { author, name }),
tag_no_case("uciok").map(|_| UciMessage::UciOk),
tag_no_case("readyok").map(|_| UciMessage::ReadyOk),
(
tag_no_case("bestmove"),
space1,
ucimove,
opt((space1, tag_no_case("ponder"), space1, ucimove).map(|(_, _, _, m)| m)),
)
.map(|(_, _, best_move, ponder)| UciMessage::BestMove { best_move, ponder }),
(tag_no_case("ponderhit"), space0, eof).map(|_| UciMessage::PonderHit),
(
tag_no_case("registration"),
space1,
alt((
tag_no_case("checking").map(|_| ProtectionState::Checking),
tag_no_case("ok").map(|_| ProtectionState::Ok),
tag_no_case("error").map(|_| ProtectionState::Error),
)),
)
.map(|(_, _, s)| UciMessage::Registration(s)),
(
tag_no_case("copyprotection"),
space1,
alt((
tag_no_case("checking").map(|_| ProtectionState::Checking),
tag_no_case("ok").map(|_| ProtectionState::Ok),
tag_no_case("error").map(|_| ProtectionState::Error),
)),
)
.map(|(_, _, s)| UciMessage::CopyProtection(s)),
map_res(
(
tag_no_case("option"),
space1,
tag_no_case("name"),
space1,
take_until(" type").map(|v: &str| v.trim()),
space1,
tag_no_case("type"),
space1,
alt((
tag_no_case("check"),
tag_no_case("spin"),
tag_no_case("combo"),
tag_no_case("string"),
tag_no_case("button"),
)),
opt((
space1,
tag_no_case("default"),
alt((
(
space1,
alt((
take_until(" min").map(|v: &str| v.trim()),
take_until(" max").map(|v: &str| v.trim()),
take_until(" var").map(|v: &str| v.trim()),
rest,
)),
)
.map(|(_, v)| v),
space0,
eof,
)),
)
.map(|(_, _, d)| String::from(d))),
opt((space1, tag_no_case("min"), space1, i64).map(|(_, _, _, m)| m)),
opt((space1, tag_no_case("max"), space1, i64).map(|(_, _, _, m)| m)),
many0(
(
space1,
tag_no_case("var"),
space1,
alt((take_until(" var"), rest)).map(|v: &str| v.trim()),
)
.map(|(_, _, _, v): (&str, &str, &str, &str)| String::from(v)),
),
),
|(_, _, _, _, name, _, _, _, t, default, min, max, var)| {
let name = String::from(name);
let option_config = match t.to_lowercase().as_str() {
"check" => UciOptionConfig::Check {
name,
default: default.and_then(|v| {
if v == "true" {
return Some(true);
}
if v == "false" {
return Some(false);
}
None
}),
},
"spin" => {
let default = match default {
Some(d) => Some(d.parse()?),
None => None,
};
UciOptionConfig::Spin {
name,
default,
max,
min,
}
}
"combo" => UciOptionConfig::Combo {
name,
default: default.map(|v| {
if v == "<empty>" {
return String::new();
}
v
}),
var,
},
"string" => UciOptionConfig::String {
name,
default: default.map(|v| {
if v == "<empty>" {
return String::new();
}
v
}),
},
"button" => UciOptionConfig::Button { name },
_ => unreachable!(),
};
Ok::<UciMessage, ParseIntError>(UciMessage::Option(option_config))
},
),
(
tag_no_case("info"),
space1,
permutation_opt((
(space0, tag_no_case("depth"), space1, u8).map(|(_, _, _, v)| v),
(space0, tag_no_case("seldepth"), space1, u8).map(|(_, _, _, v)| v),
(space0, tag_no_case("time"), space1, u64)
.map(|(_, _, _, v)| Duration::from_millis(v)),
(space0, tag_no_case("nodes"), space1, u64).map(|(_, _, _, v)| v),
(space0, tag_no_case("currmovenumber"), space1, u16).map(|(_, _, _, v)| v),
(space0, tag_no_case("currmove"), space1, ucimove).map(|(_, _, _, v)| v),
(space0, tag_no_case("hashfull"), space1, u16).map(|(_, _, _, v)| v),
(space0, tag_no_case("nps"), space1, u64).map(|(_, _, _, v)| v),
(space0, tag_no_case("tbhits"), space1, u64).map(|(_, _, _, v)| v),
(space0, tag_no_case("sbhits"), space1, u64).map(|(_, _, _, v)| v),
(space0, tag_no_case("cpuload"), space1, u16).map(|(_, _, _, v)| v),
(space0, tag_no_case("string"), space1, rest)
.map(|(_, _, _, v): (&str, &str, &str, &str)| String::from(v)),
(
space0,
tag_no_case("pv"),
space1,
separated_list1(space1, ucimove),
)
.map(|(_, _, _, v)| v),
(space0, tag_no_case("multipv"), space1, u16).map(|(_, _, _, v)| v),
(
space0,
tag_no_case("refutation"),
space1,
separated_list1(space1, ucimove),
)
.map(|(_, _, _, v)| v),
many1(
(
space0,
tag_no_case("currline"),
space1,
opt((u16, space1).map(|(c, _)| c)),
separated_list1(space1, ucimove),
space0,
)
.map(|(_, _, _, cpu_nr, moves, _)| UciInfoCurrLine { cpu_nr, moves }),
),
(
space0,
tag_no_case("score"),
space1,
alt((
(tag("cp"), space1, i32).map(|(_, _, v)| (Some(v), None)),
(tag("mate"), space1, i8).map(|(_, _, v)| (None, Some(v))),
)),
alt((
(space1, tag("lowerbound")).map(|_| (true, false)),
(space1, tag("upperbound")).map(|_| (false, true)),
success(()).map(|()| (false, false)),
)),
)
.map(
|(_, _, _, (cp, mate), (lower_bound, upper_bound))| UciInfoScore {
cp,
mate,
lower_bound,
upper_bound,
},
),
)),
)
.map(
|(
_,
_,
(
depth,
sel_depth,
time,
nodes,
curr_move_num,
curr_move,
hash_full,
nps,
tb_hits,
sb_hits,
cpu_load,
string,
pv,
multi_pv,
refutation,
curr_line,
score,
),
)| {
UciMessage::Info(UciInfo {
depth,
sel_depth,
time,
nodes,
curr_move_num,
curr_move,
hash_full,
nps,
tb_hits,
sb_hits,
cpu_load,
string,
pv: pv.unwrap_or_default(),
multi_pv,
refutation: refutation.unwrap_or_default(),
curr_line: curr_line.unwrap_or_default(),
score,
})
},
),
)),
(space0, eof),
))
.parse(input.trim())
}