From 3f1ea37b324e58fd398b0923cdb41ccf8566bbb2 Mon Sep 17 00:00:00 2001 From: Danny Hammer Date: Mon, 12 Aug 2024 15:44:01 -0600 Subject: [PATCH] feat: added support for Zobrist hashing refactor: adjusted how `CastlingRights` work internally --- .gitignore | 3 +- brogle/src/engine.rs | 6 +- brogle/src/search/search_utils.rs | 1 + brogle_core/brogle_types/src/color.rs | 15 ++ brogle_core/brogle_types/src/utils.rs | 3 + brogle_core/src/lib.rs | 6 + brogle_core/src/main.rs | 39 ++-- brogle_core/src/movegen.rs | 36 ++-- brogle_core/src/position.rs | 261 ++++++++++++++++---------- brogle_core/src/prng.rs | 52 +++++ brogle_core/src/zobrist.rs | 98 ++++++++++ brogle_tools/src/perft_bench.rs | 1 - brogle_tools/src/perftree_script.rs | 95 +--------- scripts/make-release.sh | 2 +- scripts/run-fastchess.sh | 4 +- 15 files changed, 393 insertions(+), 229 deletions(-) create mode 100644 brogle_core/src/prng.rs create mode 100644 brogle_core/src/zobrist.rs diff --git a/.gitignore b/.gitignore index bd621e9..a00748a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ releases/** # Fastchess config.json -1 \ No newline at end of file +1 +*.log \ No newline at end of file diff --git a/brogle/src/engine.rs b/brogle/src/engine.rs index 4e953b7..60b5256 100644 --- a/brogle/src/engine.rs +++ b/brogle/src/engine.rs @@ -364,7 +364,7 @@ impl Engine { } /// Executes the `perft` command, performing `perft(depth)` for benchmarking and testing. - fn perft(&self, depth: usize, pretty: bool, split: bool) { + pub fn perft(&self, depth: usize, pretty: bool, split: bool) { // Man, I wish I could just pass `split` and `pretty` in directly if split { if pretty { @@ -425,9 +425,9 @@ impl Engine { } /// Executes the `move` command, applying the provided move(s) to the current position. - fn make_move(&mut self, moves: Vec) -> Result<()> { + pub fn make_move>(&mut self, moves: impl IntoIterator) -> Result<()> { for mv_string in moves { - let mv = Move::from_uci(&self.game, &mv_string)?; + let mv = Move::from_uci(&self.game, mv_string.as_ref())?; self.game.make_move(mv); } diff --git a/brogle/src/search/search_utils.rs b/brogle/src/search/search_utils.rs index 91c0513..03cb920 100644 --- a/brogle/src/search/search_utils.rs +++ b/brogle/src/search/search_utils.rs @@ -356,6 +356,7 @@ impl<'a> Search<'a> { // eprintln!("Negamax: returning {best} at ply {ply}"); Ok(best) + // Ok(alpha) } fn quiescence(&mut self, game: &Game, ply: usize, mut alpha: i32, beta: i32) -> Result { diff --git a/brogle_core/brogle_types/src/color.rs b/brogle_core/brogle_types/src/color.rs index 9167165..2855fdd 100644 --- a/brogle_core/brogle_types/src/color.rs +++ b/brogle_core/brogle_types/src/color.rs @@ -79,6 +79,21 @@ impl Color { unsafe { std::mem::transmute(bits) } } + /// Creates a new [`Color`] from a `bool`, where `false = White`. + /// + /// # Example + /// ``` + /// # use brogle_types::Color; + /// let white = Color::from_bool(false); + /// assert_eq!(white, Color::White); + /// + /// let black = Color::from_bool(true); + /// assert_eq!(black, Color::Black); + /// ``` + pub const fn from_bool(color: bool) -> Self { + Self::from_bits_unchecked(color as u8) + } + /// Returns `true` if this [`Color`] is White. pub const fn is_white(&self) -> bool { *self as u8 & 1 == 0 diff --git a/brogle_core/brogle_types/src/utils.rs b/brogle_core/brogle_types/src/utils.rs index d496cf3..6d44ca1 100644 --- a/brogle_core/brogle_types/src/utils.rs +++ b/brogle_core/brogle_types/src/utils.rs @@ -8,7 +8,10 @@ pub const FEN_KIWIPETE: &str = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPP /// pub const MAX_NUM_MOVES: usize = 218; +pub const NUM_TILES: usize = 64; pub const NUM_PIECE_TYPES: usize = 6; +pub const NUM_PIECES: usize = 6; +pub const NUM_CASTLING_RIGHTS: usize = 16; pub const NUM_COLORS: usize = 2; diff --git a/brogle_core/src/lib.rs b/brogle_core/src/lib.rs index 64ec545..9a80eb2 100644 --- a/brogle_core/src/lib.rs +++ b/brogle_core/src/lib.rs @@ -4,12 +4,16 @@ pub mod movegen; pub mod moves; pub mod perft; pub mod position; +pub mod prng; +pub mod zobrist; // pub use magicgen::*; pub use movegen::*; pub use moves::*; pub use perft::*; pub use position::*; +pub use prng::*; +pub use zobrist::*; /// Re-exports all the things you'll need. pub mod prelude { @@ -17,4 +21,6 @@ pub mod prelude { pub use crate::moves::*; pub use crate::perft::*; pub use crate::position::*; + pub use crate::prng::*; + pub use crate::zobrist::*; } diff --git a/brogle_core/src/main.rs b/brogle_core/src/main.rs index 438e65d..6b38e0d 100644 --- a/brogle_core/src/main.rs +++ b/brogle_core/src/main.rs @@ -1,25 +1,26 @@ use brogle_core::*; -// TODO: Get rid of this file once library is published - -// include!(concat!(env!("OUT_DIR"), "/hello.rs")); fn main() { - // for from in Tile::iter() { - // for to in Tile::iter() { - // let ray = ray_between_inclusive(from, to); - // if ray.is_nonempty() { - // println!("{from} -> {to} (inclusive)\n{ray:?}"); - // } + // let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N4Q/PPPBBPPP/R3K2R b KQkq - 0 1"; // e8g8 is legal + // let fen = "r3k2r/p1pNqpb1/bn2pnp1/3P4/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 1"; // e8c8 is legal + let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/5Q1p/PPPBBPPP/RN2K2R w KQkq - 0 1"; // e1c1 is NOT legal + let game = Game::from_fen(fen).unwrap(); - // let ray = ray_between_exclusive(from, to); - // if ray.is_nonempty() { - // println!("{from} -> {to} (exclusive)\n{ray:?}"); - // } + for mv in game.legal_moves() { + if mv.from() == Tile::E1 { + println!("{mv}"); + } + } - // let ray = ray_containing(from, to); - // if ray.is_nonempty() { - // println!("{from} -> {to} (containing)\n{ray:?}"); - // } - // } - // } + /* + let mut game = Game::default(); + game.make_move(Move::from_uci(&game, "b1a3").unwrap()); + println!("pos: {}\nkey: {}", game.position(), game.zobrist_key()); + game.make_move(Move::from_uci(&game, "b8a6").unwrap()); + println!("pos: {}\nkey: {}", game.position(), game.zobrist_key()); + game.make_move(Move::from_uci(&game, "a3b1").unwrap()); + println!("pos: {}\nkey: {}", game.position(), game.zobrist_key()); + game.make_move(Move::from_uci(&game, "a6b8").unwrap()); + println!("pos: {}\nkey: {}", game.position(), game.zobrist_key()); + */ } diff --git a/brogle_core/src/movegen.rs b/brogle_core/src/movegen.rs index ccf35b7..9ce7e86 100644 --- a/brogle_core/src/movegen.rs +++ b/brogle_core/src/movegen.rs @@ -418,22 +418,22 @@ impl MoveGenerator { // A king can move anywhere that isn't attacked by the enemy let enemy_attacks = self.attacks_by_color(color.opponent()); - let castling_availability = |side: [bool; 2], rook_tile, dst_tile| { + let castling_availability = |side: [Option; NUM_COLORS], dst_tile: Tile| { // Check if we can castle at all on this side - let can_castle = Bitboard::from_bool(side[color]); + if let Some(rook_tile) = side[color] { + // No squares between the King and his destination may be under attack + let must_be_safe = ray_between_inclusive(from, dst_tile); + let is_safe = (must_be_safe & enemy_attacks).is_empty(); - // No squares between the Rook and King may be under attack by the enemy - let castling = can_castle & ray_between_inclusive(from, rook_tile); + // All squares between the King and Rook must be empty + let must_be_clear = ray_between_exclusive(from, rook_tile); + let is_clear = (must_be_clear & blockers).is_empty(); - // All squares within the castling range must be empty - let squares_between = ray_between_inclusive(from, dst_tile); - - let not_attacked = (enemy_attacks & castling).is_empty(); - // There can be at most one piece in this ray (the King) - let is_clear = (squares_between & blockers).population() <= 1; - - if not_attacked && is_clear { - castling + if is_safe && is_clear { + Bitboard::from_tile(dst_tile) + } else { + Bitboard::EMPTY_BOARD + } } else { Bitboard::EMPTY_BOARD } @@ -442,16 +442,14 @@ impl MoveGenerator { let kingside = castling_availability( self.position().castling_rights().kingside, Tile::G1.rank_relative_to(color), - Tile::G1.rank_relative_to(color), ); - // println!("kingside:\n{kingside}\n"); + // eprintln!("kingside:\n{kingside}\n"); let queenside = castling_availability( self.position().castling_rights().queenside, Tile::C1.rank_relative_to(color), - Tile::B1.rank_relative_to(color), ); - // println!("queenside:\n{queenside}\n"); + // eprintln!("queenside:\n{queenside}\n"); let attack_or_castle = pseudo_legal | kingside | queenside; // Any attacks or castling let safe_squares = !(enemy_attacks | self.discoverable_checks); // Not attacked by the enemy (even after King retreats) @@ -468,11 +466,11 @@ impl MoveGenerator { if from == Tile::KING_START_SQUARES[color] { if to == Tile::KINGSIDE_CASTLE_SQUARES[color] - && self.position().castling_rights().kingside[color] + && self.position().castling_rights().kingside[color].is_some() { kind = MoveKind::KingsideCastle; } else if to == Tile::QUEENSIDE_CASTLE_SQUARES[color] - && self.position().castling_rights().queenside[color] + && self.position().castling_rights().queenside[color].is_some() { kind = MoveKind::QueensideCastle; } diff --git a/brogle_core/src/position.rs b/brogle_core/src/position.rs index 78d07a6..9bfc2b8 100644 --- a/brogle_core/src/position.rs +++ b/brogle_core/src/position.rs @@ -5,6 +5,9 @@ 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, @@ -39,9 +42,9 @@ impl Game { } /// Consumes `self` and returns a [`Game`] after having applied the provided [`Move`]. - pub fn with_move_made(&self, chessmove: Move) -> Self { + pub fn with_move_made(&self, mv: Move) -> Self { let mut new = self.clone(); - new.make_move(chessmove); + new.make_move(mv); new } @@ -65,7 +68,6 @@ impl Game { /// ``` pub fn is_repetition(&self) -> bool { for prev_pos in self.history.iter().rev().skip(1).step_by(2) { - println!("Checking if {prev_pos} == {}", self.position()); if prev_pos == self.position() { return true; } @@ -88,28 +90,24 @@ impl Game { } /// Applies the move, if it is legal to make. If it is not legal, returns an `Err` explaining why. - pub fn make_move_checked(&mut self, chessmove: Move) -> Result<()> { - let (is_legal, reason) = self.is_legal(chessmove); - if is_legal { - self.make_move(chessmove); - Ok(()) - } else { - bail!("{reason}") - } + pub fn make_move_checked(&mut self, mv: Move) -> Result<()> { + self.check_legality_of(mv)?; + self.make_move(mv); + Ok(()) } /// Applies the provided [`Move`]. No enforcement of legality. - pub fn make_move(&mut self, chessmove: Move) { + pub fn make_move(&mut self, mv: Move) { self.history.push(self.position().clone()); - self.moves.push(chessmove); - let new_pos = self.position().clone().with_move_made(chessmove); + self.moves.push(mv); + let new_pos = self.position().clone().with_move_made(mv); self.movegen = MoveGenerator::new_legal(new_pos); } /// Applies the provided [`Move`]s. No enforcement of legality. pub fn make_moves(&mut self, moves: impl IntoIterator) { - for chessmove in moves { - self.make_move(chessmove); + for mv in moves { + self.make_move(mv); } } @@ -139,51 +137,59 @@ impl Deref for Game { } } +// TODO: Refactor this to be Option instead of bool arrays /// Represents the castling rights of both players #[derive(Clone, PartialEq, Eq, Debug, Hash, Default)] pub struct CastlingRights { - pub(crate) kingside: [bool; 2], - pub(crate) queenside: [bool; 2], + /// If a right is `Some(tile)`, then `tile` is the *rook*'s location + pub(crate) kingside: [Option; NUM_COLORS], + pub(crate) queenside: [Option; NUM_COLORS], } impl CastlingRights { - const fn new() -> Self { + pub const fn new() -> Self { Self { - kingside: [false; 2], - queenside: [false; 2], + kingside: [None; NUM_COLORS], + queenside: [None; NUM_COLORS], } } - fn from_uci(castling: &str) -> Result { - if castling.is_empty() { - bail!("Invalid castling rights: Got empty string."); + pub fn from_uci(uci: &str) -> Result { + let mut kingside = [None; NUM_COLORS]; + let mut queenside = [None; NUM_COLORS]; + + if uci.contains(['K', 'k', 'Q', 'q']) { + kingside[Color::White] = uci.contains('K').then_some(Tile::H1); + queenside[Color::White] = uci.contains('Q').then_some(Tile::A1); + kingside[Color::Black] = uci.contains('k').then_some(Tile::H8); + queenside[Color::Black] = uci.contains('q').then_some(Tile::A8); } else { - let mut kingside = [false; 2]; - let mut queenside = [false; 2]; - kingside[Color::White] = castling.contains('K'); - queenside[Color::White] = castling.contains('Q'); - kingside[Color::Black] = castling.contains('k'); - queenside[Color::Black] = castling.contains('q'); - Ok(Self { - kingside, - queenside, - }) + // TODO: Support Chess960 + // Don't we need the King's tile here? + // for c in uci.chars() { + // let color = Color::from_bool(c.is_ascii_lowercase()); + // } } + + Ok(Self { + kingside, + queenside, + }) } - fn to_uci(&self) -> String { + pub fn to_uci(&self) -> String { let mut castling = String::with_capacity(4); - if self.kingside[Color::White] { + if self.kingside[Color::White].is_some() { castling.push('K'); } - if self.queenside[Color::White] { + if self.queenside[Color::White].is_some() { castling.push('Q'); } - if self.kingside[Color::Black] { + if self.kingside[Color::Black].is_some() { castling.push('k'); } - if self.queenside[Color::Black] { + if self.queenside[Color::Black].is_some() { castling.push('q') } @@ -193,6 +199,60 @@ impl CastlingRights { castling } } + + /// Creates a `usize` for indexing into lists of 16 elements. + /// + /// # Example + /// ``` + /// # use brogle_core::CastlingRights; + /// let all = CastlingRights::from_uci("KQkq").unwrap(); + /// assert_eq!(all.index(), 15); + /// let none = CastlingRights::from_uci("").unwrap(); + /// assert_eq!(none.index(), 0); + /// ``` + pub const fn index(&self) -> usize { + (self.kingside[0].is_some() as usize) << 0 + | (self.kingside[1].is_some() as usize) << 1 + | (self.queenside[0].is_some() as usize) << 2 + | (self.queenside[1].is_some() as usize) << 3 + } +} + +impl FromStr for CastlingRights { + type Err = anyhow::Error; + fn from_str(s: &str) -> std::result::Result { + Self::from_uci(s) + } +} + +impl Index for [T; NUM_CASTLING_RIGHTS] { + type Output = T; + /// [`CastlingRights`] can be used to index into a list of 16 elements. + fn index(&self, index: CastlingRights) -> &Self::Output { + &self[index.index()] + } +} + +impl<'a, T> Index<&'a CastlingRights> for [T; NUM_CASTLING_RIGHTS] { + type Output = T; + /// [`CastlingRights`] can be used to index into a list of 16 elements. + fn index(&self, index: &'a CastlingRights) -> &Self::Output { + &self[index.index()] + } +} + +impl IndexMut for [T; NUM_CASTLING_RIGHTS] { + /// [`CastlingRights`] can be used to index into a list of 16 elements. + fn index_mut(&mut self, index: CastlingRights) -> &mut Self::Output { + &mut self[index.index()] + } +} + +impl<'a, T> IndexMut<&'a CastlingRights> for [T; NUM_CASTLING_RIGHTS] { + /// [`CastlingRights`] can be used to index into a list of 16 elements. + fn index_mut(&mut self, index: &'a CastlingRights) -> &mut Self::Output { + &mut self[index.index()] + } } impl fmt::Display for CastlingRights { @@ -289,8 +349,8 @@ impl Position { } /// Consumes `self` and returns a [`Position`] after having applied the provided [`Move`]. - pub fn with_move_made(mut self, chessmove: Move) -> Self { - self.make_move(chessmove); + pub fn with_move_made(mut self, mv: Move) -> Self { + self.make_move(mv); self } @@ -343,6 +403,10 @@ impl Position { self.fullmove } + pub fn zobrist_key(&self) -> u64 { + ZOBRIST_TABLE.hash(&self) + } + /// Returns `true` if the half-move counter is 50 or greater. pub const fn can_draw_by_fifty(&self) -> bool { self.halfmove() >= 50 @@ -365,41 +429,42 @@ impl Position { /// Returns `true` if `color` can castle (either Kingside or Queenside). pub const fn can_castle(&self, color: Color) -> bool { - self.castling_rights().kingside[color.index()] - || self.castling_rights().queenside[color.index()] + self.castling_rights().kingside[color.index()].is_some() + || self.castling_rights().queenside[color.index()].is_some() } - /// Checks if the provided move is legal to perform + /// Checks if the provided move is legal to perform. /// - /// TODO: Refactor this to not return a tuple. Maybe a result? - fn is_legal(&self, chessmove: Move) -> (bool, &str) { - let (from, to, kind) = chessmove.parts(); + /// If `Ok(())`, the move is legal. + /// If `Err(msg)`, then `msg` will be a reason as to why it's not legal. + fn check_legality_of(&self, mv: Move) -> Result<()> { + let (from, to, kind) = mv.parts(); // If there's no piece here, illegal move let Some(piece) = self.board().piece_at(from) else { - return (false, "No piece here to move"); + bail!("No piece here to move"); }; // If it's not this piece's color's turn, illegal move if piece.color() != self.current_player() { - return (false, "Tried to move a piece that wasn't yours"); + bail!("Tried to move a piece that wasn't yours"); } // If this move captures a piece, handle those cases if let Some(to_capture) = self.board().piece_at(to) { // Can't capture own pieces if to_capture.color() == piece.color() { - return (false, "Tried to capture your own piece"); + bail!("Tried to capture your own piece"); } // Can't capture king if to_capture.is_king() { - return (false, "Tried to capture enemy king"); + bail!("Tried to capture enemy king"); } // Ensure that the move is a capture or en passant, and that it captures the correct piece - if !chessmove.is_capture() { - return (false, "Captured on a non-capture move"); + if !mv.is_capture() { + bail!("Captured on a non-capture move"); } } @@ -407,56 +472,52 @@ impl Position { // If the move is pawn-specific, ensure it's a pawn moving MoveKind::EnPassantCapture | MoveKind::PawnPushTwo | MoveKind::Promote(_) => { if !piece.is_pawn() { - return (false, "Tried to do a pawn move (EP, Push 2, Promote) with a piece that isn't a pawn"); + bail!("Tried to do a pawn move (EP, Push 2, Promote) with a piece that isn't a pawn"); } } // If castling, ensure we have the right to MoveKind::KingsideCastle => { - if !self.castling_rights.kingside[piece.color()] { - return (false, "Tried to castle (kingside) without rights"); + if self.castling_rights.kingside[piece.color()].is_none() { + bail!("Tried to castle (kingside) without rights"); } } // If castling, ensure we have the right to MoveKind::QueensideCastle => { - if !self.castling_rights.queenside[piece.color()] { - return (false, "Tried to castle (queenside) without rights"); + if self.castling_rights.queenside[piece.color()].is_none() { + bail!("Tried to castle (queenside) without rights"); } } // Quiet moves are fine _ => {} } - (true, "") + Ok(()) } /// Applies the move, if it is legal to make. If it is not legal, returns an `Err` explaining why. - pub fn make_move_checked(&mut self, chessmove: Move) -> Result<()> { - let (is_legal, reason) = self.is_legal(chessmove); - if is_legal { - self.make_move(chessmove); - Ok(()) - } else { - bail!("{reason}") - } + pub fn make_move_checked(&mut self, mv: Move) -> Result<()> { + self.check_legality_of(mv)?; + self.make_move(mv); + Ok(()) } /// Apply the provided `moves` to the board. No enforcement of legality. pub fn make_moves(&mut self, moves: impl IntoIterator) { - for chessmove in moves { - self.make_move(chessmove); + for mv in moves { + self.make_move(mv); } } /// Applies the move. No enforcement of legality - pub fn make_move(&mut self, chessmove: Move) { + pub fn make_move(&mut self, mv: Move) { // Remove the piece from it's previous location, exiting early if there is no piece there - let Some(mut piece) = self.board_mut().take(chessmove.from()) else { + let Some(mut piece) = self.board_mut().take(mv.from()) else { return; }; let color = piece.color(); - let to = chessmove.to(); - let from = chessmove.from(); + let to = mv.to(); + let from = mv.from(); // Clear the EP tile from the last move self.ep_tile = None; @@ -466,9 +527,9 @@ impl Position { self.fullmove += self.current_player().index(); // First, deal with special cases like captures and castling - if chessmove.is_capture() { + if mv.is_capture() { // If this move was en passant, the piece we captured isn't at `to`, it's one square behind - let captured_tile = if chessmove.is_en_passant() { + let captured_tile = if mv.is_en_passant() { to.backward_by(color, 1).unwrap() } else { to @@ -479,19 +540,21 @@ 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 - let can_queenside = to != [Tile::A1, Tile::A8][captured_color]; - let can_kingside = to != [Tile::H1, Tile::H8][captured_color]; + if to == Tile::A1.rank_relative_to(captured_color) { + self.castling_rights.queenside[captured_color].take(); + } - self.castling_rights.queenside[captured_color] &= can_queenside; - self.castling_rights.kingside[captured_color] &= can_kingside; + if to == Tile::H1.rank_relative_to(captured_color) { + self.castling_rights.kingside[captured_color].take(); + } // Reset halfmove counter, since a capture occurred self.halfmove = 0; - } else if chessmove.is_pawn_double_push() { + } else if mv.is_pawn_double_push() { // Double pawn push, so set the EP square self.ep_tile = from.forward_by(color, 1); - } else if chessmove.is_castle() { - let castle_index = chessmove.is_short_castle() as usize; + } else if mv.is_castle() { + let castle_index = mv.is_short_castle() as usize; 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); @@ -500,8 +563,8 @@ impl Position { self.board_mut().set(rook, new_rook_tile); // Disable castling - self.castling_rights.kingside[color] = false; - self.castling_rights.queenside[color] = false; + self.castling_rights.kingside[color] = None; + self.castling_rights.queenside[color] = None; } // Next, handle special cases for Pawn (halfmove), Rook, and King (castling) @@ -509,19 +572,26 @@ 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); + // 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.castling_rights.queenside[color].take(); + } + + if from == Tile::H1.rank_relative_to(color) { + self.castling_rights.kingside[color].take(); + } } PieceKind::King => { // Disable all castling - self.castling_rights.kingside[color] = false; - self.castling_rights.queenside[color] = false; + self.castling_rights.kingside[color] = None; + self.castling_rights.queenside[color] = None; } _ => {} } // Now we check for promotions, since all special cases for Pawns and Rooks have been dealt with - if let Some(promotion) = chessmove.promotion() { + if let Some(promotion) = mv.promotion() { piece = piece.promoted(promotion); } @@ -531,12 +601,6 @@ impl Position { // Next player's turn self.toggle_current_player(); } - - /* - pub fn unmake_move(&mut self, chessmove: Move) { - // - } - */ } impl FromStr for Position { @@ -1012,6 +1076,13 @@ impl ChessBoard { self.piece_parts(piece.color(), piece.kind()) } + /// Returns an iterator over all of the pieces on this board along with their corresponding locations. + pub fn pieces(&self) -> impl ExactSizeIterator + '_ { + self.occupied() + .into_iter() + .map(|tile| (tile, self.get(tile).unwrap())) + } + /// Analogous to [`ChessBoard::piece`] with a [`Piece`]'s individual components pub const fn piece_parts(&self, color: Color, kind: PieceKind) -> Bitboard { let color = self.color(color); diff --git a/brogle_core/src/prng.rs b/brogle_core/src/prng.rs new file mode 100644 index 0000000..30bb99c --- /dev/null +++ b/brogle_core/src/prng.rs @@ -0,0 +1,52 @@ +/// Four random u64 values. +const SEEDS: [u64; 4] = [ + 0b1001000111000101101010110011110011101011111111010101101001110001, + 0b0000011010111010001001010011101110011101110110001001011111001101, + 0b1000000000010101101101011110010110011100110000100111010111101001, + 0b1111100011110100001001111111110001010100000100011101111001010011, +]; + +/// A pseudo-random number generator using the "xoshiro" algorithm. +/// +/// Source code copied from +pub struct XoShiRo([u64; 4]); + +impl XoShiRo { + /// Construct a new pseudo-random number generator from the library's seeds. + pub const fn new() -> Self { + Self::from_seeds(SEEDS) + } + + /// Construct a new pseudo-random number generator from your own seeds. + pub const fn from_seeds(seeds: [u64; 4]) -> Self { + Self(seeds) + } + + /// Generates the next pseudo-random number in the sequence. + pub fn 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) { + let (result, s) = Self::xoshiro(self.0); + (result, Self(s)) + } + + /// Inner function for computing the next pseudo-random number in the sequence. + const fn xoshiro(mut s: [u64; 4]) -> (u64, [u64; 4]) { + let result = s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9); + + let t = s[1] << 17; + + s[2] ^= s[0]; + s[3] ^= s[1]; + s[1] ^= s[2]; + s[0] ^= s[3]; + + s[2] ^= t; + + s[3] = s[3].rotate_left(45); + (result, s) + } +} diff --git a/brogle_core/src/zobrist.rs b/brogle_core/src/zobrist.rs new file mode 100644 index 0000000..ec39b9e --- /dev/null +++ b/brogle_core/src/zobrist.rs @@ -0,0 +1,98 @@ +use crate::XoShiRo; + +use super::Position; +use brogle_types::{Tile, NUM_CASTLING_RIGHTS, 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(); + +/// 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 { + /// One unique key for every possible piece and every possible tile. + piece_keys: [[u64; NUM_PIECES]; Tile::COUNT], + /// One unique key for every tile where en passant is possible. + ep_keys: [u64; Tile::COUNT], + /// 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, +} + +impl ZobristTable { + /// 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]; + let mut castling_keys = [0; NUM_CASTLING_RIGHTS]; + + let mut prng = XoShiRo::new(); + + // Initialize keys for pieces and EP + let mut i = 0; + while i < NUM_TILES { + let mut j = 0; + // Initialize keys for pieces + while j < NUM_PIECES { + let key; + (key, prng) = prng.const_next(); + piece_keys[i][j] = key; + j += 1; + } + + // Initialize keys for en passant squares + let key; + (key, prng) = prng.const_next(); + ep_keys[i] = key; + i += 1; + } + + // Initialize keys for castling rights + i = 0; + while i < NUM_CASTLING_RIGHTS { + let key; + (key, prng) = prng.const_next(); + castling_keys[i] = key; + i += 1; + } + + // Initialize keys for side-to-move + let (key, _) = prng.const_next(); + color_key = key; + + Self { + piece_keys, + ep_keys, + castling_keys, + color_key, + } + } + + pub fn hash(&self, position: &Position) -> u64 { + let mut hash = 0; + + // Hash all pieces on the board + for (tile, piece) in position.pieces() { + hash ^= self.piece_keys[tile][piece]; + } + + // Hash the en passant square, if it exists + if let Some(ep_tile) = position.ep_tile() { + hash ^= self.ep_keys[ep_tile]; + } + + // Hash the castling rights + hash ^= self.castling_keys[position.castling_rights()]; + + // Hash the side-to-move + if position.current_player().is_black() { + hash ^= self.color_key + } + + hash + } +} diff --git a/brogle_tools/src/perft_bench.rs b/brogle_tools/src/perft_bench.rs index 520f6a8..243f994 100644 --- a/brogle_tools/src/perft_bench.rs +++ b/brogle_tools/src/perft_bench.rs @@ -24,7 +24,6 @@ fn main() { .parse::() .unwrap(); let expected = perft_data.get(3..).unwrap().trim().parse::().unwrap(); - // println!("perft({depth}, \"{fen}\") := {expected}"); let position = Position::from_fen(fen).unwrap(); diff --git a/brogle_tools/src/perftree_script.rs b/brogle_tools/src/perftree_script.rs index b67fbb0..1aa347f 100644 --- a/brogle_tools/src/perftree_script.rs +++ b/brogle_tools/src/perftree_script.rs @@ -1,73 +1,8 @@ -/* -use brogle_core::{Move, MoveGenerator}; - -fn perft(movegen: MoveGenerator, depth: usize) -> usize { - if depth == 0 { - return 1; - } - - // let tab = " ".repeat(depth); - // eprintln!("\n{tab}PERFT({depth}): {position}\n{position:?}"); - - let mut nodes = 0; - - let moves = movegen.legal_moves(); - // eprintln!("LEGAL MOVES: {moves:?}"); - for chessmove in moves.iter() { - let mut cloned = position.clone(); - // eprintln!("{tab}Making : {chessmove}"); - cloned.make_move(*chessmove); - // eprintln!("{tab}{cloned}"); - nodes += perft(cloned, depth - 1); - // eprintln!("{tab}Unmaking: {chessmove}"); - // cloned.unmake_move(chessmove); - // eprintln!("{tab}{cloned}"); - } - - nodes -} - -fn main() { - let args: Vec = std::env::args().collect(); - - if args.len() < 3 { - println!("Usage: {} [moves]", args[0]); - process::exit(1); - } - - let depth: usize = args[1].parse().expect("Failed to parse depth value"); - let fen = &args[2]; - let moves = if args.len() > 3 { &args[3] } else { "" }; - - let mut position = Position::new().from_fen(fen).expect("Bad fen"); - for move_str in moves.split_ascii_whitespace() { - let parsed = Move::from_uci(&position, move_str).unwrap(); - - eprintln!("Applying {parsed} to {position}"); - position.make_move(parsed); - } - - let mut nodes = 0; - let moves = position.legal_moves(); - - for chessmove in moves.iter() { - let mut cloned = position.clone(); - cloned.make_move(*chessmove); - let new_nodes = perft(cloned, depth - 1); - // cloned.unmake_move(chessmove); - nodes += new_nodes; - - println!("{chessmove} {new_nodes}"); - } - - println!("\n{nodes}"); -} - -*/ use std::process; -use brogle_core::{perft, Game, Move}; +use brogle::Engine; +/// This script exists exclusively to be used with the [perftree](https://github.com/agausmann/perftree) program for debugging. fn main() { let args: Vec = std::env::args().collect(); @@ -80,26 +15,10 @@ fn main() { let fen = &args[2]; let moves = if args.len() > 3 { &args[3] } else { "" }; - let mut game = Game::from_fen(fen).expect("Bad fen"); - for move_str in moves.split_ascii_whitespace() { - let parsed = Move::from_uci(&game, move_str).unwrap(); - - eprintln!("Applying {parsed} to {}", game.position()); - game.make_move(parsed); - } - - let mut nodes = 0; - let moves = game.legal_moves(); - - for chessmove in moves.iter() { - let mut cloned = game.clone(); - cloned.make_move(*chessmove); - let new_nodes = perft(&cloned, depth - 1); - // cloned.unmake_move(chessmove); - nodes += new_nodes; - - println!("{chessmove} {new_nodes}"); - } + let mut engine = Engine::from_fen(fen).expect("Invalid FEN provided"); + engine + .make_move(moves.split_ascii_whitespace()) + .expect("msg"); - println!("\n{nodes}"); + engine.perft(depth, false, true); } diff --git a/scripts/make-release.sh b/scripts/make-release.sh index a5f050a..fbdbdca 100755 --- a/scripts/make-release.sh +++ b/scripts/make-release.sh @@ -10,6 +10,6 @@ fi; echo "Building $name" cargo b --release -cp target/release/brogle_engine releases/$name +cp target/release/brogle releases/$name echo "Built releases/$name" \ No newline at end of file diff --git a/scripts/run-fastchess.sh b/scripts/run-fastchess.sh index 31bfd22..1c2e2c7 100755 --- a/scripts/run-fastchess.sh +++ b/scripts/run-fastchess.sh @@ -9,5 +9,5 @@ fastchess \ -engine cmd=$engine1 name=$name1 \ -engine cmd=$engine2 name=$name2 \ -openings file=epd/8moves_v3.epd format=epd order=random \ - -each tc=8+0.08 -rounds 500 -repeat -concurrency 6 - # -each tc=8+0.08 -rounds 500 -repeat -concurrency 1 -log file=1 realtime=true + -each tc=8+0.08 -rounds 500 -repeat -concurrency 1 -log file=fastchess.log + # -each tc=8+0.08 -rounds 500 -repeat -concurrency 6