blunders_engine/search/history.rs
1//! History structure used within search.
2
3use crate::arrayvec::ArrayVec;
4use crate::coretypes::MAX_HISTORY;
5use crate::position::Game;
6use crate::zobrist::{HashKind, ZobristTable};
7
8type HashHistory = ArrayVec<HashKind, MAX_HISTORY>;
9type Unrepeatables = ArrayVec<usize, MAX_HISTORY>;
10
11/// History primary use is for tracking repeated moves to prevent threefold repetition.
12/// It is stateful, in that functions assume the next interaction comes from the next
13/// possible move in a played game.
14///
15/// It contains the hashes of all previously visited positions,
16/// and the indices of positions which cannot be repeated in future positions.
17#[derive(Debug, Clone, Eq, PartialEq)]
18pub struct History {
19 hash_history: HashHistory, // All visited position hashes in order.
20 unrepeatables: Unrepeatables, // Stack of unrepeatable position indices.
21 head: usize, // Most recent unrepeatable position.
22}
23
24impl History {
25 /// Create a new empty History.
26 pub fn empty() -> Self {
27 Self {
28 hash_history: HashHistory::new(),
29 unrepeatables: Unrepeatables::new(),
30 head: 0,
31 }
32 }
33 /// Create a new History from a game and a Zobrist Table.
34 pub fn new(game: &Game, ztable: &ZobristTable) -> Self {
35 let mut history = Self::empty();
36 let mut position = game.base_position;
37
38 // Only push a move when it is in the past (original hash after a move is applied).
39 // The final (current) move is not added to history because it is active.
40 for move_ in &game.moves {
41 let hash = ztable.generate_hash((&position).into());
42 let move_info = position.do_legal_move(*move_).expect("move not legal");
43
44 history.push(hash, move_info.is_unrepeatable());
45 }
46
47 debug_assert_eq!(position, game.position);
48 history
49 }
50
51 /// Pushes a new position into the hash history, and updates the most recent unrepeatable
52 /// index if applicable.
53 pub fn push(&mut self, hash: HashKind, is_unrepeatable: bool) {
54 self.hash_history.push(hash);
55
56 if is_unrepeatable {
57 self.unrepeatables.push(self.head);
58 self.head = self.hash_history.len().checked_sub(1).unwrap_or(0);
59 }
60 }
61
62 /// Pops a position from history stack. If the popped item was the most recent unrepeatable,
63 /// then replace it with the previous unrepeatable index.
64 pub fn pop(&mut self) {
65 self.hash_history.pop();
66
67 // If the current head exceeds the limit, replace it with the previous unrepeatable index.
68 if self.head >= self.hash_history.len() {
69 self.head = self.unrepeatables.pop().unwrap_or(0);
70 }
71 }
72
73 /// Returns true if the position occurs at least once in history.
74 /// This is done by only checking the history from the last unrepeatable index to the most recent entry.
75 /// All positions before the index cannot reoccur in the next sequence.
76 pub fn contains(&self, hash: HashKind) -> bool {
77 self.contains_n(hash, 1)
78 }
79
80 /// Returns true if the position occurs in history at least `n` times,
81 /// assuming the position to check may be the next move in this game's history.
82 pub fn contains_n(&self, hash: HashKind, count: usize) -> bool {
83 let mut counter = 0;
84 self.hash_history[self.head..].iter().rev().any(|old_hash| {
85 if *old_hash == hash {
86 counter += 1;
87 if counter >= count {
88 return true;
89 }
90 }
91 false
92 })
93 }
94
95 /// Returns true if the position occurs twice in history, indicating that the given
96 /// position is the second repetition (position occurs total of three times).
97 pub fn is_threefold_repetition(&self, hash: HashKind) -> bool {
98 self.contains_n(hash, 2)
99 }
100
101 /// Returns true if the position occurs once in history, indicating that
102 /// the given position is the first repetition (position occurs total of two times).
103 pub fn is_twofold_repetition(&self, hash: HashKind) -> bool {
104 self.contains(hash)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::Position;
112
113 #[test]
114 fn position_with_no_history() {
115 let ztable = ZobristTable::new();
116 let game = Game::from(Position::start_position());
117
118 let history = History::new(&game, &ztable);
119
120 assert_eq!(history.head, 0);
121 assert_eq!(history.hash_history.len(), 0);
122 assert_eq!(history.unrepeatables.len(), 0);
123 }
124}