chessfriend/explorer/src/main.rs

256 lines
8.3 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
use chessfriend_board::{fen::FromFenStr, Board};
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_moves::Builder as MoveBuilder;
use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position, ValidateMove};
use clap::{Arg, Command};
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use thiserror::Error;
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,
}
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)
.subcommand_value_name("CMD")
.subcommand_help_heading("COMMANDS")
.help_template(PARSER_TEMPLATE)
.subcommand(Command::new("fen").about("Print the current position as a FEN string"))
.subcommand(
Command::new("make")
.arg(Arg::new("from").required(true))
.arg(Arg::new("to").required(true))
.alias("m")
.about("Make a move"),
)
.subcommand(
Command::new("place")
.arg(Arg::new("color").required(true))
.arg(Arg::new("piece").required(true))
.arg(Arg::new("square").required(true))
.alias("p")
.about("Place a piece on the board"),
)
.subcommand(
Command::new("sight")
.arg(Arg::new("square").required(true))
.about("Show sight of a piece on a square"),
)
.subcommand(
Command::new("moves")
.arg(Arg::new("square").required(true))
.about("Show moves of a piece on a square"),
)
.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"),
)
.subcommand(Command::new("print").about("Print the board"))
.subcommand(Command::new("quit").alias("exit").about("Quit the program"))
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
enum CommandHandlingError<'a> {
#[error("lexer error")]
LexerError,
#[error("missing {0} argument")]
MissingArgument(&'a str),
}
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)?;
let mut result = CommandResult::default();
match matches.subcommand() {
Some(("print", _matches)) => {}
Some(("quit", _matches)) => {
result.should_continue = false;
result.should_print_position = false;
}
Some(("fen", _matches)) => {
println!("{}", state.position.to_fen_str()?);
result.should_print_position = false;
}
Some(("make", matches)) => {
let from_square = Square::from_algebraic_str(
matches
.get_one::<String>("from")
.ok_or(CommandHandlingError::MissingArgument("from"))?,
)?;
let to_square = Square::from_algebraic_str(
matches
.get_one::<String>("to")
.ok_or(CommandHandlingError::MissingArgument("to"))?,
)?;
let ply = MoveBuilder::new().from(from_square).to(to_square).build()?;
state.position.make_move(ply, ValidateMove::Yes)?;
}
Some(("place", matches)) => {
let color = matches
.get_one::<String>("color")
.ok_or(CommandHandlingError::MissingArgument("color"))?;
let color = color.parse::<Color>()?;
let shape = matches
.get_one::<String>("piece")
.ok_or(CommandHandlingError::MissingArgument("piece"))?;
let shape = shape.parse::<Shape>()?;
let square = matches
.get_one::<String>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let square = Square::from_algebraic_str(square)?;
let piece = Piece::new(color, shape);
state
.position
.place_piece(piece, square, PlacePieceStrategy::default())?;
}
Some(("sight", matches)) => {
let square = matches
.get_one::<String>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let square = square.parse::<Square>()?;
let sight = state.position.sight(square);
let display = state.position.display().highlight(sight);
println!("\n{display}");
result.should_print_position = false;
}
Some(("moves", matches)) => {
let square = matches
.get_one::<String>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let square = square.parse::<Square>()?;
let movement = state.position.movement(square);
let display = state.position.display().highlight(movement);
println!("\n{display}");
result.should_print_position = false;
}
Some(("starting", _matches)) => {
let starting_position = Position::starting();
state.position = starting_position;
}
Some(("reset", matches)) => result = do_reset_command(state, matches)?,
Some((name, _matches)) => unimplemented!("{name}"),
None => unreachable!("Subcommand required"),
}
Ok(result)
}
fn do_reset_command(
state: &mut State,
matches: &clap::ArgMatches,
) -> anyhow::Result<CommandResult> {
match matches.subcommand() {
None | Some(("clear", _)) => state.position = Position::empty(),
Some(("starting", _)) => state.position = Position::starting(),
Some(("fen", matches)) => {
let fen = matches
.get_one::<String>("fen")
.ok_or(CommandHandlingError::MissingArgument("fen"))?;
let board = Board::from_fen_str(fen)?;
state.position = Position::new(board);
}
Some((name, _matches)) => unimplemented!("{name}"),
}
Ok(CommandResult::default())
}
fn main() -> Result<(), String> {
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?;
let starting_position = Position::starting();
let mut state = State {
position: starting_position,
};
let mut should_print_position = true;
loop {
if should_print_position {
println!("{}", &state.position);
println!("{} to move.", state.position.board.active_color);
}
let readline = editor.readline("\n? ");
match readline {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
match respond(line, &mut state) {
Ok(result) => {
should_print_position = result.should_print_position;
if !result.should_continue {
break;
}
}
Err(message) => println!("{message}"),
}
}
Err(ReadlineError::Interrupted) => {
println!("CTRL-C");
break;
}
Err(ReadlineError::Eof) => {
println!("CTRL-D");
break;
}
Err(err) => {
println!("Error: {err}");
break;
}
}
}
Ok(())
}