game_player/transposition_table.rs
1//! Transposition Table Module
2//!
3//! This module implements a transposition table, which is a map of game state values referenced by the states' fingerprints.
4
5/// A map of game state values referenced by the states' fingerprints.
6///
7/// A game state can be the result of different sequences of the same (or a different) set of moves. This technique is used to
8/// cache the value of a game state regardless of the moves used to reach it, thus the name "transposition" table. The purpose of
9/// the "transposition" table has been extended to become simply a cache of game state values, so it is more aptly named "game
10/// state value cache" -- but the old name persists.
11///
12/// As a speed and memory optimization in this implementation, slots in the table are not unique to the state being stored, and a
13/// value may be overwritten when a new value is added. A value is overwritten only when its "quality" is less than or equal to the
14/// "quality" of the value being added.
15///
16/// # Note
17/// The fingerprint is assumed to be a random and uniformly distributed 64-bit value. It is assumed to never be u64::MAX.
18///
19/// # Examples
20///
21/// ```rust
22/// # use game_player::transposition_table::TranspositionTable;
23/// let mut table = TranspositionTable::new(1000, 100);
24///
25/// // Store a value
26/// table.update(12345, 0.75, 5);
27///
28/// // Retrieve the value
29/// if let Some((value, quality)) = table.check(12345, 0) {
30/// assert_eq!(value, 0.75);
31/// assert_eq!(quality, 5);
32/// }
33/// ```
34pub struct TranspositionTable {
35 /// The table of entries
36 table: Vec<Entry>,
37 /// The maximum age of entries allowed in the table
38 max_age: i16,
39}
40
41// Table entry
42//
43// A note about age and quality: There are expected to be collisions in the table, so the quality is used to determine if a new
44// entry should replace an existing one. Now, an entry that has not been referenced for a while will probably never be
45// referenced again, so it should eventually be allowed to be replaced by a newer entry, regardless of the quality of the new
46// entry.
47#[derive(Clone)]
48#[repr(C, packed)] // 16 bytes
49struct Entry {
50 fingerprint: u64, // The state's fingerprint
51 value: f32, // The state's value
52 q: i16, // The quality of the value
53 age: i16, // The number of turns since the entry has been referenced
54}
55
56impl Entry {
57 const UNUSED: u64 = u64::MAX;
58
59 fn clear(&mut self) {
60 self.fingerprint = Self::UNUSED;
61 }
62}
63
64impl Default for Entry {
65 fn default() -> Self {
66 Self {
67 fingerprint: Self::UNUSED,
68 value: 0.0,
69 q: 0,
70 age: 0,
71 }
72 }
73}
74
75// Check that the size of Entry is 16 bytes. The size is not required to be 16 bytes, but 16 bytes is an optimal size.
76static_assertions::assert_eq_size!(f32, [u8; 4]); // float should be 32 bits
77static_assertions::assert_eq_size!(Entry, [u8; 16]); // Entry should be 16 bytes
78
79impl TranspositionTable {
80 /// Creates a new TranspositionTable
81 ///
82 /// # Arguments
83 /// * `size` - Number of entries in the table
84 /// * `max_age` - Maximum age of entries allowed in the table
85 ///
86 /// # Examples
87 ///
88 /// ```rust
89 /// # use game_player::transposition_table::TranspositionTable;
90 /// let table = TranspositionTable::new(1000, 50);
91 /// // Table is ready to use with 1000 entries and max age of 50
92 /// ```
93 ///
94 /// # Panics
95 ///
96 /// Panics if `size` is 0 or `max_age` is 0 or negative.
97 ///
98 /// ```should_panic
99 /// # use game_player::transposition_table::TranspositionTable;
100 /// let table = TranspositionTable::new(0, 50); // This will panic
101 /// ```
102 pub fn new(size: usize, max_age: i16) -> Self {
103 assert!(size > 0);
104 assert!(max_age > 0);
105 Self {
106 table: vec![Entry::default(); size],
107 max_age,
108 }
109 }
110
111 /// Returns the value and quality of a state if they are stored in the table and its quality is above the specified minimum (if
112 /// specified). Otherwise, None is returned.
113 ///
114 /// # Arguments
115 /// * `fingerprint` - Fingerprint of state to be checked for
116 /// * `min_q` - Minimum quality. If less than 0, it is not used.
117 ///
118 /// # Returns
119 /// optional result as (value, quality)
120 ///
121 /// # Panics
122 /// Panics if `fingerprint` is `u64::MAX`.
123 ///
124 /// # Side Effects
125 /// * Resets the age of the entry to 0 if found.
126 ///
127 /// # Examples
128 /// ```rust
129 /// # use game_player::transposition_table::TranspositionTable;
130 /// let mut table = TranspositionTable::new(100, 10);
131 ///
132 /// // Store a value with quality 5
133 /// table.update(12345, 1.5, 5);
134 ///
135 /// // Check with no minimum quality
136 /// assert_eq!(table.check(12345, -1), Some((1.5, 5)));
137 ///
138 /// // Check with minimum quality of 3 (should succeed)
139 /// assert_eq!(table.check(12345, 3), Some((1.5, 5)));
140 ///
141 /// // Check with minimum quality of 10 (should fail)
142 /// assert_eq!(table.check(12345, 10), None);
143 ///
144 /// // Check non-existent entry
145 /// assert_eq!(table.check(99999, -1), None);
146 /// ```
147 pub fn check(&mut self, fingerprint: u64, min_q: i16) -> Option<(f32, i16)> {
148 assert_ne!(fingerprint, Entry::UNUSED, "fingerprint != u64::MAX");
149
150 // Find the entry
151 let entry = self.find(fingerprint);
152 if entry.fingerprint != fingerprint {
153 return None; // Not found
154 }
155
156 // The entry was accessed so reset its age
157 entry.age = 0;
158
159 // Check the quality if min_q >= 0
160 if min_q >= 0 && entry.q < min_q {
161 return None; // Insufficient quality
162 }
163
164 Some((entry.value, entry.q))
165 }
166
167 /// Updates (or adds) an entry in the table if its quality is greater than or equal to the existing entry's quality
168 ///
169 /// # Arguments
170 /// * `fingerprint` - Fingerprint of state to be stored
171 /// * `value` - Value to be stored
172 /// * `quality` - Quality of the value
173 ///
174 /// # Panics
175 /// Panics if `fingerprint` is `u64::MAX`.
176 /// Panics if `quality` is negative.
177 ///
178 /// # Examples
179 /// ```rust
180 /// # use game_player::transposition_table::TranspositionTable;
181 /// let mut table = TranspositionTable::new(100, 10);
182 ///
183 /// // Add a new entry
184 /// table.update(12345, 1.0, 5);
185 /// assert_eq!(table.check(12345, -1), Some((1.0, 5)));
186 ///
187 /// // Try to update with lower quality (should not replace)
188 /// table.update(12345, 2.0, 3);
189 /// assert_eq!(table.check(12345, -1), Some((1.0, 5))); // Original value
190 ///
191 /// // Update with higher quality (should replace)
192 /// table.update(12345, 2.0, 7);
193 /// assert_eq!(table.check(12345, -1), Some((2.0, 7))); // New value
194 /// ```
195 pub fn update(&mut self, fingerprint: u64, value: f32, quality: i16) {
196 assert_ne!(fingerprint, Entry::UNUSED, "fingerprint != u64::MAX");
197 assert!(quality >= 0);
198
199 // Find the entry for the fingerprint
200 let entry = self.find(fingerprint);
201 let is_unused = entry.fingerprint == Entry::UNUSED;
202
203 // If the entry is unused or if the new quality >= the stored quality, then store the new value. Note: It is assumed to be
204 // better to replace values of equal quality in order to dispose of old entries that are less likely to be relevant.
205
206 if is_unused || quality >= entry.q {
207 *entry = Entry {
208 fingerprint,
209 value,
210 q: quality,
211 age: 0,
212 };
213 }
214 }
215
216 /// Sets an entry in the table.
217 ///
218 /// This method adds or updates an entry in the table, regardless of its quality.
219 ///
220 /// # Arguments
221 /// * `fingerprint` - Fingerprint of state to be stored
222 /// * `value` - Value to be stored
223 /// * `quality` - Quality of the value
224 ///
225 /// # Panics
226 /// Panics if `fingerprint` is `u64::MAX`.
227 /// Panics if `quality` is negative.
228 ///
229 /// # Examples
230 /// ```rust
231 /// # use game_player::transposition_table::TranspositionTable;
232 /// let mut table = TranspositionTable::new(100, 10);
233 ///
234 /// // Set an entry
235 /// table.set(12345, 1.5, 5);
236 /// assert_eq!(table.check(12345, -1), Some((1.5, 5)));
237 ///
238 /// // Set again with lower quality (should still replace)
239 /// table.set(12345, 2.5, 3);
240 /// assert_eq!(table.check(12345, -1), Some((2.5, 3)));
241 /// ```
242 pub fn set(&mut self, fingerprint: u64, value: f32, quality: i16) {
243 assert_ne!(fingerprint, Entry::UNUSED, "fingerprint != u64::MAX");
244 assert!(quality >= 0);
245
246 // Find the entry for the fingerprint
247 let entry = self.find(fingerprint);
248
249 // Store the state, value and quality
250 *entry = Entry {
251 fingerprint,
252 value,
253 q: quality,
254 age: 0,
255 };
256 }
257
258 /// The T-table is persistent. So in order to gradually dispose of entries that are no longer relevant, entries that have not
259 /// been referenced for a while are removed.
260 ///
261 /// This method increments the age of all entries and removes entries that exceed the maximum age.
262 ///
263 /// # Examples
264 ///
265 /// ```rust
266 /// # use game_player::transposition_table::TranspositionTable;
267 /// let mut table = TranspositionTable::new(100, 2); // max_age = 2
268 ///
269 /// // Add an entry
270 /// table.set(12345, 1.0, 5);
271 /// assert_eq!(table.check(12345, -1), Some((1.0, 5)));
272 ///
273 /// // Age the table once - entry should still be there
274 /// table.age();
275 /// assert_eq!(table.check(12345, -1), Some((1.0, 5))); // Age reset by check
276 ///
277 /// // Age twice more without accessing - entry should be removed
278 /// table.age();
279 /// table.age();
280 /// table.age();
281 /// assert_eq!(table.check(12345, -1), None); // Entry aged out
282 /// ```
283 pub fn age(&mut self) {
284 self.table
285 .iter_mut()
286 .filter(|entry| entry.fingerprint != Entry::UNUSED)
287 .for_each(|entry| {
288 entry.age += 1;
289 if entry.age > self.max_age {
290 entry.clear();
291 }
292 });
293 }
294
295 // Find the entry slot for a fingerprint using simple modulo hashing
296 fn find(&mut self, hash: u64) -> &mut Entry {
297 let i = (hash as usize) % self.table.len();
298 &mut self.table[i]
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_new_valid_parameters() {
308 let table = TranspositionTable::new(100, 10);
309 assert_eq!(table.table.len(), 100);
310 assert_eq!(table.max_age, 10);
311 }
312
313 #[test]
314 #[should_panic(expected = "assertion failed: size > 0")]
315 fn test_new_zero_size_panics() {
316 TranspositionTable::new(0, 10);
317 }
318
319 #[test]
320 #[should_panic(expected = "assertion failed: max_age > 0")]
321 fn test_new_zero_max_age_panics() {
322 TranspositionTable::new(100, 0);
323 }
324
325 #[test]
326 #[should_panic(expected = "assertion failed: max_age > 0")]
327 fn test_new_negative_max_age_panics() {
328 TranspositionTable::new(100, -1);
329 }
330
331 #[test]
332 fn test_check_nonexistent_entry() {
333 let mut table = TranspositionTable::new(100, 10);
334 assert_eq!(table.check(12345, -1), None);
335 assert_eq!(table.check(12345, 0), None);
336 assert_eq!(table.check(12345, 5), None);
337 }
338
339 #[test]
340 fn test_update_and_check_basic() {
341 let mut table = TranspositionTable::new(100, 10);
342
343 // Add an entry
344 table.update(12345, 1.5, 5);
345
346 // Check it's there
347 assert_eq!(table.check(12345, -1), Some((1.5, 5)));
348 assert_eq!(table.check(12345, 0), Some((1.5, 5)));
349 assert_eq!(table.check(12345, 5), Some((1.5, 5)));
350 }
351
352 #[test]
353 fn test_check_minimum_quality() {
354 let mut table = TranspositionTable::new(100, 10);
355
356 table.update(12345, 2.0, 5);
357
358 // Check with various minimum qualities
359 assert_eq!(table.check(12345, -1), Some((2.0, 5))); // No minimum
360 assert_eq!(table.check(12345, 0), Some((2.0, 5))); // Below stored quality
361 assert_eq!(table.check(12345, 3), Some((2.0, 5))); // Below stored quality
362 assert_eq!(table.check(12345, 5), Some((2.0, 5))); // Equal to stored quality
363 assert_eq!(table.check(12345, 6), None); // Above stored quality
364 assert_eq!(table.check(12345, 10), None); // Well above stored quality
365 }
366
367 #[test]
368 fn test_update_quality_replacement_rules() {
369 let mut table = TranspositionTable::new(100, 10);
370
371 // Add initial entry
372 table.update(12345, 1.0, 5);
373 assert_eq!(table.check(12345, -1), Some((1.0, 5)));
374
375 // Try to update with lower quality (should not replace)
376 table.update(12345, 2.0, 3);
377 assert_eq!(table.check(12345, -1), Some((1.0, 5))); // Original value
378
379 // Update with equal quality (should replace)
380 table.update(12345, 3.0, 5);
381 assert_eq!(table.check(12345, -1), Some((3.0, 5))); // New value
382
383 // Update with higher quality (should replace)
384 table.update(12345, 4.0, 7);
385 assert_eq!(table.check(12345, -1), Some((4.0, 7))); // New value
386 }
387
388 #[test]
389 fn test_set_always_replaces() {
390 let mut table = TranspositionTable::new(100, 10);
391
392 // Add initial entry
393 table.set(12345, 1.0, 5);
394 assert_eq!(table.check(12345, -1), Some((1.0, 5)));
395
396 // Set with lower quality (should still replace)
397 table.set(12345, 2.0, 3);
398 assert_eq!(table.check(12345, -1), Some((2.0, 3)));
399
400 // Set with higher quality (should replace)
401 table.set(12345, 3.0, 7);
402 assert_eq!(table.check(12345, -1), Some((3.0, 7)));
403 }
404
405 #[test]
406 fn test_age_resets_on_check() {
407 let mut table = TranspositionTable::new(100, 2);
408
409 // Add an entry
410 table.set(12345, 1.0, 5);
411
412 // Age once
413 table.age();
414
415 // Check (should reset age)
416 assert_eq!(table.check(12345, -1), Some((1.0, 5)));
417 }
418
419 #[test]
420 fn test_age_removes_old_entries() {
421 let mut table = TranspositionTable::new(100, 2);
422
423 // Add an entry
424 table.set(12345, 1.0, 5);
425 assert_eq!(table.check(12345, -1), Some((1.0, 5)));
426
427 // Age beyond max_age without accessing
428 table.age(); // age = 1
429 table.age(); // age = 2
430 table.age(); // age = 3, should be removed (> max_age = 2)
431
432 // Entry should be gone
433 assert_eq!(table.check(12345, -1), None);
434 }
435
436 #[test]
437 fn test_multiple_entries() {
438 let mut table = TranspositionTable::new(100, 10);
439
440 // Add multiple entries
441 table.update(1, 1.0, 1);
442 table.update(2, 2.0, 2);
443 table.update(3, 3.0, 3);
444
445 // Check all entries exist
446 assert_eq!(table.check(1, -1), Some((1.0, 1)));
447 assert_eq!(table.check(2, -1), Some((2.0, 2)));
448 assert_eq!(table.check(3, -1), Some((3.0, 3)));
449
450 // Check non-existent entry
451 assert_eq!(table.check(4, -1), None);
452 }
453
454 #[test]
455 fn test_hash_collision_handling() {
456 // Create a small table to force collisions
457 let mut table = TranspositionTable::new(1, 10); // Only 1 slot
458
459 // Add first entry
460 table.update(1, 1.0, 5);
461 assert_eq!(table.check(1, -1), Some((1.0, 5)));
462
463 // Add second entry that will hash to same slot
464 // Since we have quality-based replacement, this should replace
465 table.update(2, 2.0, 5);
466 assert_eq!(table.check(2, -1), Some((2.0, 5)));
467 assert_eq!(table.check(1, -1), None); // First entry should be gone
468 }
469
470 #[test]
471 fn test_floating_point_values() {
472 let mut table = TranspositionTable::new(100, 10);
473
474 // Test various floating point values
475 let test_values = [
476 0.0,
477 -0.0,
478 1.0,
479 -1.0,
480 std::f32::consts::PI,
481 -std::f32::consts::E,
482 f32::MIN,
483 f32::MAX,
484 f32::EPSILON,
485 -f32::EPSILON,
486 ];
487
488 for (i, &value) in test_values.iter().enumerate() {
489 let fingerprint = (i + 1) as u64;
490 table.update(fingerprint, value, 1);
491 assert_eq!(table.check(fingerprint, -1), Some((value, 1)));
492 }
493 }
494
495 #[test]
496 fn test_edge_case_qualities() {
497 let mut table = TranspositionTable::new(100, 10);
498
499 // Test with quality 0
500 table.update(1, 1.0, 0);
501 assert_eq!(table.check(1, -1), Some((1.0, 0)));
502 assert_eq!(table.check(1, 0), Some((1.0, 0)));
503 assert_eq!(table.check(1, 1), None);
504
505 // Test with high quality
506 table.update(2, 2.0, i16::MAX);
507 assert_eq!(table.check(2, -1), Some((2.0, i16::MAX)));
508 assert_eq!(table.check(2, i16::MAX), Some((2.0, i16::MAX)));
509 }
510
511 #[test]
512 #[should_panic(expected = "assertion `left != right` failed: fingerprint != u64::MAX")]
513 fn test_check_with_invalid_fingerprint() {
514 let mut table = TranspositionTable::new(100, 10);
515 table.check(u64::MAX, -1);
516 }
517
518 #[test]
519 #[should_panic(expected = "assertion `left != right` failed: fingerprint != u64::MAX")]
520 fn test_update_with_invalid_fingerprint() {
521 let mut table = TranspositionTable::new(100, 10);
522 table.update(u64::MAX, 1.0, 5);
523 }
524
525 #[test]
526 #[should_panic(expected = "assertion failed: quality >= 0")]
527 fn test_update_with_negative_quality() {
528 let mut table = TranspositionTable::new(100, 10);
529 table.update(12345, 1.0, -1);
530 }
531
532 #[test]
533 #[should_panic(expected = "assertion `left != right` failed: fingerprint != u64::MAX")]
534 fn test_set_with_invalid_fingerprint() {
535 let mut table = TranspositionTable::new(100, 10);
536 table.set(u64::MAX, 1.0, 5);
537 }
538
539 #[test]
540 #[should_panic(expected = "assertion failed: quality >= 0")]
541 fn test_set_with_negative_quality() {
542 let mut table = TranspositionTable::new(100, 10);
543 table.set(12345, 1.0, -1);
544 }
545
546 #[test]
547 fn test_large_table() {
548 let mut table = TranspositionTable::new(10000, 100);
549
550 // Add many entries
551 for i in 1..=1000 {
552 table.update(i, i as f32 * 0.1, (i % 20) as i16);
553 }
554
555 // Check some entries exist
556 assert_eq!(table.check(1, -1), Some((0.1, 1)));
557 assert_eq!(table.check(500, -1), Some((50.0, 0)));
558 assert_eq!(table.check(1000, -1), Some((100.0, 0)));
559 }
560
561 #[test]
562 fn test_aging_multiple_entries() {
563 let mut table = TranspositionTable::new(100, 3);
564
565 // Add multiple entries
566 table.set(1, 1.0, 1);
567 table.set(2, 2.0, 2);
568 table.set(3, 3.0, 3);
569
570 // Age twice
571 table.age();
572 table.age();
573
574 // Access one entry to reset its age
575 assert_eq!(table.check(2, -1), Some((2.0, 2)));
576
577 // Age beyond max_age
578 table.age();
579 table.age();
580
581 // Only the accessed entry should remain
582 assert_eq!(table.check(1, -1), None);
583 assert_eq!(table.check(2, -1), Some((2.0, 2)));
584 assert_eq!(table.check(3, -1), None);
585 }
586}