1use crate::board::Move;
11use std::cell::Cell;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(u8)]
19pub enum Bound {
20 Exact = 0,
21 Lower = 1,
22 Upper = 2,
23}
24
25#[derive(Debug, Clone, Copy)]
27pub struct TtEntry {
28 pub key: u64, pub score: i32, pub depth: u8, pub bound: Bound, pub best_move: u16, }
34
35impl TtEntry {
36 pub const EMPTY: Self = TtEntry {
37 key: 0,
38 score: 0,
39 depth: 0,
40 bound: Bound::Exact,
41 best_move: u16::MAX, };
43
44 #[inline]
45 pub fn is_empty(&self) -> bool {
46 self.best_move == u16::MAX && self.key == 0
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
52struct Bucket {
53 depth_pref: TtEntry,
54 always_replace: TtEntry,
55}
56
57const EMPTY_BUCKET: Bucket = Bucket {
58 depth_pref: TtEntry::EMPTY,
59 always_replace: TtEntry::EMPTY,
60};
61
62#[derive(Debug, Default, Clone, Copy)]
65pub struct TtStats {
66 pub probes: u64, pub hits: u64, pub stores: u64, pub displaced_depth_pref: u64, pub stored_to_always: u64, pub depth_hist: [u64; 16],
73}
74
75pub struct TranspositionTable {
76 buckets: Vec<Bucket>,
77 mask: usize, probes: Cell<u64>,
79 hits: Cell<u64>,
80 stores: Cell<u64>,
81 displaced_depth_pref: Cell<u64>,
82 stored_to_always: Cell<u64>,
83 depth_hist: [Cell<u64>; 16],
84}
85
86impl TranspositionTable {
87 pub fn new(bucket_count_pow2: u32) -> Self {
89 let n = 1usize << bucket_count_pow2;
90 Self {
91 buckets: vec![EMPTY_BUCKET; n],
92 mask: n - 1,
93 probes: Cell::new(0),
94 hits: Cell::new(0),
95 stores: Cell::new(0),
96 displaced_depth_pref: Cell::new(0),
97 stored_to_always: Cell::new(0),
98 depth_hist: Default::default(),
99 }
100 }
101
102 pub fn clear(&mut self) {
104 for b in self.buckets.iter_mut() {
105 b.depth_pref = TtEntry::EMPTY;
106 b.always_replace = TtEntry::EMPTY;
107 }
108 }
109
110 pub fn reset_stats(&self) {
112 self.probes.set(0);
113 self.hits.set(0);
114 self.stores.set(0);
115 self.displaced_depth_pref.set(0);
116 self.stored_to_always.set(0);
117 for c in &self.depth_hist {
118 c.set(0);
119 }
120 }
121
122 pub fn stats(&self) -> TtStats {
123 let mut hist = [0u64; 16];
124 for (i, c) in self.depth_hist.iter().enumerate() {
125 hist[i] = c.get();
126 }
127 TtStats {
128 probes: self.probes.get(),
129 hits: self.hits.get(),
130 stores: self.stores.get(),
131 displaced_depth_pref: self.displaced_depth_pref.get(),
132 stored_to_always: self.stored_to_always.get(),
133 depth_hist: hist,
134 }
135 }
136
137 pub fn occupancy(&self) -> (usize, usize, usize) {
140 let mut dp = 0usize;
141 let mut ar = 0usize;
142 for b in &self.buckets {
143 if !b.depth_pref.is_empty() {
144 dp += 1;
145 }
146 if !b.always_replace.is_empty() {
147 ar += 1;
148 }
149 }
150 (dp, ar, self.buckets.len())
151 }
152
153 #[inline]
155 pub fn probe(&self, key: u64) -> Option<TtEntry> {
156 self.probes.set(self.probes.get() + 1);
157 let bucket = &self.buckets[(key as usize) & self.mask];
158 if bucket.depth_pref.key == key && !bucket.depth_pref.is_empty() {
159 self.hits.set(self.hits.get() + 1);
160 return Some(bucket.depth_pref);
161 }
162 if bucket.always_replace.key == key && !bucket.always_replace.is_empty() {
163 self.hits.set(self.hits.get() + 1);
164 return Some(bucket.always_replace);
165 }
166 None
167 }
168
169 #[inline]
178 pub fn store(
179 &mut self,
180 key: u64,
181 score: i32,
182 depth: u8,
183 bound: Bound,
184 best_move: Option<Move>,
185 ) {
186 self.stores.set(self.stores.get() + 1);
187 let depth_idx = (depth as usize).min(15);
188 let h = &self.depth_hist[depth_idx];
189 h.set(h.get() + 1);
190 let entry = TtEntry {
191 key,
192 score,
193 depth,
194 bound,
195 best_move: best_move.map(|m| m as u16).unwrap_or(u16::MAX),
196 };
197 let bucket = &mut self.buckets[(key as usize) & self.mask];
198 if bucket.depth_pref.is_empty() || depth >= bucket.depth_pref.depth {
199 if !bucket.depth_pref.is_empty() {
200 self.displaced_depth_pref
201 .set(self.displaced_depth_pref.get() + 1);
202 if bucket.depth_pref.key != key {
205 bucket.always_replace = bucket.depth_pref;
206 }
207 }
208 bucket.depth_pref = entry;
209 } else {
210 self.stored_to_always.set(self.stored_to_always.get() + 1);
211 bucket.always_replace = entry;
212 }
213 }
214
215 pub fn capacity(&self) -> usize {
216 self.buckets.len() * 2
217 }
218
219 #[inline]
229 pub fn prefetch(&self, key: u64) {
230 #[cfg(target_arch = "x86_64")]
231 unsafe {
232 use std::arch::x86_64::{_MM_HINT_T0, _mm_prefetch};
233 let idx = (key as usize) & self.mask;
234 let bucket_ptr = self.buckets.as_ptr().add(idx);
235 _mm_prefetch(bucket_ptr as *const i8, _MM_HINT_T0);
236 }
237 #[cfg(not(target_arch = "x86_64"))]
238 let _ = key;
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn store_and_probe_roundtrip() {
248 let mut tt = TranspositionTable::new(4); assert!(tt.probe(0xDEAD_BEEF).is_none());
250 tt.store(0xDEAD_BEEF, 123, 5, Bound::Exact, Some(42));
251 let e = tt.probe(0xDEAD_BEEF).expect("should hit");
252 assert_eq!(e.score, 123);
253 assert_eq!(e.depth, 5);
254 assert_eq!(e.bound, Bound::Exact);
255 assert_eq!(e.best_move, 42);
256 }
257
258 #[test]
259 fn depth_preferred_slot_keeps_higher_depth() {
260 let mut tt = TranspositionTable::new(4);
261 let k1 = 0x0000_0000_0000_0000u64; let k2 = 0x0000_0001_0000_0000u64; tt.store(k1, 100, 6, Bound::Exact, Some(1));
265 tt.store(k2, 200, 4, Bound::Exact, Some(2));
266 assert_eq!(tt.probe(k1).unwrap().score, 100);
268 assert_eq!(tt.probe(k2).unwrap().score, 200);
269 tt.store(k1, 50, 2, Bound::Exact, Some(3));
271 let probed = tt.probe(k1).unwrap();
272 assert_eq!(probed.score, 100);
274 }
275}