Skip to content
This repository has been archived by the owner on Oct 8, 2024. It is now read-only.

Commit

Permalink
feat: zobrist hashing is used to detect repetitions
Browse files Browse the repository at this point in the history
  • Loading branch information
dannyhammer committed Aug 12, 2024
1 parent 3f1ea37 commit bb96270
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 67 deletions.
6 changes: 3 additions & 3 deletions brogle/src/search/search_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ impl<'a> Search<'a> {
for i in 0..moves.len() {
let mv = moves[i];
// Make the current move on the position, getting a new position in return
let new_pos = self.game.with_move_made(mv);
let new_pos = self.game.clone().with_move_made(mv);

if new_pos.is_repetition() || new_pos.can_draw_by_fifty() {
// eprintln!("Repetition in Search after {mv} on {}", new_pos.fen());
Expand Down Expand Up @@ -310,7 +310,7 @@ impl<'a> Search<'a> {
let mv = moves[i];

// Make the current move on the position, getting a new position in return
let new_pos = game.with_move_made(mv);
let new_pos = game.clone().with_move_made(mv);
if new_pos.is_repetition() || new_pos.can_draw_by_fifty() {
// eprintln!("Repetition in Negamax after {mv} on {}", new_pos.fen());
continue;
Expand Down Expand Up @@ -396,7 +396,7 @@ impl<'a> Search<'a> {
}

// Make the current move on the position, getting a new position in return
let new_pos = game.with_move_made(mv);
let new_pos = game.clone().with_move_made(mv);
if new_pos.is_repetition() {
eprintln!("Repetition in QSearch after {mv} on {}", new_pos.fen());
continue;
Expand Down
31 changes: 16 additions & 15 deletions brogle_core/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
use brogle_core::*;

/// All contents of this file should be ignored. I just use this `main` to test small things in `brogle_core`.
fn main() {
// 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();

for mv in game.legal_moves() {
if mv.from() == Tile::E1 {
println!("{mv}");
}
}

/*
let mut game = Game::default();
println!("pos: {}\nkey: {}", game.position(), game.key());
game.make_move(Move::from_uci(&game, "b1a3").unwrap());
println!("pos: {}\nkey: {}", game.position(), game.zobrist_key());
println!("pos: {}\nkey: {}", game.position(), game.key());
game.make_move(Move::from_uci(&game, "b8a6").unwrap());
println!("pos: {}\nkey: {}", game.position(), game.zobrist_key());
println!("pos: {}\nkey: {}", game.position(), game.key());
game.make_move(Move::from_uci(&game, "a3b1").unwrap());
println!("pos: {}\nkey: {}", game.position(), game.zobrist_key());
println!("pos: {}\nkey: {}", game.position(), game.key());
game.make_move(Move::from_uci(&game, "a6b8").unwrap());
println!("pos: {}\nkey: {}", game.position(), game.zobrist_key());
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());
}
2 changes: 1 addition & 1 deletion brogle_core/src/movegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ include!("blobs/magics.rs"); // TODO: Make these into blobs

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MoveGenerator {
position: Position,
pub(crate) position: Position,
attacks_by_color: [Bitboard; NUM_COLORS],
attacks_by_tile: [Bitboard; Tile::COUNT],
pinmasks: (Bitboard, Bitboard),
Expand Down
89 changes: 48 additions & 41 deletions brogle_core/src/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,34 @@ use super::{

#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Game {
movegen: MoveGenerator,
history: Vec<Position>,
moves: Vec<Move>,
pub(crate) movegen: MoveGenerator,
prev_positions: Vec<Position>,
history: Vec<Move>,
}

impl Game {
/// Creates a new, empty [`Game`].
pub fn new() -> Self {
Self {
movegen: MoveGenerator::new_legal(Position::new()),
prev_positions: Vec::with_capacity(128),
history: Vec::with_capacity(128),
moves: Vec::with_capacity(128),
}
}

/// Creates a new [`Game`] from the provided FEN string.
pub fn from_fen(fen: &str) -> Result<Self> {
let position = Position::from_fen(fen)?;
Ok(Self {
movegen: MoveGenerator::new_legal(position),
movegen: MoveGenerator::new_legal(Position::from_fen(fen)?),
prev_positions: Vec::with_capacity(128),
history: Vec::with_capacity(128),
moves: Vec::with_capacity(128),
})
}

/// Consumes `self` and returns a [`Game`] after having applied the provided [`Move`].
pub fn with_move_made(&self, mv: Move) -> Self {
let mut new = self.clone();
new.make_move(mv);
new
pub fn with_move_made(mut self, mv: Move) -> Self {
self.make_move(mv);
self
}

/// Returns `true` if the game is in a position that is identical to the position it was in before.
Expand All @@ -67,13 +65,10 @@ impl Game {
/// assert_eq!(game.is_repetition(), true);
/// ```
pub fn is_repetition(&self) -> bool {
for prev_pos in self.history.iter().rev().skip(1).step_by(2) {
if prev_pos == self.position() {
for prev_pos in self.prev_positions.iter().rev().skip(1).step_by(2) {
if prev_pos.key() == self.position().key() {
return true;
}
// if prev_pos.halfmove == 0 {
// return false;
// }
}

false
Expand All @@ -98,8 +93,8 @@ impl Game {

/// Applies the provided [`Move`]. No enforcement of legality.
pub fn make_move(&mut self, mv: Move) {
self.history.push(self.position().clone());
self.moves.push(mv);
self.prev_positions.push(self.position().clone());
self.history.push(mv);
let new_pos = self.position().clone().with_move_made(mv);
self.movegen = MoveGenerator::new_legal(new_pos);
}
Expand All @@ -113,16 +108,16 @@ impl Game {

/// Returns a list of all [`Move`]s made during this game.
pub fn history(&self) -> &[Move] {
&self.moves
&self.history
}

/// Undo the previously-made move, if there was one, and restore the position.
pub fn unmake_move(&mut self) {
let Some(prev_pos) = self.history.pop() else {
let Some(prev_pos) = self.prev_positions.pop() else {
return;
};

let Some(_mv) = self.moves.pop() else {
let Some(_mv) = self.history.pop() else {
return;
};

Expand Down Expand Up @@ -264,7 +259,7 @@ impl fmt::Display for CastlingRights {
/// Represents the current state of the game, including move counters
///
/// Analogous to a FEN string.
#[derive(Clone)]
#[derive(Clone, PartialEq, Eq)]
pub struct Position {
/// Bitboard representation of the game board.
board: ChessBoard,
Expand All @@ -286,6 +281,9 @@ pub struct Position {
/// Number of moves since the beginning of the game.
/// A fullmove is a complete turn by white and then by black.
fullmove: usize,

/// Zobrist hash key of this position
key: u64,
}

impl Position {
Expand All @@ -304,14 +302,17 @@ impl Position {
/// assert_eq!(state.to_fen(), "8/8/8/8/8/8/8/8 w - - 0 1");
/// ```
pub fn new() -> Self {
Self {
let mut pos = Self {
board: ChessBoard::new(),
current_player: Color::White,
castling_rights: CastlingRights::new(),
ep_tile: None,
halfmove: 0,
fullmove: 1,
}
key: 0,
};
pos.update_zobrist_key();
pos
}

/// Creates a new [`Position`] from the provided FEN string.
Expand Down Expand Up @@ -345,6 +346,8 @@ impl Position {
"Invalid FEN string: FEN string must have valid fullmove counter. Got {fullmove}"
)))?;

pos.update_zobrist_key();

Ok(pos)
}

Expand Down Expand Up @@ -403,8 +406,9 @@ impl Position {
self.fullmove
}

pub fn zobrist_key(&self) -> u64 {
ZOBRIST_TABLE.hash(&self)
/// Fetch the Zobrist hash key of this position.
pub fn key(&self) -> u64 {
self.key
}

/// Returns `true` if the half-move counter is 50 or greater.
Expand Down Expand Up @@ -433,9 +437,24 @@ impl Position {
|| self.castling_rights().queenside[color.index()].is_some()
}

/// Two positions are considered repetitions if they share the same piece layout, castling rights, and en passant square.
///
/// Fullmove and Halfmove clocks are ignored.
pub fn is_repetition_of(&self, other: &Self) -> bool {
self.board() == other.board()
&& self.current_player() == other.current_player()
&& self.castling_rights() == other.castling_rights()
&& 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.
/// 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();
Expand Down Expand Up @@ -600,6 +619,8 @@ impl Position {

// Next player's turn
self.toggle_current_player();

self.update_zobrist_key();
}
}

Expand All @@ -624,20 +645,6 @@ impl Default for Position {
}
}

impl PartialEq for Position {
/// Two positions are considered equal if they share the same piece layout, castling rights, and en passant square.
///
/// Fullmove and Halfmove clocks are ignored.
fn eq(&self, other: &Self) -> bool {
self.board() == other.board()
&& self.current_player() == other.current_player()
&& self.castling_rights() == other.castling_rights()
&& self.ep_tile() == other.ep_tile()
}
}

impl Eq for Position {}

impl fmt::Display for Position {
/// Display this position's FEN string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down
29 changes: 22 additions & 7 deletions brogle_core/src/zobrist.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::XoShiRo;
use crate::{CastlingRights, ChessBoard, XoShiRo};

use super::Position;
use brogle_types::{Tile, NUM_CASTLING_RIGHTS, NUM_PIECES, NUM_TILES};
use brogle_types::{Color, Tile, NUM_CASTLING_RIGHTS, NUM_PIECES, NUM_TILES};

/// Stores Zobrist hash keys, for hashing [`Position`]s.
///
Expand Down Expand Up @@ -72,27 +72,42 @@ impl ZobristTable {
}
}

pub fn hash(&self, position: &Position) -> u64 {
pub fn hash_parts(
&self,
board: &ChessBoard,
ep_tile: Option<Tile>,
castling_rights: &CastlingRights,
color: Color,
) -> u64 {
let mut hash = 0;

// Hash all pieces on the board
for (tile, piece) in position.pieces() {
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) = position.ep_tile() {
if let Some(ep_tile) = ep_tile {
hash ^= self.ep_keys[ep_tile];
}

// Hash the castling rights
hash ^= self.castling_keys[position.castling_rights()];
hash ^= self.castling_keys[castling_rights];

// Hash the side-to-move
if position.current_player().is_black() {
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(),
)
}
}

0 comments on commit bb96270

Please sign in to comment.