blunders_engine/
zobrist.rs

1//! Zobrist Hashing
2
3use std::collections::HashSet;
4use std::ops::Index;
5
6use rand::prelude::*;
7
8use crate::boardrepr::PieceSets;
9use crate::coretypes::{Castling, Color, File, Piece, PieceKind, Rank, Square, SquareIndexable};
10use crate::coretypes::{MoveInfo, MoveKind, Square::*};
11use crate::coretypes::{NUM_FILES, NUM_PIECE_KINDS, NUM_SQUARES};
12use crate::position::{Cache, Position};
13
14/// HashKind is an alias for the underlying type of a Zobrist Hash.
15pub type HashKind = u64;
16
17/// Key contains all data needed to generate a hash.
18pub type Key<'a> = (&'a PieceSets, &'a Color, &'a Castling, &'a Option<Square>);
19
20/// Convert a Position reference into a Key.
21impl<'a> From<&'a Position> for Key<'a> {
22    fn from(pos_ref: &'a Position) -> Self {
23        (
24            pos_ref.pieces(),
25            pos_ref.player(),
26            pos_ref.castling(),
27            pos_ref.en_passant(),
28        )
29    }
30}
31
32// Strategy:
33// A TT has a maximum size. That size is pre-allocated, and never changes.
34// The Key of a table is a Position, which has a cached hash.
35
36/// Zobrist Hashing is a quick and incremental way to hash a chess position.
37/// ZobristTable contains unique, pseudo-randomly generated values
38/// used for calculating Zobrist Hash of a chess position.
39///
40/// Each Piece gets a unique number for each square.
41/// A single side to move gets a unique number.
42/// Each possible combination of castling rights gets a unique number.
43/// Each possible file for En-Passant gets a unique number.
44#[derive(Debug, Clone, Eq, PartialEq)]
45pub struct ZobristTable {
46    piece_hash: [[HashKind; NUM_SQUARES]; NUM_PIECE_KINDS],
47    ep_hash: [HashKind; NUM_FILES],
48    castling_hash: [HashKind; Castling::ENUMERATIONS],
49    pub(crate) player_hash: HashKind,
50}
51
52impl ZobristTable {
53    const TOGGLE_PLAYER: Color = Color::Black;
54
55    /// Returns a new ZobristTable with randomly seeded, unique values.
56    pub fn new() -> Self {
57        Self::with_rng(StdRng::from_entropy())
58    }
59
60    /// Returns a new ZobristTable with unique values generated from seeded rng.
61    pub fn with_seed(seed: u64) -> Self {
62        Self::with_rng(StdRng::seed_from_u64(seed))
63    }
64
65    /// Returns a new ZobristTable with unique values generated from rng.
66    fn with_rng(mut rng: StdRng) -> Self {
67        // Ensure there are no duplicates in Table. Each value used must be unique.
68        let mut used_values = HashSet::new();
69
70        let mut piece_hash = [[HashKind::default(); NUM_SQUARES]; NUM_PIECE_KINDS];
71        let mut ep_hash = [HashKind::default(); NUM_FILES];
72        let mut castling_hash = [HashKind::default(); Castling::ENUMERATIONS];
73
74        // arbitrarily large number to prevent infinite loops for bad rng.
75        let mut inf_loop_protection: usize = 10_000;
76
77        // Initialize array parts of Table.
78        for item in piece_hash
79            .iter_mut()
80            .flatten()
81            .chain(ep_hash.iter_mut())
82            .chain(castling_hash.iter_mut())
83        {
84            let mut new_unique_value: HashKind = rng.gen();
85            // insert returns false if item was already in set.
86            // Loop until unique value is found.
87            while !used_values.insert(new_unique_value) {
88                new_unique_value = rng.gen();
89
90                inf_loop_protection -= 1;
91                if inf_loop_protection < 10 {
92                    panic!("Encountered excessively repeated random numbers.");
93                }
94            }
95            *item = new_unique_value;
96        }
97
98        // Initialize scalar parts of table.
99        let mut player_hash: HashKind = rng.gen();
100        while !used_values.insert(player_hash) {
101            player_hash = rng.gen();
102
103            inf_loop_protection -= 1;
104            if inf_loop_protection < 10 {
105                panic!("Encountered excessively repeated random numbers.");
106            }
107        }
108
109        Self {
110            piece_hash,
111            ep_hash,
112            castling_hash,
113            player_hash,
114        }
115    }
116
117    /// Generate a hash value from provided key in context of this ZobristTable.
118    pub fn generate_hash(&self, key: Key) -> HashKind {
119        let mut hash = HashKind::default();
120
121        // For each piece, xor its value from ztable into the hash.
122        for color in Color::iter() {
123            for piece_kind in PieceKind::iter() {
124                let piece = Piece::new(color, piece_kind);
125                let squares = key.0[piece];
126
127                for square in squares {
128                    hash ^= self[(piece, square)];
129                }
130            }
131        }
132
133        // Hash the en-passant file if it exists.
134        if let Some(ep_square) = key.3 {
135            hash ^= self[ep_square.file()];
136        }
137
138        // Hash castling rights.
139        hash ^= self[*key.2];
140
141        // Hash player. Only need to hash when active player is Black.
142        // This allows incremental hashing when making a move in a single xor for player.
143        if *key.1 == ZobristTable::TOGGLE_PLAYER {
144            hash ^= self.player_hash;
145        }
146
147        hash
148    }
149
150    /// Update a hash from a Position and its MoveInfo. The move that resulted in MoveInfo
151    /// must already be applied to the position.
152    /// update_hash works both directions, it can apply and remove a move from a position's hash.
153    ///
154    /// # Arguments
155    /// `hash`: The hash value to directly update.
156    /// `key`: A key taken from an updated Position.
157    /// `move_info`: The MoveInfo that was applied to some position.
158    /// `cache`: The original cache of the Position, before a move.
159    pub fn update_hash(&self, hash: &mut HashKind, key: Key, move_info: MoveInfo, cache: Cache) {
160        let moved_player = !key.1;
161        let passive_player = *key.1;
162
163        // Always toggle player hash because player always alternates.
164        *hash ^= self.player_hash;
165        // Always toggle both old and new Castling, as each enumeration even none has a hash.
166        *hash ^= self[cache.castling];
167        *hash ^= self[*key.2];
168        // Always toggle piece on "from" square.
169        let moved_piece = Piece::new(moved_player, move_info.piece_kind);
170        *hash ^= self[(moved_piece, move_info.from)];
171
172        // Toggle both old and new en-passant, if they exist.
173        let old_ep = cache.en_passant;
174        let new_ep = key.3;
175        if let Some(ep_square) = old_ep {
176            *hash ^= self[ep_square.file()];
177        }
178        if let Some(ep_square) = new_ep {
179            *hash ^= self[ep_square.file()];
180        }
181
182        // Toggle moved piece on "to" square. If promoted, instead toggle promoted_piece.
183        let to_piece_kind: PieceKind = match move_info.promotion {
184            Some(promoted_pk) => promoted_pk,
185            None => move_info.piece_kind,
186        };
187        let to_piece = Piece::new(moved_player, to_piece_kind);
188        *hash ^= self[(to_piece, move_info.to)];
189
190        match move_info.move_kind {
191            // Toggle passive player's captured piece if a normal capture occurred.
192            MoveKind::Capture(captured_pk) => {
193                let captured_piece = Piece::new(passive_player, captured_pk);
194                *hash ^= self[(captured_piece, move_info.to)];
195            }
196
197            // Toggle passive player's pawn if en-passant occurred.
198            MoveKind::EnPassant => {
199                let ep_square = cache.en_passant.unwrap();
200                let pawn_square = match ep_square.rank() {
201                    Rank::R3 => ep_square.increment_rank().unwrap(),
202                    _ => ep_square.decrement_rank().unwrap(),
203                };
204                let captured_pawn = Piece::new(passive_player, PieceKind::Pawn);
205                *hash ^= self[(captured_pawn, pawn_square)];
206            }
207
208            // Toggle both castling squares for rook.
209            MoveKind::Castle => {
210                let (rook_from, rook_to) = match move_info.to {
211                    G1 => (H1, F1),
212                    C1 => (A1, D1),
213                    G8 => (H8, F8),
214                    C8 => (A8, D8),
215                    _ => panic!("Processing Castling Move, but to square was not valid."),
216                };
217                let castled_rook = Piece::new(moved_player, PieceKind::Rook);
218                *hash ^= self[(castled_rook, rook_from)];
219                *hash ^= self[(castled_rook, rook_to)];
220            }
221
222            // Nothing extra is toggled for quiet moves.
223            MoveKind::Quiet => (),
224        };
225    }
226}
227
228/// Default for ZobristTable is a table with a random seed.
229impl Default for ZobristTable {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235/// Index used for accessing piece_hash.
236impl Index<(Piece, Square)> for ZobristTable {
237    type Output = HashKind;
238    fn index(&self, index: (Piece, Square)) -> &Self::Output {
239        let (piece, square) = index;
240        &self.piece_hash[piece.zobrist_offset()][square.idx()]
241    }
242}
243
244// Index used for accessing ep_hash (en-passant hash).
245impl Index<File> for ZobristTable {
246    type Output = HashKind;
247    fn index(&self, index: File) -> &Self::Output {
248        &self.ep_hash[index as usize]
249    }
250}
251
252// Index used for accessing castling_hash.
253impl Index<Castling> for ZobristTable {
254    type Output = HashKind;
255    fn index(&self, index: Castling) -> &Self::Output {
256        &self.castling_hash[index.bits() as usize]
257    }
258}
259
260// Implementations for Color, PieceKind, and Piece to allow indexing into ZobristTable.
261// This code is copied from piece_sets.rs.
262// TODO: Consider consolidating.
263
264impl Color {
265    /// Get the position of the start of the block for a color.
266    /// There are 6 piece_kinds per color, so one should start at 0, and the other at 6.
267    #[inline(always)]
268    const fn zobrist_offset_block(&self) -> usize {
269        match self {
270            Color::White => 0,
271            Color::Black => 6,
272        }
273    }
274}
275
276impl PieceKind {
277    /// Get the offset of a piece_kind within a block.
278    /// Values must cover all numbers of [0, 1, 2, 3, 4, 5].
279    #[inline(always)]
280    const fn zobrist_offset_pk(&self) -> usize {
281        match self {
282            PieceKind::King => 0,
283            PieceKind::Pawn => 1,
284            PieceKind::Knight => 2,
285            PieceKind::Queen => 3,
286            PieceKind::Rook => 4,
287            PieceKind::Bishop => 5,
288        }
289    }
290}
291
292impl Piece {
293    /// Get the completely qualified index for a piece.
294    #[inline(always)]
295    const fn zobrist_offset(&self) -> usize {
296        self.color.zobrist_offset_block() + self.piece_kind.zobrist_offset_pk()
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use crate::coretypes::Move;
304    use crate::fen::Fen;
305    use crate::Position;
306
307    fn test_before_and_after(
308        table: ZobristTable,
309        before: Position,
310        after: Position,
311        legal_move: Move,
312    ) {
313        let hash_before = table.generate_hash(Key::from(&before));
314        let hash_after = table.generate_hash(Key::from(&after));
315
316        // Multiple hashes generated from same table and position are equal.
317        let hash_extra_before = table.generate_hash(Key::from(&before));
318        let hash_extra_after = table.generate_hash(Key::from(&after));
319        assert_eq!(hash_before, hash_extra_before);
320        assert_eq!(hash_after, hash_extra_after);
321
322        // Check that hashes and positions are the same before applying move.
323        let mut pos = before.clone();
324        let mut hash = table.generate_hash(Key::from(&pos));
325        assert_eq!(pos, before);
326        assert_eq!(hash, hash_before);
327
328        // Check that newly updated hash and position
329        // equal individually generated hash and position.
330        let cache = pos.cache();
331        let move_info = pos.do_move(legal_move);
332        table.update_hash(&mut hash, Key::from(&pos), move_info, cache);
333        assert_eq!(pos, after);
334        assert_eq!(hash, hash_after);
335
336        // Check that updating hash a second time undoes the previous update.
337        table.update_hash(&mut hash, Key::from(&pos), move_info, cache);
338        assert_eq!(hash, hash_before);
339
340        // Check that updating hash a third time acts like the first update.
341        table.update_hash(&mut hash, Key::from(&pos), move_info, cache);
342        assert_eq!(hash, hash_after);
343    }
344
345    #[test]
346    fn hash_start_position() {
347        let table = ZobristTable::new();
348        let legal_move = Move::new(D2, D4, None);
349        let start_position = Position::start_position();
350        let queens_pawn_game = start_position.make_move(legal_move);
351
352        test_before_and_after(table, start_position, queens_pawn_game, legal_move);
353    }
354
355    #[test]
356    fn hash_en_passant_position() {
357        let table = ZobristTable::new();
358        let legal_move = Move::new(D5, E6, None);
359        let pos_before =
360            Position::parse_fen("rnbqkbnr/pp1p1ppp/8/2pPp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 3")
361                .unwrap();
362        let pos_after =
363            Position::parse_fen("rnbqkbnr/pp1p1ppp/4P3/2p5/8/8/PPP1PPPP/RNBQKBNR b KQkq - 0 3")
364                .unwrap();
365
366        test_before_and_after(table, pos_before, pos_after, legal_move);
367    }
368
369    #[test]
370    fn hash_castling_position() {
371        let table = ZobristTable::new();
372        let legal_move = Move::new(E1, G1, None);
373        let pos_before = Position::parse_fen(
374            "rnb1k1nr/pp3ppp/3bp3/q2p4/2Pp4/2NBPN2/PP3PPP/R1BQK2R w KQkq - 0 7",
375        )
376        .unwrap();
377        let pos_after =
378            Position::parse_fen("rnb1k1nr/pp3ppp/3bp3/q2p4/2Pp4/2NBPN2/PP3PPP/R1BQ1RK1 b kq - 1 7")
379                .unwrap();
380
381        test_before_and_after(table, pos_before, pos_after, legal_move);
382    }
383}