use crate::board::Move;
use std::cell::Cell;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Bound {
Exact = 0,
Lower = 1,
Upper = 2,
}
#[derive(Debug, Clone, Copy)]
pub struct TtEntry {
pub key: u64, pub score: i32, pub depth: u8, pub bound: Bound, pub best_move: u16, }
impl TtEntry {
pub const EMPTY: Self = TtEntry {
key: 0,
score: 0,
depth: 0,
bound: Bound::Exact,
best_move: u16::MAX, };
#[inline]
pub fn is_empty(&self) -> bool {
self.best_move == u16::MAX && self.key == 0
}
}
#[derive(Debug, Clone, Copy)]
struct Bucket {
depth_pref: TtEntry,
always_replace: TtEntry,
}
const EMPTY_BUCKET: Bucket = Bucket {
depth_pref: TtEntry::EMPTY,
always_replace: TtEntry::EMPTY,
};
#[derive(Debug, Default, Clone, Copy)]
pub struct TtStats {
pub probes: u64, pub hits: u64, pub stores: u64, pub displaced_depth_pref: u64, pub stored_to_always: u64, pub depth_hist: [u64; 16],
}
pub struct TranspositionTable {
buckets: Vec<Bucket>,
mask: usize, probes: Cell<u64>,
hits: Cell<u64>,
stores: Cell<u64>,
displaced_depth_pref: Cell<u64>,
stored_to_always: Cell<u64>,
depth_hist: [Cell<u64>; 16],
}
impl TranspositionTable {
pub fn new(bucket_count_pow2: u32) -> Self {
let n = 1usize << bucket_count_pow2;
Self {
buckets: vec![EMPTY_BUCKET; n],
mask: n - 1,
probes: Cell::new(0),
hits: Cell::new(0),
stores: Cell::new(0),
displaced_depth_pref: Cell::new(0),
stored_to_always: Cell::new(0),
depth_hist: Default::default(),
}
}
pub fn clear(&mut self) {
for b in self.buckets.iter_mut() {
b.depth_pref = TtEntry::EMPTY;
b.always_replace = TtEntry::EMPTY;
}
}
pub fn reset_stats(&self) {
self.probes.set(0);
self.hits.set(0);
self.stores.set(0);
self.displaced_depth_pref.set(0);
self.stored_to_always.set(0);
for c in &self.depth_hist {
c.set(0);
}
}
pub fn stats(&self) -> TtStats {
let mut hist = [0u64; 16];
for (i, c) in self.depth_hist.iter().enumerate() {
hist[i] = c.get();
}
TtStats {
probes: self.probes.get(),
hits: self.hits.get(),
stores: self.stores.get(),
displaced_depth_pref: self.displaced_depth_pref.get(),
stored_to_always: self.stored_to_always.get(),
depth_hist: hist,
}
}
pub fn occupancy(&self) -> (usize, usize, usize) {
let mut dp = 0usize;
let mut ar = 0usize;
for b in &self.buckets {
if !b.depth_pref.is_empty() { dp += 1; }
if !b.always_replace.is_empty() { ar += 1; }
}
(dp, ar, self.buckets.len())
}
#[inline]
pub fn probe(&self, key: u64) -> Option<TtEntry> {
self.probes.set(self.probes.get() + 1);
let bucket = &self.buckets[(key as usize) & self.mask];
if bucket.depth_pref.key == key && !bucket.depth_pref.is_empty() {
self.hits.set(self.hits.get() + 1);
return Some(bucket.depth_pref);
}
if bucket.always_replace.key == key && !bucket.always_replace.is_empty() {
self.hits.set(self.hits.get() + 1);
return Some(bucket.always_replace);
}
None
}
#[inline]
pub fn store(&mut self, key: u64, score: i32, depth: u8, bound: Bound, best_move: Option<Move>) {
self.stores.set(self.stores.get() + 1);
let depth_idx = (depth as usize).min(15);
let h = &self.depth_hist[depth_idx];
h.set(h.get() + 1);
let entry = TtEntry {
key,
score,
depth,
bound,
best_move: best_move.map(|m| m as u16).unwrap_or(u16::MAX),
};
let bucket = &mut self.buckets[(key as usize) & self.mask];
if bucket.depth_pref.is_empty() || depth >= bucket.depth_pref.depth {
if !bucket.depth_pref.is_empty() {
self.displaced_depth_pref.set(self.displaced_depth_pref.get() + 1);
if bucket.depth_pref.key != key {
bucket.always_replace = bucket.depth_pref;
}
}
bucket.depth_pref = entry;
} else {
self.stored_to_always.set(self.stored_to_always.get() + 1);
bucket.always_replace = entry;
}
}
pub fn capacity(&self) -> usize {
self.buckets.len() * 2
}
#[inline]
pub fn prefetch(&self, key: u64) {
#[cfg(target_arch = "x86_64")]
unsafe {
use std::arch::x86_64::{_mm_prefetch, _MM_HINT_T0};
let idx = (key as usize) & self.mask;
let bucket_ptr = self.buckets.as_ptr().add(idx);
_mm_prefetch(bucket_ptr as *const i8, _MM_HINT_T0);
}
#[cfg(not(target_arch = "x86_64"))]
let _ = key;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_and_probe_roundtrip() {
let mut tt = TranspositionTable::new(4); assert!(tt.probe(0xDEAD_BEEF).is_none());
tt.store(0xDEAD_BEEF, 123, 5, Bound::Exact, Some(42));
let e = tt.probe(0xDEAD_BEEF).expect("should hit");
assert_eq!(e.score, 123);
assert_eq!(e.depth, 5);
assert_eq!(e.bound, Bound::Exact);
assert_eq!(e.best_move, 42);
}
#[test]
fn depth_preferred_slot_keeps_higher_depth() {
let mut tt = TranspositionTable::new(4);
let k1 = 0x0000_0000_0000_0000u64; let k2 = 0x0000_0001_0000_0000u64; tt.store(k1, 100, 6, Bound::Exact, Some(1));
tt.store(k2, 200, 4, Bound::Exact, Some(2));
assert_eq!(tt.probe(k1).unwrap().score, 100);
assert_eq!(tt.probe(k2).unwrap().score, 200);
tt.store(k1, 50, 2, Bound::Exact, Some(3));
let probed = tt.probe(k1).unwrap();
assert_eq!(probed.score, 100);
}
}