chessfriend/board/src/board.rs
Eryn Wells 634876822b [explorer] Add a load command
Loads a board position from a FEN string.

Plumb through setting the Zobrist state on an existing board. Rebuild the hash
when setting the state.
2025-06-08 16:57:36 -07:00

398 lines
11 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);
}
}
pub fn zobrist_state(&self) -> Option<Arc<ZobristState>> {
self.zobrist_hash.as_ref().map(ZobristHash::state)
}
pub fn set_zobrist_state(&mut self, state: Arc<ZobristState>) {
self.zobrist_hash = Some(ZobristHash::new(state));
self.recompute_zobrist_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, random::RandomNumberGenerator};
#[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)));
}
// MARK: - Zobrist Hashing
fn test_state() -> ZobristState {
let mut rng = RandomNumberGenerator::default();
ZobristState::new(&mut rng)
}
#[test]
fn zobrist_hash_set_for_empty_board() {
let state = Arc::new(test_state());
let board = Board::empty(Some(state.clone()));
let hash = board.zobrist_hash();
assert_eq!(hash, Some(0));
}
#[test]
fn zobrist_hash_set_for_starting_position_board() {
let state = Arc::new(test_state());
let board = Board::starting(Some(state.clone()));
let hash = board.zobrist_hash();
assert!(hash.is_some());
}
#[test]
fn zobrist_hash_updated_when_changing_active_color() {
let state = Arc::new(test_state());
let mut board = Board::empty(Some(state.clone()));
board.set_active_color(Color::Black);
// Just verify that the value is real and has changed. The actual value
// computation is covered by the tests in zobrist.rs.
let hash = board.zobrist_hash();
assert!(hash.is_some());
assert_ne!(hash, Some(0));
}
#[test]
fn zobrist_hash_updated_when_changing_en_passant_target() {
let state = Arc::new(test_state());
let mut board = Board::empty(Some(state.clone()));
board.set_en_passant_target(Square::C3);
// Just verify that the value is real and has changed. The actual value
// computation is covered by the tests in zobrist.rs.
let hash = board.zobrist_hash();
assert!(hash.is_some());
assert_ne!(hash, Some(0));
}
}