use super::*;
use crate::board::GameResult;
const ROOT_HISTORY: [Move; 27] = [
112, 110, 140, 124, 152, 95, 64, 137, 83, 53, 105, 91, 126, 98, 154, 168, 151, 153, 82, 81,
109, 123, 138, 169, 185, 139, 107,
];
const ATTACK: Move = 167;
const COUNTER: Move = 106;
const CLASSIFY_PATHS: [(bool, bool); 3] = [(false, false), (true, false), (true, true)];
fn transform(cell: Move, symmetry: usize) -> Move {
let (mut row, mut col) = (cell / BOARD_SIZE, cell % BOARD_SIZE);
if symmetry >= 4 {
col = BOARD_SIZE - 1 - col;
}
for _ in 0..symmetry % 4 {
(row, col) = (col, BOARD_SIZE - 1 - row);
}
row * BOARD_SIZE + col
}
fn replay(history: &[Move]) -> Board {
let mut board = Board::new();
for &mv in history {
assert_eq!(board.game_result(), GameResult::Ongoing);
assert!(board.is_legal_move(mv), "illegal fixture move {mv}");
board.make_move(mv);
}
assert_eq!(board.game_result(), GameResult::Ongoing);
board
}
fn assert_board_restored(actual: &Board, expected: &Board) {
assert_eq!(
(actual.black.lo, actual.black.hi),
(expected.black.lo, expected.black.hi)
);
assert_eq!(
(actual.white.lo, actual.white.hi),
(expected.white.lo, expected.white.hi)
);
assert_eq!(actual.side_to_move, expected.side_to_move);
assert_eq!(actual.move_count, expected.move_count);
assert_eq!(actual.last_move, expected.last_move);
assert_eq!(actual.history, expected.history);
assert_eq!(actual.zobrist, expected.zobrist);
assert_eq!(actual.line_pattern_ids, expected.line_pattern_ids);
assert_eq!(actual.rule_set, expected.rule_set);
assert_eq!(actual.exact5, expected.exact5);
}
fn defenses_for_path(
board: &Board,
attack: Move,
kind: ThreatKind,
fast: bool,
indexed: bool,
use_reach: bool,
) -> Vec<Move> {
assert!(!indexed || fast);
let cfg = VctConfig {
use_fast_classify: fast,
use_threat_index: indexed,
use_reach_mask: use_reach,
..VctConfig::default()
};
assert!(
!cfg.enable_gap_four,
"attacker gap-four vocabulary stays disabled"
);
let flags = cfg.jump_three_flags();
let index = indexed.then(|| VctThreatIndex::new(board));
let reach = use_reach.then(|| reach_mask_for_side(board, board.side_to_move));
let mut scratch = VctScratch::default();
let mut defenses = find_defenses_with_counters(
board,
attack,
kind,
flags,
index.as_ref(),
reach.as_ref(),
None,
&mut scratch,
false,
);
for &mv in &defenses {
assert!(board.is_legal_move(mv));
}
defenses.sort_unstable();
assert!(defenses.windows(2).all(|pair| pair[0] != pair[1]));
defenses
}
#[test]
fn recorded_remote_counter_is_present_in_all_paths_and_d4_images() {
for symmetry in 0..8 {
let history: Vec<_> = ROOT_HISTORY
.iter()
.map(|&mv| transform(mv, symmetry))
.collect();
let mut board = replay(&history);
assert_eq!(board.side_to_move, Stone::White);
let attack = transform(ATTACK, symmetry);
let counter = transform(COUNTER, symmetry);
let kind = classify_move_rules(
&board.white,
&board.black,
attack,
Stone::White,
RuleSet::Freestyle,
);
assert!(kind.is_forcing());
assert!(!is_vct_terminal_win(kind));
board.make_move(attack);
let saved = board.clone();
assert_eq!(board.side_to_move, Stone::Black);
assert!(!find_defenses(&board, attack, false, false).contains(&counter));
assert!(reach_mask_for_side(&board, Stone::Black).get(counter));
let mut reference = None;
for (fast, indexed) in CLASSIFY_PATHS {
for use_reach in [false, true] {
let defenses = defenses_for_path(&board, attack, kind, fast, indexed, use_reach);
assert!(
defenses.contains(&counter),
"missing recorded counter: symmetry={symmetry}, fast={fast}, index={indexed}, reach={use_reach}"
);
if let Some(expected) = &reference {
assert_eq!(
&defenses, expected,
"counter paths must agree for the recorded position"
);
} else {
reference = Some(defenses);
}
assert_board_restored(&board, &saved);
}
}
}
}
fn broken_four_fixture(existing: &[Move; 3], defender: Stone, symmetry: usize) -> Board {
let remote_attack = 0;
let fillers = [14, 210, 224];
let mut history = Vec::new();
match defender {
Stone::Black => {
for index in 0..3 {
history.push(existing[index]);
history.push(if index == 2 {
remote_attack
} else {
fillers[index]
});
}
}
Stone::White => {
for index in 0..3 {
history.push(fillers[index]);
history.push(existing[index]);
}
history.push(remote_attack);
}
}
let history: Vec<_> = history
.into_iter()
.map(|mv| transform(mv, symmetry))
.collect();
let board = replay(&history);
assert_eq!(board.side_to_move, defender);
board
}
#[test]
fn broken_four_counter_shapes_cover_rotations_and_both_colors() {
let cases = [
([110, 111, 112], 108, 109), ([108, 111, 112], 109, 110), ([108, 109, 112], 110, 111), ];
for (existing, new_stone, gap) in cases {
for defender in [Stone::Black, Stone::White] {
for symmetry in 0..4 {
let board = broken_four_fixture(&existing, defender, symmetry);
let attack = board.last_move.unwrap();
let counter = transform(new_stone, symmetry);
let gap = transform(gap, symmetry);
assert!(!find_defenses(&board, attack, false, false).contains(&counter));
let (mine, opp) = bb_pair(&board, defender);
let mut after_counter = *mine;
after_counter.set(counter);
assert!(completes_five_at(
&after_counter,
opp,
gap,
defender,
RuleSet::Freestyle
));
let mut reference = None;
for (fast, indexed) in CLASSIFY_PATHS {
for use_reach in [false, true] {
let defenses = defenses_for_path(
&board,
attack,
ThreatKind::OpenThree,
fast,
indexed,
use_reach,
);
assert!(
defenses.contains(&counter),
"missing shape counter={counter}, defender={defender:?}, rotation={symmetry}, fast={fast}, index={indexed}, reach={use_reach}"
);
if let Some(expected) = &reference {
assert_eq!(
&defenses, expected,
"counter paths must agree for synthetic geometry"
);
} else {
reference = Some(defenses);
}
}
}
}
}
}
}
#[test]
fn selected_white_167_attack_is_refuted_without_deadline_or_node_stop() {
let mut board = replay(&ROOT_HISTORY);
let attacker = board.side_to_move;
let kind = classify_move_fast(&board, ATTACK, attacker);
assert!(kind.is_forcing());
assert!(!is_vct_terminal_win(kind));
board.make_move(ATTACK);
let saved = board.clone();
let cfg = VctConfig {
max_depth: 14,
time_budget: None,
node_budget: Some(25_000),
..VctConfig::default()
};
let mut sequence = vec![ATTACK];
let mut tt = TransTable::with_capacity(65_536);
let mut stats = VctSearchStats::default();
let mut scratch = VctScratch::default();
let mut index = None;
let proved = vct_and(
&mut board,
attacker,
kind,
13,
0,
None,
cfg.node_budget,
cfg.jump_three_flags(),
&mut None,
&mut index,
&mut sequence,
&mut tt,
&mut stats,
&mut scratch,
);
assert_board_restored(&board, &saved);
assert!(stats.nodes > 0);
assert!(
!stats.hit_stop(),
"a budget stop is inconclusive, stats={stats:?}"
);
assert!(
!proved,
"White 167 has the independently proved Black 106 counter"
);
assert_eq!(
sequence,
vec![ATTACK],
"failed AND restores the caller's sequence checkpoint"
);
}