[explorer] Add several commands to help with debugging

flags
: Print flags for the current board position. This prints the castling rights
and whether the player can castle (regardless of whether they have the right).

make
: Finally reimplement the make command. Change the format so it takes a move in
the UCI long algebraic style.

perft
: Run perft to a given depth on the current board position.
This commit is contained in:
Eryn Wells 2025-06-18 08:21:21 -07:00
parent 6996cbeb15
commit bf17017694

View file

@ -1,12 +1,14 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_board::ZobristState;
use chessfriend_board::castle::CastleEvaluationError;
use chessfriend_board::{Board, fen::FromFenStr};
use chessfriend_board::{CastleParameters, ZobristState};
use chessfriend_core::random::RandomNumberGenerator;
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_moves::GeneratedMove;
use chessfriend_core::{Color, Piece, Shape, Square, Wing};
use chessfriend_moves::algebraic::AlgebraicMoveComponents;
use chessfriend_moves::{GeneratedMove, ValidateMove};
use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr};
use clap::{Arg, Command};
use clap::{Arg, Command, value_parser};
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
use std::sync::Arc;
@ -45,6 +47,7 @@ fn command_line() -> Command {
.subcommand_help_heading("COMMANDS")
.help_template(PARSER_TEMPLATE)
.subcommand(Command::new("fen").about("Print the current position as a FEN string"))
.subcommand(Command::new("flags").about("Print flags for the current position"))
.subcommand(
Command::new("load")
.arg(Arg::new("fen").required(true))
@ -53,8 +56,7 @@ fn command_line() -> Command {
)
.subcommand(
Command::new("make")
.arg(Arg::new("from").required(true))
.arg(Arg::new("to").required(true))
.arg(Arg::new("move").required(true))
.alias("m")
.about("Make a move"),
)
@ -81,6 +83,14 @@ fn command_line() -> Command {
.arg(Arg::new("square").required(true))
.about("Show moves of a piece on a square"),
)
.subcommand(
Command::new("perft")
.arg(Arg::new("depth")
.required(true)
.value_parser(value_parser!(usize))
)
.about("Run Perft on the current position to the given depth")
)
.subcommand(
Command::new("reset")
.subcommand(Command::new("clear").about("Reset to a cleared board"))
@ -107,6 +117,12 @@ enum CommandHandlingError<'a> {
#[error("no piece on {0}")]
NoPiece(Square),
#[error("{value:?} is not a valid value for {argument_name:?}")]
ValueError {
argument_name: &'static str,
value: String,
},
}
fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
@ -116,6 +132,7 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
let mut result = CommandResult::default();
match matches.subcommand() {
Some(("flags", matches)) => result = do_flags_command(state, matches),
Some(("load", matches)) => result = do_load_command(state, matches)?,
Some(("print", _matches)) => {}
Some(("quit", _matches)) => {
@ -126,9 +143,8 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
println!("{}", state.position.to_fen_str()?);
result.should_print_position = false;
}
Some(("make", _matches)) => {
unimplemented!()
}
Some(("make", matches)) => result = do_make_command(state, matches)?,
Some(("perft", matches)) => result = do_perft_command(state, matches)?,
Some(("place", matches)) => {
let color = matches
.get_one::<String>("color")
@ -175,6 +191,34 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
Ok(result)
}
fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult {
let board = &state.position.board;
println!("Castling:");
for (color, wing) in [
(Color::White, Wing::KingSide),
(Color::White, Wing::QueenSide),
(Color::Black, Wing::KingSide),
(Color::Black, Wing::QueenSide),
] {
let has_right = board.color_has_castling_right_unwrapped(color, wing);
let can_castle = board.color_can_castle(wing, Some(color));
let can_castle_message = match can_castle {
Ok(_) => "ok".to_string(),
Err(error) => format!("{error}"),
};
println!(" {color} {wing}: {has_right}, {can_castle_message}");
}
CommandResult {
should_continue: true,
should_print_position: false,
}
}
fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result<CommandResult> {
let fen_string = matches
.get_one::<String>("fen")
@ -191,6 +235,26 @@ fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Res
})
}
fn do_make_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result<CommandResult> {
let move_string = matches
.get_one::<String>("move")
.ok_or(CommandHandlingError::MissingArgument("move"))?;
let algebraic_move: AlgebraicMoveComponents = move_string.parse()?;
let encoded_move = state
.position
.move_from_algebraic_components(algebraic_move)
.ok_or(CommandHandlingError::ValueError {
argument_name: "move",
value: move_string.to_string(),
})?;
state.position.make_move(encoded_move, ValidateMove::Yes)?;
Ok(CommandResult::default())
}
fn do_reset_command(
state: &mut State,
matches: &clap::ArgMatches,
@ -282,6 +346,25 @@ fn do_movement_command(
})
}
fn do_perft_command(
state: &mut State,
matches: &clap::ArgMatches,
) -> anyhow::Result<CommandResult> {
let depth = *matches
.get_one::<usize>("depth")
.ok_or(CommandHandlingError::MissingArgument("depth"))?;
let mut position = state.position.clone();
let nodes_count = position.perft(depth);
println!("nodes {nodes_count}");
Ok(CommandResult {
should_continue: true,
should_print_position: false,
})
}
fn do_zobrist_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult {
if let Some(hash) = state.position.zobrist_hash() {
println!("hash:{hash}");