[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.
This commit is contained in:
Eryn Wells 2025-06-05 08:21:32 -07:00
parent 404212363e
commit d7f426697d
11 changed files with 395 additions and 31 deletions

View file

@ -2,14 +2,16 @@
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 {
@ -28,6 +30,7 @@ impl Default for CommandResult {
struct State {
position: Position,
zobrist: Arc<ZobristState>,
}
fn command_line() -> Command {
@ -171,10 +174,6 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
}
Some(("moves", matches)) => result = do_moves_command(state, matches)?,
Some(("movement", matches)) => result = do_movement_command(state, matches)?,
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"),
@ -188,8 +187,8 @@ fn do_reset_command(
matches: &clap::ArgMatches,
) -> anyhow::Result<CommandResult> {
match matches.subcommand() {
None | Some(("clear", _)) => state.position = Position::empty(),
Some(("starting", _)) => state.position = Position::starting(),
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")
@ -273,9 +272,13 @@ fn do_movement_command(
fn main() -> Result<(), String> {
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?;
let starting_position = Position::starting();
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;