bitstackchess 0.1.1

A bitboard‐based chess game engine with 10 × u128 move history
Documentation
// src/engine/zobrist.rs

//! Zobrist hashing for GameCore positions.
//! 
//! We generate a random u64 for each (piece_id 0..31, square 0..63), a random u64 for side-to-move,
//! and one random u64 per en-passant file (0..7).  Then the position hash is:
//! 
//!   hash = 0
//!   for each pid with mapping.piece_square[pid] = Some(sq):
//!       hash ^= ZOBRIST_PIECE_SQUARE[pid][sq];
//!   if side_to_move == Black { hash ^= ZOBRIST_SIDE; }
//!   if let Some(ep_sq) = en_passant_target {
//!       let file = ep_sq & 7;
//!       hash ^= ZOBRIST_EP_FILE[file as usize];
//!   }
//! 
//! We use a simple xorshift64 generator to initialize the table exactly once.

use std::sync::Once;

/// A very simple xorshift64 PRNG.
struct XorShift64 {
    state: u64,
}

impl XorShift64 {
    /// Initialize with a non-zero seed.
    pub fn new(seed: u64) -> XorShift64 {
        debug_assert!(seed != 0, "seed must be non-zero");
        XorShift64 { state: seed }
    }

    /// Return the next pseudorandom u64.
    pub fn next(&mut self) -> u64 {
        // xorshift64* variant
        let mut x = self.state;
        x ^= x << 13;
        x ^= x >> 7;
        x ^= x << 17;
        self.state = x;
        x
    }
}

/// Hold all Zobrist values in statics, initialized once.
static INIT: Once = Once::new();

static mut ZOBRIST_PIECE_SQUARE: [[u64; 64]; 32] = [[0; 64]; 32];
static mut ZOBRIST_SIDE: u64 = 0;
static mut ZOBRIST_EP_FILE: [u64; 8] = [0; 8];

/// Initialize the Zobrist tables exactly once using xorshift64.
fn init_zobrist() {
    // Use a fixed non-zero seed for reproducibility.
    let mut rng = XorShift64::new(0x1234_5678_9ABC_DEF0);
    unsafe {
        for pid in 0..32 {
            for sq in 0..64 {
                ZOBRIST_PIECE_SQUARE[pid][sq] = rng.next();
            }
        }
        ZOBRIST_SIDE = rng.next();
        for f in 0..8 {
            ZOBRIST_EP_FILE[f] = rng.next();
        }
    }
}

/// Get the Zobrist value for a given (piece_id, square).
#[inline]
fn piece_square_value(pid: u8, sq: u8) -> u64 {
    INIT.call_once(init_zobrist);
    unsafe { ZOBRIST_PIECE_SQUARE[pid as usize][sq as usize] }
}

/// Get the Zobrist value for “side to move = Black”.
#[inline]
fn side_value() -> u64 {
    INIT.call_once(init_zobrist);
    unsafe { ZOBRIST_SIDE }
}

/// Get the Zobrist value for an en-passant file (0..7).
#[inline]
fn ep_file_value(file: u8) -> u64 {
    INIT.call_once(init_zobrist);
    unsafe { ZOBRIST_EP_FILE[file as usize] }
}

/// Compute the full Zobrist hash for a given GameCore‐like state.
///
/// Arguments:
///   • `mapping`: PieceMapping (which pid sits on which square)
///   • `side_to_move`: 0=White or 1=Black
///   • `en_passant_target`: Option<u8> (square 0..63)
///
/// Returns:
///   A 64‐bit Zobrist hash.
pub fn compute_hash(
    mapping: &crate::board::PieceMapping,
    side_to_move: u8,
    en_passant_target: Option<u8>,
) -> u64 {
    let mut h: u64 = 0;
    // XOR in every (pid, sq) that is occupied:
    for pid in 0..32 {
        if let Some(sq) = mapping.piece_square[pid as usize] {
            h ^= piece_square_value(pid as u8, sq);
        }
    }
    // XOR side‐to‐move (if Black)
    if side_to_move == 1 {
        h ^= side_value();
    }
    // XOR en-passant file (if any)
    if let Some(ep_sq) = en_passant_target {
        let file = ep_sq & 7;
        h ^= ep_file_value(file);
    }
    h
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::board::{init_chess_positions, PieceMapping};

    #[test]
    fn same_position_same_hash() {
        let mut mapping1 = PieceMapping::new_empty();
        let mut mapping2 = PieceMapping::new_empty();
        for &(pid, sq) in &init_chess_positions() {
            mapping1.place_piece(pid, sq);
            mapping2.place_piece(pid, sq);
        }
        let h1 = compute_hash(&mapping1, 0, None);
        let h2 = compute_hash(&mapping2, 0, None);
        assert_eq!(h1, h2);
    }

    #[test]
    fn different_side_different_hash() {
        let mut mapping = PieceMapping::new_empty();
        for &(pid, sq) in &init_chess_positions() {
            mapping.place_piece(pid, sq);
        }
        let h_white = compute_hash(&mapping, 0, None);
        let h_black = compute_hash(&mapping, 1, None);
        assert_ne!(h_white, h_black);
    }

    #[test]
    fn ep_target_changes_hash() {
        let mut mapping = PieceMapping::new_empty();
        for &(pid, sq) in &init_chess_positions() {
            mapping.place_piece(pid, sq);
        }
        // pretend White just did a pawn two-step, ep sq = 17 (b3)
        let h_no_ep = compute_hash(&mapping, 1, None);
        let h_with_ep = compute_hash(&mapping, 1, Some(17));
        assert_ne!(h_no_ep, h_with_ep);
    }
}