diff --git a/brogle/src/engine.rs b/brogle/src/engine.rs index 60b5256..008f268 100644 --- a/brogle/src/engine.rs +++ b/brogle/src/engine.rs @@ -16,7 +16,7 @@ use threadpool::ThreadPool; use super::{ protocols::{UciCommand, UciEngine, UciOption, UciResponse, UciSearchOptions}, - search::{Search, SearchResult}, + search::{SearchResult, Searcher}, Evaluator, }; @@ -29,6 +29,26 @@ enum EngineProtocol { Uci, } +/* +pub enum ScoreBound { + // The score is exact + Exact, + /// The score is less than alpha + Upper, + /// The score is greater than or equal to beta + Lower, +} + +pub struct TranspositionTableEntry { + key: u64, + bestmove: Move, + depth: usize, + score: i32, + score_bound: ScoreBound, + age: usize, +} + */ + /// A chess engine responds to inputs (such as from a GUI or terminal) and /// responds with computed outputs. The most common modern protocol is UCI. /// @@ -597,7 +617,7 @@ impl UciEngine for Engine { let cloned_result = Arc::clone(&result); // Start the search - let search = Search::new( + let search = Searcher::new( &game, timeout, cloned_stopper, diff --git a/brogle/src/lib.rs b/brogle/src/lib.rs index e10c4ff..8beb8d8 100644 --- a/brogle/src/lib.rs +++ b/brogle/src/lib.rs @@ -2,8 +2,8 @@ pub mod engine; pub use engine::*; pub mod search { - pub mod search_utils; - pub use search_utils::*; + pub mod searcher; + pub use searcher::*; } pub mod eval { diff --git a/brogle/src/protocols/uci.rs b/brogle/src/protocols/uci.rs index d917ab1..d7b9d79 100644 --- a/brogle/src/protocols/uci.rs +++ b/brogle/src/protocols/uci.rs @@ -830,19 +830,15 @@ pub trait UciEngine { /// score [cp | mate | lowerbound | upperbound] /// ``` /// - /// - `cp ` - /// The score from the engine's point of view in centipawns. + /// - `cp ` - The score from the engine's point of view in centipawns. /// - /// - `mate ` - /// Mate in `y` moves, not plies. + /// - `mate ` - Mate in `y` moves, not plies. /// /// If the engine is getting mated, use negative values for `y`. /// - /// - `lowerbound` - /// The score is just a lower bound. + /// - `lowerbound` - The score is just a lower bound. /// - /// - `upperbound` - /// The score is just an upper bound. + /// - `upperbound` - The score is just an upper bound. /// /// ```text /// currmove @@ -1597,19 +1593,13 @@ pub struct UciInfo { /// score [cp | mate | lowerbound | upperbound] /// ``` /// - /// - `cp ` - /// The score from the engine's point of view in centipawns. - /// - /// - `mate ` - /// Mate in `y` moves, not plies. + /// - `cp ` - The score from the engine's point of view in centipawns. + /// - `mate ` - Mate in `y` moves, not plies. /// /// If the engine is getting mated, use negative values for `y`. /// - /// - `lowerbound` - /// The score is just a lower bound. - /// - /// - `upperbound` - /// The score is just an upper bound. + /// - `lowerbound` - The score is just a lower bound. + /// - `upperbound` - The score is just an upper bound. pub score: Option, /// ```text diff --git a/brogle/src/search/search_utils.rs b/brogle/src/search/searcher.rs similarity index 99% rename from brogle/src/search/search_utils.rs rename to brogle/src/search/searcher.rs index 413e26a..80b836d 100644 --- a/brogle/src/search/search_utils.rs +++ b/brogle/src/search/searcher.rs @@ -2,7 +2,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::Sender; use std::sync::{Arc, RwLock}; use std::time::Duration; -use std::usize; use std::{ops::Neg, time::Instant}; use anyhow::{bail, Result}; @@ -84,7 +83,7 @@ impl Default for SearchData { } /// A struct to encapsulate the logic of searching through moves for a given a chess position. -pub struct Search<'a> { +pub struct Searcher<'a> { game: &'a Game, timeout: Duration, stopper: Arc, @@ -101,7 +100,7 @@ pub struct Search<'a> { */ } -impl<'a> Search<'a> { +impl<'a> Searcher<'a> { /// Create a new search that will search the provided position at a depth of 1. pub fn new( game: &'a Game, diff --git a/brogle_core/brogle_types/src/magicgen.rs b/brogle_core/brogle_types/src/magicgen.rs index b821ebe..38ce18b 100644 --- a/brogle_core/brogle_types/src/magicgen.rs +++ b/brogle_core/brogle_types/src/magicgen.rs @@ -13,9 +13,10 @@ struct MagicEntry { fn magic_index(entry: &MagicEntry, blockers: Bitboard) -> usize { let blockers = blockers.0 & entry.mask; let hash = blockers.wrapping_mul(entry.magic); - let index = (hash >> entry.shift) as usize; + // let index = (hash >> entry.shift) as usize; // entry.offset + index - index + // index + (hash >> entry.shift) as usize } // #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -111,7 +112,7 @@ fn find_and_print_all_magics(slider: &SlidingPiece, piece_name: &str) { let mut table_size = 0; for tile in Tile::iter() { - let index_bits = slider.blockers(tile).population() as u8; + let index_bits = slider.blockers(tile).population(); let (entry, table) = find_magic(slider, tile, index_bits); println!( diff --git a/brogle_core/brogle_types/src/tile.rs b/brogle_core/brogle_types/src/tile.rs index 53ed374..a7ca4f8 100644 --- a/brogle_core/brogle_types/src/tile.rs +++ b/brogle_core/brogle_types/src/tile.rs @@ -21,7 +21,7 @@ use super::{Bitboard, Color}; /// /// This bit pattern is also known as [Least Significant File Mapping](https://www.chessprogramming.org/Square_Mapping_Considerations#Deduction_on_Files_and_Ranks), /// so `tile = file + rank * 8`. -#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] #[repr(transparent)] pub struct Tile(pub(crate) u8); @@ -695,7 +695,7 @@ impl fmt::Debug for Tile { } } -#[derive(Clone, Copy, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Eq, PartialOrd, Ord, Hash, Default)] #[repr(transparent)] pub struct Rank(pub(crate) u8); @@ -836,6 +836,11 @@ impl Rank { } } + /// `const` analog of `==`. + pub const fn is(&self, other: &Self) -> bool { + self.0 == other.0 + } + // Index in Little Endian (default) pub const fn index_le(&self) -> usize { self.0 as usize @@ -1053,7 +1058,7 @@ impl fmt::Debug for Rank { } } -#[derive(Clone, Copy, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Eq, PartialOrd, Ord, Hash, Default)] pub struct File(pub(crate) u8); impl File { @@ -1131,6 +1136,11 @@ impl File { Self::new(file_int) } + /// `const` analog of `==`. + pub const fn is(&self, other: &Self) -> bool { + self.0 == other.0 + } + pub const fn inner(&self) -> u8 { self.0 } diff --git a/brogle_core/src/main.rs b/brogle_core/src/main.rs index e348288..a5bc472 100644 --- a/brogle_core/src/main.rs +++ b/brogle_core/src/main.rs @@ -15,13 +15,26 @@ fn main() { println!("pos: {}\nkey: {}", game.position(), game.key()); */ - let mut game = Game::default(); - game.make_move(Move::from_uci(&game, "b1a3").unwrap()); - println!("repetition? {}", game.is_repetition()); - game.make_move(Move::from_uci(&game, "b8a6").unwrap()); - println!("repetition? {}", game.is_repetition()); - game.make_move(Move::from_uci(&game, "a3b1").unwrap()); - println!("repetition? {}", game.is_repetition()); - game.make_move(Move::from_uci(&game, "a6b8").unwrap()); - println!("repetition? {}", game.is_repetition()); + // let fen = FEN_STARTPOS; + // let moves = ["b1a3", "b8a6", "a3b1", "a6b8"]; + // let fen = "k7/8/8/8/3p4/8/4P3/K7 w - - 0 1"; // Testing hash keys with en passant + // let moves = ["e2e4", "d4e3"]; + let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; + let moves = ["e1g1"]; + let mut game = Game::from_fen(fen).unwrap(); + + for mv in moves { + game.make_move(Move::from_uci(&game, mv).unwrap()); + // println!("repetition? {}", game.is_repetition()); + } + + // let mut game = Game::default(); + // game.make_move(Move::from_uci(&game, "b1a3").unwrap()); + // println!("repetition? {}", game.is_repetition()); + // game.make_move(Move::from_uci(&game, "b8a6").unwrap()); + // println!("repetition? {}", game.is_repetition()); + // game.make_move(Move::from_uci(&game, "a3b1").unwrap()); + // println!("repetition? {}", game.is_repetition()); + // game.make_move(Move::from_uci(&game, "a6b8").unwrap()); + // println!("repetition? {}", game.is_repetition()); } diff --git a/brogle_core/src/position.rs b/brogle_core/src/position.rs index b336c7e..62fbafb 100644 --- a/brogle_core/src/position.rs +++ b/brogle_core/src/position.rs @@ -5,13 +5,10 @@ use std::{ }; use anyhow::{anyhow, bail, Result}; -use brogle_types::NUM_CASTLING_RIGHTS; - -use crate::ZOBRIST_TABLE; use super::{ - Bitboard, Color, File, Move, MoveGenerator, MoveKind, Piece, PieceKind, Rank, Tile, - FEN_STARTPOS, NUM_COLORS, NUM_PIECE_TYPES, + Bitboard, Color, File, Move, MoveGenerator, MoveKind, Piece, PieceKind, Rank, Tile, ZobristKey, + FEN_STARTPOS, NUM_CASTLING_RIGHTS, NUM_COLORS, NUM_PIECE_TYPES, }; #[derive(Clone, Debug, PartialEq, Eq, Default)] @@ -206,7 +203,7 @@ impl CastlingRights { /// assert_eq!(none.index(), 0); /// ``` pub const fn index(&self) -> usize { - (self.kingside[0].is_some() as usize) << 0 + (self.kingside[0].is_some() as usize) | (self.kingside[1].is_some() as usize) << 1 | (self.queenside[0].is_some() as usize) << 2 | (self.queenside[1].is_some() as usize) << 3 @@ -283,7 +280,7 @@ pub struct Position { fullmove: usize, /// Zobrist hash key of this position - key: u64, + key: ZobristKey, } impl Position { @@ -302,17 +299,22 @@ impl Position { /// assert_eq!(state.to_fen(), "8/8/8/8/8/8/8/8 w - - 0 1"); /// ``` pub fn new() -> Self { - let mut pos = Self { - board: ChessBoard::new(), - current_player: Color::White, - castling_rights: CastlingRights::new(), - ep_tile: None, + let board = ChessBoard::new(); + let castling_rights = CastlingRights::new(); + let current_player = Color::White; + let ep_tile = None; + + let key = ZobristKey::from_parts(&board, ep_tile, &castling_rights, current_player); + + Self { + board, + current_player, + castling_rights, + ep_tile, halfmove: 0, fullmove: 1, - key: 0, - }; - pos.update_zobrist_key(); - pos + key, + } } /// Creates a new [`Position`] from the provided FEN string. @@ -346,7 +348,7 @@ impl Position { "Invalid FEN string: FEN string must have valid fullmove counter. Got {fullmove}" )))?; - pos.update_zobrist_key(); + pos.key = ZobristKey::new(&pos); Ok(pos) } @@ -407,8 +409,8 @@ impl Position { } /// Fetch the Zobrist hash key of this position. - pub fn key(&self) -> u64 { - self.key + pub fn key(&self) -> &ZobristKey { + &self.key } /// Returns `true` if the half-move counter is 50 or greater. @@ -447,11 +449,6 @@ impl Position { && self.ep_tile() == other.ep_tile() } - /// Recompute the Zobrist hash key of this position. - fn update_zobrist_key(&mut self) { - self.key = ZOBRIST_TABLE.hash(self); - } - /// Checks if the provided move is legal to perform. /// /// If `Ok()`, the move is legal. @@ -538,8 +535,14 @@ impl Position { let to = mv.to(); let from = mv.from(); - // Clear the EP tile from the last move - self.ep_tile = None; + // Un-hash the piece at `from`. + self.key.hash_piece(from, piece); + + // Clear the EP tile from the last move (and un-hash it) + self.key.hash_optional_ep_tile(self.ep_tile.take()); + + // Un-hash the castling rights + self.key.hash_castling_rights(&self.castling_rights); // Increment move counters self.halfmove += 1; // This is reset if a capture occurs or a pawn moves @@ -559,12 +562,17 @@ impl Position { // If the capture was on a rook's starting square, disable that side's castling. // Either a rook was captured, or there wasn't a rook there, in which case castling on that side is already disabled + // TODO: Chess960 if to == Tile::A1.rank_relative_to(captured_color) { + self.key.hash_castling_rights(&self.castling_rights); self.castling_rights.queenside[captured_color].take(); + self.key.hash_castling_rights(&self.castling_rights); } if to == Tile::H1.rank_relative_to(captured_color) { + self.key.hash_castling_rights(&self.castling_rights); self.castling_rights.kingside[captured_color].take(); + self.key.hash_castling_rights(&self.castling_rights); } // Reset halfmove counter, since a capture occurred @@ -572,8 +580,10 @@ impl Position { } else if mv.is_pawn_double_push() { // Double pawn push, so set the EP square self.ep_tile = from.forward_by(color, 1); + self.key.hash_optional_ep_tile(self.ep_tile()); } else if mv.is_castle() { let castle_index = mv.is_short_castle() as usize; + // TODO: Chess960 let old_rook_tile = [Tile::A1, Tile::H1][castle_index].rank_relative_to(color); let new_rook_tile = [Tile::D1, Tile::F1][castle_index].rank_relative_to(color); @@ -582,8 +592,11 @@ impl Position { self.board_mut().set(rook, new_rook_tile); // Disable castling + // Hashing must be done before and after castling rights are changed so that the proper hash key is used + self.key.hash_castling_rights(&self.castling_rights); self.castling_rights.kingside[color] = None; self.castling_rights.queenside[color] = None; + self.key.hash_castling_rights(&self.castling_rights); } // Next, handle special cases for Pawn (halfmove), Rook, and King (castling) @@ -591,20 +604,24 @@ impl Position { PieceKind::Pawn => self.halfmove = 0, PieceKind::Rook => { // Disable castling if a rook moved - // self.castling_rights.kingside[color] &= from != Tile::H1.rank_relative_to(color); - // self.castling_rights.queenside[color] &= from != Tile::A1.rank_relative_to(color); if from == Tile::A1.rank_relative_to(color) { + self.key.hash_castling_rights(&self.castling_rights); self.castling_rights.queenside[color].take(); + self.key.hash_castling_rights(&self.castling_rights); } if from == Tile::H1.rank_relative_to(color) { + self.key.hash_castling_rights(&self.castling_rights); self.castling_rights.kingside[color].take(); + self.key.hash_castling_rights(&self.castling_rights); } } PieceKind::King => { // Disable all castling + self.key.hash_castling_rights(&self.castling_rights); self.castling_rights.kingside[color] = None; self.castling_rights.queenside[color] = None; + self.key.hash_castling_rights(&self.castling_rights); } _ => {} } @@ -617,10 +634,14 @@ impl Position { // Place the piece in it's new position self.board_mut().set(piece, to); + // Hash the piece at `to`. + self.key.hash_piece(to, piece); + // Next player's turn self.toggle_current_player(); - self.update_zobrist_key(); + // Toggle the hash of the current player + self.key.hash_side_to_move(self.current_player()); } } diff --git a/brogle_core/src/prng.rs b/brogle_core/src/prng.rs index 30bb99c..1772918 100644 --- a/brogle_core/src/prng.rs +++ b/brogle_core/src/prng.rs @@ -23,12 +23,12 @@ impl XoShiRo { } /// Generates the next pseudo-random number in the sequence. - pub fn next(&mut self) -> u64 { + pub fn get_next(&mut self) -> u64 { Self::xoshiro(self.0).0 } /// `const` analog of [`XoShiRo::next`], returning `(next, Self)`. - pub const fn const_next(self) -> (u64, Self) { + pub const fn get_next_const(self) -> (u64, Self) { let (result, s) = Self::xoshiro(self.0); (result, Self(s)) } @@ -50,3 +50,9 @@ impl XoShiRo { (result, s) } } + +impl Default for XoShiRo { + fn default() -> Self { + Self::new() + } +} diff --git a/brogle_core/src/zobrist.rs b/brogle_core/src/zobrist.rs index 4c20fe0..a6a73a8 100644 --- a/brogle_core/src/zobrist.rs +++ b/brogle_core/src/zobrist.rs @@ -1,33 +1,122 @@ -use crate::{CastlingRights, ChessBoard, XoShiRo}; +use std::fmt; -use super::Position; -use brogle_types::{Color, Tile, NUM_CASTLING_RIGHTS, NUM_PIECES, NUM_TILES}; +use super::{ + CastlingRights, ChessBoard, Color, Piece, Position, Rank, Tile, XoShiRo, NUM_CASTLING_RIGHTS, + NUM_COLORS, NUM_PIECES, NUM_TILES, +}; /// Stores Zobrist hash keys, for hashing [`Position`]s. /// /// Initialized upon program startup with library-supplied keys that remain constant between compilations. -pub const ZOBRIST_TABLE: ZobristTable = ZobristTable::new(); +const ZOBRIST_TABLE: ZobristHashTable = ZobristHashTable::new(); + +/// Represents a key generated from a Zobrist Hash +#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone)] +pub struct ZobristKey(u64); + +impl ZobristKey { + /// Generates a new [`ZobristKey`] from the supplied [`Position`]. + pub fn new(position: &Position) -> Self { + Self::from_parts( + position.board(), + position.ep_tile(), + position.castling_rights(), + position.current_player(), + ) + } + + /// Generates a [`ZobristKey`] from the provided components of a [`Position`]. + pub fn from_parts( + board: &ChessBoard, + ep_tile: Option, + castling_rights: &CastlingRights, + color: Color, + ) -> Self { + let mut key = Self::default(); + + // Hash all pieces on the board + for (tile, piece) in board.pieces() { + key.hash_piece(tile, piece); + } + + // Hash the en passant square, if it exists + key.hash_optional_ep_tile(ep_tile); + + // Hash the castling rights + key.hash_castling_rights(castling_rights); + + // Hash the side-to-move + key.hash_side_to_move(color); + + key + } + + /// Adds/removes `hash_key` to this [`ZobristKey`]. + /// + /// This is done internally with the XOR operator. + pub fn hash(&mut self, hash_key: u64) { + self.0 ^= hash_key; + } + + /// Adds/removes the hash for the provided `piece`/`tile` combo to this [`ZobristKey`]. + pub fn hash_piece(&mut self, tile: Tile, piece: Piece) { + self.hash(ZOBRIST_TABLE.piece_keys[tile][piece]); + } + + /// Adds/removes the hash for the provided `ep_tile` to this [`ZobristKey`]. + pub fn hash_ep_tile(&mut self, ep_tile: Tile) { + self.hash(ZOBRIST_TABLE.ep_keys[ep_tile]); + } + + /// Adds/removes the hash for the provided `ep_tile` to this [`ZobristKey`]. + pub fn hash_optional_ep_tile(&mut self, ep_tile: Option) { + // This works because all tiles where EP isn't possible (including Tile::default) have a hash value of 0 + self.hash_ep_tile(ep_tile.unwrap_or_default()); + } + + /// Adds/removes the hash for the provided `castling_rights` to this [`ZobristKey`]. + pub fn hash_castling_rights(&mut self, castling_rights: &CastlingRights) { + self.hash(ZOBRIST_TABLE.castling_keys[castling_rights]); + } + + /// Adds/removes the hash for when the side-to-move is Black to this [`ZobristKey`]. + pub fn hash_side_to_move(&mut self, color: Color) { + self.hash(ZOBRIST_TABLE.color_key[color]); + } +} + +impl fmt::Display for ZobristKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} /// Encapsulates the logic of Zobrist hashing. /// /// Primarily used to create the [`ZOBRIST_TABLE`] constant, though this struct remains public in case it has other uses... -pub struct ZobristTable { +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct ZobristHashTable { /// One unique key for every possible piece and every possible tile. - piece_keys: [[u64; NUM_PIECES]; Tile::COUNT], + piece_keys: [[u64; NUM_PIECES]; NUM_TILES], + /// One unique key for every tile where en passant is possible. - ep_keys: [u64; Tile::COUNT], + ep_keys: [u64; NUM_TILES], + /// One key for every possible combination of castling rights. castling_keys: [u64; NUM_CASTLING_RIGHTS], - /// One key for the side-to-move (only if side-to-move is Black). - color_key: u64, + + /// One key for the side-to-move (only if side-to-move is Black- White's key is 0). + color_key: [u64; NUM_COLORS], } -impl ZobristTable { +impl ZobristHashTable { /// Initialize this table, generating keys via the [`XoShiRo`] struct. - pub const fn new() -> Self { - let mut piece_keys = [[0; NUM_PIECES]; Tile::COUNT]; - let color_key; - let mut ep_keys = [0; Tile::COUNT]; + /// + /// This is only done once, at compilation, and is stored in the global [`ZOBRIST_TABLE`] constant. + const fn new() -> Self { + let mut piece_keys = [[0; NUM_PIECES]; NUM_TILES]; + let mut color_key = [0; NUM_COLORS]; + let mut ep_keys = [0; NUM_TILES]; let mut castling_keys = [0; NUM_CASTLING_RIGHTS]; let mut prng = XoShiRo::new(); @@ -39,15 +128,20 @@ impl ZobristTable { // Initialize keys for pieces while j < NUM_PIECES { let key; - (key, prng) = prng.const_next(); + (key, prng) = prng.get_next_const(); piece_keys[i][j] = key; j += 1; } // Initialize keys for en passant squares - let key; - (key, prng) = prng.const_next(); - ep_keys[i] = key; + let rank = Tile::from_index_unchecked(i).rank(); + if rank.is(&Rank::THREE) || rank.is(&Rank::SIX) { + // Since en passant can only happen on ranks 3 and 6, we only need to store hash keys for those ranks + let key; + (key, prng) = prng.get_next_const(); + ep_keys[i] = key; + } + i += 1; } @@ -55,14 +149,15 @@ impl ZobristTable { i = 0; while i < NUM_CASTLING_RIGHTS { let key; - (key, prng) = prng.const_next(); + (key, prng) = prng.get_next_const(); castling_keys[i] = key; i += 1; } // Initialize keys for side-to-move - let (key, _) = prng.const_next(); - color_key = key; + // Only Black has a key, since White's is just 0 + let (key, _) = prng.get_next_const(); + color_key[Color::Black.index()] = key; Self { piece_keys, @@ -71,43 +166,4 @@ impl ZobristTable { color_key, } } - - pub fn hash_parts( - &self, - board: &ChessBoard, - ep_tile: Option, - castling_rights: &CastlingRights, - color: Color, - ) -> u64 { - let mut hash = 0; - - // Hash all pieces on the board - for (tile, piece) in board.pieces() { - hash ^= self.piece_keys[tile][piece]; - } - - // Hash the en passant square, if it exists - if let Some(ep_tile) = ep_tile { - hash ^= self.ep_keys[ep_tile]; - } - - // Hash the castling rights - hash ^= self.castling_keys[castling_rights]; - - // Hash the side-to-move - if color.is_black() { - hash ^= self.color_key - } - - hash - } - - pub fn hash(&self, position: &Position) -> u64 { - self.hash_parts( - position.board(), - position.ep_tile(), - position.castling_rights(), - position.current_player(), - ) - } }