use crate::board::{Board, BitBoard, Move, Stone, BOARD_SIZE, NUM_CELLS};
use crate::heuristic::{scan_line, DIR};
use noru::trainer::SimpleRng;
use std::collections::HashMap;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
static ZOBRIST_KEYS: OnceLock<[[u64; 2]; NUM_CELLS]> = OnceLock::new();
const ZOBRIST_SIDE_WHITE: u64 = 0x5A5A_5A5A_A5A5_A5A5;
fn zobrist_keys() -> &'static [[u64; 2]; NUM_CELLS] {
ZOBRIST_KEYS.get_or_init(|| {
let mut rng = SimpleRng::new(0xDEAD_BEEF_CAFE_BABE);
let mut arr = [[0u64; 2]; NUM_CELLS];
for slot in arr.iter_mut() {
slot[0] = rng.next_u64();
slot[1] = rng.next_u64();
}
arr
})
}
fn zobrist_hash(board: &Board) -> u64 {
let keys = zobrist_keys();
let mut h = 0u64;
for idx in 0..NUM_CELLS {
if board.black.get(idx) {
h ^= keys[idx][0];
}
if board.white.get(idx) {
h ^= keys[idx][1];
}
}
if board.side_to_move == Stone::White {
h ^= ZOBRIST_SIDE_WHITE;
}
h
}
#[derive(Clone, Copy)]
struct TtEntry {
depth: u32,
result: TtResult,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TtResult {
AttackerWins,
Fails,
}
type TransTable = HashMap<u64, TtEntry>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum LineThreat {
None,
OpenTwo, ClosedThree, OpenThree, ClosedFour, OpenFour, Five, }
fn classify_line(count: u32, open_ends: u32) -> LineThreat {
match (count, open_ends) {
(5.., _) => LineThreat::Five,
(4, 2) => LineThreat::OpenFour,
(4, 1) => LineThreat::ClosedFour,
(3, 2) => LineThreat::OpenThree,
(3, 1) => LineThreat::ClosedThree,
(2, 2) => LineThreat::OpenTwo,
_ => LineThreat::None,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ThreatKind {
None,
ClosedFour,
OpenThree,
Five,
OpenFour,
DoubleFour, FourThree, DoubleThree, }
impl ThreatKind {
pub fn is_winning(self) -> bool {
matches!(
self,
ThreatKind::Five
| ThreatKind::OpenFour
| ThreatKind::DoubleFour
| ThreatKind::FourThree
| ThreatKind::DoubleThree
)
}
pub fn is_forcing(self) -> bool {
matches!(self, ThreatKind::ClosedFour | ThreatKind::OpenThree)
|| self.is_winning()
}
}
pub fn classify_move(my_bb: &BitBoard, opp_bb: &BitBoard, mv: Move) -> ThreatKind {
let row = (mv / BOARD_SIZE) as i32;
let col = (mv % BOARD_SIZE) as i32;
let mut my_tmp = my_bb.clone();
my_tmp.set(mv);
let mut fours = 0u32; let mut open_fours = 0u32;
let mut open_threes = 0u32;
let mut closed_fours = 0u32;
let mut fives = 0u32;
for &(dr, dc) in &DIR {
let info = scan_line(&my_tmp, opp_bb, row, col, dr, dc);
let open_ends = info.open_front as u32 + info.open_back as u32;
match classify_line(info.count, open_ends) {
LineThreat::Five => fives += 1,
LineThreat::OpenFour => {
open_fours += 1;
fours += 1;
}
LineThreat::ClosedFour => {
closed_fours += 1;
fours += 1;
}
LineThreat::OpenThree => open_threes += 1,
_ => {}
}
}
if fives >= 1 {
return ThreatKind::Five;
}
if open_fours >= 1 {
return ThreatKind::OpenFour;
}
if fours >= 2 {
return ThreatKind::DoubleFour;
}
if closed_fours >= 1 && open_threes >= 1 {
return ThreatKind::FourThree;
}
if open_threes >= 2 {
return ThreatKind::DoubleThree;
}
if closed_fours >= 1 {
return ThreatKind::ClosedFour;
}
if open_threes >= 1 {
return ThreatKind::OpenThree;
}
ThreatKind::None
}
pub struct VctConfig {
pub max_depth: u32,
pub time_budget: Option<Duration>,
}
impl Default for VctConfig {
fn default() -> Self {
Self {
max_depth: 16,
time_budget: Some(Duration::from_millis(500)),
}
}
}
pub fn search_vct(board: &mut Board, cfg: &VctConfig) -> Option<Vec<Move>> {
let deadline = cfg.time_budget.map(|d| Instant::now() + d);
let attacker = board.side_to_move;
let mut sequence = Vec::with_capacity(cfg.max_depth as usize * 2);
let mut tt: TransTable = HashMap::with_capacity(4096);
if vct_or(board, attacker, cfg.max_depth, deadline, &mut sequence, &mut tt) {
Some(sequence)
} else {
None
}
}
fn vct_or(
board: &mut Board,
attacker: Stone,
depth: u32,
deadline: Option<Instant>,
sequence: &mut Vec<Move>,
tt: &mut TransTable,
) -> bool {
if depth == 0 {
return false;
}
if timed_out(deadline) {
return false;
}
debug_assert_eq!(board.side_to_move, attacker);
let hash = zobrist_hash(board);
if let Some(entry) = tt.get(&hash) {
if entry.depth >= depth {
return matches!(entry.result, TtResult::AttackerWins);
}
}
let (my, opp) = bb_pair(board, attacker);
let opp_has_immediate_five = has_immediate_five(opp, my);
let attack_moves = gather_attack_moves(my, opp);
if attack_moves.is_empty() {
tt.insert(hash, TtEntry { depth, result: TtResult::Fails });
return false;
}
for (mv, kind) in attack_moves {
if kind.is_winning() {
if opp_has_immediate_five && kind != ThreatKind::Five {
continue;
}
sequence.push(mv);
tt.insert(hash, TtEntry { depth, result: TtResult::AttackerWins });
return true;
}
if opp_has_immediate_five {
continue;
}
sequence.push(mv);
board.make_move(mv);
let won = vct_and(board, attacker, depth - 1, deadline, sequence, tt);
board.undo_move();
if won {
tt.insert(hash, TtEntry { depth, result: TtResult::AttackerWins });
return true;
}
sequence.pop();
}
tt.insert(hash, TtEntry { depth, result: TtResult::Fails });
false
}
fn vct_and(
board: &mut Board,
attacker: Stone,
depth: u32,
deadline: Option<Instant>,
sequence: &mut Vec<Move>,
tt: &mut TransTable,
) -> bool {
if depth == 0 {
return false;
}
if timed_out(deadline) {
return false;
}
debug_assert_ne!(board.side_to_move, attacker);
let (def_my, def_opp) = bb_pair(board, board.side_to_move);
if has_immediate_five(def_my, def_opp) {
return false;
}
let defenses = match board.last_move {
Some(attack_mv) => find_defenses_with_counters(board, attack_mv),
None => board.candidate_moves(),
};
if defenses.is_empty() {
return false;
}
let checkpoint = sequence.len();
for mv in defenses {
sequence.truncate(checkpoint);
sequence.push(mv);
board.make_move(mv);
let attacker_still_wins = vct_or(board, attacker, depth - 1, deadline, sequence, tt);
board.undo_move();
if !attacker_still_wins {
sequence.truncate(checkpoint);
return false;
}
}
true
}
fn gather_attack_moves(my: &BitBoard, opp: &BitBoard) -> Vec<(Move, ThreatKind)> {
let mut out = Vec::new();
let cells = my.count_ones() + opp.count_ones();
if cells == 0 {
return out;
}
for idx in 0..(BOARD_SIZE * BOARD_SIZE) {
if my.get(idx) || opp.get(idx) {
continue;
}
let kind = classify_move(my, opp, idx);
if kind.is_forcing() {
out.push((idx, kind));
}
}
out.sort_by_key(|(_, k)| threat_priority(*k));
out
}
fn threat_priority(k: ThreatKind) -> i32 {
match k {
ThreatKind::Five => 0,
ThreatKind::OpenFour => 1,
ThreatKind::DoubleFour => 2,
ThreatKind::FourThree => 3,
ThreatKind::DoubleThree => 4,
ThreatKind::ClosedFour => 5,
ThreatKind::OpenThree => 6,
ThreatKind::None => 100,
}
}
fn has_immediate_five(my: &BitBoard, opp: &BitBoard) -> bool {
for idx in 0..(BOARD_SIZE * BOARD_SIZE) {
if my.get(idx) || opp.get(idx) {
continue;
}
if classify_move(my, opp, idx) == ThreatKind::Five {
return true;
}
}
false
}
#[inline]
fn in_board(r: i32, c: i32) -> bool {
r >= 0 && r < BOARD_SIZE as i32 && c >= 0 && c < BOARD_SIZE as i32
}
fn find_defenses_with_counters(board: &Board, attack_move: Move) -> Vec<Move> {
let mut defenses = find_defenses(board, attack_move);
let mut seen = BitBoard::EMPTY;
for &d in &defenses {
seen.set(d);
}
let (def_my, def_opp) = bb_pair(board, board.side_to_move);
for idx in 0..NUM_CELLS {
if def_my.get(idx) || def_opp.get(idx) || seen.get(idx) {
continue;
}
let kind = classify_move(def_my, def_opp, idx);
if kind.is_forcing() {
seen.set(idx);
defenses.push(idx);
}
}
defenses
}
fn find_defenses(board: &Board, attack_move: Move) -> Vec<Move> {
let row = (attack_move / BOARD_SIZE) as i32;
let col = (attack_move % BOARD_SIZE) as i32;
let mut seen = BitBoard::EMPTY;
let mut out = Vec::with_capacity(24);
for dr in -2..=2 {
for dc in -2..=2 {
if dr == 0 && dc == 0 {
continue;
}
let nr = row + dr;
let nc = col + dc;
if !in_board(nr, nc) {
continue;
}
let idx = (nr as usize) * BOARD_SIZE + (nc as usize);
if board.is_empty(idx) && !seen.get(idx) {
seen.set(idx);
out.push(idx);
}
}
}
for &(dr, dc) in &DIR {
for step in [-4i32, -3, 3, 4] {
let nr = row + dr * step;
let nc = col + dc * step;
if !in_board(nr, nc) {
continue;
}
let idx = (nr as usize) * BOARD_SIZE + (nc as usize);
if board.is_empty(idx) && !seen.get(idx) {
seen.set(idx);
out.push(idx);
}
}
}
out
}
fn bb_pair(board: &Board, side: Stone) -> (&BitBoard, &BitBoard) {
match side {
Stone::Black => (&board.black, &board.white),
Stone::White => (&board.white, &board.black),
}
}
fn timed_out(deadline: Option<Instant>) -> bool {
if let Some(d) = deadline {
if Instant::now() >= d {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::board::to_idx;
#[test]
fn test_classify_move_five() {
let mut board = Board::new();
board.make_move(to_idx(7, 3));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 4));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(14, 0));
board.make_move(to_idx(7, 6));
let k1 = classify_move(&board.black, &board.white, to_idx(7, 2));
let k2 = classify_move(&board.black, &board.white, to_idx(7, 7));
assert_eq!(k1, ThreatKind::Five, "(7,2) should complete Five");
assert_eq!(k2, ThreatKind::Five, "(7,7) should complete Five");
}
#[test]
fn test_classify_move_open_four() {
let mut board = Board::new();
board.make_move(to_idx(7, 4));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(7, 6));
let k = classify_move(&board.black, &board.white, to_idx(7, 7));
assert_eq!(k, ThreatKind::OpenFour);
}
#[test]
fn test_vct_open_four_mate_in_1() {
let mut board = Board::new();
board.make_move(to_idx(7, 3));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 4));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(14, 0));
board.make_move(to_idx(7, 6));
board.make_move(to_idx(14, 14));
let cfg = VctConfig::default();
let seq = search_vct(&mut board, &cfg);
assert!(seq.is_some(), "should find mate");
let seq = seq.unwrap();
assert_eq!(seq.len(), 1, "mate in 1");
assert!(
[to_idx(7, 2), to_idx(7, 7)].contains(&seq[0]),
"got {:?}",
seq[0]
);
}
#[test]
fn test_classify_move_double_three() {
let mut board = Board::new();
board.make_move(to_idx(7, 4));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(5, 6));
board.make_move(to_idx(14, 0));
board.make_move(to_idx(6, 6));
let k = classify_move(&board.black, &board.white, to_idx(7, 6));
assert_eq!(k, ThreatKind::DoubleThree, "should be double three, got {:?}", k);
}
#[test]
fn test_classify_move_four_three() {
let mut board = Board::new();
board.make_move(to_idx(7, 3));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 4));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(14, 0));
board.make_move(to_idx(5, 6));
board.make_move(to_idx(14, 14));
board.make_move(to_idx(6, 6));
let k = classify_move(&board.black, &board.white, to_idx(7, 6));
assert_eq!(k, ThreatKind::OpenFour, "open four dominates; got {:?}", k);
}
#[test]
fn test_vct_double_three_mate_in_3() {
let mut board = Board::new();
board.make_move(to_idx(7, 4));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(5, 6));
board.make_move(to_idx(14, 0));
board.make_move(to_idx(6, 6));
board.make_move(to_idx(14, 14));
let cfg = VctConfig::default();
let seq = search_vct(&mut board, &cfg);
assert!(seq.is_some(), "should find VCT mate");
let seq = seq.unwrap();
assert!(seq.len() >= 1, "non-empty sequence");
assert_eq!(seq[0], to_idx(7, 6), "first move must be (7,6) DoubleThree");
}
#[test]
fn test_vct_no_winning_sequence() {
let mut board = Board::new();
board.make_move(to_idx(7, 7));
board.make_move(to_idx(6, 6));
let cfg = VctConfig {
max_depth: 8,
time_budget: Some(Duration::from_millis(100)),
};
let seq = search_vct(&mut board, &cfg);
assert!(seq.is_none(), "no VCT should exist, got {:?}", seq);
}
#[test]
fn test_vct_loses_to_faster_counter_threat() {
let mut board = Board::new();
board.make_move(to_idx(7, 3));
board.make_move(to_idx(8, 0));
board.make_move(to_idx(7, 4));
board.make_move(to_idx(8, 1));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(8, 2));
board.make_move(to_idx(7, 6));
board.make_move(to_idx(8, 3));
let cfg = VctConfig::default();
let seq = search_vct(&mut board, &cfg);
assert!(seq.is_some(), "Five wins before opponent's 4");
let seq = seq.unwrap();
assert_eq!(seq.len(), 1);
assert!([to_idx(7, 2), to_idx(7, 7)].contains(&seq[0]));
}
#[test]
fn test_vct_mate_in_5_chain() {
let mut board = Board::new();
board.make_move(to_idx(7, 5));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 6));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(7, 7));
board.make_move(to_idx(0, 7));
let cfg = VctConfig {
max_depth: 8,
time_budget: Some(Duration::from_millis(300)),
};
let seq = search_vct(&mut board, &cfg);
assert!(seq.is_some(), "should find mate via open-three chain");
}
#[test]
fn test_vct_tt_consistency() {
let mut board = Board::new();
board.make_move(to_idx(7, 5));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(7, 6));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(7, 7));
board.make_move(to_idx(0, 7));
let cfg = VctConfig {
max_depth: 8,
time_budget: Some(Duration::from_millis(500)),
};
let s1 = search_vct(&mut board, &cfg);
let s2 = search_vct(&mut board, &cfg);
assert_eq!(s1.is_some(), s2.is_some(), "VCT should be deterministic");
}
#[test]
fn test_vct_cannot_ignore_opponent_five_threat_for_forcing() {
let mut board = Board::new();
board.make_move(to_idx(7, 4));
board.make_move(to_idx(8, 0));
board.make_move(to_idx(7, 5));
board.make_move(to_idx(8, 1));
board.make_move(to_idx(0, 0));
board.make_move(to_idx(8, 2));
board.make_move(to_idx(0, 14));
board.make_move(to_idx(8, 3));
let cfg = VctConfig::default();
let seq = search_vct(&mut board, &cfg);
assert!(seq.is_none(), "no VCT when opponent has immediate Five");
}
}