1pub mod base58;
2pub mod basis;
3pub use basis::{Basis, POLYGLOT, STANDARD};
4
5use crate::{
19 board::{Board, Player, Role},
20 game::Game,
21 position::{Castles, EnPassant, Position, Side},
22};
23
24#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
26pub struct Id(pub u128);
27
28impl Id {
29 pub const fn xor(self, other: Id) -> Id {
30 Id(self.0 ^ other.0)
31 }
32
33 pub const fn u128(self) -> u128 {
34 self.0
35 }
36
37 pub fn nonce() -> Self {
38 let nonce = base58::Base58::random_str(12);
39 let bytes = nonce.as_bytes();
40 let mut id = [0; 16];
41 let start = id.len() - bytes.len();
42 id[start..].copy_from_slice(bytes);
43 Id(u128::from_be_bytes(id))
44 }
45}
46
47type U256 = [u8; 32];
48
49pub const fn hash(bytes: &[u8]) -> Id {
50 Id(fold(sha256(bytes)))
51}
52
53const fn sha256(bytes: &[u8]) -> U256 {
54 sha2_const::Sha256::new().update(bytes).finalize()
55}
56
57const fn fold(hash: U256) -> u128 {
58 let (upper, lower) = split(hash);
59 upper ^ lower
60}
61
62const fn split(hash: U256) -> (u128, u128) {
63 let mut upper = [0u8; 16];
64 let mut lower = [0u8; 16];
65
66 let mut i = 0;
67 while i < 16 {
68 upper[i] = hash[i];
69 lower[i] = hash[i + 16];
70 i += 1;
71 }
72
73 (u128::from_be_bytes(upper), u128::from_be_bytes(lower))
74}
75
76impl Game {
77 pub fn id(&self) -> Id {
79 use crate::game::cursor::Mainline;
80
81 let mut hash = sha2_const::Sha256::new()
82 .update(b"game:")
83 .update(&self.start().id().u128().to_be_bytes());
84
85 for play in Mainline::new(self) {
86 hash = hash.update(b":").update(play.play().uci_960().to_string().as_bytes());
87 }
88
89 Id(fold(hash.finalize()))
90 }
91
92 }
106
107impl Position {
108 pub fn id(self) -> Id {
110 self.standard_id()
111 .xor(counter_id("reversible", self.reversible()))
112 .xor(counter_id("round", self.round().get()))
113 }
114
115 pub const fn standard_id(self) -> Id {
117 self.transposition_id(&STANDARD)
118 }
119
120 pub const fn polyglot_id(self) -> Id {
122 self.transposition_id(&POLYGLOT)
123 }
124
125 pub const fn transposition_id(self, basis: &Basis) -> Id {
126 self.apparent_id(basis)
128 .xor(castle_id(basis, self.castles()))
129 .xor(en_passant_id(basis, self.en_passant()))
130 }
131 pub const fn apparent_id(self, basis: &Basis) -> Id {
133 self.board().id(basis).xor(turn_id(basis, self.turn()))
134 }
135}
136
137impl Board {
138 pub const fn polyglot_id(self) -> Id {
139 self.id(&POLYGLOT)
140 }
141
142 pub const fn standard_id(self) -> Id {
143 self.id(&STANDARD)
144 }
145
146 pub const fn id(self, basis: &Basis) -> Id {
147 let mut id = Id(0);
148
149 finite_for!(player in Player {
150 finite_for!(role in Role {
151 let mut squares = self.player(player).intersection(self.role(role));
152 while let Some(square) = squares.pop_first() {
153 id = id.xor(basis.board.get(square).get(player).get(role));
154 }
155 });
156 });
157
158 id
159 }
160}
161
162fn counter_id(prefix: &str, counter: u32) -> Id {
163 hash(format!("{prefix}:{counter}").as_bytes())
164}
165
166const fn turn_id(basis: &Basis, turn: Player) -> Id {
167 basis.turn.get(turn)
168}
169
170const fn en_passant_id(basis: &Basis, en_passant: Option<EnPassant>) -> Id {
171 match en_passant {
172 Some(square) => basis.en_passant.get(square),
173 None => Id(0),
174 }
175}
176
177const fn castle_id(basis: &Basis, castles: Castles) -> Id {
178 let mut id = Id(0);
179
180 finite_for!(player in Player {
181 finite_for!(side in Side {
182 if let Some(file) = castles.get(player, side) {
183 id = id.xor(basis.castle.get(player).get(file));
184 }
185 });
186 });
187
188 id
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::{Move, board::Role::*, game::Cursor, square::Square::*};
195
196 const COLLISION_FEN: &str = "2b1kqr1/p2p3p/3p4/p2PpP2/PpP2p2/6P1/8/RRB1KQ1N w - - 21 32";
197 const COLLISION_GAME: &str = "
198 1. e4 c5 2. Ba6 bxa6 3. b4 g6 4. f4 Bh6 5. f5 Bf4 6. g3 Nf6
199 7. gxf4 Nh5 8. c4 cxb4 9. Ne2 Ng3 10. Nd4 a5 11. hxg3 Nc6
200 12. Nxc6 Rb8 13. Nxb8 g5 14. d4 f6 15. Na6 gxf4 16. Nc5 Rg8
201 17. Nb7 Rh8 18. Nd6+ exd6 19. e5 fxe5 20. d5 Qf6 21. a4 Rg8
202 22. Nd2 Qf7 23. Rb1 Qf6 24. Ne4 Qf7 25. Rh2 Qf6 26. Nf2 Qf7
203 27. Nh1 Qf6 28. Qd3 Qf7 29. Qf1 Qf6 30. Ra2 Qf7 31. Raa1 Qf8 *";
204 const START: Position = Position::start();
205
206 #[test]
207 fn documents_game_ids() {
208 use crate::formats::{Parser as _, pgn};
209
210 let game = Game::chess(START).unwrap();
212 let id = game.id();
213 assert_eq!(id.to_string(), "36MkWJmbeTWaYEDrfrGxNn");
214 assert_eq!(id.u128(), 22523061550585735571242541243028587795);
215
216 let mut cursor = Cursor::new(game);
218 cursor.push(Move::normal(Pawn, E2, E4)).unwrap();
219 let e4 = cursor.into_inner();
220 assert_eq!(e4.id().to_string(), "46osJVmEGh3XxSk2FKcFbr");
221 assert_eq!(e4.id().u128(), 33370984391190127392068542153345756661);
222
223 let pgn = pgn::game.parse(COLLISION_GAME).unwrap();
225 let game: Game = pgn.try_into().unwrap();
226 assert_eq!(game.id().to_string(), "DL7b2eDrC8LVtpBQFL9BYT");
227 assert_eq!(game.id().u128(), 132719545793121815798994835819316023416);
228 }
229
230 #[test]
231 fn documents_polyglot_start_position_hash() {
232 assert_eq!(START.polyglot_id(), Id(0x463b_9618_1691_fc9c));
233 }
234
235 #[test]
236 fn documents_polyglot_start_position_collision() {
237 let start = Position::start();
241 let position = Position::from_fen(COLLISION_FEN).unwrap();
242
243 assert_eq!(position.polyglot_id(), start.polyglot_id());
244 assert_ne!(position.standard_id(), start.standard_id());
245 }
246
247 #[test]
248 fn reaches_polyglot_collision_position_from_linked_movetext() {
249 use crate::formats::{Parser as _, pgn};
250
251 let pgn = pgn::game.parse(COLLISION_GAME).unwrap();
255 let game: Game = pgn.try_into().unwrap();
256 let mut cursor = Cursor::new(game);
257 cursor.end();
258 let constructed = cursor.position();
259
260 let expected = Position::from_fen(COLLISION_FEN).unwrap();
261
262 assert_eq!(constructed.fen(), expected.fen());
263 assert_eq!(constructed.transposition_fen(), expected.transposition_fen());
264 assert_eq!(constructed.standard_id(), expected.standard_id());
265 assert_eq!(constructed.polyglot_id(), expected.polyglot_id());
266 }
267
268 #[test]
269 fn documents_polyglot_zero_hash_position() {
270 let position =
274 Position::from_fen("2b1k3/4p3/3p1p2/p2P2p1/P2P4/2P2PP1/4P3/2NQKB2 b - - 0 1").unwrap();
275
276 assert_eq!(position.polyglot_id(), Id(0));
277 assert_ne!(position.transposition_id(&STANDARD), Id(0));
278 }
279}