use crate::types::{Color, Hand, HandPiece, 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
}
}
#[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
}
}
impl From<u64> for ZobristKey {
#[inline]
fn from(value: u64) -> Self {
Self::from_u64(value)
}
}
impl std::ops::BitXor for ZobristKey {
type Output = Self;
#[inline]
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 {
#[inline]
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; HAND_SLOT_COUNT]; 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 HAND_COUNT_MASK: [u32; PieceType::COUNT] = hand_count_mask_table();
const HAND_OFFSET: [u16; PieceType::COUNT] = hand_offset_table();
const HAND_SLOT_COUNT: usize = hand_slot_count();
const fn hand_count_mask_table() -> [u32; PieceType::COUNT] {
let mut table = [0; PieceType::COUNT];
let mut idx = 1;
while idx < HAND_PIECES {
if let Some(hp) = HandPiece::from_piece_type(PieceType::new(idx as i8)) {
table[idx] = Hand::count_mask(hp);
}
idx += 1;
}
table
}
const fn hand_offset_table() -> [u16; PieceType::COUNT] {
let masks = hand_count_mask_table();
let mut table = [0; PieceType::COUNT];
let mut idx = 1;
let mut acc = 0;
while idx < HAND_PIECES {
table[idx] = acc;
acc += masks[idx] as u16 + 1;
idx += 1;
}
table
}
const fn hand_slot_count() -> usize {
let masks = hand_count_mask_table();
let mut idx = 1;
let mut acc = 0;
while idx < HAND_PIECES {
acc += masks[idx] as usize + 1;
idx += 1;
}
acc
}
const _: () = {
assert!(HAND_SLOT_COUNT == 72);
let mut idx = HAND_PIECES;
while idx < PieceType::COUNT {
assert!(HAND_OFFSET[idx] == 0 && HAND_COUNT_MASK[idx] == 0);
idx += 1;
}
assert!(HAND_OFFSET[0] == 0 && HAND_COUNT_MASK[0] == 0);
};
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_SLOT_COUNT]; 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 {
let offset = HAND_OFFSET[hand_piece_idx] as usize;
let max = HAND_COUNT_MASK[hand_piece_idx] as usize;
let mut count = 1;
while count <= max {
hand[color_idx][offset + count] = next_key(&mut rng_state);
count += 1;
}
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();
#[inline]
fn hand_slot(piece_idx: usize, count: u32) -> usize {
HAND_OFFSET[piece_idx] as usize + (count & HAND_COUNT_MASK[piece_idx]) as usize
}
pub struct Zobrist;
impl ZobristTable {
pub const HAND_SLOT_COUNT: usize = HAND_SLOT_COUNT;
#[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, slot_idx: usize) -> ZobristKey {
self.hand[color_idx][slot_idx]
}
}
impl Zobrist {
#[must_use]
pub fn instance() -> &'static ZobristTable {
&ZOBRIST
}
#[inline]
#[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]
}
#[inline]
#[must_use]
pub fn hand(color: Color, piece_type: PieceType, count: u32) -> ZobristKey {
let piece_idx = piece_type.to_index();
Self::instance().hand[color.to_index()][hand_slot(piece_idx, count)]
}
#[inline]
#[must_use]
pub fn hand_delta(color: Color, piece_type: PieceType, from: u32, to: u32) -> ZobristKey {
let piece_idx = piece_type.to_index();
let row = &Self::instance().hand[color.to_index()];
row[hand_slot(piece_idx, from)] ^ row[hand_slot(piece_idx, to)]
}
#[inline]
#[must_use]
pub fn side() -> ZobristKey {
Self::instance().side()
}
#[inline]
#[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 slot in 0..HAND_SLOT_COUNT {
if ZOBRIST.hand_at_index(color, slot) != 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 slot in 0..HAND_SLOT_COUNT {
let hash = ZOBRIST.hand_at_index(color, slot);
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(), HAND_SLOT_COUNT);
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 [Color::BLACK, Color::WHITE] {
for (piece_idx, &offset) in HAND_OFFSET.iter().enumerate().take(HAND_PIECES).skip(1) {
let piece_type = PieceType::new(i8::try_from(piece_idx).expect("index fits in i8"));
assert_eq!(
Zobrist::hand(color, piece_type, 0),
ZobristKey::default(),
"hand hash for count 0 should be zero: color={color:?} piece_type={piece_type:?}"
);
assert_eq!(
ZOBRIST.hand_at_index(color.to_index(), offset as usize),
ZobristKey::default(),
"slot at the head of each piece type should be zero: piece_type={piece_type:?}"
);
}
assert_eq!(
Zobrist::hand(color, PieceType::NONE, 0),
ZobristKey::default(),
"hand hash for NO_PIECE_TYPE should be zero"
);
}
}
#[test]
fn test_zobrist_hand_keys_are_unique_and_delta_is_consistent() {
let mut hashes = HashSet::new();
for color in [Color::BLACK, Color::WHITE] {
for (piece_idx, &max) in HAND_COUNT_MASK.iter().enumerate().take(HAND_PIECES).skip(1) {
let piece_type = PieceType::new(i8::try_from(piece_idx).expect("index fits in i8"));
for count in 1..=max {
let key = Zobrist::hand(color, piece_type, count);
assert_ne!(key, ZobristKey::default(), "non-zero count must have a key");
assert!(
hashes.insert(key),
"duplicate hand hash: color={color:?} piece_type={piece_type:?} count={count}"
);
}
for from in 0..=max {
for to in 0..=max {
assert_eq!(
Zobrist::hand_delta(color, piece_type, from, to),
Zobrist::hand(color, piece_type, from)
^ Zobrist::hand(color, piece_type, to),
"hand_delta must equal the XOR of both counts"
);
}
}
}
}
}
#[test]
fn test_zobrist_hand_count_wraps_at_field_width() {
let color = Color::BLACK;
assert_eq!(Zobrist::hand(color, PieceType::PAWN, 32), ZobristKey::default());
assert_eq!(
Zobrist::hand(color, PieceType::PAWN, 33),
Zobrist::hand(color, PieceType::PAWN, 1)
);
assert_ne!(
Zobrist::hand(color, PieceType::PAWN, 19),
Zobrist::hand(color, PieceType::PAWN, 18)
);
assert_eq!(Zobrist::hand(color, PieceType::KING, 1), ZobristKey::default());
assert_eq!(Zobrist::hand(color, PieceType::HORSE, 3), ZobristKey::default());
}
#[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}");
}
}
}