chessfriend/explorer/src/main.rs
Eryn Wells d7f426697d [board, position] Implement Zobrist hashing
This change builds on several previous changes to implement Zobrist hashing of the
board. This hash can be updated incrementally as changes are made to the board.
In order to do that, various properties of the Board struct had to made internal.
In the setters and various mutating members of Board, the hash is updated as
state changes.

The entire hashing mechanism is optional. If no ZobristState is provided when the
Board is created, the hash is never computed.

Plumb the Zobrist state through Position as well so that clients of Position (the
ultimate interface for interacting with the chess engine) can provide global
state to the whole engine.

The explorer crate gives an example of how this works. Some global state is
computed during initialization and then passed to the Position when it's created.
2025-06-05 08:22:34 -07:00

326 lines
10 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
mod make_command;
use chessfriend_board::ZobristState;
use chessfriend_board::{fen::FromFenStr, Board};
use chessfriend_core::random::RandomNumberGenerator;
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_moves::{Builder as MoveBuilder, GeneratedMove, MakeMove, ValidateMove};
use chessfriend_position::{fen::ToFenStr, PlacePieceStrategy, Position};
use clap::{Arg, Command};
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use std::sync::Arc;
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,
zobrist: Arc<ZobristState>,
}
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(false))
.about("Show moves of a piece on a square. With no argument, show all moves for the active color."),
)
.subcommand(
Command::new("movement")
.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),
#[error("no piece on {0}")]
NoPiece(Square),
}
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)) => result = do_moves_command(state, matches)?,
Some(("movement", matches)) => result = do_movement_command(state, matches)?,
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(state.zobrist.clone())),
Some(("starting", _)) => state.position = Position::starting(Some(state.zobrist.clone())),
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 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| {
let piece = state.position.get_piece(ply.origin()).unwrap();
format!("{piece}{ply}")
})
.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 {
should_continue: true,
should_print_position: false,
})
}
fn main() -> Result<(), String> {
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?;
let mut rng = RandomNumberGenerator::default();
let zobrist_state = Arc::new(ZobristState::new(&mut rng));
let starting_position = Position::starting(Some(zobrist_state.clone()));
let mut state = State {
position: starting_position,
zobrist: zobrist_state,
};
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(())
}