2024-02-25 09:52:49 -08:00
|
|
|
// Eryn Wells <eryn@erynwells.me>
|
|
|
|
|
2025-06-18 23:44:40 +00:00
|
|
|
use chessfriend_board::{Board, ZobristState, fen::FromFenStr};
|
|
|
|
use chessfriend_core::{Color, Piece, Shape, Square, Wing, random::RandomNumberGenerator};
|
|
|
|
use chessfriend_moves::{GeneratedMove, ValidateMove, algebraic::AlgebraicMoveComponents};
|
2025-06-16 13:49:22 -07:00
|
|
|
use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr};
|
2025-06-18 08:21:21 -07:00
|
|
|
use clap::{Arg, Command, value_parser};
|
2025-06-18 23:44:40 +00:00
|
|
|
use rustyline::{DefaultEditor, error::ReadlineError};
|
2025-06-05 08:21:32 -07:00
|
|
|
use std::sync::Arc;
|
2025-05-19 14:18:31 -07:00
|
|
|
use thiserror::Error;
|
2023-12-28 15:09:15 -07:00
|
|
|
|
2024-01-24 17:16:33 -08:00
|
|
|
struct CommandResult {
|
|
|
|
should_continue: bool,
|
|
|
|
should_print_position: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for CommandResult {
|
|
|
|
fn default() -> Self {
|
|
|
|
CommandResult {
|
|
|
|
should_continue: true,
|
|
|
|
should_print_position: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct State {
|
|
|
|
position: Position,
|
2025-06-05 08:21:32 -07:00
|
|
|
zobrist: Arc<ZobristState>,
|
2023-12-28 15:09:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn command_line() -> Command {
|
|
|
|
// strip out usage
|
|
|
|
const PARSER_TEMPLATE: &str = "\
|
|
|
|
{all-args}
|
|
|
|
";
|
|
|
|
|
|
|
|
Command::new("explorer")
|
|
|
|
.multicall(true)
|
|
|
|
.arg_required_else_help(true)
|
|
|
|
.subcommand_required(true)
|
2024-01-28 09:50:39 -08:00
|
|
|
.subcommand_value_name("CMD")
|
|
|
|
.subcommand_help_heading("COMMANDS")
|
2023-12-28 15:09:15 -07:00
|
|
|
.help_template(PARSER_TEMPLATE)
|
2024-01-28 09:50:39 -08:00
|
|
|
.subcommand(Command::new("fen").about("Print the current position as a FEN string"))
|
2025-06-18 08:21:21 -07:00
|
|
|
.subcommand(Command::new("flags").about("Print flags for the current position"))
|
2025-06-08 16:49:55 -07:00
|
|
|
.subcommand(
|
|
|
|
Command::new("load")
|
|
|
|
.arg(Arg::new("fen").required(true))
|
|
|
|
.alias("l")
|
|
|
|
.about("Load a board position from a FEN string"),
|
|
|
|
)
|
2024-01-28 09:50:39 -08:00
|
|
|
.subcommand(
|
|
|
|
Command::new("make")
|
2025-06-18 08:21:21 -07:00
|
|
|
.arg(Arg::new("move").required(true))
|
2025-05-21 08:27:15 -07:00
|
|
|
.alias("m")
|
2024-01-28 09:50:39 -08:00
|
|
|
.about("Make a move"),
|
|
|
|
)
|
2023-12-28 15:09:15 -07:00
|
|
|
.subcommand(
|
|
|
|
Command::new("place")
|
2024-01-28 09:50:39 -08:00
|
|
|
.arg(Arg::new("color").required(true))
|
2023-12-28 15:09:15 -07:00
|
|
|
.arg(Arg::new("piece").required(true))
|
2024-01-28 09:50:39 -08:00
|
|
|
.arg(Arg::new("square").required(true))
|
2025-05-21 08:27:15 -07:00
|
|
|
.alias("p")
|
2024-01-28 09:50:39 -08:00
|
|
|
.about("Place a piece on the board"),
|
2023-12-28 15:09:15 -07:00
|
|
|
)
|
2025-05-19 08:28:23 -07:00
|
|
|
.subcommand(
|
|
|
|
Command::new("sight")
|
|
|
|
.arg(Arg::new("square").required(true))
|
|
|
|
.about("Show sight of a piece on a square"),
|
|
|
|
)
|
|
|
|
.subcommand(
|
|
|
|
Command::new("moves")
|
2025-05-28 16:25:55 -07:00
|
|
|
.arg(Arg::new("square").required(false))
|
|
|
|
.about("Show moves of a piece on a square. With no argument, show all moves for the active color."),
|
|
|
|
)
|
|
|
|
.subcommand(
|
|
|
|
Command::new("movement")
|
2025-05-19 08:28:23 -07:00
|
|
|
.arg(Arg::new("square").required(true))
|
2025-06-08 16:49:55 -07:00
|
|
|
.about("Show moves of a piece on a square"),
|
2025-05-19 08:28:23 -07:00
|
|
|
)
|
2025-06-18 08:21:21 -07:00
|
|
|
.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")
|
|
|
|
)
|
2025-05-19 08:42:34 -07:00
|
|
|
.subcommand(
|
|
|
|
Command::new("reset")
|
|
|
|
.subcommand(Command::new("clear").about("Reset to a cleared board"))
|
|
|
|
.subcommand(Command::new("starting").about("Reset to the starting position"))
|
|
|
|
.subcommand(
|
|
|
|
Command::new("fen")
|
|
|
|
.arg(Arg::new("fen").required(true))
|
|
|
|
.about("Reset to a position described by a FEN string"),
|
|
|
|
)
|
|
|
|
.about("Reset the board"),
|
|
|
|
)
|
2024-01-28 09:50:39 -08:00
|
|
|
.subcommand(Command::new("print").about("Print the board"))
|
|
|
|
.subcommand(Command::new("quit").alias("exit").about("Quit the program"))
|
2025-06-07 08:09:36 -07:00
|
|
|
.subcommand(Command::new("zobrist").about("Print the Zobrist hash of the current board"))
|
2023-12-28 15:09:15 -07:00
|
|
|
}
|
|
|
|
|
2025-05-19 14:18:31 -07:00
|
|
|
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
|
|
|
enum CommandHandlingError<'a> {
|
|
|
|
#[error("lexer error")]
|
|
|
|
LexerError,
|
2025-05-28 16:25:55 -07:00
|
|
|
|
2025-05-19 14:18:31 -07:00
|
|
|
#[error("missing {0} argument")]
|
|
|
|
MissingArgument(&'a str),
|
2025-05-28 16:25:55 -07:00
|
|
|
|
|
|
|
#[error("no piece on {0}")]
|
|
|
|
NoPiece(Square),
|
2025-06-18 08:21:21 -07:00
|
|
|
|
|
|
|
#[error("{value:?} is not a valid value for {argument_name:?}")]
|
|
|
|
ValueError {
|
|
|
|
argument_name: &'static str,
|
|
|
|
value: String,
|
|
|
|
},
|
2025-05-19 14:18:31 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
|
|
|
|
let args = shlex::split(line).ok_or(CommandHandlingError::LexerError)?;
|
|
|
|
let matches = command_line().try_get_matches_from(args)?;
|
2023-12-28 15:09:15 -07:00
|
|
|
|
2024-01-24 17:16:33 -08:00
|
|
|
let mut result = CommandResult::default();
|
|
|
|
|
2023-12-28 15:09:15 -07:00
|
|
|
match matches.subcommand() {
|
2025-06-18 08:21:21 -07:00
|
|
|
Some(("flags", matches)) => result = do_flags_command(state, matches),
|
2025-06-08 16:49:55 -07:00
|
|
|
Some(("load", matches)) => result = do_load_command(state, matches)?,
|
2023-12-28 15:09:15 -07:00
|
|
|
Some(("print", _matches)) => {}
|
|
|
|
Some(("quit", _matches)) => {
|
2024-01-24 17:16:33 -08:00
|
|
|
result.should_continue = false;
|
|
|
|
result.should_print_position = false;
|
2023-12-28 15:09:15 -07:00
|
|
|
}
|
2024-01-28 09:50:39 -08:00
|
|
|
Some(("fen", _matches)) => {
|
2025-05-19 14:18:31 -07:00
|
|
|
println!("{}", state.position.to_fen_str()?);
|
2024-01-28 09:50:39 -08:00
|
|
|
result.should_print_position = false;
|
|
|
|
}
|
2025-06-18 08:21:21 -07:00
|
|
|
Some(("make", matches)) => result = do_make_command(state, matches)?,
|
|
|
|
Some(("perft", matches)) => result = do_perft_command(state, matches)?,
|
2024-01-28 09:50:39 -08:00
|
|
|
Some(("place", matches)) => {
|
|
|
|
let color = matches
|
|
|
|
.get_one::<String>("color")
|
2025-05-19 14:18:31 -07:00
|
|
|
.ok_or(CommandHandlingError::MissingArgument("color"))?;
|
|
|
|
let color = color.parse::<Color>()?;
|
2024-01-28 09:50:39 -08:00
|
|
|
|
|
|
|
let shape = matches
|
|
|
|
.get_one::<String>("piece")
|
2025-05-19 14:18:31 -07:00
|
|
|
.ok_or(CommandHandlingError::MissingArgument("piece"))?;
|
|
|
|
let shape = shape.parse::<Shape>()?;
|
2024-01-28 09:50:39 -08:00
|
|
|
|
|
|
|
let square = matches
|
|
|
|
.get_one::<String>("square")
|
2025-05-19 14:18:31 -07:00
|
|
|
.ok_or(CommandHandlingError::MissingArgument("square"))?;
|
|
|
|
let square = Square::from_algebraic_str(square)?;
|
2023-12-28 15:09:15 -07:00
|
|
|
|
2025-05-19 08:41:48 -07:00
|
|
|
let piece = Piece::new(color, shape);
|
2024-01-28 09:50:39 -08:00
|
|
|
|
2025-05-19 08:41:48 -07:00
|
|
|
state
|
|
|
|
.position
|
2025-05-19 14:18:31 -07:00
|
|
|
.place_piece(piece, square, PlacePieceStrategy::default())?;
|
2024-01-28 09:50:39 -08:00
|
|
|
}
|
2025-05-19 08:28:23 -07:00
|
|
|
Some(("sight", matches)) => {
|
|
|
|
let square = matches
|
|
|
|
.get_one::<String>("square")
|
2025-05-19 14:18:31 -07:00
|
|
|
.ok_or(CommandHandlingError::MissingArgument("square"))?;
|
|
|
|
let square = square.parse::<Square>()?;
|
2025-05-19 08:28:23 -07:00
|
|
|
|
|
|
|
let sight = state.position.sight(square);
|
|
|
|
|
|
|
|
let display = state.position.display().highlight(sight);
|
|
|
|
println!("\n{display}");
|
|
|
|
|
|
|
|
result.should_print_position = false;
|
|
|
|
}
|
2025-05-28 16:25:55 -07:00
|
|
|
Some(("moves", matches)) => result = do_moves_command(state, matches)?,
|
|
|
|
Some(("movement", matches)) => result = do_movement_command(state, matches)?,
|
2025-05-19 14:18:31 -07:00
|
|
|
Some(("reset", matches)) => result = do_reset_command(state, matches)?,
|
2025-06-07 08:09:36 -07:00
|
|
|
Some(("zobrist", matches)) => result = do_zobrist_command(state, matches),
|
2023-12-28 15:09:15 -07:00
|
|
|
Some((name, _matches)) => unimplemented!("{name}"),
|
|
|
|
None => unreachable!("Subcommand required"),
|
|
|
|
}
|
|
|
|
|
2024-01-24 17:16:33 -08:00
|
|
|
Ok(result)
|
2023-12-28 15:09:15 -07:00
|
|
|
}
|
|
|
|
|
2025-06-18 08:21:21 -07:00
|
|
|
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),
|
|
|
|
] {
|
2025-06-18 23:44:40 +00:00
|
|
|
let has_right = board.has_castling_right_unwrapped(color, wing.into());
|
2025-06-18 08:21:21 -07:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-08 16:49:55 -07:00
|
|
|
fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result<CommandResult> {
|
|
|
|
let fen_string = matches
|
|
|
|
.get_one::<String>("fen")
|
|
|
|
.ok_or(CommandHandlingError::MissingArgument("fen"))?;
|
|
|
|
|
|
|
|
let mut board = Board::from_fen_str(fen_string.as_str())?;
|
|
|
|
board.set_zobrist_state(state.zobrist.clone());
|
|
|
|
|
|
|
|
state.position = Position::new(board);
|
|
|
|
|
|
|
|
Ok(CommandResult {
|
|
|
|
should_continue: true,
|
|
|
|
should_print_position: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2025-06-18 08:21:21 -07:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2025-05-19 14:18:31 -07:00
|
|
|
fn do_reset_command(
|
|
|
|
state: &mut State,
|
|
|
|
matches: &clap::ArgMatches,
|
|
|
|
) -> anyhow::Result<CommandResult> {
|
2025-05-19 08:42:34 -07:00
|
|
|
match matches.subcommand() {
|
2025-06-05 08:21:32 -07:00
|
|
|
None | Some(("clear", _)) => state.position = Position::empty(Some(state.zobrist.clone())),
|
|
|
|
Some(("starting", _)) => state.position = Position::starting(Some(state.zobrist.clone())),
|
2025-05-19 08:42:34 -07:00
|
|
|
Some(("fen", matches)) => {
|
2025-05-19 14:18:31 -07:00
|
|
|
let fen = matches
|
|
|
|
.get_one::<String>("fen")
|
|
|
|
.ok_or(CommandHandlingError::MissingArgument("fen"))?;
|
|
|
|
let board = Board::from_fen_str(fen)?;
|
2025-05-19 08:42:34 -07:00
|
|
|
state.position = Position::new(board);
|
|
|
|
}
|
|
|
|
Some((name, _matches)) => unimplemented!("{name}"),
|
|
|
|
}
|
|
|
|
|
2025-05-19 14:18:31 -07:00
|
|
|
Ok(CommandResult::default())
|
2025-05-19 08:42:34 -07:00
|
|
|
}
|
|
|
|
|
2025-05-28 16:25:55 -07:00
|
|
|
fn do_moves_command(
|
|
|
|
state: &mut State,
|
|
|
|
matches: &clap::ArgMatches,
|
|
|
|
) -> anyhow::Result<CommandResult> {
|
|
|
|
let moves: Vec<GeneratedMove> = if let Some(square) = matches
|
|
|
|
.get_one::<String>("square")
|
|
|
|
.and_then(|square| square.parse::<Square>().ok())
|
|
|
|
{
|
|
|
|
state
|
|
|
|
.position
|
|
|
|
.moves_for_piece(square)
|
|
|
|
.map(|it| it.filter(|ply| ply.origin() == square))
|
|
|
|
.map(Iterator::collect)
|
|
|
|
.ok_or(CommandHandlingError::NoPiece(square))?
|
|
|
|
} else {
|
|
|
|
state.position.all_moves(None).collect()
|
|
|
|
};
|
|
|
|
|
|
|
|
let formatted_moves: Vec<String> = moves
|
|
|
|
.iter()
|
|
|
|
.map(|ply| {
|
2025-06-11 08:15:06 -07:00
|
|
|
let origin = ply.origin();
|
|
|
|
if let Some(piece) = state.position.get_piece(origin) {
|
|
|
|
format!("{piece}{ply}")
|
|
|
|
} else {
|
|
|
|
format!("{ply}??")
|
|
|
|
}
|
2025-05-28 16:25:55 -07:00
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
if !formatted_moves.is_empty() {
|
|
|
|
let max_length = formatted_moves
|
|
|
|
.iter()
|
|
|
|
.map(|s| s.chars().count())
|
|
|
|
.max()
|
|
|
|
.unwrap_or(8)
|
|
|
|
+ 2;
|
|
|
|
|
|
|
|
let columns_count = 80 / max_length;
|
|
|
|
for row in formatted_moves.chunks(columns_count) {
|
|
|
|
for ply in row {
|
|
|
|
print!("{ply:<max_length$}");
|
|
|
|
}
|
|
|
|
println!();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(CommandResult {
|
|
|
|
should_continue: true,
|
|
|
|
should_print_position: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn do_movement_command(
|
|
|
|
state: &mut State,
|
|
|
|
matches: &clap::ArgMatches,
|
|
|
|
) -> anyhow::Result<CommandResult> {
|
|
|
|
let square = *matches
|
|
|
|
.get_one::<Square>("square")
|
|
|
|
.ok_or(CommandHandlingError::MissingArgument("square"))?;
|
|
|
|
|
|
|
|
let movement = state.position.movement(square);
|
|
|
|
let display = state.position.display().highlight(movement);
|
|
|
|
println!("\n{display}");
|
|
|
|
|
|
|
|
Ok(CommandResult {
|
2025-06-18 08:21:21 -07:00
|
|
|
should_continue: true,
|
|
|
|
should_print_position: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2025-05-28 16:25:55 -07:00
|
|
|
should_continue: true,
|
|
|
|
should_print_position: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2025-06-07 08:09:36 -07:00
|
|
|
fn do_zobrist_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult {
|
|
|
|
if let Some(hash) = state.position.zobrist_hash() {
|
|
|
|
println!("hash:{hash}");
|
|
|
|
} else {
|
|
|
|
println!("No Zobrist hash available");
|
|
|
|
}
|
|
|
|
|
|
|
|
CommandResult {
|
|
|
|
should_continue: true,
|
|
|
|
should_print_position: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-28 15:09:15 -07:00
|
|
|
fn main() -> Result<(), String> {
|
2025-05-19 08:41:48 -07:00
|
|
|
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?;
|
2023-12-28 15:09:15 -07:00
|
|
|
|
2025-06-05 08:21:32 -07:00
|
|
|
let mut rng = RandomNumberGenerator::default();
|
|
|
|
let zobrist_state = Arc::new(ZobristState::new(&mut rng));
|
|
|
|
|
|
|
|
let starting_position = Position::starting(Some(zobrist_state.clone()));
|
2024-01-24 17:16:33 -08:00
|
|
|
let mut state = State {
|
|
|
|
position: starting_position,
|
2025-06-05 08:21:32 -07:00
|
|
|
zobrist: zobrist_state,
|
2024-01-24 17:16:33 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
let mut should_print_position = true;
|
2023-12-28 15:09:15 -07:00
|
|
|
|
|
|
|
loop {
|
2024-01-24 17:16:33 -08:00
|
|
|
if should_print_position {
|
|
|
|
println!("{}", &state.position);
|
2025-06-02 17:29:52 -07:00
|
|
|
println!("{} to move.", state.position.board.active_color());
|
2024-01-24 17:16:33 -08:00
|
|
|
}
|
2023-12-28 15:09:15 -07:00
|
|
|
|
2024-01-28 09:50:39 -08:00
|
|
|
let readline = editor.readline("\n? ");
|
2023-12-28 15:09:15 -07:00
|
|
|
match readline {
|
|
|
|
Ok(line) => {
|
|
|
|
let line = line.trim();
|
|
|
|
if line.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-01-24 17:16:33 -08:00
|
|
|
match respond(line, &mut state) {
|
|
|
|
Ok(result) => {
|
|
|
|
should_print_position = result.should_print_position;
|
|
|
|
if !result.should_continue {
|
2023-12-28 15:09:15 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2025-05-19 08:41:48 -07:00
|
|
|
Err(message) => println!("{message}"),
|
2023-12-28 15:09:15 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(ReadlineError::Interrupted) => {
|
|
|
|
println!("CTRL-C");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Err(ReadlineError::Eof) => {
|
|
|
|
println!("CTRL-D");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Err(err) => {
|
2025-05-19 08:41:48 -07:00
|
|
|
println!("Error: {err}");
|
2023-12-28 15:09:15 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|