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.
398 lines
11 KiB
Rust
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));
|
|
}
|
|
}
|