1use std::collections::HashMap;
13use std::sync::OnceLock;
14
15pub type LineWindow = [u8; 11];
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[repr(u8)]
27pub enum Cell {
28 Empty = 0,
29 Mine = 1,
30 Opp = 2,
31 Boundary = 3,
32}
33
34impl From<u8> for Cell {
35 #[inline]
36 fn from(v: u8) -> Self {
37 match v {
38 0 => Cell::Empty,
39 1 => Cell::Mine,
40 2 => Cell::Opp,
41 3 => Cell::Boundary,
42 _ => panic!("invalid cell value"),
43 }
44 }
45}
46
47#[inline]
50pub fn pack_window(w: &LineWindow) -> u32 {
51 let mut packed = 0u32;
52 for &c in w {
53 debug_assert!(c < 4);
54 packed = (packed << 2) | (c as u32);
55 }
56 packed
57}
58
59#[inline]
62pub fn unpack_window(packed: u32) -> LineWindow {
63 let mut w = [0u8; 11];
64 for i in 0..11 {
65 let shift = (10 - i) * 2;
66 w[i] = ((packed >> shift) & 0b11) as u8;
67 }
68 w
69}
70
71#[inline]
76pub fn canonicalize(w: &LineWindow) -> LineWindow {
77 let reversed: LineWindow = std::array::from_fn(|i| w[10 - i]);
78 let p1 = pack_window(w);
79 let p2 = pack_window(&reversed);
80 if p1 <= p2 {
81 *w
82 } else {
83 reversed
84 }
85}
86
87pub fn is_realizable(w: &LineWindow) -> bool {
94 let mut left_b = 0;
96 while left_b < 11 && w[left_b] == 3 {
97 left_b += 1;
98 }
99 let mut right_b = 0;
101 while right_b < 11 && w[10 - right_b] == 3 {
102 right_b += 1;
103 }
104 if left_b + right_b > 11 {
105 return false;
106 }
107 for i in left_b..(11 - right_b) {
109 if w[i] == 3 {
110 return false;
111 }
112 }
113 true
114}
115
116pub type PatternId = u32;
119
120pub fn enumerate_patterns() -> HashMap<u32, PatternId> {
127 let mut table: HashMap<u32, PatternId> = HashMap::new();
128 let mut next_id: PatternId = 0;
129
130 for raw in 0..(1u32 << 22) {
132 let w = unpack_window(raw);
133 if !is_realizable(&w) {
134 continue;
135 }
136 let canonical = canonicalize(&w);
137 let canonical_packed = pack_window(&canonical);
138 if !table.contains_key(&canonical_packed) {
139 table.insert(canonical_packed, next_id);
140 next_id += 1;
141 }
142 }
143 table
144}
145
146pub const PATTERN_TOP_K: usize = 4096;
152pub 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");
159
160fn dense_mapped_table() -> &'static Vec<u16> {
167 static TABLE: OnceLock<Vec<u16>> = OnceLock::new();
168 TABLE.get_or_init(build_dense_mapped_table)
169}
170
171fn build_dense_mapped_table() -> Vec<u16> {
172 let n = 1usize << 22;
173 let mut t = vec![PATTERN_RARE_ID; n];
174
175 let mut canonical_to_mapped: HashMap<u32, u16> = HashMap::with_capacity(PATTERN_TOP_K);
177 debug_assert!(TOPK_BYTES.len() == PATTERN_TOP_K * 4);
178 for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
179 let p = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
180 canonical_to_mapped.insert(p, i as u16);
181 }
182
183 for raw in 0..(n as u32) {
185 let w = unpack_window(raw);
186 if !is_realizable(&w) {
187 continue; }
189 let canonical_packed = pack_window(&canonicalize(&w));
190 let mapped = canonical_to_mapped
191 .get(&canonical_packed)
192 .copied()
193 .unwrap_or(PATTERN_RARE_ID);
194 t[raw as usize] = mapped;
195 }
196
197 t
198}
199
200#[inline]
203pub fn lookup_mapped_id(packed: u32) -> u16 {
204 debug_assert!((packed as usize) < (1usize << 22));
205 dense_mapped_table()[packed as usize]
206}
207
208#[inline]
212pub fn empty_pattern_mapped_id() -> u16 {
213 let all_empty: LineWindow = [0u8; 11];
214 lookup_mapped_id(pack_window(&all_empty))
215}
216
217fn swap_table() -> &'static [u16; PATTERN_NUM_IDS] {
225 static TABLE: OnceLock<Box<[u16; PATTERN_NUM_IDS]>> = OnceLock::new();
226 TABLE.get_or_init(|| Box::new(build_swap_table()))
227}
228
229fn build_swap_table() -> [u16; PATTERN_NUM_IDS] {
230 let mut t = [PATTERN_RARE_ID; PATTERN_NUM_IDS];
231
232 let mut canonical_to_mapped: HashMap<u32, u16> =
235 HashMap::with_capacity(PATTERN_TOP_K);
236 for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
237 let p = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
238 canonical_to_mapped.insert(p, i as u16);
239 }
240
241 for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
243 let canonical_packed =
244 u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
245 let w = unpack_window(canonical_packed);
246 let swapped: LineWindow = std::array::from_fn(|j| match w[j] {
247 1 => 2,
248 2 => 1,
249 x => x,
250 });
251 let swapped_canonical = pack_window(&canonicalize(&swapped));
252 let mapped = canonical_to_mapped
253 .get(&swapped_canonical)
254 .copied()
255 .unwrap_or(PATTERN_RARE_ID);
256 t[i] = mapped;
257 }
258 t[PATTERN_RARE_ID as usize] = PATTERN_RARE_ID;
260 t
261}
262
263#[inline]
265pub fn swap_mapped_id(id: u16) -> u16 {
266 debug_assert!((id as usize) < PATTERN_NUM_IDS);
267 swap_table()[id as usize]
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
275#[repr(u8)]
276pub enum WindowThreat {
277 None = 0,
278 OpenTwo = 1,
279 ClosedThree = 2,
280 OpenThree = 3,
281 ClosedFour = 4,
282 OpenFour = 5,
283 Five = 6,
284}
285
286fn classify_window_anchor_mine(w: &LineWindow) -> WindowThreat {
291 debug_assert_eq!(w[5], 1);
292 let mut count = 1u32;
293 let mut open_front = false;
295 for off in 1usize..=5 {
296 match w[5 + off] {
297 1 => count += 1,
298 0 => {
299 open_front = true;
300 break;
301 }
302 _ => break, }
304 }
305 let mut open_back = false;
307 for off in 1usize..=5 {
308 match w[5 - off] {
309 1 => count += 1,
310 0 => {
311 open_back = true;
312 break;
313 }
314 _ => break,
315 }
316 }
317 let open_ends = open_front as u32 + open_back as u32;
318 match (count, open_ends) {
319 (5..=u32::MAX, _) => WindowThreat::Five,
320 (4, 2) => WindowThreat::OpenFour,
321 (4, 1) => WindowThreat::ClosedFour,
322 (3, 2) => WindowThreat::OpenThree,
323 (3, 1) => WindowThreat::ClosedThree,
324 (2, 2) => WindowThreat::OpenTwo,
325 _ => WindowThreat::None,
326 }
327}
328
329pub fn pattern_threat_after_my_play(pid: u16) -> WindowThreat {
341 debug_assert!((pid as usize) < PATTERN_NUM_IDS);
342 threat_after_my_play_table()[pid as usize]
343}
344
345fn threat_after_my_play_table() -> &'static [WindowThreat; PATTERN_NUM_IDS] {
346 static TABLE: OnceLock<Box<[WindowThreat; PATTERN_NUM_IDS]>> = OnceLock::new();
347 TABLE.get_or_init(|| Box::new(build_threat_after_my_play_table()))
348}
349
350fn build_threat_after_my_play_table() -> [WindowThreat; PATTERN_NUM_IDS] {
351 let mut t = [WindowThreat::None; PATTERN_NUM_IDS];
352
353 for (i, chunk) in TOPK_BYTES.chunks(4).enumerate() {
361 let canonical_packed = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
362 let mut w = unpack_window(canonical_packed);
363 if w[5] != 0 {
364 if w[5] == 1 {
365 t[i] = classify_window_anchor_mine(&w);
366 }
367 continue;
368 }
369 w[5] = 1; t[i] = classify_window_anchor_mine(&w);
371 }
372 t
375}
376
377#[inline]
380pub fn read_window(
381 mine: &crate::board::BitBoard,
382 opp: &crate::board::BitBoard,
383 row: i32,
384 col: i32,
385 dr: i32,
386 dc: i32,
387) -> LineWindow {
388 use crate::board::BOARD_SIZE;
389 let mut w = [3u8; 11]; for off in -5i32..=5 {
391 let r = row + dr * off;
392 let c = col + dc * off;
393 if r < 0 || r >= BOARD_SIZE as i32 || c < 0 || c >= BOARD_SIZE as i32 {
394 continue;
395 }
396 let idx = (r as usize) * BOARD_SIZE + c as usize;
397 let slot = (off + 5) as usize;
398 if mine.get(idx) {
399 w[slot] = 1;
400 } else if opp.get(idx) {
401 w[slot] = 2;
402 } else {
403 w[slot] = 0;
404 }
405 }
406 w
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn pack_unpack_roundtrip() {
415 let w: LineWindow = [3, 3, 0, 1, 2, 1, 0, 2, 0, 3, 3];
416 let packed = pack_window(&w);
417 let unpacked = unpack_window(packed);
418 assert_eq!(w, unpacked);
419 }
420
421 #[test]
422 fn canonicalize_idempotent() {
423 let w: LineWindow = [3, 3, 0, 1, 2, 1, 0, 2, 0, 3, 3];
424 let c1 = canonicalize(&w);
425 let c2 = canonicalize(&c1);
426 assert_eq!(c1, c2);
427 }
428
429 #[test]
430 fn canonicalize_pairs_with_reverse() {
431 let w: LineWindow = [3, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0];
432 let reversed: LineWindow = std::array::from_fn(|i| w[10 - i]);
433 assert_eq!(canonicalize(&w), canonicalize(&reversed));
434 }
435
436 #[test]
437 fn realizability_rejects_internal_boundary() {
438 let bad: LineWindow = [0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0];
440 assert!(!is_realizable(&bad));
441
442 let good: LineWindow = [3, 3, 0, 1, 2, 1, 0, 0, 0, 3, 3];
444 assert!(is_realizable(&good));
445
446 let empty: LineWindow = [0; 11];
448 assert!(is_realizable(&empty));
449
450 let all_b: LineWindow = [3; 11];
454 assert!(!is_realizable(&all_b));
456
457 let edge: LineWindow = [3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0];
460 assert!(is_realizable(&edge));
461 }
462
463 #[test]
464 fn enumerate_patterns_smoke() {
465 let table = enumerate_patterns();
468 let w1: LineWindow = [3, 3, 0, 1, 2, 0, 0, 0, 0, 3, 3];
470 let w2: LineWindow = std::array::from_fn(|i| w1[10 - i]);
471 let id1 = table[&pack_window(&canonicalize(&w1))];
472 let id2 = table[&pack_window(&canonicalize(&w2))];
473 assert_eq!(id1, id2, "left-right reflection should share canonical ID");
474
475 let n = table.len();
477 eprintln!("[pattern_table] enumerated {n} canonical patterns");
478 assert!(n > 1000, "too few patterns: {n}");
479 assert!(n < 200_000, "too many patterns (u16 overflow risk): {n}");
480 }
481
482 #[test]
483 fn read_window_centers_on_anchor() {
484 use crate::board::BitBoard;
485 let mut mine = BitBoard::EMPTY;
486 let mut opp = BitBoard::EMPTY;
487 mine.set(7 * 15 + 7); opp.set(7 * 15 + 8); let w = read_window(&mine, &opp, 7, 7, 0, 1);
491 assert_eq!(w[5], 1, "anchor should be mine");
493 assert_eq!(w[6], 2, "right neighbor should be opp");
494 assert_eq!(w[4], 0, "left neighbor empty");
495
496 let mine2 = BitBoard::EMPTY;
498 let opp2 = BitBoard::EMPTY;
499 let w2 = read_window(&mine2, &opp2, 0, 0, 0, 1);
500 for i in 0..5 {
501 assert_eq!(w2[i], 3, "out-of-board should be boundary at slot {i}");
502 }
503 assert_eq!(w2[5], 0, "anchor (0,0) is empty");
504 }
505}