use std::collections::HashMap;
use std::sync::OnceLock;
pub type LineWindow = [u8; 11];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Cell {
Empty = 0,
Mine = 1,
Opp = 2,
Boundary = 3,
}
impl From<u8> for Cell {
#[inline]
fn from(v: u8) -> Self {
match v {
0 => Cell::Empty,
1 => Cell::Mine,
2 => Cell::Opp,
3 => Cell::Boundary,
_ => panic!("invalid cell value"),
}
}
}
#[inline]
pub fn pack_window(w: &LineWindow) -> u32 {
let mut packed = 0u32;
for &c in w {
debug_assert!(c < 4);
packed = (packed << 2) | (c as u32);
}
packed
}
#[inline]
pub fn unpack_window(packed: u32) -> LineWindow {
let mut w = [0u8; 11];
for i in 0..11 {
let shift = (10 - i) * 2;
w[i] = ((packed >> shift) & 0b11) as u8;
}
w
}
#[inline]
pub fn canonicalize(w: &LineWindow) -> LineWindow {
let reversed: LineWindow = std::array::from_fn(|i| w[10 - i]);
let p1 = pack_window(w);
let p2 = pack_window(&reversed);
if p1 <= p2 {
*w
} else {
reversed
}
}
pub fn is_realizable(w: &LineWindow) -> bool {
let mut left_b = 0;
while left_b < 11 && w[left_b] == 3 {
left_b += 1;
}
let mut right_b = 0;
while right_b < 11 && w[10 - right_b] == 3 {
right_b += 1;
}
if left_b + right_b > 11 {
return false;
}
for i in left_b..(11 - right_b) {
if w[i] == 3 {
return false;
}
}
true
}
pub type PatternId = u32;
pub fn enumerate_patterns() -> HashMap<u32, PatternId> {
let mut table: HashMap<u32, PatternId> = HashMap::new();
let mut next_id: PatternId = 0;
for raw in 0..(1u32 << 22) {
let w = unpack_window(raw);
if !is_realizable(&w) {
continue;
}
let canonical = canonicalize(&w);
let canonical_packed = pack_window(&canonical);
if !table.contains_key(&canonical_packed) {
table.insert(canonical_packed, next_id);
next_id += 1;
}
}
table
}
pub const PATTERN_TOP_K: usize = 4096;
pub const PATTERN_RARE_ID: u16 = PATTERN_TOP_K as u16; pub const PATTERN_NUM_IDS: usize = PATTERN_TOP_K + 1;
const TOPK_BYTES: &[u8] = include_bytes!("../data/topk.bin");
fn dense_mapped_table() -> &'static Vec<u16> {
static TABLE: OnceLock<Vec<u16>> = OnceLock::new();
TABLE.get_or_init(build_dense_mapped_table)
}
fn build_dense_mapped_table() -> Vec<u16> {
let n = 1usize << 22;
let mut t = vec![PATTERN_RARE_ID; n];
let mut canonical_to_mapped: HashMap<u32, u16> = HashMap::with_capacity(PATTERN_TOP_K);
debug_assert!(TOPK_BYTES.len() == PATTERN_TOP_K * 4);
for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
let p = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
canonical_to_mapped.insert(p, i as u16);
}
for raw in 0..(n as u32) {
let w = unpack_window(raw);
if !is_realizable(&w) {
continue; }
let canonical_packed = pack_window(&canonicalize(&w));
let mapped = canonical_to_mapped
.get(&canonical_packed)
.copied()
.unwrap_or(PATTERN_RARE_ID);
t[raw as usize] = mapped;
}
t
}
#[inline]
pub fn lookup_mapped_id(packed: u32) -> u16 {
debug_assert!((packed as usize) < (1usize << 22));
dense_mapped_table()[packed as usize]
}
#[inline]
pub fn empty_pattern_mapped_id() -> u16 {
let all_empty: LineWindow = [0u8; 11];
lookup_mapped_id(pack_window(&all_empty))
}
fn swap_table() -> &'static [u16; PATTERN_NUM_IDS] {
static TABLE: OnceLock<Box<[u16; PATTERN_NUM_IDS]>> = OnceLock::new();
TABLE.get_or_init(|| Box::new(build_swap_table()))
}
fn build_swap_table() -> [u16; PATTERN_NUM_IDS] {
let mut t = [PATTERN_RARE_ID; PATTERN_NUM_IDS];
let mut canonical_to_mapped: HashMap<u32, u16> =
HashMap::with_capacity(PATTERN_TOP_K);
for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
let p = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
canonical_to_mapped.insert(p, i as u16);
}
for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
let canonical_packed =
u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
let w = unpack_window(canonical_packed);
let swapped: LineWindow = std::array::from_fn(|j| match w[j] {
1 => 2,
2 => 1,
x => x,
});
let swapped_canonical = pack_window(&canonicalize(&swapped));
let mapped = canonical_to_mapped
.get(&swapped_canonical)
.copied()
.unwrap_or(PATTERN_RARE_ID);
t[i] = mapped;
}
t[PATTERN_RARE_ID as usize] = PATTERN_RARE_ID;
t
}
#[inline]
pub fn swap_mapped_id(id: u16) -> u16 {
debug_assert!((id as usize) < PATTERN_NUM_IDS);
swap_table()[id as usize]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum WindowThreat {
None = 0,
OpenTwo = 1,
ClosedThree = 2,
OpenThree = 3,
ClosedFour = 4,
OpenFour = 5,
Five = 6,
}
fn classify_window_anchor_mine(w: &LineWindow) -> WindowThreat {
debug_assert_eq!(w[5], 1);
let mut count = 1u32;
let mut open_front = false;
for off in 1usize..=5 {
match w[5 + off] {
1 => count += 1,
0 => {
open_front = true;
break;
}
_ => break, }
}
let mut open_back = false;
for off in 1usize..=5 {
match w[5 - off] {
1 => count += 1,
0 => {
open_back = true;
break;
}
_ => break,
}
}
let open_ends = open_front as u32 + open_back as u32;
match (count, open_ends) {
(5..=u32::MAX, _) => WindowThreat::Five,
(4, 2) => WindowThreat::OpenFour,
(4, 1) => WindowThreat::ClosedFour,
(3, 2) => WindowThreat::OpenThree,
(3, 1) => WindowThreat::ClosedThree,
(2, 2) => WindowThreat::OpenTwo,
_ => WindowThreat::None,
}
}
pub fn pattern_threat_after_my_play(pid: u16) -> WindowThreat {
debug_assert!((pid as usize) < PATTERN_NUM_IDS);
threat_after_my_play_table()[pid as usize]
}
fn threat_after_my_play_table() -> &'static [WindowThreat; PATTERN_NUM_IDS] {
static TABLE: OnceLock<Box<[WindowThreat; PATTERN_NUM_IDS]>> = OnceLock::new();
TABLE.get_or_init(|| Box::new(build_threat_after_my_play_table()))
}
fn build_threat_after_my_play_table() -> [WindowThreat; PATTERN_NUM_IDS] {
let mut t = [WindowThreat::None; PATTERN_NUM_IDS];
for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
let canonical_packed = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
let mut w = unpack_window(canonical_packed);
if w[5] != 0 {
if w[5] == 1 {
t[i] = classify_window_anchor_mine(&w);
}
continue;
}
w[5] = 1; t[i] = classify_window_anchor_mine(&w);
}
t
}
#[inline]
pub fn read_window(
mine: &crate::board::BitBoard,
opp: &crate::board::BitBoard,
row: i32,
col: i32,
dr: i32,
dc: i32,
) -> LineWindow {
use crate::board::BOARD_SIZE;
let mut w = [3u8; 11]; for off in -5i32..=5 {
let r = row + dr * off;
let c = col + dc * off;
if r < 0 || r >= BOARD_SIZE as i32 || c < 0 || c >= BOARD_SIZE as i32 {
continue;
}
let idx = (r as usize) * BOARD_SIZE + c as usize;
let slot = (off + 5) as usize;
if mine.get(idx) {
w[slot] = 1;
} else if opp.get(idx) {
w[slot] = 2;
} else {
w[slot] = 0;
}
}
w
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_unpack_roundtrip() {
let w: LineWindow = [3, 3, 0, 1, 2, 1, 0, 2, 0, 3, 3];
let packed = pack_window(&w);
let unpacked = unpack_window(packed);
assert_eq!(w, unpacked);
}
#[test]
fn canonicalize_idempotent() {
let w: LineWindow = [3, 3, 0, 1, 2, 1, 0, 2, 0, 3, 3];
let c1 = canonicalize(&w);
let c2 = canonicalize(&c1);
assert_eq!(c1, c2);
}
#[test]
fn canonicalize_pairs_with_reverse() {
let w: LineWindow = [3, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0];
let reversed: LineWindow = std::array::from_fn(|i| w[10 - i]);
assert_eq!(canonicalize(&w), canonicalize(&reversed));
}
#[test]
fn realizability_rejects_internal_boundary() {
let bad: LineWindow = [0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0];
assert!(!is_realizable(&bad));
let good: LineWindow = [3, 3, 0, 1, 2, 1, 0, 0, 0, 3, 3];
assert!(is_realizable(&good));
let empty: LineWindow = [0; 11];
assert!(is_realizable(&empty));
let all_b: LineWindow = [3; 11];
assert!(!is_realizable(&all_b));
let edge: LineWindow = [3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0];
assert!(is_realizable(&edge));
}
#[test]
fn enumerate_patterns_smoke() {
let table = enumerate_patterns();
let w1: LineWindow = [3, 3, 0, 1, 2, 0, 0, 0, 0, 3, 3];
let w2: LineWindow = std::array::from_fn(|i| w1[10 - i]);
let id1 = table[&pack_window(&canonicalize(&w1))];
let id2 = table[&pack_window(&canonicalize(&w2))];
assert_eq!(id1, id2, "left-right reflection should share canonical ID");
let n = table.len();
eprintln!("[pattern_table] enumerated {n} canonical patterns");
assert!(n > 1000, "too few patterns: {n}");
assert!(n < 200_000, "too many patterns (u16 overflow risk): {n}");
}
#[test]
fn read_window_centers_on_anchor() {
use crate::board::BitBoard;
let mut mine = BitBoard::EMPTY;
let mut opp = BitBoard::EMPTY;
mine.set(7 * 15 + 7); opp.set(7 * 15 + 8);
let w = read_window(&mine, &opp, 7, 7, 0, 1);
assert_eq!(w[5], 1, "anchor should be mine");
assert_eq!(w[6], 2, "right neighbor should be opp");
assert_eq!(w[4], 0, "left neighbor empty");
let mine2 = BitBoard::EMPTY;
let opp2 = BitBoard::EMPTY;
let w2 = read_window(&mine2, &opp2, 0, 0, 0, 1);
for i in 0..5 {
assert_eq!(w2[i], 3, "out-of-board should be boundary at slot {i}");
}
assert_eq!(w2[5], 0, "anchor (0,0) is empty");
}
}