1use crate::bitboard::Bitboard;
2use std::sync::OnceLock;
3
4type PosMap = [u8; 16];
6
7const D4_MAPS: [PosMap; 8] = {
8 let mut maps = [[0u8; 16]; 8];
9 let mut i: u8 = 0;
10 while i < 16 {
11 let r = i / 4;
12 let c = i % 4;
13
14 maps[0][i as usize] = r * 4 + c; maps[1][i as usize] = c * 4 + (3 - r); maps[2][i as usize] = (3 - r) * 4 + (3 - c); maps[3][i as usize] = (3 - c) * 4 + r; maps[4][i as usize] = r * 4 + (3 - c); maps[5][i as usize] = (3 - r) * 4 + c; maps[6][i as usize] = c * 4 + r; maps[7][i as usize] = (3 - c) * 4 + (3 - r); i += 1;
24 }
25 maps
26};
27
28const SHAPE_PERMS: [[u8; 4]; 24] = generate_shape_perms();
30
31const fn generate_shape_perms() -> [[u8; 4]; 24] {
32 let mut perms = [[0u8; 4]; 24];
33 let mut idx = 0;
34 let mut a: u8 = 0;
35 while a < 4 {
36 let mut b: u8 = 0;
37 while b < 4 {
38 if b == a {
39 b += 1;
40 continue;
41 }
42 let mut c: u8 = 0;
43 while c < 4 {
44 if c == a || c == b {
45 c += 1;
46 continue;
47 }
48 let d = 6 - a - b - c; perms[idx] = [a, b, c, d];
50 idx += 1;
51 c += 1;
52 }
53 b += 1;
54 }
55 a += 1;
56 }
57 perms
58}
59
60struct Perm16Lut {
65 tables: Vec<Vec<u16>>, }
67
68fn build_perm16_lut() -> Perm16Lut {
69 let mut tables: Vec<Vec<u16>> = Vec::with_capacity(8);
70 for map in &D4_MAPS {
71 let mut t = vec![0u16; 65536];
72 for x in 0u32..65536 {
73 let mut y = 0u16;
74 let mut m = x as u16;
75 let mut i = 0u8;
76 while m != 0 {
77 if m & 1 != 0 {
78 y |= 1u16 << map[i as usize];
79 }
80 i += 1;
81 m >>= 1;
82 }
83 t[x as usize] = y;
84 }
85 tables.push(t);
86 }
87 Perm16Lut { tables }
88}
89
90static PERM16_LUT: OnceLock<Perm16Lut> = OnceLock::new();
91
92fn lut() -> &'static Perm16Lut {
93 PERM16_LUT.get_or_init(build_perm16_lut)
94}
95
96#[inline]
97fn permute16(mask: u16, d4_idx: usize) -> u16 {
98 lut().tables[d4_idx][mask as usize]
99}
100
101pub struct SymmetryHandler;
102
103impl SymmetryHandler {
104 pub fn find_canonical(bb: &Bitboard) -> Bitboard {
107 let mut best: Option<[u16; 8]> = None;
108
109 for d4_idx in 0..8 {
110 let g0: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s], d4_idx));
111 let g1: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s + 4], d4_idx));
112
113 for perm in &SHAPE_PERMS {
114 let candidate: [u16; 8] = [
115 g0[perm[0] as usize],
116 g0[perm[1] as usize],
117 g0[perm[2] as usize],
118 g0[perm[3] as usize],
119 g1[perm[0] as usize],
120 g1[perm[1] as usize],
121 g1[perm[2] as usize],
122 g1[perm[3] as usize],
123 ];
124
125 let is_better = match &best {
126 None => true,
127 Some(b) => le_bytes_less(&candidate, b),
128 };
129 if is_better {
130 best = Some(candidate);
131 }
132 }
133 }
134 Bitboard::new(best.unwrap_or([0; 8]))
135 }
136
137 pub fn canonical_payload(bb: &Bitboard) -> [u8; 16] {
139 Self::find_canonical(bb).to_le_bytes()
140 }
141
142 pub fn orbit_size(bb: &Bitboard) -> usize {
144 let mut seen = std::collections::HashSet::new();
145
146 for d4_idx in 0..8 {
147 let g0: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s], d4_idx));
148 let g1: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s + 4], d4_idx));
149
150 for perm in &SHAPE_PERMS {
151 let candidate: [u16; 8] = [
152 g0[perm[0] as usize],
153 g0[perm[1] as usize],
154 g0[perm[2] as usize],
155 g0[perm[3] as usize],
156 g1[perm[0] as usize],
157 g1[perm[1] as usize],
158 g1[perm[2] as usize],
159 g1[perm[3] as usize],
160 ];
161 seen.insert(candidate);
162 }
163 }
164 seen.len()
165 }
166}
167
168fn le_bytes_less(a: &[u16; 8], b: &[u16; 8]) -> bool {
170 for i in 0..8 {
171 let ab = a[i].to_le_bytes();
172 let bb = b[i].to_le_bytes();
173 for j in 0..2 {
174 match ab[j].cmp(&bb[j]) {
175 std::cmp::Ordering::Less => return true,
176 std::cmp::Ordering::Greater => return false,
177 std::cmp::Ordering::Equal => {}
178 }
179 }
180 }
181 false }
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn identity_permutation() {
190 assert_eq!(permute16(1, 0), 1); }
192
193 #[test]
194 fn canonical_empty_is_empty() {
195 let canon = SymmetryHandler::find_canonical(&Bitboard::EMPTY);
196 assert_eq!(canon, Bitboard::EMPTY);
197 }
198
199 #[test]
200 fn canonical_is_idempotent() {
201 let bb = Bitboard::EMPTY.with_move(0, 0, 0).with_move(1, 1, 5);
202 let c1 = SymmetryHandler::find_canonical(&bb);
203 let c2 = SymmetryHandler::find_canonical(&c1);
204 assert_eq!(c1, c2);
205 }
206
207 #[test]
208 fn rotated_boards_share_canonical_form() {
209 let corners = [0u8, 3, 12, 15];
211 let canonicals: Vec<Bitboard> = corners
212 .iter()
213 .map(|&pos| {
214 let bb = Bitboard::EMPTY.with_move(0, 0, pos);
215 SymmetryHandler::find_canonical(&bb)
216 })
217 .collect();
218 assert!(canonicals.windows(2).all(|w| w[0] == w[1]));
219 }
220
221 #[test]
222 fn orbit_size_single_piece() {
223 let bb = Bitboard::EMPTY.with_move(0, 0, 0);
224 let size = SymmetryHandler::orbit_size(&bb);
225 assert_eq!(size, 16);
227 }
228
229 #[test]
230 fn shape_perms_count() {
231 assert_eq!(SHAPE_PERMS.len(), 24);
232 }
233
234 #[test]
235 fn d4_maps_are_permutations() {
236 for map in &D4_MAPS {
237 let mut sorted = map.to_vec();
238 sorted.sort();
239 let expected: Vec<u8> = (0..16).collect();
240 assert_eq!(sorted, expected);
241 }
242 }
243}