1pub mod base58;
2#[cfg(any(rust_chess_compile_time_polyglot_basis, test))]
3pub mod polyglot;
4#[cfg(any(rust_chess_compile_time_standard_basis, test))]
5pub mod standard;
6
7use crate::{
8 board::{Board, Player, PlayerTable, Role, RoleTable},
9 finite::Empty as _,
10 game::{Game, Line},
11 position::{Castles, EnPassant, EnPassantTable, Move, Position, Side},
12 square::{FileTable, SquareTable},
13};
14
15#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
17pub struct Key(pub u128);
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct Basis {
29 pub board: BoardTable<Key>,
30 pub turn: PlayerTable<Key>,
31 pub castle: CastleTable<Key>,
32 pub en_passant: EnPassantTable<Key>,
33}
34
35impl Position {
36 pub const fn key(self) -> Key {
38 self.key_with_basis(Basis::standard())
39 }
40
41 pub const fn polyglot_key(self) -> Key {
43 self.key_with_basis(Basis::polyglot())
44 }
45
46 pub const fn key_with_basis(self, basis: &Basis) -> Key {
47 self.apparent_key(basis)
49 .xor(castle_key(basis, self.castles()))
50 .xor(en_passant_key(basis, self.en_passant()))
51 }
52
53 pub const fn apparent_key(self, basis: &Basis) -> Key {
55 self.board().key_with_basis(basis).xor(turn_key(basis, self.turn()))
56 }
57
58 pub fn full_key(self) -> Key {
60 self.key()
61 .xor(counter_key("reversible", self.reversible()))
62 .xor(counter_key("round", self.round().get()))
63 }
64}
65
66impl Board {
67 pub const fn polyglot_key(self) -> Key {
68 self.key_with_basis(Basis::polyglot())
69 }
70
71 pub const fn key(self) -> Key {
72 self.key_with_basis(Basis::standard())
73 }
74
75 pub const fn key_with_basis(self, basis: &Basis) -> Key {
76 let mut key = Key(0);
77
78 finite_for!(player in Player {
79 finite_for!(role in Role {
80 let mut squares = self.player(player).intersection(self.role(role));
81 while let Some(square) = squares.pop_first() {
82 key = key.xor(basis.board.get(square).get(player).get(role));
83 }
84 });
85 });
86
87 key
88 }
89}
90
91impl Game {
92 pub fn key(&self) -> Key {
94 let mut hash = sha2_const::Sha256::new()
95 .update(b"game:")
96 .update(&self.start().position().full_key().u128().to_be_bytes());
97
98 let mut position = self.start();
99 while let Ok(play) = position.main() {
100 hash = hash.update(b":").update(play.play().uci_960().to_string().as_bytes());
101 position = play.position();
102 }
103
104 Key(fold(hash.finalize()))
105 }
106}
107
108impl Line<Position, Move> {
109 pub fn key(&self) -> Key {
110 let mut hash = sha2_const::Sha256::new()
111 .update(b"line:")
112 .update(self.position.key().to_string().as_bytes());
113
114 for play in &self.moves {
115 hash = hash.update(b":").update(play.to_string().as_bytes());
116 }
117
118 Key(fold(hash.finalize()))
119 }
120}
121
122impl Key {
123 pub const fn xor(self, other: Key) -> Key {
124 Key(self.0 ^ other.0)
125 }
126
127 pub const fn u128(self) -> u128 {
128 self.0
129 }
130
131 pub const fn be_bytes(self) -> [u8; 16] {
132 self.0.to_be_bytes()
133 }
134
135 pub fn nonce() -> Self {
136 let nonce = base58::Base58::random_str(12);
137 let bytes = nonce.as_bytes();
138 let mut key = [0; 16];
139 let start = key.len() - bytes.len();
140 key[start..].copy_from_slice(bytes);
141 Key(u128::from_be_bytes(key))
142 }
143}
144
145type U256 = [u8; 32];
146
147pub const fn hash(bytes: &[u8]) -> Key {
148 Key(fold(sha256(bytes)))
149}
150
151const fn sha256(bytes: &[u8]) -> U256 {
152 sha2_const::Sha256::new().update(bytes).finalize()
153}
154
155const fn fold(hash: U256) -> u128 {
156 let (upper, lower) = split(hash);
157 upper ^ lower
158}
159
160const fn split(hash: U256) -> (u128, u128) {
161 let mut upper = [0u8; 16];
162 let mut lower = [0u8; 16];
163
164 let mut i = 0;
165 while i < 16 {
166 upper[i] = hash[i];
167 lower[i] = hash[i + 16];
168 i += 1;
169 }
170
171 (u128::from_be_bytes(upper), u128::from_be_bytes(lower))
172}
173
174type BoardTable<T> = SquareTable<PlayerTable<RoleTable<T>>>;
175type CastleTable<T> = PlayerTable<FileTable<T>>;
176
177impl Basis {
178 pub const fn empty() -> Self {
179 Self {
180 board: BoardTable::EMPTY,
181 turn: PlayerTable::EMPTY,
182 castle: CastleTable::EMPTY,
183 en_passant: EnPassantTable::EMPTY,
184 }
185 }
186
187 pub const fn polyglot() -> &'static Self {
188 #[cfg(rust_chess_compile_time_polyglot_basis)]
189 static BASIS: Basis = polyglot::compute_basis();
190 #[cfg(not(rust_chess_compile_time_polyglot_basis))]
191 static BASIS: Basis = include!("key/POLYGLOT-BASIS.rs");
192
193 &BASIS
194 }
195
196 pub const fn standard() -> &'static Self {
197 #[cfg(rust_chess_compile_time_standard_basis)]
198 #[allow(long_running_const_eval)]
199 static BASIS: Basis = standard::compute_table();
200 #[cfg(not(rust_chess_compile_time_standard_basis))]
201 static BASIS: Basis = include!("key/STANDARD-BASIS.rs");
202
203 &BASIS
204 }
205}
206
207fn counter_key(prefix: &str, counter: u32) -> Key {
208 hash(format!("{prefix}:{counter}").as_bytes())
209}
210
211const fn turn_key(basis: &Basis, turn: Player) -> Key {
212 basis.turn.get(turn)
213}
214
215const fn en_passant_key(basis: &Basis, en_passant: Option<EnPassant>) -> Key {
216 match en_passant {
217 Some(square) => basis.en_passant.get(square),
218 None => Key(0),
219 }
220}
221
222const fn castle_key(basis: &Basis, castles: Castles) -> Key {
223 let mut key = Key(0);
224
225 finite_for!(player in Player {
226 finite_for!(side in Side {
227 if let Some(file) = castles.get(player, side) {
228 key = key.xor(basis.castle.get(player).get(file));
229 }
230 });
231 });
232
233 key
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::{Move, board::Role::*, game::Cursor, square::Square::*};
240
241 const COLLISION_FEN: &str = "2b1kqr1/p2p3p/3p4/p2PpP2/PpP2p2/6P1/8/RRB1KQ1N w - - 21 32";
242 const COLLISION_GAME: &str = "
243 1. e4 c5 2. Ba6 bxa6 3. b4 g6 4. f4 Bh6 5. f5 Bf4 6. g3 Nf6
244 7. gxf4 Nh5 8. c4 cxb4 9. Ne2 Ng3 10. Nd4 a5 11. hxg3 Nc6
245 12. Nxc6 Rb8 13. Nxb8 g5 14. d4 f6 15. Na6 gxf4 16. Nc5 Rg8
246 17. Nb7 Rh8 18. Nd6+ exd6 19. e5 fxe5 20. d5 Qf6 21. a4 Rg8
247 22. Nd2 Qf7 23. Rb1 Qf6 24. Ne4 Qf7 25. Rh2 Qf6 26. Nf2 Qf7
248 27. Nh1 Qf6 28. Qd3 Qf7 29. Qf1 Qf6 30. Ra2 Qf7 31. Raa1 Qf8 *";
249 const START: Position = Position::start();
250
251 #[test]
252 fn document_line_keys() {
253 let d4 = Move::normal(Pawn, D2, D4);
254 let e4 = Move::normal(Pawn, E2, E4);
255 let e5 = Move::normal(Pawn, E7, E5);
256 let c5 = Move::normal(Pawn, C7, C5);
257 let nf3 = Move::normal(Knight, G1, F3);
258 let key =
259 |moves: &[Move]| Line { position: START, moves: moves.to_vec() }.key().to_string();
260
261 assert_eq!(key(&[]), "Y4LCEFvKgeZxrQoFJeeQZ8");
262 assert_eq!(key(&[e4]), "95asNCfiG3MeWa1q5jsZ1n");
263 assert_eq!(key(&[e4, c5]), "JQt9mENjV4RUbvog6oWGtb");
264 assert_eq!(key(&[e4, e5, nf3]), "LR2zFDKgxYWDcLHp9nME2i");
265 assert_eq!(key(&[nf3, d4]), "75SZLN4dvpcbErRJaDcnKR");
266 }
267
268 #[test]
269 fn document_game_keys() {
270 use crate::formats::{Parser as _, pgn};
271
272 let game = Game::chess(START).unwrap();
274 let key = game.key();
275 assert_eq!(key.to_string(), "36MkWJmbeTWaYEDrfrGxNn");
276 assert_eq!(key.u128(), 22523061550585735571242541243028587795);
277
278 let mut cursor = Cursor::new(game);
280 cursor.option_mut_or_push(Move::normal(Pawn, E2, E4)).unwrap();
281 let e4 = cursor.into_game();
282 assert_eq!(e4.key().to_string(), "46osJVmEGh3XxSk2FKcFbr");
283 assert_eq!(e4.key().u128(), 33370984391190127392068542153345756661);
284
285 let pgn = pgn::pgn.parse(COLLISION_GAME).unwrap();
287 let game: Game = pgn.try_into().unwrap();
288 assert_eq!(game.key().to_string(), "DL7b2eDrC8LVtpBQFL9BYT");
289 assert_eq!(game.key().u128(), 132719545793121815798994835819316023416);
290 }
291
292 #[test]
293 fn document_polyglot_start_position_hash() {
294 assert_eq!(START.polyglot_key(), Key(0x463b_9618_1691_fc9c));
295 }
296
297 #[test]
298 fn document_polyglot_start_position_collision() {
299 let start = Position::start();
303 let position = Position::from_fen(COLLISION_FEN).unwrap();
304
305 assert_eq!(position.polyglot_key(), start.polyglot_key());
306 assert_ne!(position.key(), start.key());
307 }
308
309 #[test]
310 fn reach_polyglot_collision_position_from_linked_movetext() {
311 use crate::formats::{Parser as _, pgn};
312
313 let pgn = pgn::pgn.parse(COLLISION_GAME).unwrap();
317 let game: Game = pgn.try_into().unwrap();
318 let mut cursor = Cursor::new(game);
319 cursor.mainline_end();
320 let constructed = cursor.position();
321
322 let expected = Position::from_fen(COLLISION_FEN).unwrap();
323
324 assert_eq!(constructed.fen(), expected.fen());
325 assert_eq!(constructed.transposition_fen(), expected.transposition_fen());
326 assert_eq!(constructed.key(), expected.key());
327 assert_eq!(constructed.polyglot_key(), expected.polyglot_key());
328 }
329
330 #[test]
331 fn document_polyglot_zero_hash_position() {
332 let position =
336 Position::from_fen("2b1k3/4p3/3p1p2/p2P2p1/P2P4/2P2PP1/4P3/2NQKB2 b - - 0 1").unwrap();
337
338 assert_eq!(position.polyglot_key(), Key(0));
339 assert_ne!(position.key_with_basis(Basis::standard()), Key(0));
340 }
341}