use crate::types::{Color, Piece, PieceType, Square};
#[cfg(not(feature = "hash-128"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ZobristKey(u64);
#[cfg(not(feature = "hash-128"))]
impl ZobristKey {
#[must_use]
pub const fn new(low: u64, _high: u64) -> Self {
Self(low)
}
#[must_use]
pub const fn from_u64(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn low_u64(self) -> u64 {
self.0
}
#[must_use]
pub const fn high_u64(self) -> u64 {
0
}
pub fn xor(&mut self, other: Self) {
self.0 ^= other.0;
}
pub fn add(&mut self, other: Self) {
self.0 = self.0.wrapping_add(other.0);
}
pub fn sub(&mut self, other: Self) {
self.0 = self.0.wrapping_sub(other.0);
}
#[must_use]
pub const fn mul_u64(self, rhs: u64) -> Self {
Self(self.0.wrapping_mul(rhs))
}
}
#[cfg(feature = "hash-128")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ZobristKey {
low: u64,
high: u64,
}
#[cfg(feature = "hash-128")]
impl ZobristKey {
#[must_use]
pub const fn new(low: u64, high: u64) -> Self {
Self { low, high }
}
#[must_use]
pub const fn from_u64(value: u64) -> Self {
Self { low: value, high: 0 }
}
#[must_use]
pub const fn low_u64(self) -> u64 {
self.low
}
#[must_use]
pub const fn high_u64(self) -> u64 {
self.high
}
pub fn xor(&mut self, other: Self) {
self.low ^= other.low;
self.high ^= other.high;
}
pub fn add(&mut self, other: Self) {
self.low = self.low.wrapping_add(other.low);
self.high = self.high.wrapping_add(other.high);
}
pub fn sub(&mut self, other: Self) {
self.low = self.low.wrapping_sub(other.low);
self.high = self.high.wrapping_sub(other.high);
}
#[must_use]
pub const fn mul_u64(self, rhs: u64) -> Self {
Self { low: self.low.wrapping_mul(rhs), high: self.high.wrapping_mul(rhs) }
}
}
impl From<u64> for ZobristKey {
fn from(value: u64) -> Self {
Self::from_u64(value)
}
}
impl std::ops::BitXor for ZobristKey {
type Output = Self;
fn bitxor(self, rhs: Self) -> Self::Output {
#[cfg(not(feature = "hash-128"))]
{
Self(self.0 ^ rhs.0)
}
#[cfg(feature = "hash-128")]
{
Self { low: self.low ^ rhs.low, high: self.high ^ rhs.high }
}
}
}
impl std::ops::BitXorAssign for ZobristKey {
fn bitxor_assign(&mut self, rhs: Self) {
#[cfg(not(feature = "hash-128"))]
{
self.0 ^= rhs.0;
}
#[cfg(feature = "hash-128")]
{
self.low ^= rhs.low;
self.high ^= rhs.high;
}
}
}
#[derive(Debug)]
pub struct ZobristTable {
board: [[ZobristKey; Square::COUNT_WITH_NONE]; Piece::COUNT],
hand: [[ZobristKey; PieceType::HAND_TABLE_SIZE]; Color::COUNT],
no_pawns: ZobristKey,
side: ZobristKey,
}
pub const HASH_KEY_BITS: u32 = if cfg!(feature = "hash-128") { 128 } else { 64 };
const BOARD_PIECES: usize = Piece::COUNT;
const BOARD_SQUARES: usize = Square::COUNT;
const HAND_PIECES: usize = PieceType::HAND_TABLE_SIZE;
const MATERIAL_SQUARE_INDEX: usize = 8;
const ZOBRIST_SEED: u64 = 20_151_225;
const PRNG_MULTIPLIER: u64 = 2_685_821_657_736_338_717;
const fn prng_next_u64(state: &mut u64) -> u64 {
let mut s = *state;
s ^= s >> 12;
s ^= s << 25;
s ^= s >> 27;
*state = s;
s.wrapping_mul(PRNG_MULTIPLIER)
}
#[allow(clippy::large_stack_arrays)]
const fn generate_zobrist_table() -> ZobristTable {
let mut rng_state = ZOBRIST_SEED;
let mut board = [[ZobristKey::from_u64(0); Square::COUNT_WITH_NONE]; BOARD_PIECES];
let mut hand = [[ZobristKey::from_u64(0); HAND_PIECES]; Color::COUNT];
let side = next_key(&mut rng_state);
let no_pawns = next_key(&mut rng_state);
let mut piece_idx = 1;
while piece_idx < BOARD_PIECES {
let mut square_idx = 0;
while square_idx < BOARD_SQUARES {
board[piece_idx][square_idx] = next_key(&mut rng_state);
square_idx += 1;
}
piece_idx += 1;
}
let mut color_idx = 0;
while color_idx < Color::COUNT {
let mut hand_piece_idx = 1;
while hand_piece_idx < HAND_PIECES {
hand[color_idx][hand_piece_idx] = next_key(&mut rng_state);
hand_piece_idx += 1;
}
color_idx += 1;
}
ZobristTable { board, hand, no_pawns, side }
}
const fn next_key(state: &mut u64) -> ZobristKey {
let low = prng_next_u64(state);
let high = if HASH_KEY_BITS >= 128 { prng_next_u64(state) } else { 0 };
let remaining = if HASH_KEY_BITS >= 128 { 2 } else { 3 };
let mut i = 0;
while i < remaining {
let _ = prng_next_u64(state);
i += 1;
}
ZobristKey::new(low, high)
}
static ZOBRIST: ZobristTable = generate_zobrist_table();
pub struct Zobrist;
impl ZobristTable {
#[inline]
#[must_use]
pub const fn side(&self) -> ZobristKey {
self.side
}
#[inline]
#[must_use]
pub const fn no_pawns(&self) -> ZobristKey {
self.no_pawns
}
#[inline]
#[must_use]
pub fn board_at_index(&self, piece_idx: usize, square_idx: usize) -> ZobristKey {
self.board[piece_idx][square_idx]
}
#[inline]
#[must_use]
pub fn hand_at_index(&self, color_idx: usize, piece_type_idx: usize) -> ZobristKey {
self.hand[color_idx][piece_type_idx]
}
}
impl Zobrist {
#[must_use]
pub fn instance() -> &'static ZobristTable {
&ZOBRIST
}
#[must_use]
pub fn psq(sq: Square, piece: Piece) -> ZobristKey {
let table = Self::instance();
let piece_idx = piece.to_index();
let square_idx = sq.to_index_with_none();
table.board[piece_idx][square_idx]
}
#[must_use]
pub fn material(piece: Piece) -> ZobristKey {
let table = Self::instance();
let piece_idx = piece.to_index();
table.board[piece_idx][MATERIAL_SQUARE_INDEX]
}
#[must_use]
pub fn hand(color: Color, piece_type: PieceType, count: usize) -> ZobristKey {
let table = Self::instance();
let piece_idx = piece_type.to_index();
if piece_idx == 0 || piece_idx >= PieceType::HAND_TABLE_SIZE || count == 0 {
return ZobristKey::default();
}
let base = table.hand[color.to_index()][piece_idx];
base.mul_u64(count as u64)
}
#[must_use]
pub fn side() -> ZobristKey {
Self::instance().side()
}
#[must_use]
pub fn no_pawns() -> ZobristKey {
Self::instance().no_pawns()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_zobrist_table_structure() {
assert_ne!(ZOBRIST.side(), ZobristKey::default(), "Side hash should not be zero");
assert_ne!(ZOBRIST.no_pawns(), ZobristKey::default(), "No-pawns hash should not be zero");
let mut non_zero_count = 0;
for piece in 0..Piece::COUNT {
for square in 0..Square::COUNT {
if ZOBRIST.board_at_index(piece, square) != ZobristKey::default() {
non_zero_count += 1;
}
}
}
assert!(non_zero_count > 0, "Board hashes should not all be zero");
non_zero_count = 0;
for color in 0..Color::COUNT {
for piece in 1..PieceType::HAND_TABLE_SIZE {
if ZOBRIST.hand_at_index(color, piece) != ZobristKey::default() {
non_zero_count += 1;
}
}
}
assert!(non_zero_count > 0, "Hand hashes should not all be zero");
}
#[test]
fn test_zobrist_table_uniqueness() {
let mut hashes = HashSet::new();
hashes.insert(ZOBRIST.side());
hashes.insert(ZOBRIST.no_pawns());
for piece in 0..Piece::COUNT {
for square in 0..Square::COUNT {
let hash = ZOBRIST.board_at_index(piece, square);
if hash != ZobristKey::default() {
assert!(
hashes.insert(hash),
"Duplicate hash found in board table: low={:#016x} high={:#016x}",
hash.low_u64(),
hash.high_u64()
);
}
}
}
for color in 0..Color::COUNT {
for piece in 1..PieceType::HAND_TABLE_SIZE {
let hash = ZOBRIST.hand_at_index(color, piece);
if hash != ZobristKey::default() {
assert!(
hashes.insert(hash),
"Duplicate hash found in hand table: low={:#016x} high={:#016x}",
hash.low_u64(),
hash.high_u64()
);
}
}
}
let unique_count = hashes.len();
assert!(unique_count > 100, "Too few unique hashes: {unique_count}");
}
#[test]
fn test_zobrist_table_size_verification() {
assert_eq!(ZOBRIST.board.len(), Piece::COUNT);
assert_eq!(ZOBRIST.board[0].len(), Square::COUNT_WITH_NONE);
assert_eq!(ZOBRIST.hand.len(), Color::COUNT);
assert_eq!(ZOBRIST.hand[0].len(), PieceType::HAND_TABLE_SIZE);
assert_ne!(ZOBRIST.side(), ZobristKey::default());
}
#[test]
fn test_zobrist_no_piece_is_zero() {
for square in 0..Square::COUNT_WITH_NONE {
assert_eq!(
ZOBRIST.board_at_index(0, square),
ZobristKey::default(),
"NO_PIECE hash at square {square} should be 0"
);
}
}
#[test]
fn test_zobrist_zero_count_is_zero() {
for color in 0..Color::COUNT {
assert_eq!(
ZOBRIST.hand_at_index(color, 0),
ZobristKey::default(),
"Hash for NO_PIECE_TYPE in hand should be 0 for color {color}"
);
}
}
#[test]
fn test_zobrist_reproducibility() {
assert_eq!(ZOBRIST.board_at_index(1, 0), ZOBRIST.board_at_index(1, 0));
assert_ne!(ZOBRIST.side(), ZobristKey::default());
}
#[test]
fn test_zobrist_distribution() {
let mut bit_count = [0u32; 64];
for piece in 1..Piece::COUNT {
for square in 0..Square::COUNT {
let hash = ZOBRIST.board_at_index(piece, square).low_u64();
for (bit, count) in bit_count.iter_mut().enumerate() {
if (hash >> bit) & 1 == 1 {
*count += 1;
}
}
}
}
let piece_count = u32::try_from(Piece::COUNT).expect("piece count fits in u32");
let square_count = u32::try_from(Square::COUNT).expect("square count fits in u32");
let total: u32 = (piece_count - 1) * square_count;
for (bit, count) in bit_count.iter().enumerate() {
let ratio = f64::from(*count) / f64::from(total);
assert!((0.3..0.7).contains(&ratio), "Bit {bit} has unusual distribution: {ratio:.2}");
}
}
}