chessfriend/board/src/board.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

338 lines
9.5 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
use crate::{
castle,
display::DiagramFormatter,
piece_sets::{PlacePieceError, PlacePieceStrategy},
zobrist::{ZobristHash, ZobristState},
PieceSet,
};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square, Wing};
use std::sync::Arc;
pub type HalfMoveClock = u32;
pub type FullMoveClock = u32;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Board {
active_color: Color,
pieces: PieceSet,
castling_rights: castle::Rights,
en_passant_target: Option<Square>,
pub half_move_clock: HalfMoveClock,
pub full_move_number: FullMoveClock,
zobrist_hash: Option<ZobristHash>,
}
impl Board {
/// An empty board
#[must_use]
pub fn empty(zobrist: Option<Arc<ZobristState>>) -> Self {
let mut board = Self {
zobrist_hash: zobrist.map(ZobristHash::new),
..Default::default()
};
board.recompute_zobrist_hash();
board
}
/// The starting position
#[must_use]
pub fn starting(zobrist: Option<Arc<ZobristState>>) -> Self {
const BLACK_PIECES: [BitBoard; Shape::NUM] = [
BitBoard::new(0b0000_0000_1111_1111 << 48),
BitBoard::new(0b0100_0010_0000_0000 << 48),
BitBoard::new(0b0010_0100_0000_0000 << 48),
BitBoard::new(0b1000_0001_0000_0000 << 48),
BitBoard::new(0b0000_1000_0000_0000 << 48),
BitBoard::new(0b0001_0000_0000_0000 << 48),
];
const WHITE_PIECES: [BitBoard; Shape::NUM] = [
BitBoard::new(0b1111_1111_0000_0000),
BitBoard::new(0b0000_0000_0100_0010),
BitBoard::new(0b0000_0000_0010_0100),
BitBoard::new(0b0000_0000_1000_0001),
BitBoard::new(0b0000_0000_0000_1000),
BitBoard::new(0b0000_0000_0001_0000),
];
let mut board = Self {
pieces: PieceSet::new([WHITE_PIECES, BLACK_PIECES]),
zobrist_hash: zobrist.map(ZobristHash::new),
..Default::default()
};
board.recompute_zobrist_hash();
board
}
}
impl Board {
#[must_use]
pub fn active_color(&self) -> Color {
self.active_color
}
pub fn set_active_color(&mut self, color: Color) {
if color == self.active_color {
return;
}
self.active_color = color;
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_setting_active_color(color);
}
}
}
impl Board {
#[must_use]
pub fn castling_rights(&self) -> castle::Rights {
self.castling_rights
}
pub fn set_castling_rights(&mut self, rights: castle::Rights) {
if rights == self.castling_rights {
return;
}
let old_rights = self.castling_rights;
self.castling_rights = rights;
self.update_zobrist_hash_castling_rights(old_rights);
}
#[must_use]
pub fn active_color_has_castling_right(&self, wing: Wing) -> bool {
self.color_has_castling_right(self.active_color, wing)
}
#[must_use]
pub fn color_has_castling_right(&self, color: Color, wing: Wing) -> bool {
self.castling_rights.color_has_right(color, wing)
}
pub fn grant_castling_right(&mut self, color: Color, wing: Wing) {
let old_rights = self.castling_rights;
self.castling_rights.grant(color, wing);
self.update_zobrist_hash_castling_rights(old_rights);
}
pub fn revoke_all_castling_rights(&mut self) {
let old_rights = self.castling_rights;
self.castling_rights.revoke_all();
self.update_zobrist_hash_castling_rights(old_rights);
}
pub fn revoke_castling_right(&mut self, color: Color, wing: Wing) {
let old_rights = self.castling_rights;
self.castling_rights.revoke(color, wing);
self.update_zobrist_hash_castling_rights(old_rights);
}
fn update_zobrist_hash_castling_rights(&mut self, old_rights: castle::Rights) {
let new_rights = self.castling_rights;
if old_rights == new_rights {
return;
}
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_modifying_castling_rights(new_rights, old_rights);
}
}
}
impl Board {
/// Returns a copy of the current en passant square, if one exists.
#[must_use]
pub fn en_passant_target(&self) -> Option<Square> {
self.en_passant_target
}
pub fn set_en_passant_target(&mut self, square: Square) {
self.set_en_passant_target_option(Some(square));
}
pub fn set_en_passant_target_option(&mut self, square: Option<Square>) {
let old_target = self.en_passant_target;
self.en_passant_target = square;
self.update_zobrist_hash_en_passant_target(old_target);
}
pub fn clear_en_passant_target(&mut self) {
let old_target = self.en_passant_target;
self.en_passant_target = None;
self.update_zobrist_hash_en_passant_target(old_target);
}
fn update_zobrist_hash_en_passant_target(&mut self, old_target: Option<Square>) {
let new_target = self.en_passant_target;
if old_target == new_target {
return;
}
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_setting_en_passant_target(old_target, new_target);
}
}
}
impl Board {
#[must_use]
pub fn get_piece(&self, square: Square) -> Option<Piece> {
self.pieces.get(square)
}
pub fn find_pieces(&self, piece: Piece) -> BitBoard {
self.pieces.find_pieces(piece)
}
/// Place a piece on the board.
///
/// ## Errors
///
/// When is called with [`PlacePieceStrategy::PreserveExisting`], and a
/// piece already exists on `square`, this method returns a
/// [`PlacePieceError::ExistingPiece`] error.
///
pub fn place_piece(
&mut self,
piece: Piece,
square: Square,
strategy: PlacePieceStrategy,
) -> Result<Option<Piece>, PlacePieceError> {
let place_result = self.pieces.place(piece, square, strategy);
if let Ok(Some(existing_piece)) = place_result.as_ref() {
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_removing_piece(square, *existing_piece);
zobrist.update_adding_piece(square, piece);
}
}
place_result
}
pub fn remove_piece(&mut self, square: Square) -> Option<Piece> {
let removed_piece = self.pieces.remove(square);
if let Some(piece) = removed_piece {
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_removing_piece(square, piece);
}
}
removed_piece
}
}
impl Board {
pub fn iter(&self) -> impl Iterator<Item = (Square, Piece)> {
self.pieces.iter()
}
/// A [`BitBoard`] of squares occupied by pieces of all colors.
pub fn occupancy(&self) -> BitBoard {
self.pieces.occpuancy()
}
/// A [`BitBoard`] of squares that are vacant.
pub fn vacancy(&self) -> BitBoard {
!self.occupancy()
}
pub fn friendly_occupancy(&self, color: Color) -> BitBoard {
self.pieces.friendly_occupancy(color)
}
pub fn opposing_occupancy(&self, color: Color) -> BitBoard {
self.pieces.opposing_occupancy(color)
}
pub fn enemies(&self, color: Color) -> BitBoard {
self.pieces.opposing_occupancy(color)
}
/// Return a [`BitBoard`] of all pawns of a given color.
pub fn pawns(&self, color: Color) -> BitBoard {
self.pieces.find_pieces(Piece::pawn(color))
}
pub fn knights(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::knight(color))
}
pub fn bishops(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::bishop(color))
}
pub fn rooks(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::rook(color))
}
pub fn queens(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::queen(color))
}
pub fn kings(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::king(color))
}
}
impl Board {
pub fn zobrist_hash(&self) -> Option<u64> {
self.zobrist_hash.as_ref().map(ZobristHash::hash_value)
}
pub fn recompute_zobrist_hash(&mut self) {
// Avoid overlapping borrows when borrowing zobrist_hash.as_mut() and
// then also borrowing self to update the board hash by computing the
// hash with the static function first, and then setting the hash value
// on the zobrist instance. Unfortuantely this requires unwrapping
// self.zobrist_hash twice. C'est la vie.
let new_hash = self.zobrist_hash.as_ref().map(|zobrist| {
let state = zobrist.state();
ZobristHash::compute_board_hash(self, state.as_ref())
});
if let (Some(new_hash), Some(zobrist)) = (new_hash, self.zobrist_hash.as_mut()) {
zobrist.set_hash_value(new_hash);
}
}
}
impl Board {
pub fn display(&self) -> DiagramFormatter<'_> {
DiagramFormatter::new(self)
}
}
impl Board {
#[must_use]
pub fn unwrap_color(&self, color: Option<Color>) -> Color {
color.unwrap_or(self.active_color)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_board;
use chessfriend_core::piece;
#[test]
fn get_piece_on_square() {
let board = test_board![
Black Bishop on F7,
];
assert_eq!(board.get_piece(Square::F7), Some(piece!(Black Bishop)));
}
}