use crate::game::board::Pos;
use crate::game::line::{Dir, Line};
use crate::game::moves::Move;
pub fn apply_transform(t: usize, (x, y): Pos, k: i16) -> Pos {
match t {
0 => (x, y), 1 => (k - y, x), 2 => (k - x, k - y), 3 => (y, k - x), 4 => (x, k - y), 5 => (k - x, y), 6 => (y, x), 7 => (k - y, k - x), _ => unreachable!(),
}
}
pub fn zobrist_value((x, y): Pos) -> u64 {
let mut h = (x as i64 as u64).wrapping_mul(0x9e3779b97f4a7c15)
^ (y as i64 as u64).wrapping_mul(0x6c62272e07bb0142);
h ^= h >> 30;
h = h.wrapping_mul(0xbf58476d1ce4e5b9);
h ^= h >> 27;
h = h.wrapping_mul(0x94d049bb133111eb);
h ^= h >> 31;
h
}
pub fn transform_dir(t: usize, dir: Dir) -> Dir {
use Dir::*;
match t {
0 | 2 => dir, 1 | 3 => match dir {
H => V,
V => H,
DP => DN,
DN => DP,
}, 4 | 5 => match dir {
H => H,
V => V,
DP => DN,
DN => DP,
}, 6 | 7 => match dir {
H => V,
V => H,
DP => DP,
DN => DN,
}, _ => unreachable!(),
}
}
#[cfg(test)]
fn line_min_endpoint(line: &Line, n: i16) -> Pos {
let (dx, dy) = line.dir.delta();
let e0 = line.origin;
let e1 = (e0.0 + (n - 1) * dx, e0.1 + (n - 1) * dy);
e0.min(e1)
}
pub fn transform_line(t: usize, line: &Line, k: i16, n: i16) -> Line {
let (dx, dy) = line.dir.delta();
let e0 = line.origin;
let e1 = (e0.0 + (n - 1) * dx, e0.1 + (n - 1) * dy);
let te0 = apply_transform(t, e0, k);
let te1 = apply_transform(t, e1, k);
Line::new(te0.min(te1), transform_dir(t, line.dir))
}
pub fn transformed_move_key(t: usize, m: &Move, k: i16, n: i16) -> (Pos, Pos, u8) {
let (dx, dy) = m.line.dir.delta();
let e0 = m.line.origin;
let e1 = (e0.0 + (n - 1) * dx, e0.1 + (n - 1) * dy);
let tp = apply_transform(t, m.pos, k);
let te0 = apply_transform(t, e0, k);
let te1 = apply_transform(t, e1, k);
(tp, te0.min(te1), transform_dir(t, m.line.dir) as u8)
}
pub fn is_orbit_min(m: &Move, stab: u8, k: i16, n: i16) -> bool {
let key0 = transformed_move_key(0, m, k, n);
for t in 1..8 {
if stab & (1 << t) != 0 && transformed_move_key(t, m, k, n) < key0 {
return false;
}
}
true
}
pub fn stab_after(stab: u8, m: &Move, k: i16, n: i16) -> u8 {
let key0 = transformed_move_key(0, m, k, n);
let mut out = 0u8;
for t in 0..8 {
if stab & (1 << t) != 0 && transformed_move_key(t, m, k, n) == key0 {
out |= 1 << t;
}
}
out
}
#[derive(Debug, Clone)]
pub struct SymmetryHashes {
hashes: [u64; 8],
k: i16,
}
impl SymmetryHashes {
pub fn new(k: i16) -> Self {
Self {
hashes: [0u64; 8],
k,
}
}
pub fn toggle(&mut self, pos: Pos) {
for t in 0..8 {
self.hashes[t] ^= zobrist_value(apply_transform(t, pos, self.k));
}
}
pub fn canonical(&self) -> u64 {
self.hashes.iter().copied().min().unwrap()
}
pub fn move_coder(&self) -> MoveCoder {
let cmin = self.canonical();
let mut active = [0u8; 8];
let mut len = 0usize;
for t in 0..8 {
if self.hashes[t] == cmin {
active[len] = t as u8;
len += 1;
}
}
MoveCoder {
cmin,
k: self.k,
n: (self.k + 1) / 2,
active,
len: len as u8,
}
}
}
pub struct MoveCoder {
cmin: u64,
k: i16,
n: i16,
active: [u8; 8],
len: u8,
}
impl MoveCoder {
#[inline]
pub fn code(&self, mv: &Move) -> u64 {
let move_code = self.active[..self.len as usize]
.iter()
.map(|&t| {
let (p, e, d) = transformed_move_key(t as usize, mv, self.k, self.n);
zobrist_value(p)
^ zobrist_value(e).wrapping_mul(0x517cc1b727220a95)
^ (d as u64).wrapping_mul(0x6c62272e07bb0142)
})
.min()
.unwrap();
self.cmin.wrapping_mul(0x9e3779b97f4a7c15) ^ move_code
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game::{moves::legal_moves, rules::Variant, state::GameState};
fn cross_stab(state: &GameState, k: i16) -> u8 {
let mut stab = 0u8;
for t in 0..8 {
if state
.board
.cells
.iter()
.all(|&c| state.board.contains(apply_transform(t, c, k)))
{
stab |= 1 << t;
}
}
stab
}
#[test]
fn identity_key_is_the_natural_move() {
let state = GameState::new(Variant::T5);
let m = legal_moves(&state)[0];
let (k, n) = (9, 5);
let (p, end, d) = transformed_move_key(0, &m, k, n);
assert_eq!(p, m.pos);
assert_eq!(end, line_min_endpoint(&m.line, n));
assert_eq!(d, m.line.dir as u8);
}
#[test]
fn t5_cross_is_full_d4() {
let state = GameState::new(Variant::T5);
assert_eq!(cross_stab(&state, 9), 0xFF, "5T cross must be full D4");
}
#[test]
fn orbit_reduction_partitions_first_moves() {
use std::collections::HashSet;
for (variant, k, n) in [
(Variant::T5, 9, 5),
(Variant::D5, 9, 5),
(Variant::D4, 7, 4),
] {
let state = GameState::new(variant);
let stab = cross_stab(&state, k);
let moves = legal_moves(&state);
let kept: Vec<&Move> = moves
.iter()
.filter(|m| is_orbit_min(m, stab, k, n))
.collect();
let kept_keys: HashSet<_> = kept
.iter()
.map(|m| transformed_move_key(0, m, k, n))
.collect();
for m in &moves {
let minkey = (0..8)
.filter(|&t| stab & (1 << t) != 0)
.map(|t| transformed_move_key(t, m, k, n))
.min()
.unwrap();
assert!(
kept_keys.contains(&minkey),
"{variant:?}: orbit min not kept"
);
}
}
}
#[test]
fn first_move_shrinks_the_stabiliser() {
let state = GameState::new(Variant::T5);
let (k, n) = (9, 5);
let stab = cross_stab(&state, k);
let m = *legal_moves(&state)
.iter()
.find(|m| is_orbit_min(m, stab, k, n))
.unwrap();
let child = stab_after(stab, &m, k, n);
assert_eq!(child & 1, 1, "identity always fixes a move");
assert!(child < stab, "a first move must break some symmetry");
}
}