use crate::bitboard::Bitboard;
use std::sync::OnceLock;
type PosMap = [u8; 16];
const D4_MAPS: [PosMap; 8] = {
let mut maps = [[0u8; 16]; 8];
let mut i: u8 = 0;
while i < 16 {
let r = i / 4;
let c = i % 4;
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;
}
maps
};
const SHAPE_PERMS: [[u8; 4]; 24] = generate_shape_perms();
const fn generate_shape_perms() -> [[u8; 4]; 24] {
let mut perms = [[0u8; 4]; 24];
let mut idx = 0;
let mut a: u8 = 0;
while a < 4 {
let mut b: u8 = 0;
while b < 4 {
if b == a {
b += 1;
continue;
}
let mut c: u8 = 0;
while c < 4 {
if c == a || c == b {
c += 1;
continue;
}
let d = 6 - a - b - c; perms[idx] = [a, b, c, d];
idx += 1;
c += 1;
}
b += 1;
}
a += 1;
}
perms
}
struct Perm16Lut {
tables: Vec<Vec<u16>>, }
fn build_perm16_lut() -> Perm16Lut {
let mut tables: Vec<Vec<u16>> = Vec::with_capacity(8);
for map in &D4_MAPS {
let mut t = vec![0u16; 65536];
for x in 0u32..65536 {
let mut y = 0u16;
let mut m = x as u16;
let mut i = 0u8;
while m != 0 {
if m & 1 != 0 {
y |= 1u16 << map[i as usize];
}
i += 1;
m >>= 1;
}
t[x as usize] = y;
}
tables.push(t);
}
Perm16Lut { tables }
}
static PERM16_LUT: OnceLock<Perm16Lut> = OnceLock::new();
fn lut() -> &'static Perm16Lut {
PERM16_LUT.get_or_init(build_perm16_lut)
}
#[inline]
fn permute16(mask: u16, d4_idx: usize) -> u16 {
lut().tables[d4_idx][mask as usize]
}
pub struct SymmetryHandler;
impl SymmetryHandler {
pub fn find_canonical(bb: &Bitboard) -> Bitboard {
let mut best: Option<[u16; 8]> = None;
for d4_idx in 0..8 {
let g0: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s], d4_idx));
let g1: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s + 4], d4_idx));
for perm in &SHAPE_PERMS {
let candidate: [u16; 8] = [
g0[perm[0] as usize],
g0[perm[1] as usize],
g0[perm[2] as usize],
g0[perm[3] as usize],
g1[perm[0] as usize],
g1[perm[1] as usize],
g1[perm[2] as usize],
g1[perm[3] as usize],
];
let is_better = match &best {
None => true,
Some(b) => le_bytes_less(&candidate, b),
};
if is_better {
best = Some(candidate);
}
}
}
Bitboard::new(best.unwrap_or([0; 8]))
}
pub fn canonical_payload(bb: &Bitboard) -> [u8; 16] {
Self::find_canonical(bb).to_le_bytes()
}
pub fn orbit_size(bb: &Bitboard) -> usize {
let mut seen = std::collections::HashSet::new();
for d4_idx in 0..8 {
let g0: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s], d4_idx));
let g1: [u16; 4] = std::array::from_fn(|s| permute16(bb.planes[s + 4], d4_idx));
for perm in &SHAPE_PERMS {
let candidate: [u16; 8] = [
g0[perm[0] as usize],
g0[perm[1] as usize],
g0[perm[2] as usize],
g0[perm[3] as usize],
g1[perm[0] as usize],
g1[perm[1] as usize],
g1[perm[2] as usize],
g1[perm[3] as usize],
];
seen.insert(candidate);
}
}
seen.len()
}
}
fn le_bytes_less(a: &[u16; 8], b: &[u16; 8]) -> bool {
for i in 0..8 {
let ab = a[i].to_le_bytes();
let bb = b[i].to_le_bytes();
for j in 0..2 {
match ab[j].cmp(&bb[j]) {
std::cmp::Ordering::Less => return true,
std::cmp::Ordering::Greater => return false,
std::cmp::Ordering::Equal => {}
}
}
}
false }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_permutation() {
assert_eq!(permute16(1, 0), 1); }
#[test]
fn canonical_empty_is_empty() {
let canon = SymmetryHandler::find_canonical(&Bitboard::EMPTY);
assert_eq!(canon, Bitboard::EMPTY);
}
#[test]
fn canonical_is_idempotent() {
let bb = Bitboard::EMPTY.with_move(0, 0, 0).with_move(1, 1, 5);
let c1 = SymmetryHandler::find_canonical(&bb);
let c2 = SymmetryHandler::find_canonical(&c1);
assert_eq!(c1, c2);
}
#[test]
fn rotated_boards_share_canonical_form() {
let corners = [0u8, 3, 12, 15];
let canonicals: Vec<Bitboard> = corners
.iter()
.map(|&pos| {
let bb = Bitboard::EMPTY.with_move(0, 0, pos);
SymmetryHandler::find_canonical(&bb)
})
.collect();
assert!(canonicals.windows(2).all(|w| w[0] == w[1]));
}
#[test]
fn orbit_size_single_piece() {
let bb = Bitboard::EMPTY.with_move(0, 0, 0);
let size = SymmetryHandler::orbit_size(&bb);
assert_eq!(size, 16);
}
#[test]
fn shape_perms_count() {
assert_eq!(SHAPE_PERMS.len(), 24);
}
#[test]
fn d4_maps_are_permutations() {
for map in &D4_MAPS {
let mut sorted = map.to_vec();
sorted.sort();
let expected: Vec<u8> = (0..16).collect();
assert_eq!(sorted, expected);
}
}
}