use crate::game::board::{GRID, OFFSET};
use crate::game::moves::Move;
use crate::game::state::GameState;
pub const R: i16 = 4;
pub const SIDE: usize = (2 * R + 1) as usize;
pub const CELLS: usize = SIDE * SIDE;
pub const FEATURE_LEN: usize = CELLS * 2 + 2;
#[inline]
fn index(dx: i16, dy: i16) -> usize {
(dy + R) as usize * SIDE + (dx + R) as usize
}
#[inline]
fn lin(t: usize, (x, y): (i16, i16)) -> (i16, i16) {
match t {
0 => (x, y),
1 => (-y, x),
2 => (-x, -y),
3 => (y, -x),
4 => (x, -y),
5 => (-x, y),
6 => (y, x),
_ => (-y, -x),
}
}
fn transform_plane(t: usize, plane: &[bool; CELLS]) -> [bool; CELLS] {
let mut out = [false; CELLS];
for dy in -R..=R {
for dx in -R..=R {
if plane[index(dx, dy)] {
let (tx, ty) = lin(t, (dx, dy));
out[index(tx, ty)] = true;
}
}
}
out
}
fn pack(plane: &[bool; CELLS]) -> u128 {
let mut bits = 0u128;
for (i, &b) in plane.iter().enumerate() {
if b {
bits |= 1u128 << i;
}
}
bits
}
pub fn canonicalize() -> bool {
false
}
fn canonical_transform(occ: &[bool; CELLS], line: &[bool; CELLS]) -> usize {
let mut best_t = 0usize;
let mut best_key = (pack(occ), pack(line));
for t in 1..8 {
let key = (
pack(&transform_plane(t, occ)),
pack(&transform_plane(t, line)),
);
if key < best_key {
best_key = key;
best_t = t;
}
}
best_t
}
fn raw_planes(state: &GameState, mv: &Move) -> ([bool; CELLS], [bool; CELLS], u32) {
let (cx, cy) = mv.pos;
let line_len = state.variant.len();
let line_cells: Vec<(i16, i16)> = mv.line.positions(line_len).collect();
let mut occ = [false; CELLS];
let mut line = [false; CELLS];
let mut occ_count = 0u32;
for dy in -R..=R {
for dx in -R..=R {
let p = (cx + dx, cy + dy);
let i = index(dx, dy);
if on_grid(p) && state.board.contains(p) {
occ[i] = true;
occ_count += 1;
}
if line_cells.contains(&p) {
line[i] = true;
}
}
}
(occ, line, occ_count)
}
fn features_from_planes(
occ: &[bool; CELLS],
line: &[bool; CELLS],
occ_count: u32,
hist_len: usize,
) -> Vec<f32> {
let mut feats = Vec::with_capacity(FEATURE_LEN);
feats.extend(occ.iter().map(|&b| b as u8 as f32));
feats.extend(line.iter().map(|&b| b as u8 as f32));
feats.push(hist_len as f32 / 178.0);
feats.push(occ_count as f32 / CELLS as f32);
feats
}
#[inline]
fn on_grid(p: (i16, i16)) -> bool {
let gx = p.0 + OFFSET;
let gy = p.1 + OFFSET;
(0..GRID).contains(&gx) && (0..GRID).contains(&gy)
}
pub fn encode(state: &GameState, mv: &Move) -> Vec<f32> {
encode_keyed(state, mv).1
}
pub type PatchKey = (u128, u128);
pub fn encode_keyed(state: &GameState, mv: &Move) -> (PatchKey, Vec<f32>) {
let (occ, line, occ_count) = raw_planes(state, mv);
let (occ, line) = if canonicalize() {
let t = canonical_transform(&occ, &line);
(transform_plane(t, &occ), transform_plane(t, &line))
} else {
(occ, line)
};
let key = (pack(&occ), pack(&line));
let feats = features_from_planes(&occ, &line, occ_count, state.history.len());
(key, feats)
}
pub fn encode_orientation(state: &GameState, mv: &Move, t: usize) -> Vec<f32> {
let (occ, line, occ_count) = raw_planes(state, mv);
let occ_t = transform_plane(t, &occ);
let line_t = transform_plane(t, &line);
features_from_planes(&occ_t, &line_t, occ_count, state.history.len())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game::moves::legal_moves;
use crate::game::rules::Variant;
#[test]
fn canonicalisation_is_d4_invariant() {
let mut occ = [false; CELLS];
let mut line = [false; CELLS];
for &(dx, dy) in &[(0, 0), (1, 0), (2, 0), (-1, 1), (2, -2), (3, 1)] {
occ[index(dx, dy)] = true;
}
for &(dx, dy) in &[(0, 0), (1, 0), (2, 0), (-1, 0), (-2, 0)] {
line[index(dx, dy)] = true;
}
let canon = |o: &[bool; CELLS], l: &[bool; CELLS]| {
let t = canonical_transform(o, l);
(pack(&transform_plane(t, o)), pack(&transform_plane(t, l)))
};
let reference = canon(&occ, &line);
for t in 0..8 {
let (o, l) = (transform_plane(t, &occ), transform_plane(t, &line));
assert_eq!(canon(&o, &l), reference, "transform {t} folds elsewhere");
}
}
#[test]
fn transforms_permute_the_patch() {
for t in 0..8 {
let mut seen = [false; CELLS];
for dy in -R..=R {
for dx in -R..=R {
let (tx, ty) = lin(t, (dx, dy));
assert!((-R..=R).contains(&tx) && (-R..=R).contains(&ty));
let i = index(tx, ty);
assert!(!seen[i], "transform {t} is not injective");
seen[i] = true;
}
}
}
}
#[test]
fn orientation_augmentation() {
let mut st = GameState::new(Variant::T5);
for _ in 0..6 {
let ms = legal_moves(&st);
st.apply(ms[0]);
}
let mv = legal_moves(&st)[0];
assert_eq!(encode(&st, &mv), encode_orientation(&st, &mv, 0));
let centre = index(0, 0);
let variants: Vec<Vec<f32>> = (0..8).map(|t| encode_orientation(&st, &mv, t)).collect();
for v in &variants {
assert_eq!(v.len(), FEATURE_LEN);
assert_eq!(
v[CELLS + centre],
1.0,
"centre stays on the line in every image"
);
}
assert!(
variants.iter().any(|v| v != &variants[0]),
"a generic patch should differ across orientations"
);
}
#[test]
fn encode_shape_and_centre() {
let st = GameState::new(Variant::T5);
let mv = legal_moves(&st)[0];
let f = encode(&st, &mv);
assert_eq!(f.len(), FEATURE_LEN);
let centre = index(0, 0);
assert_eq!(f[centre], 0.0, "placed point must be empty in occupancy");
assert_eq!(
f[CELLS + centre],
1.0,
"placed point must be on its own line"
);
}
#[test]
fn distinct_moves_encode_distinctly() {
let mut st = GameState::new(Variant::T5);
for _ in 0..20 {
let ms = legal_moves(&st);
if let Some((a, b)) = ms
.iter()
.enumerate()
.find_map(|(i, m)| ms[i + 1..].iter().find(|n| n.pos == m.pos).map(|n| (m, n)))
{
assert_eq!(encode(&st, a), encode(&st, a), "encoding is deterministic");
assert_ne!(encode(&st, a), encode(&st, b), "same point, different line");
return;
}
st.apply(ms[0]);
}
let ms = legal_moves(&st);
assert_eq!(encode(&st, &ms[0]), encode(&st, &ms[0]));
}
}