blunders_engine/
transposition.rs

1//! Shared Transposition Table.
2
3use std::convert::TryFrom;
4use std::fmt::Debug;
5use std::hash::{Hash, Hasher};
6use std::mem;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Mutex;
9
10use crate::coretypes::{Cp, Move, MoveInfo, PieceKind::*, PlyKind, Square};
11use crate::position::{Cache, Position};
12use crate::zobrist::{HashKind, ZobristTable};
13
14/// The type of a node in a search tree.
15/// See [Node Types](https://www.chessprogramming.org/Node_Types).
16#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
17#[repr(u8)]
18pub enum NodeKind {
19    /// An All node has had all of its children searched.
20    All,
21    /// A Cut node, or a node that was pruned because it caused a beta-cutoff.
22    Cut,
23    /// A principal variation node from a previous search.
24    Pv,
25}
26
27impl TryFrom<u8> for NodeKind {
28    type Error = ();
29    fn try_from(value: u8) -> Result<Self, Self::Error> {
30        const ALL: u8 = NodeKind::All as u8;
31        const CUT: u8 = NodeKind::Cut as u8;
32        const PV: u8 = NodeKind::Pv as u8;
33
34        match value {
35            ALL => Ok(NodeKind::All),
36            CUT => Ok(NodeKind::Cut),
37            PV => Ok(NodeKind::Pv),
38            _ => Err(()),
39        }
40    }
41}
42
43/// Entry contains information about a single previously searched position.
44#[derive(Debug, Copy, Clone, Eq, PartialEq)]
45pub struct Entry {
46    /// Full hash value for a position.
47    pub hash: HashKind,
48    /// Best move or refutation move of position.
49    pub key_move: Move,
50    /// The Score in centipawns for the position.
51    pub score: Cp,
52    /// The ply/depth that was searched to in this position's subtree.
53    pub ply: PlyKind,
54    /// Type of Node this position has in search tree.
55    pub node_kind: NodeKind,
56}
57
58impl Entry {
59    /// Returns new Entry from provided information.
60    pub fn new(
61        hash: HashKind,
62        key_move: Move,
63        score: Cp,
64        ply: PlyKind,
65        node_kind: NodeKind,
66    ) -> Self {
67        Self {
68            hash,
69            key_move,
70            score,
71            ply,
72            node_kind,
73        }
74    }
75
76    /// Returns a new Entry with illegal information.
77    pub fn illegal() -> Self {
78        Self {
79            hash: 0,
80            key_move: Move::illegal(),
81            score: Cp(0),
82            ply: 0,
83            node_kind: NodeKind::All,
84        }
85    }
86}
87
88impl Hash for Entry {
89    fn hash<H: Hasher>(&self, h: &mut H) {
90        h.write_u64(self.hash)
91    }
92}
93
94impl Default for Entry {
95    fn default() -> Self {
96        Self::illegal()
97    }
98}
99
100/// Transposition Table Bucket that holds 2 entries,
101/// consisting of a priority slot and a general slot.
102pub trait TwoBucket: Debug + Default + Sync {
103    /// The number of entries held by this bucket.
104    fn len() -> usize {
105        2
106    }
107
108    /// Returns an entry if its corresponding hash exists in this bucket.
109    /// If no entry's hash matches the given hash, returns None.
110    fn get(&self, hash: HashKind) -> Option<Entry>;
111
112    /// Returns true if this bucket has any entry which contains the given hash.
113    fn contains(&self, hash: HashKind) -> bool;
114
115    /// Unconditionally store the entry in the general slot, without updating age.
116    fn store(&self, general_entry: Entry);
117
118    /// Unconditionally place the entry in the priority slot and update age.
119    fn replace(&self, priority_entry: Entry, age: u8);
120
121    /// Move the existing priority entry to the general slot,
122    /// then place the new priority entry into the priority slot and update age.
123    fn swap_replace(&self, priority_entry: Entry, age: u8);
124
125    /// Replaces the `priority` slot if `should_replace` returns true,
126    /// otherwise the `general` slot is replaced.
127    ///
128    /// # Example:
129    /// if should_replace {
130    ///     priority := entry
131    /// } else {
132    ///     general := entry
133    /// }
134    ///
135    /// FnOnce signature:
136    ///
137    /// should_replace(&new_entry, new_age, &existing_priority_entry, existing_age) -> bool
138    fn replace_by<F>(&self, entry: Entry, age: u8, should_replace: F)
139    where
140        F: FnOnce(&Entry, u8, &Entry, u8) -> bool;
141
142    /// If should_replace returns true, then swap_replace with the given entry.
143    ///
144    /// Example:
145    ///
146    /// if should_replace {
147    ///     general := priority
148    ///     priority := entry
149    /// } else {
150    ///     general := entry
151    /// }
152    ///
153    /// FnOnce signature:
154    ///
155    /// should_replace(&new_entry, new_age, &existing_priority_entry, existing_age) -> bool
156    fn swap_replace_by<F>(&self, entry: Entry, age: u8, should_replace: F)
157    where
158        F: FnOnce(&Entry, u8, &Entry, u8) -> bool;
159}
160
161/// Dummy Bucket holds no data.
162/// This is useful for running a search effectively without a transposition table.
163#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
164pub struct DummyBucket;
165
166impl TwoBucket for DummyBucket {
167    fn get(&self, _hash: HashKind) -> Option<Entry> {
168        None
169    }
170    fn contains(&self, _hash: HashKind) -> bool {
171        false
172    }
173    fn store(&self, _general_entry: Entry) {}
174    fn replace(&self, _priority_entry: Entry, _age: u8) {}
175    fn swap_replace(&self, _priority_entry: Entry, _age: u8) {}
176    fn replace_by<F>(&self, _entry: Entry, _age: u8, _should_replace: F)
177    where
178        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
179    {
180    }
181    fn swap_replace_by<F>(&self, _entry: Entry, _age: u8, _should_replace: F)
182    where
183        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
184    {
185    }
186}
187
188/// Age type alias used for age of a Priority Entry.
189pub type AgeKind = u8;
190
191/// Bucket holds all items that correspond to an index in the Transposition Table.
192/// This bucket holds two Entries in order to allow the best of both worlds for replacement schemes:
193/// 1. Replace on condition, and 2. Always replace.
194///
195/// A replacement scheme is provided when attempting to replace an entry in this bucket.
196/// `scheme_entry` is always checked first.
197#[derive(Debug, Copy, Clone, Eq, PartialEq)]
198struct LockInner {
199    /// Entry that gets updated only if a replacement scheme is passed.
200    pub priority: Entry,
201    /// Entry that always gets replaced if the replacement scheme fails for `scheme_entry`.
202    pub general: Entry,
203    /// Age of `scheme_entry`. Useful for custom replacement schemes.
204    pub age: AgeKind,
205}
206
207impl LockInner {
208    /// Replace priority slot with new priority entry and update age.
209    #[inline]
210    fn inner_replace(&mut self, priority_entry: Entry, age: AgeKind) {
211        self.priority = priority_entry;
212        self.age = age;
213    }
214
215    #[inline]
216    fn inner_swap_replace(&mut self, priority_entry: Entry, age: AgeKind) {
217        self.general = mem::replace(&mut self.priority, priority_entry);
218        self.age = age;
219    }
220
221    #[inline]
222    fn inner_store(&mut self, general_entry: Entry) {
223        self.general = general_entry;
224    }
225}
226
227/// Bucket implemented with a Mutex for sync and lock.
228#[derive(Debug)]
229pub struct LockBucket {
230    mu: Mutex<LockInner>,
231}
232
233impl LockBucket {
234    /// Illegal initial value.
235    fn illegal() -> Self {
236        LockBucket {
237            mu: Mutex::new(LockInner {
238                age: 0,
239                priority: Entry::illegal(),
240                general: Entry::illegal(),
241            }),
242        }
243    }
244}
245
246impl Default for LockBucket {
247    fn default() -> Self {
248        Self::illegal()
249    }
250}
251
252impl TwoBucket for LockBucket {
253    #[inline]
254    fn get(&self, hash: HashKind) -> Option<Entry> {
255        let inner: LockInner = { *self.mu.lock().unwrap() };
256
257        if inner.priority.hash == hash {
258            Some(inner.priority)
259        } else if inner.general.hash == hash {
260            Some(inner.general)
261        } else {
262            None
263        }
264    }
265
266    #[inline]
267    fn contains(&self, hash: HashKind) -> bool {
268        let (priority_hash, general_hash) = {
269            let lock = self.mu.lock().unwrap();
270            (lock.priority.hash, lock.general.hash)
271        };
272        priority_hash == hash || general_hash == hash
273    }
274
275    #[inline]
276    fn store(&self, general_entry: Entry) {
277        let mut lock = self.mu.lock().unwrap();
278        lock.inner_store(general_entry);
279    }
280
281    #[inline]
282    fn replace(&self, priority_entry: Entry, age: AgeKind) {
283        let mut lock = self.mu.lock().unwrap();
284        lock.inner_replace(priority_entry, age);
285    }
286
287    #[inline]
288    fn swap_replace(&self, priority_entry: Entry, age: AgeKind) {
289        let mut lock = self.mu.lock().unwrap();
290        lock.inner_swap_replace(priority_entry, age);
291    }
292
293    #[inline]
294    fn replace_by<F>(&self, entry: Entry, age: AgeKind, should_replace: F)
295    where
296        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
297    {
298        let mut lock = self.mu.lock().unwrap();
299        match should_replace(&entry, age, &lock.priority, lock.age) {
300            true => lock.inner_replace(entry, age),
301            false => lock.inner_store(entry),
302        };
303    }
304
305    #[inline]
306    fn swap_replace_by<F>(&self, entry: Entry, age: AgeKind, should_replace: F)
307    where
308        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
309    {
310        let mut lock = self.mu.lock().unwrap();
311        match should_replace(&entry, age, &lock.priority, lock.age) {
312            true => lock.inner_swap_replace(entry, age),
313            false => lock.inner_store(entry),
314        };
315    }
316}
317
318/// Aligned Packed Data Format for AtomicEntry.
319///
320/// # Current Data Format Sizes
321/// key_move: u24, 3/8
322/// score: i16, 2/8
323/// ply: u8, 1/8
324/// node_kind: u8, 1/8
325/// optional_age: u8, 1/8
326///
327/// # Move Serialization
328/// from: u8, 1/8
329/// to: u8, 1/8
330/// promotion: match u8, 1/8
331///
332/// # Current Data Format Packed U64
333/// Hex F = 0b1111
334/// u64 hex: FFFFFFFFFFFFFFFF
335///
336/// age <- node_kind <- ply <- score <- key_move =
337/// age <- node_kind <- ply <- score <- promotion, to, from
338#[rustfmt::skip]
339#[allow(dead_code)] // For assertion bytes.
340mod adf {
341    pub const FROM_MASK: u64      = 0x00000000000000FF;
342    pub const TO_MASK: u64        = 0x000000000000FF00;
343    pub const PROMOTION_MASK: u64 = 0x0000000000FF0000;
344    pub const SCORE_MASK: u64     = 0x000000FFFF000000;
345    pub const PLY_MASK: u64       = 0x0000FF0000000000;
346    pub const NODE_KIND_MASK: u64 = 0x00FF000000000000;
347    pub const AGE_MASK: u64       = 0xFF00000000000000;
348
349    pub const FROM_SHIFT: u8      = 0;
350    pub const TO_SHIFT: u8        = 8;
351    pub const PROMOTION_SHIFT: u8 = 16;
352    pub const SCORE_SHIFT: u8     = 24;
353    pub const PLY_SHIFT: u8       = 40;
354    pub const NODE_KIND_SHIFT: u8 = 48;
355    pub const AGE_SHIFT: u8       = 56;
356
357    pub const FROM_BYTES: usize           = 1;
358    pub const TO_BYTES: usize             = 1;
359    pub const PROMOTION_BYTES: usize      = 1;
360    pub const SCORE_BYTES: usize          = 2;
361    pub const PLY_BYTES: usize            = 1;
362    pub const NODE_KIND_BYTES: usize      = 1;
363    pub const AGE_BYTES: usize            = 1;
364    pub const OPT_PIECE_KIND_BYTES: usize = 1;
365    pub const SQUARE_BYTES: usize         = 1;
366}
367
368/// AtomicEntry holds an Entry without an age in a unique format: As 2 AtomicU64 integers.
369/// Importantly, the only data that can be corrupted from an entry is its hash.
370#[derive(Debug)]
371pub struct AtomicEntry {
372    /// All the data from an Entry excluding its hash, packed into a single u64.
373    data: AtomicU64,
374    /// The hash of an Entry, XORed with the u64 representation of the rest of its data.
375    hash_xor_data: AtomicU64,
376}
377
378impl AtomicEntry {
379    /// Atomically load (read from) all fields of AtomicEntry.
380    fn load(&self, ordering: Ordering) -> LoadedAtomicEntry {
381        LoadedAtomicEntry {
382            data: self.data.load(ordering),
383            hash_xor_data: self.hash_xor_data.load(ordering),
384        }
385    }
386
387    /// Atomically store (write to) all fields of AtomicEntry.
388    fn store(&self, loaded_entry: LoadedAtomicEntry, ordering: Ordering) {
389        self.data.store(loaded_entry.data, ordering);
390        self.hash_xor_data
391            .store(loaded_entry.hash_xor_data, ordering);
392    }
393}
394
395impl From<LoadedAtomicEntry> for AtomicEntry {
396    fn from(loaded_entry: LoadedAtomicEntry) -> Self {
397        Self {
398            data: AtomicU64::new(loaded_entry.data),
399            hash_xor_data: AtomicU64::new(loaded_entry.hash_xor_data),
400        }
401    }
402}
403
404impl Default for AtomicEntry {
405    fn default() -> Self {
406        Self::from(LoadedAtomicEntry::default())
407    }
408}
409
410/// Exactly the same as AtomicEntry but with u64.
411#[derive(Debug, Copy, Clone, Eq, PartialEq)]
412struct LoadedAtomicEntry {
413    data: u64,
414    hash_xor_data: u64,
415}
416
417impl LoadedAtomicEntry {
418    /// Returns the hash value of this entry.
419    const fn hash(&self) -> HashKind {
420        self.data ^ self.hash_xor_data
421    }
422
423    /// Returns the unpacked Entry of this LoadedAtomicEntry.
424    fn entry(&self) -> Entry {
425        self.unpack().0
426    }
427
428    #[inline]
429    fn pack_move(move_: Move) -> u64 {
430        let from_u8 = move_.from as u8;
431        let to_u8 = move_.to as u8;
432        let promotion_u8 = match move_.promotion {
433            None => 0,
434            Some(King) => 1,
435            Some(Pawn) => 2,
436            Some(Knight) => 3,
437            Some(Rook) => 4,
438            Some(Queen) => 5,
439            Some(Bishop) => 6,
440        };
441
442        let from_pack = Self::pack_u8(from_u8, adf::FROM_SHIFT);
443        let to_pack = Self::pack_u8(to_u8, adf::TO_SHIFT);
444        let promotion_pack = Self::pack_u8(promotion_u8, adf::PROMOTION_SHIFT);
445        from_pack | to_pack | promotion_pack
446    }
447
448    /// Promotion unpacking must mirror the packing in Self::pack_move(..).
449    fn unpack_move(packed: u64) -> Move {
450        let from_u8 = Self::unpack_u8(packed, adf::FROM_SHIFT, adf::FROM_MASK);
451        let to_u8 = Self::unpack_u8(packed, adf::TO_SHIFT, adf::TO_MASK);
452        let promo_u8 = Self::unpack_u8(packed, adf::PROMOTION_SHIFT, adf::PROMOTION_MASK);
453
454        let from = Square::try_from(from_u8).unwrap();
455        let to = Square::try_from(to_u8).unwrap();
456        let promotion = match promo_u8 {
457            0 => None,
458            1 => Some(King),
459            2 => Some(Pawn),
460            3 => Some(Knight),
461            4 => Some(Rook),
462            5 => Some(Queen),
463            6 => Some(Bishop),
464            _ => None,
465        };
466
467        Move::new(from, to, promotion)
468    }
469
470    #[inline]
471    const fn pack_i16(value: i16, shift: u8, mask: u64) -> u64 {
472        ((value as u64) << shift) & mask
473    }
474
475    /// Extract an i16 from the containing u64 packed data.
476    #[inline]
477    const fn unpack_i16(packed_data: u64, shift: u8, mask: u64) -> i16 {
478        ((packed_data & mask) >> shift) as i16
479    }
480
481    #[inline]
482    const fn pack_u8(value: u8, shift: u8) -> u64 {
483        (value as u64) << shift
484    }
485
486    #[inline]
487    const fn unpack_u8(packed_data: u64, shift: u8, mask: u64) -> u8 {
488        ((packed_data & mask) >> shift) as u8
489    }
490
491    /// Pack an entry and an age into a single u64 integer.
492    fn pack(&mut self, entry: Entry, age: u8) {
493        let hash = entry.hash;
494        let mut data: u64 = 0;
495
496        data |= Self::pack_move(entry.key_move);
497        data |= Self::pack_i16(entry.score.0, adf::SCORE_SHIFT, adf::SCORE_MASK);
498        data |= Self::pack_u8(entry.ply, adf::PLY_SHIFT);
499        data |= Self::pack_u8(entry.node_kind as u8, adf::NODE_KIND_SHIFT);
500        data |= Self::pack_u8(age, adf::AGE_SHIFT);
501        self.data = data;
502        self.hash_xor_data = hash ^ data;
503    }
504
505    /// Unpack requires that AtomicEntry packed data was packed from a valid Entry.
506    fn unpack(&self) -> (Entry, AgeKind) {
507        let data = self.data;
508        let hash_xor_data = self.hash_xor_data;
509        let hash: u64 = data ^ hash_xor_data;
510
511        let key_move = Self::unpack_move(data);
512        let score = Cp(Self::unpack_i16(data, adf::SCORE_SHIFT, adf::SCORE_MASK));
513        let ply: PlyKind = Self::unpack_u8(data, adf::PLY_SHIFT, adf::PLY_MASK);
514        let node_kind = NodeKind::try_from(Self::unpack_u8(
515            data,
516            adf::NODE_KIND_SHIFT,
517            adf::NODE_KIND_MASK,
518        ))
519        .unwrap();
520
521        let age: AgeKind = Self::unpack_u8(data, adf::AGE_SHIFT, adf::AGE_MASK);
522        let entry = Entry::new(hash, key_move, score, ply, node_kind);
523        (entry, age)
524    }
525}
526
527impl Default for LoadedAtomicEntry {
528    fn default() -> Self {
529        Self::from(Entry::illegal())
530    }
531}
532
533impl From<Entry> for LoadedAtomicEntry {
534    fn from(entry: Entry) -> Self {
535        let mut loaded_atomic_entry = LoadedAtomicEntry {
536            data: 0,
537            hash_xor_data: 0,
538        };
539        loaded_atomic_entry.pack(entry, 0);
540        loaded_atomic_entry
541    }
542}
543
544impl From<(Entry, AgeKind)> for LoadedAtomicEntry {
545    fn from((entry, age): (Entry, AgeKind)) -> Self {
546        let mut loaded_atomic_entry = LoadedAtomicEntry {
547            data: 0,
548            hash_xor_data: 0,
549        };
550        loaded_atomic_entry.pack(entry, age);
551        loaded_atomic_entry
552    }
553}
554
555/// Bucket implemented with an XOR atomic trick for sync.
556#[derive(Debug, Default)]
557pub struct AtomicBucket {
558    priority: AtomicEntry,
559    general: AtomicEntry,
560}
561
562impl TwoBucket for AtomicBucket {
563    fn get(&self, hash: HashKind) -> Option<Entry> {
564        let loaded_priority = self.priority.load(Ordering::Acquire);
565        let loaded_general = self.general.load(Ordering::Acquire);
566
567        if hash == loaded_priority.hash() {
568            Some(loaded_priority.entry())
569        } else if hash == loaded_general.hash() {
570            Some(loaded_general.entry())
571        } else {
572            None
573        }
574    }
575
576    /// Returns true if this bucket has any entry which contains the given hash.
577    fn contains(&self, hash: HashKind) -> bool {
578        let loaded_priority = self.priority.load(Ordering::Acquire);
579        let loaded_general = self.general.load(Ordering::Acquire);
580        hash == loaded_priority.hash() || hash == loaded_general.hash()
581    }
582
583    /// Unconditionally store the entry in the general slot, without updating age.
584    fn store(&self, general_entry: Entry) {
585        self.general.store(general_entry.into(), Ordering::Release);
586    }
587
588    /// Unconditionally place the entry in the priority slot and update age.
589    fn replace(&self, priority_entry: Entry, age: u8) {
590        self.priority
591            .store((priority_entry, age).into(), Ordering::Release);
592    }
593
594    /// Move the existing priority entry to the general slot,
595    /// then place the new priority entry into the priority slot and update age.
596    fn swap_replace(&self, priority_entry: Entry, age: u8) {
597        let new_general = self.priority.load(Ordering::Acquire);
598        self.replace(priority_entry, age);
599        self.general.store(new_general, Ordering::Release);
600    }
601
602    /// Replaces the `priority` slot if `should_replace` returns true,
603    /// otherwise the `general` slot is replaced.
604    ///
605    /// # Example:
606    /// if should_replace {
607    ///     priority := entry
608    /// } else {
609    ///     general := entry
610    /// }
611    ///
612    /// FnOnce signature:
613    ///
614    /// should_replace(&new_entry, new_age, &existing_priority_entry, existing_age) -> bool
615    fn replace_by<F>(&self, entry: Entry, age: u8, should_replace: F)
616    where
617        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
618    {
619        let priority = self.priority.load(Ordering::Acquire);
620        let (existing_entry, existing_age) = priority.unpack();
621
622        match should_replace(&entry, age, &existing_entry, existing_age) {
623            true => self.replace(entry, age),
624            false => self.store(entry),
625        }
626    }
627
628    /// If should_replace returns true, then swap_replace with the given entry.
629    ///
630    /// Example:
631    ///
632    /// if should_replace {
633    ///     general := priority
634    ///     priority := entry
635    /// } else {
636    ///     general := entry
637    /// }
638    ///
639    /// FnOnce signature:
640    ///
641    /// should_replace(&new_entry, new_age, &existing_priority_entry, existing_age) -> bool
642    fn swap_replace_by<F>(&self, entry: Entry, age: u8, should_replace: F)
643    where
644        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
645    {
646        let priority = self.priority.load(Ordering::Acquire);
647        let (existing_entry, existing_age) = priority.unpack();
648
649        if should_replace(&entry, age, &existing_entry, existing_age) {
650            self.replace(entry, age);
651            self.general.store(priority, Ordering::Release);
652        } else {
653            self.store(entry);
654        }
655    }
656}
657
658/// Fill a Vector to capacity.
659fn fill_with_default<Bucket: TwoBucket>(v: &mut Vec<Bucket>) {
660    let capacity = v.capacity();
661    while v.len() < capacity {
662        v.push(Bucket::default());
663    }
664    debug_assert_eq!(v.len(), capacity);
665    debug_assert_eq!(v.capacity(), capacity);
666}
667
668/// A Transposition Table (tt) with a fixed size, memoizing previously evaluated
669/// chess positions. The table is safely sharable between threads as immutable.
670/// Slots may be updated from an immutable reference as each slot has its own lock.
671///
672/// The table uses a two layer system which ensures that new entries are always inserted
673/// into the table while also allowing important entries to remain for as long as they need.
674///
675/// The first layer is the replacement scheme layer, which allows the user to decide
676/// when to replace the entry based on a conditional test.
677///
678/// The second layer is the always replace layer, which gets replaced if the first layer does not.
679///
680/// Example:
681/// ```rust
682/// # use std::sync::Arc;
683/// # use blunders_engine::transposition::{Entry, NodeKind, TranspositionTable};
684/// # use blunders_engine::coretypes::{Move, Cp, Square::*};
685/// let tt = Arc::new(TranspositionTable::with_capacity(100));
686/// let age = 1;
687/// let hash = 100;
688/// let entry = Entry::new(hash, Move::new(D2, D4, None), Cp(3), 5, NodeKind::Pv);
689///
690/// tt.replace(entry, age);
691/// assert_eq!(tt.get(hash), Some(entry));
692/// ```
693pub struct TranspositionTable<Bucket: TwoBucket = AtomicBucket> {
694    /// Number of buckets in transpositions vector.
695    bucket_capacity: usize,
696    /// ZobristTable used to unify all entry hashes to the same hash generator.
697    ztable: ZobristTable,
698    /// Bucketed vector of transpositions.
699    transpositions: Vec<Bucket>,
700}
701
702/// Transposition Table functions that use the default generic parameter bucket.
703impl TranspositionTable {
704    /// Returns a new Transposition Table using the default bucket type.
705    pub fn new() -> Self {
706        Self::new_in()
707    }
708
709    /// Returns a new Transposition Table that holds `entry_capacity` entries
710    /// using the default bucket type.
711    pub fn with_capacity(entry_capacity: usize) -> Self {
712        Self::with_capacity_in(entry_capacity)
713    }
714
715    /// Returns a new Transposition Table with a capacity that fills given Megabytes
716    /// using the default bucket type.
717    pub fn with_mb(mb: usize) -> Self {
718        Self::with_mb_in(mb)
719    }
720
721    /// Returns a new TranspositionTable with provided ZobristTable with pre-allocated
722    /// default max capacity and default bucket type.
723    pub fn with_zobrist(ztable: ZobristTable) -> Self {
724        Self::with_zobrist_in(ztable)
725    }
726
727    /// Returns a new TranspositionTable with capacity in Megabytes and a given ZobristTable
728    /// using the default bucket type.
729    pub fn with_mb_and_zobrist(mb: usize, ztable: ZobristTable) -> Self {
730        Self::with_mb_and_zobrist_in(mb, ztable)
731    }
732
733    /// Returns a new TranspositionTable with provided ZobristTable and capacity in entries pre-allocated
734    /// using the default bucket type.
735    pub fn with_capacity_and_zobrist(entry_capacity: usize, ztable: ZobristTable) -> Self {
736        Self::with_capacity_and_zobrist_in(entry_capacity, ztable)
737    }
738}
739
740/// Generic Transposition Table functions.
741impl<Bucket: TwoBucket> TranspositionTable<Bucket> {
742    /// Number of entries table holds by default.
743    const DEFAULT_MAX_ENTRIES: usize = 100_000;
744
745    /// Converts a size in Megabytes to a capacity of inner vector.
746    fn mb_to_bucket_capacity(mb: usize) -> usize {
747        assert!(mb > 0, "mb cannot be 0");
748        (mb * 1_000_000) / mem::size_of::<Bucket>()
749    }
750
751    fn mb_to_entry_capacity(mb: usize) -> usize {
752        assert!(mb > 0, "mb cannot be 0");
753        let bucket_capacity = Self::mb_to_bucket_capacity(mb);
754        bucket_capacity * Bucket::len()
755    }
756
757    /// Returns a reference to the zobrist table.
758    pub fn zobrist_table(&self) -> &ZobristTable {
759        &self.ztable
760    }
761
762    /// Returns a new TranspositionTable with a randomly generated ZobristTable
763    /// and a pre-allocated default max entry capacity.
764    pub fn new_in() -> Self {
765        let ztable = ZobristTable::new();
766        Self::with_capacity_and_zobrist_in(Self::DEFAULT_MAX_ENTRIES, ztable)
767    }
768
769    /// Returns a new TranspositionTable with a randomly generated ZobristTable
770    /// with given capacity pre-allocated, where capacity is the number of entries in table.
771    pub fn with_capacity_in(entry_capacity: usize) -> Self {
772        let ztable = ZobristTable::new();
773        Self::with_capacity_and_zobrist_in(entry_capacity, ztable)
774    }
775
776    /// Returns a new TranspositionTable with a randomly generated ZobristTable
777    /// with capacity calculated to fill given Megabytes.
778    pub fn with_mb_in(mb: usize) -> Self {
779        let entry_capacity = Self::mb_to_entry_capacity(mb);
780        let ztable = ZobristTable::new();
781        Self::with_capacity_and_zobrist_in(entry_capacity, ztable)
782    }
783
784    /// Returns a new TranspositionTable with provided ZobristTable
785    /// with pre-allocated default max capacity.
786    pub fn with_zobrist_in(ztable: ZobristTable) -> Self {
787        let entry_capacity = Self::DEFAULT_MAX_ENTRIES;
788        Self::with_capacity_and_zobrist_in(entry_capacity, ztable)
789    }
790
791    /// Returns a new TranspositionTable with capacity in Megabytes and a given ZobristTable.
792    pub fn with_mb_and_zobrist_in(mb: usize, ztable: ZobristTable) -> Self {
793        let entry_capacity = Self::mb_to_entry_capacity(mb);
794        Self::with_capacity_and_zobrist_in(entry_capacity, ztable)
795    }
796
797    /// Returns a new TranspositionTable with provided ZobristTable
798    /// and capacity in entries pre-allocated.
799    pub fn with_capacity_and_zobrist_in(entry_capacity: usize, ztable: ZobristTable) -> Self {
800        // Add Bucket::len - 1 to guarantee minimum capacity due to integer division floor.
801        let bucket_capacity = (entry_capacity + Bucket::len() - 1) / Bucket::len();
802
803        let mut transpositions = Vec::with_capacity(bucket_capacity);
804        fill_with_default(&mut transpositions);
805
806        assert_eq!(bucket_capacity, transpositions.capacity());
807        assert_eq!(bucket_capacity, transpositions.len());
808        Self {
809            bucket_capacity,
810            ztable,
811            transpositions,
812        }
813    }
814
815    /// Returns the capacity of entries of the TranspositionTable.
816    pub fn capacity(&self) -> usize {
817        assert_eq!(self.bucket_capacity, self.transpositions.capacity());
818        self.transpositions.capacity() * Bucket::len()
819    }
820
821    /// Returns the capacity of buckets in this TranspositionTable.
822    pub fn bucket_capacity(&self) -> usize {
823        assert_eq!(self.bucket_capacity, self.transpositions.capacity());
824        self.bucket_capacity
825    }
826
827    /// Removes all items from TranspositionTable.
828    /// Since the TT uniquely holds its inner vector, this operation is safely guarded
829    /// by its signature `&mut self`, as it cannot be held by any other thread.
830    pub fn clear(&mut self) {
831        for bucket in &mut self.transpositions {
832            *bucket = Bucket::default();
833        }
834        debug_assert_eq!(self.bucket_capacity, self.transpositions.capacity());
835        debug_assert_eq!(self.bucket_capacity, self.transpositions.len());
836    }
837
838    /// Drops original table and allocates a new table of size `new_mb`.
839    /// Entries in the original table are not preserved.
840    /// Returns the table's new entry capacity.
841    pub fn set_mb(&mut self, new_mb: usize) -> usize {
842        let entry_capacity = Self::mb_to_entry_capacity(new_mb);
843        let ztable = self.ztable.clone();
844        *self = Self::with_capacity_and_zobrist_in(entry_capacity, ztable);
845        self.capacity()
846    }
847
848    /// Generate a hash for a Position with context to this TranspositionTable.
849    /// Hashes used for this table must be generated from it's context, because a hash for
850    /// any position are likely to be different between different TranspositionTables.
851    pub fn generate_hash(&self, position: &Position) -> HashKind {
852        self.ztable.generate_hash(position.into())
853    }
854
855    /// Update hash for the application of a Move on Position.
856    pub fn update_hash(
857        &self,
858        hash: &mut HashKind,
859        position: &Position,
860        move_info: MoveInfo,
861        cache: Cache,
862    ) {
863        self.ztable
864            .update_hash(hash, position.into(), move_info, cache);
865    }
866
867    /// Generate a new hash from a Move applied to an existing Hash and Position.
868    pub fn update_from_hash(
869        &self,
870        mut hash: HashKind,
871        position: &Position,
872        move_info: MoveInfo,
873        cache: Cache,
874    ) -> HashKind {
875        self.ztable
876            .update_hash(&mut hash, position.into(), move_info, cache);
877        hash
878    }
879
880    /// Convert a full hash to an index for this TranspositionTable.
881    pub fn hash_to_index(&self, hash: HashKind) -> usize {
882        (hash % self.bucket_capacity as HashKind) as usize
883    }
884
885    /// Returns true if a TranspositionTable bucket contains an entry with the given hash.
886    /// Key collisions are expected to be rare but possible,
887    /// so care should be taken with the return value.
888    pub fn contains(&self, hash: HashKind) -> bool {
889        let index = self.hash_to_index(hash);
890        self.transpositions[index].contains(hash)
891    }
892
893    /// Returns Entry if hash exists in the indexed bucket, None otherwise.
894    pub fn get(&self, hash: HashKind) -> Option<Entry> {
895        let index = self.hash_to_index(hash);
896        self.transpositions[index].get(hash)
897    }
898
899    /// Unconditionally replace an existing item in the TranspositionTable
900    /// where replace_by true would place it.
901    /// Capacity of the table remains unchanged.
902    pub fn replace(&self, priority_entry: Entry, age: AgeKind) {
903        let index = self.hash_to_index(priority_entry.hash);
904        self.transpositions[index].replace(priority_entry, age);
905
906        debug_assert_eq!(self.bucket_capacity, self.transpositions.capacity());
907        debug_assert_eq!(self.bucket_capacity, self.transpositions.len());
908    }
909
910    /// Move entry in priority slot to general slot then place priority_entry into priority slot.
911    pub fn swap_replace(&self, priority_entry: Entry, age: AgeKind) {
912        let index = self.hash_to_index(priority_entry.hash);
913        self.transpositions[index].swap_replace(priority_entry, age);
914    }
915
916    /// Store the entry into the index bucket's general slot, without changing age or scheme slot.
917    pub fn store(&self, general_entry: Entry) {
918        let index = self.hash_to_index(general_entry.hash);
919        self.transpositions[index].store(general_entry);
920    }
921
922    /// Attempt to insert an item into the tt depending on a replacement scheme.
923    /// If the replacement scheme evaluates to true, the entry replaces the bucket priority slot.
924    /// Otherwise, it is inserted into the general slot.
925    ///
926    /// Closure signature: should_replace(&replacing_entry, age, &existing_priority_entry, existing_age) -> bool.
927    ///
928    /// ## Example:
929    /// ```rust
930    /// # use blunders_engine::transposition::{Entry, NodeKind, TranspositionTable};
931    /// # use blunders_engine::coretypes::{Cp, Move, Square::*};
932    /// # let node_kind = NodeKind::All;
933    /// # let best_move = Move::new(D2, D4, None);
934    /// # let score = Cp(1);
935    /// let mut tt = TranspositionTable::with_capacity(2);
936    /// assert_eq!(tt.bucket_capacity(), 1); // All hashes index same bucket.
937    /// let age = 1;
938    ///
939    /// let deep_hash = 0;
940    /// let deep_ply = 10;
941    /// let deep_entry = Entry::new(deep_hash, best_move, score, deep_ply, node_kind);
942    ///
943    /// let shallow_hash = 8;
944    /// let shallow_ply = 2;
945    /// let shallow_entry = Entry::new(shallow_hash, best_move, score, shallow_ply, node_kind);
946    ///
947    /// fn replacement_scheme(entry: &Entry, age: u8, existing: &Entry, existing_age: u8) -> bool {
948    ///     age != existing_age || entry.ply > existing.ply
949    /// }
950    ///
951    /// // Hash slot starts empty, so tt_entry replaces priority slot.
952    /// tt.replace_by(deep_entry, age, replacement_scheme);
953    /// assert_eq!(tt.get(deep_hash).unwrap(), deep_entry);
954    ///
955    /// // Shallow entry does not pass replacement test, so it is placed in always slot.
956    /// tt.replace_by(shallow_entry, age, replacement_scheme);
957    /// assert_eq!(tt.get(deep_hash).unwrap(), deep_entry);
958    /// assert_eq!(tt.get(shallow_hash).unwrap(), shallow_entry);
959    ///
960    /// let other_hash = 101;
961    /// let other_ply = 1;
962    /// let other_entry = Entry::new(other_hash, best_move, score, other_ply, node_kind);
963    ///
964    /// // Other entry does not pass test for priority, so it replaces the always slot.
965    /// tt.replace_by(other_entry, age, replacement_scheme);
966    /// assert_eq!(tt.get(shallow_hash), None);
967    /// assert_eq!(tt.get(deep_hash).unwrap(), deep_entry);
968    /// assert_eq!(tt.get(other_hash).unwrap(), other_entry);
969    pub fn replace_by<F>(&self, entry: Entry, age: AgeKind, should_replace: F)
970    where
971        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
972    {
973        let index = self.hash_to_index(entry.hash);
974        self.transpositions[index].replace_by(entry, age, should_replace);
975    }
976
977    /// If entry passes the should_replace test, then the existing entry in the priority slot
978    /// is moved to the general slot and new entry gets placed in the priority slot.
979    /// Otherwise, the new entry is placed in the general slot.
980    pub fn swap_replace_by<F>(&self, entry: Entry, age: AgeKind, should_replace: F)
981    where
982        F: FnOnce(&Entry, u8, &Entry, u8) -> bool,
983    {
984        let index = self.hash_to_index(entry.hash);
985        self.transpositions[index].swap_replace_by(entry, age, should_replace)
986    }
987}
988
989#[cfg(test)]
990mod tests {
991    use super::*;
992    use crate::coretypes::{PieceKind, Square::*};
993    use std::mem::size_of;
994
995    #[test]
996    fn atomic_pack_sizes() {
997        //! AtomicEntry requires an exact data layout for struct that it packs.
998        //! This test ensures that if there is a change in the data layout externally,
999        //! it does not result in unexpected behavior as the test fails.
1000        assert_eq!(adf::FROM_BYTES, size_of::<Square>());
1001        assert_eq!(adf::TO_BYTES, size_of::<Square>());
1002        assert_eq!(adf::PROMOTION_BYTES, size_of::<Option<PieceKind>>());
1003        assert_eq!(adf::SCORE_BYTES, size_of::<Cp>());
1004        assert_eq!(adf::PLY_BYTES, size_of::<PlyKind>());
1005        assert_eq!(adf::NODE_KIND_BYTES, size_of::<NodeKind>());
1006        assert_eq!(adf::AGE_BYTES, size_of::<AgeKind>());
1007    }
1008
1009    #[test]
1010    fn loaded_atomic_entry() {
1011        {
1012            // Illegal entry test.
1013            let entry = Entry::illegal();
1014            let loaded = LoadedAtomicEntry::from(entry);
1015            assert_eq!(entry.hash, loaded.hash());
1016            assert_eq!(entry, loaded.entry());
1017        }
1018        {
1019            // Random entry test.
1020            let entry = Entry::new(500, Move::new(D2, D4, None), Cp(5000), 5, NodeKind::Pv);
1021            let age: AgeKind = 7;
1022
1023            let loaded = LoadedAtomicEntry::from((entry, age));
1024            let (loaded_entry, loaded_age) = loaded.unpack();
1025            assert_eq!(entry, loaded_entry);
1026            assert_eq!(age, loaded_age);
1027        }
1028        {
1029            // Random entry test with negative.
1030            let entry = Entry::new(
1031                0xAAFFEE,
1032                Move::new(H7, H8, Some(Knight)),
1033                Cp(-51),
1034                10,
1035                NodeKind::Cut,
1036            );
1037            let age: AgeKind = 7;
1038
1039            let loaded = LoadedAtomicEntry::from((entry, age));
1040            let (loaded_entry, loaded_age) = loaded.unpack();
1041            assert_eq!(entry, loaded_entry);
1042            assert_eq!(age, loaded_age);
1043        }
1044    }
1045
1046    // TODO
1047    //#[test]
1048    //fn size_of_requirements() {
1049    //    // Want a single entry to fit into L1 cache line?
1050    //    // Need to verify that this is how this works, not sure since Mutex is used.
1051    //    use std::mem::size_of;
1052    //    let size = size_of::<TtEntry>();
1053    //    println!("size_of::<TtEntry>() = {}", size);
1054    //    assert!(size <= 64);
1055    //}
1056
1057    #[test]
1058    fn new_tt_no_panic() {
1059        let hash: HashKind = 100;
1060        let tt = TranspositionTable::new();
1061        let tt_entry = Entry {
1062            hash,
1063            node_kind: NodeKind::All,
1064            key_move: Move::new(A2, A3, None),
1065            ply: 3,
1066            score: Cp(100),
1067        };
1068
1069        tt.store(tt_entry);
1070        assert!(tt.contains(hash));
1071    }
1072
1073    #[test]
1074    fn tt_single_capacity_replaces() {
1075        let tt = TranspositionTable::with_capacity(1);
1076        let age = 1;
1077        let tt_entry1 = Entry {
1078            hash: 100,
1079            node_kind: NodeKind::All,
1080            key_move: Move::new(A2, A3, None),
1081            ply: 3,
1082            score: Cp(100),
1083        };
1084        let tt_entry2 = Entry {
1085            hash: 200,
1086            node_kind: NodeKind::All,
1087            key_move: Move::new(B5, B3, None),
1088            ply: 4,
1089            score: Cp(-200),
1090        };
1091
1092        // Starts empty.
1093        assert!(!tt.contains(tt_entry1.hash));
1094        assert!(!tt.contains(tt_entry2.hash));
1095        assert_eq!(tt.get(tt_entry1.hash), None);
1096        assert_eq!(tt.get(tt_entry2.hash), None);
1097
1098        // Inserts one item correctly.
1099        tt.replace(tt_entry1, age);
1100        assert!(tt.contains(tt_entry1.hash));
1101        assert!(!tt.contains(tt_entry2.hash));
1102        assert_eq!(tt.get(tt_entry1.hash), Some(tt_entry1));
1103        assert_eq!(tt.get(tt_entry2.hash), None);
1104
1105        // Replaces previous item in index priority slot, should move to always slot.
1106        tt.replace(tt_entry2, age);
1107        assert!(!tt.contains(tt_entry1.hash));
1108        assert!(tt.contains(tt_entry2.hash));
1109        assert_eq!(tt.get(tt_entry1.hash), None);
1110        assert_eq!(tt.get(tt_entry2.hash), Some(tt_entry2));
1111    }
1112
1113    #[test]
1114    fn tt_start_position() {
1115        let tt = TranspositionTable::with_capacity(10000);
1116        let pos = Position::start_position();
1117        let hash = tt.generate_hash(&pos);
1118        let age = 1;
1119        let tt_entry = Entry {
1120            hash,
1121            node_kind: NodeKind::All,
1122            key_move: Move::new(D2, D4, None),
1123            ply: 5,
1124            score: Cp(0),
1125        };
1126
1127        // Starts without Entry.
1128        assert!(!tt.contains(hash));
1129        assert_eq!(tt.get(hash), None);
1130
1131        // Finds correct Entry from large table.
1132        tt.replace(tt_entry, age);
1133        assert!(tt.contains(hash));
1134        assert_eq!(tt.get(hash), Some(tt_entry));
1135    }
1136}