use super::*;
use TimecatError::*;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum UserCommand {
TerminateEngine,
EngineVersion,
#[cfg(feature = "debug")]
RunTest,
ChangeToUCIMode {
verbose: bool,
},
ChangeToConsoleMode {
verbose: bool,
},
SetDebugMode(bool),
PrintText(String),
DisplayBoard,
#[cfg(feature = "inbuilt_nnue")]
DisplayBoardEvaluation,
PrintUCIInfo,
UCIMode,
UCINewGame,
IsReady,
Stop,
Help,
Perft(Depth),
Go(SearchConfig),
PushMoves(String),
PopMoves(u16),
SetFen(String),
#[cfg(feature = "colored")]
SetColor(bool),
SetUCIOption {
user_input: String,
},
SelfPlay(SearchConfig),
}
impl UserCommand {
fn print_engine_uci_info<T: ChessEngine>(uci_state_manager: &UCIStateManager<T>) {
println_wasm!(
"{}",
format!("id name {}", get_engine_version()).colorize(INFO_MESSAGE_STYLE)
);
println_wasm!(
"{}",
format!("id author {}", ENGINE_AUTHOR).colorize(INFO_MESSAGE_STYLE)
);
for option in uci_state_manager.get_all_options() {
println_wasm!("{option}");
}
}
pub fn generate_help_message() -> String {
"Sadly, the help message is till now not implemented. But type uci to go into the uci mode and visit the link \"https://backscattering.de/chess/uci/\" to know the necessary commands required to use an uci chess engine.".colorize(ERROR_MESSAGE_STYLE)
}
pub fn run_command<T: ChessEngine>(
&self,
engine: &mut T,
uci_state_manager: &UCIStateManager<T>,
) -> Result<()> {
match self {
Self::TerminateEngine => engine.set_termination(true),
Self::EngineVersion => print_engine_version(),
#[cfg(feature = "debug")]
Self::RunTest => test.run_and_print_time(engine)?,
&Self::ChangeToUCIMode { verbose } => GLOBAL_TIMECAT_STATE.set_uci_mode(true, verbose),
&Self::ChangeToConsoleMode { verbose } => {
GLOBAL_TIMECAT_STATE.set_console_mode(true, verbose)
}
&Self::SetDebugMode(b) => GLOBAL_TIMECAT_STATE.set_debug_mode(b),
Self::PrintText(s) => println_wasm!("{s}"),
Self::DisplayBoard => println_wasm!("{}", engine.get_board()),
#[cfg(feature = "inbuilt_nnue")]
Self::DisplayBoardEvaluation => force_println_info(
"Current Score",
engine.evaluate_current_position().stringify(),
),
Self::PrintUCIInfo => Self::print_engine_uci_info(uci_state_manager),
Self::UCIMode => {
Self::PrintUCIInfo.run_command(engine, uci_state_manager)?;
println_wasm!("{}", "uciok".colorize(SUCCESS_MESSAGE_STYLE));
}
Self::UCINewGame => {
Self::SetUCIOption {
user_input: "setoption name Clear Hash".to_string(),
}
.run_command(engine, uci_state_manager)?;
Self::SetFen(STARTING_POSITION_FEN.to_string())
.run_command(engine, uci_state_manager)?;
}
Self::IsReady => println_wasm!("{}", "readyok".colorize(SUCCESS_MESSAGE_STYLE)),
Self::Stop => {
if GLOBAL_TIMECAT_STATE.is_in_console_mode() {
return Err(EngineNotRunning);
}
}
Self::Help => println_wasm!("{}", Self::generate_help_message()),
&Self::Perft(depth) => GoAndPerft::run_perft_command(engine, depth)?,
Self::Go(config) => GoAndPerft::run_search(engine, config)?,
Self::PushMoves(user_input) => {
let binding = Parser::sanitize_string(user_input);
Push::push_moves(engine, &binding.split_whitespace().collect_vec())?
}
&Self::PopMoves(num_moves) => Pop::pop_moves(engine, num_moves)?,
Self::SetFen(fen) => Set::set_board_fen(engine, fen)?,
#[cfg(feature = "colored")]
&Self::SetColor(b) => Set::set_color(b)?,
Self::SetUCIOption { user_input } => {
uci_state_manager.run_command(engine, user_input)?
}
Self::SelfPlay(config) => self_play(engine, config, true, None)?,
}
Ok(())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TimecatResponse {}
impl From<UserCommand> for Result<Vec<UserCommand>> {
fn from(value: UserCommand) -> Self {
Ok(vec![value])
}
}
struct GoAndPerft;
impl GoAndPerft {
pub fn parse_sub_commands(commands: &[&str]) -> Result<Vec<UserCommand>> {
let second_command = commands.get(1).ok_or(UnknownCommand)?.to_lowercase();
if second_command == "perft" {
UserCommand::Perft(commands.get(2).ok_or(UnknownCommand)?.parse()?).into()
} else {
UserCommand::Go(SearchConfig::try_from(commands)?).into()
}
}
fn run_perft_command(engine: &mut impl ChessEngine, depth: Depth) -> Result<()> {
if GLOBAL_TIMECAT_STATE.is_in_console_mode() {
println_wasm!("{}\n", engine.get_board());
}
let clock = Instant::now();
let position_count = engine.get_board_mut().perft(depth);
let elapsed_time = clock.elapsed();
let nps: String = format!(
"{} nodes/sec",
(position_count as u128 * 10u128.pow(9)) / elapsed_time.as_nanos()
);
println_wasm!();
force_println_info("Position Count", position_count);
force_println_info("Time", elapsed_time.stringify());
force_println_info("Speed", nps);
Ok(())
}
fn run_search(engine: &mut impl ChessEngine, config: &SearchConfig) -> Result<()> {
if let Some(moves_to_search) = config.get_moves_to_search() {
let legal_moves = engine.get_board().generate_legal_moves();
if moves_to_search
.iter()
.any(|move_| !legal_moves.contains(move_))
{
return Err(TimecatError::IllegalSearchMoves {
illegal_moves: moves_to_search
.iter()
.copied()
.filter(|move_| !legal_moves.contains(move_))
.collect_vec(),
});
}
}
if GLOBAL_TIMECAT_STATE.is_in_console_mode() {
println_wasm!("{}\n", engine.get_board());
}
let clock = Instant::now();
let response = engine.go_verbose(config);
let best_move = response.get_best_move().ok_or(BestMoveNotFound {
fen: engine.get_board().get_fen(),
})?;
let elapsed_time = clock.elapsed();
let pv_string = get_pv_string(engine.get_board().get_position(), response.get_pv());
if GLOBAL_TIMECAT_STATE.is_in_console_mode() {
println_wasm!();
}
println_info("Score", response.get_score().stringify());
println_info("PV Line", pv_string);
println_info("Time", elapsed_time.stringify());
let position_count = response.get_num_nodes_searched();
let nps = format!(
"{} Nodes/sec",
position_count.map_or(STRINGIFY_NONE.to_string(), |position_count| {
((position_count as u128 * 10u128.pow(9)) / elapsed_time.as_nanos()).to_string()
})
);
println_info(
"Position Count",
position_count.map_or(STRINGIFY_NONE.to_string(), |position_count| {
position_count.to_string()
}),
);
println_info("Speed", nps);
if GLOBAL_TIMECAT_STATE.is_in_console_mode() {
println_info(
"Best Move",
best_move
.stringify_move(engine.get_board().get_position())
.unwrap(),
);
} else {
let mut move_text = format_info(
"bestmove",
best_move
.stringify_move(engine.get_board().get_position())
.unwrap(),
false,
);
if let Some(ponder_move) = response.get_ponder_move() {
move_text += " ";
move_text += &format_info(
"ponder",
ponder_move
.stringify_move(&engine.get_board().get_position().make_move_new(best_move))
.unwrap(),
false,
);
}
println_wasm!("{}", move_text);
}
Ok(())
}
}
struct Set;
impl Set {
fn extract_board_fen(commands: &[&str]) -> Result<Vec<UserCommand>> {
let fen = commands[3..].join(" ");
if fen == "startpos" {
return UserCommand::SetFen(STARTING_POSITION_FEN.to_string()).into();
}
UserCommand::SetFen(fen).into()
}
#[cfg(feature = "colored")]
fn extract_color(commands: &[&str]) -> Result<Vec<UserCommand>> {
if commands.get(3).is_some() {
return Err(UnknownCommand);
}
let third_command = commands.get(2).ok_or(UnknownCommand)?.to_lowercase();
let b = third_command.parse()?;
UserCommand::SetColor(b).into()
}
pub fn parse_sub_commands(commands: &[&str]) -> Result<Vec<UserCommand>> {
let second_command = commands.get(1).ok_or(UnknownCommand)?.to_lowercase();
match second_command.as_str() {
"board" => {
let third_command = commands.get(2).ok_or(UnknownCommand)?.to_lowercase();
match third_command.as_str() {
"startpos" | "fen" => Self::extract_board_fen(commands),
_ => Err(UnknownCommand),
}
}
#[cfg(feature = "colored")]
"color" => Self::extract_color(commands),
#[cfg(not(feature = "colored"))]
"color" => Err(FeatureNotEnabled {
s: "colored".to_string(),
}),
_ => Err(UnknownCommand),
}
}
fn set_board_fen(engine: &mut impl ChessEngine, fen: &str) -> Result<()> {
engine.set_fen(fen)?;
if GLOBAL_TIMECAT_STATE.is_in_console_mode() {
println_wasm!("{}", engine.get_board());
}
Ok(())
}
#[cfg(feature = "colored")]
fn set_color(b: bool) -> Result<()> {
if GLOBAL_TIMECAT_STATE.is_colored_output() == b {
return Err(ColoredOutputUnchanged { b });
}
GLOBAL_TIMECAT_STATE.set_colored_output(b, true);
Ok(())
}
}
struct Push;
impl Push {
fn push_moves(engine: &mut impl ChessEngine, commands: &[&str]) -> Result<()> {
let second_command = commands.get(1).ok_or(UnknownCommand)?.to_lowercase();
for move_text in commands.iter().skip(2) {
let valid_or_null_move = match second_command.as_str() {
"san" | "sans" => engine.get_board().parse_san(move_text),
"lan" | "lans" => engine.get_board().parse_lan(move_text),
"uci" | "ucis" => engine.get_board().parse_uci(move_text),
"move" | "moves" => engine.get_board().parse_move(move_text),
_ => Err(UnknownCommand),
}?;
engine.get_board_mut().push(valid_or_null_move)?;
println_info("Pushed move", move_text);
}
Ok(())
}
}
struct Pop;
impl Pop {
fn pop_moves(engine: &mut impl ChessEngine, num_moves: u16) -> Result<()> {
for _ in 0..num_moves {
if engine.get_board().has_empty_stack() {
return Err(EmptyStack);
}
let last_move = engine.get_board_mut().pop();
println_info(
"Popped move",
last_move
.stringify_move(engine.get_board().get_position())
.unwrap(),
);
}
Ok(())
}
pub fn parse_sub_commands(commands: &[&str]) -> Result<Vec<UserCommand>> {
let second_command = commands.get(1).unwrap_or(&"1");
if commands.get(2).is_some() {
return Err(UnknownCommand);
}
let num_pop = second_command.parse()?;
UserCommand::PopMoves(num_pop).into()
}
}
struct SelfPlay;
impl SelfPlay {
fn parse_sub_commands(commands: &[&str]) -> Result<Vec<UserCommand>> {
let mut commands = commands.to_vec();
commands[0] = "go";
let config = if commands.get(1).is_some() {
SearchConfig::try_from(commands)?
} else {
DEFAULT_SELFPLAY_COMMAND
};
UserCommand::SelfPlay(config).into()
}
}
struct DebugMode;
impl DebugMode {
fn get_debug_mode(second_command: &str) -> Result<bool> {
match second_command {
"on" => Ok(true),
"off" => Ok(false),
_ => Err(UnknownDebugCommand {
command: second_command.to_string(),
}),
}
}
fn parse_sub_commands(commands: &[&str]) -> Result<Vec<UserCommand>> {
if commands.get(2).is_some() {
return Err(UnknownCommand);
}
let second_command = commands.get(1).ok_or(UnknownCommand)?.to_lowercase();
let debug_mode = Self::get_debug_mode(&second_command)?;
UserCommand::SetDebugMode(debug_mode).into()
}
}
struct Position;
impl Position {
fn parse_sub_commands(commands: &[&str]) -> Result<Vec<UserCommand>> {
if commands.first() != Some(&"position") {
return Err(UnknownCommand);
}
let second_command = commands.get(1).ok_or(UnknownCommand)?.to_lowercase();
let mut user_commands = Vec::with_capacity(2);
user_commands.push(match second_command.as_str() {
"startpos" => UserCommand::SetFen(STARTING_POSITION_FEN.to_string()),
"fen" => {
let fen = commands
.iter()
.skip(2)
.take_while(|&&s| s != "moves")
.join(" ");
UserCommand::SetFen(fen)
}
_ => return Err(UnknownCommand),
});
let move_texts_joined = commands
.iter()
.skip_while(|&&s| s != "moves")
.skip(1)
.join(" ");
if !move_texts_joined.is_empty() {
user_commands.push(UserCommand::PushMoves(format!(
"push moves {move_texts_joined}"
)));
}
Ok(user_commands)
}
}
pub struct Parser;
impl Parser {
pub fn sanitize_string(raw_input: &str) -> String {
let user_input = raw_input.trim();
let mut user_input = user_input.to_string();
for _char in [",", ":"] {
user_input = user_input.replace(_char, " ")
}
user_input = remove_double_spaces_and_trim(&user_input);
user_input
}
fn parse_single_command(single_input: &str) -> Result<Vec<UserCommand>> {
match single_input.to_lowercase().as_str() {
"q" | "quit" | "quit()" | "quit(0)" | "exit" | "exit()" | "exit(0)" => {
UserCommand::TerminateEngine.into()
}
"uci" | "ucinewgame" => Ok(vec![
UserCommand::ChangeToUCIMode { verbose: false },
match single_input {
"uci" => UserCommand::UCIMode,
"ucinewgame" => UserCommand::UCINewGame,
_ => unreachable!(),
},
]),
"ucimode" => {
if GLOBAL_TIMECAT_STATE.is_in_uci_mode() {
Err(UCIModeUnchanged)
} else {
UserCommand::ChangeToUCIMode { verbose: true }.into()
}
}
"console" | "consolemode" => {
if GLOBAL_TIMECAT_STATE.is_in_console_mode() {
Err(ConsoleModeUnchanged)
} else {
UserCommand::ChangeToConsoleMode { verbose: true }.into()
}
}
"isready" => UserCommand::IsReady.into(),
"d" => UserCommand::DisplayBoard.into(),
#[cfg(feature = "inbuilt_nnue")]
"eval" => UserCommand::DisplayBoardEvaluation.into(),
#[cfg(not(feature = "inbuilt_nnue"))]
"eval" => Err(TimecatError::FeatureNotEnabled {
s: "inbuilt nnue".to_string(),
}),
"reset board" => UserCommand::SetFen(STARTING_POSITION_FEN.to_owned()).into(),
"stop" => UserCommand::Stop.into(),
"help" => UserCommand::Help.into(),
_ => {
let commands = single_input.split_whitespace().collect_vec();
let first_command = commands.first().ok_or(UnknownCommand)?.to_lowercase();
match first_command.as_str() {
"go" => GoAndPerft::parse_sub_commands(&commands),
"set" => Set::parse_sub_commands(&commands),
"setoption" => UserCommand::SetUCIOption {
user_input: single_input.to_string(),
}
.into(),
"push" => UserCommand::PushMoves(single_input.to_string()).into(),
"pop" => Pop::parse_sub_commands(&commands),
"position" => Position::parse_sub_commands(&commands),
"selfplay" => SelfPlay::parse_sub_commands(&commands),
"debug" => DebugMode::parse_sub_commands(&commands),
_ => Err(UnknownCommand),
}
}
}
}
pub fn parse_command(raw_input: &str) -> Result<Vec<UserCommand>> {
if raw_input.is_empty() {
return Ok(vec![
UserCommand::PrintText("".to_string()),
UserCommand::TerminateEngine,
]);
}
if raw_input.trim().is_empty() {
return Err(NoInput);
}
Self::sanitize_string(raw_input)
.split("&&")
.try_fold(Vec::new(), |mut vec, input| {
vec.extend(Self::parse_single_command(input.trim())?);
Ok(vec)
})
}
}