use std::collections::HashSet;
use csp_solver::domain::Domain;
use csp_solver::ordering::Ordering;
use csp_solver::puzzles::killer::{
KillerCage, create_killer_csp, generate_killer_seeded, solve_killer,
};
use csp_solver::puzzles::sudoku::Difficulty;
use csp_solver::{Pruning, SolveConfig};
fn enumerate_config(max_solutions: usize) -> SolveConfig {
SolveConfig {
pruning: Pruning::Ac3,
ordering: Ordering::FailFirst,
max_solutions,
..Default::default()
}
}
fn cages_hold(solution: &[u32], cages: &[KillerCage]) -> bool {
cages.iter().all(|cage| {
let sum: u32 = cage.cells.iter().map(|&c| solution[c]).sum();
let mut seen = HashSet::new();
let distinct = cage.cells.iter().all(|&c| seen.insert(solution[c]));
sum == cage.sum && distinct
})
}
#[test]
fn dealt_killer_boards_are_unique_by_construction() {
for &n in &[2u32, 3] {
for &difficulty in &[Difficulty::Easy, Difficulty::Medium] {
for &seed in &[1u64, 7, 42, 2026] {
let (board, cages) = generate_killer_seeded(n, difficulty, seed);
let solutions = {
let (mut csp, given) = create_killer_csp(&board, n, &cages);
csp.solve_with_given(&enumerate_config(2), &given)
};
assert_eq!(
solutions.len(),
1,
"killer n={n} {difficulty:?} seed={seed} is not unique ({} solutions)",
solutions.len()
);
assert!(
cages_hold(&solutions[0], &cages),
"killer n={n} {difficulty:?} seed={seed}: solution violates a cage"
);
for (i, &v) in board.iter().enumerate() {
if v != 0 {
assert_eq!(
v, solutions[0][i],
"given cell {i} contradicts the solution"
);
}
}
}
}
}
}
#[test]
fn dealt_boards_carry_holes_and_a_cage_partition() {
let (board, cages) = generate_killer_seeded(3, Difficulty::Easy, 99);
assert_eq!(board.len(), 81);
let holes = board.iter().filter(|&&v| v == 0).count();
assert!(holes > 0, "an Easy 9×9 must be dug (had {holes} holes)");
assert!(!cages.is_empty(), "a dealt board carries cages");
let mut seen = [false; 81];
for cage in &cages {
assert!(!cage.cells.is_empty(), "an empty cage is nonsense");
for &c in &cage.cells {
assert!(c < 81, "cage cell {c} out of range");
assert!(!seen[c], "cell {c} shared between two cages");
seen[c] = true;
}
assert!(cage.sum > 0, "a cage sum is positive");
}
assert!(seen.iter().all(|&s| s), "every cell must belong to a cage");
}
#[test]
fn solve_killer_finds_the_dealt_solution() {
let (board, cages) = generate_killer_seeded(2, Difficulty::Medium, 7);
let config = enumerate_config(1);
let solved = solve_killer(&board, 2, &cages, &config).expect("dealt board solves");
assert_eq!(solved.len(), 16);
assert!(solved.iter().all(|&v| (1..=4).contains(&v)));
assert!(cages_hold(&solved, &cages));
}
#[test]
fn killer_consumes_cage_sum_and_prunes_at_the_root() {
let n = 3u32; let board = vec![0u32; 81];
let cages = vec![KillerCage {
sum: 6,
cells: vec![0, 1, 2], }];
let (mut csp, given) = create_killer_csp(&board, n, &cages);
assert!(given.is_empty());
csp.propagate()
.expect("an empty 9×9 with one sum-6 cage is consistent");
for &c in &[0usize, 1, 2] {
let dom = &csp.variables[c].domain;
assert!(
dom.contains(&1) && dom.contains(&4),
"member {c} lost a supported value (1 or 4)"
);
assert!(
!dom.contains(&5) && !dom.contains(&9),
"member {c} kept 5/9 — CageSum did not prune; Killer would search blind"
);
}
}