use csp_solver::domain::Domain;
use csp_solver::ordering::Ordering;
use csp_solver::puzzles::sudoku::Difficulty;
use csp_solver::puzzles::thermo::{create_thermo_csp, generate_thermo_seeded, solve_thermo};
use csp_solver::{Pruning, SolveConfig};
fn enumerate_config(max_solutions: usize) -> SolveConfig {
SolveConfig {
pruning: Pruning::Ac3,
ordering: Ordering::FailFirst,
max_solutions,
..Default::default()
}
}
fn thermometers_hold(solution: &[u32], thermos: &[Vec<usize>]) -> bool {
thermos.iter().all(|t| {
t.windows(2)
.all(|pair| solution[pair[1]] > solution[pair[0]])
})
}
#[test]
fn dealt_thermo_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, thermos) = generate_thermo_seeded(n, difficulty, seed);
let solutions = {
let (mut csp, given) = create_thermo_csp(&board, n, &thermos);
csp.solve_with_given(&enumerate_config(2), &given)
};
assert_eq!(
solutions.len(),
1,
"thermo n={n} {difficulty:?} seed={seed} is not unique ({} solutions)",
solutions.len()
);
assert!(
thermometers_hold(&solutions[0], &thermos),
"thermo n={n} {difficulty:?} seed={seed}: solution violates a tube"
);
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_thermometers() {
let (board, thermos) = generate_thermo_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!(!thermos.is_empty(), "a dealt board carries thermometers");
let mut seen = std::collections::HashSet::new();
for t in &thermos {
assert!(t.len() >= 2);
for &c in t {
assert!(c < 81, "thermo cell {c} out of range");
assert!(seen.insert(c), "thermo cell {c} shared between two tubes");
}
}
}
#[test]
fn solve_thermo_finds_the_dealt_solution() {
let (board, thermos) = generate_thermo_seeded(2, Difficulty::Medium, 7);
let config = enumerate_config(1);
let solved = solve_thermo(&board, 2, &thermos, &config).expect("dealt board solves");
assert_eq!(solved.len(), 16);
assert!(solved.iter().all(|&v| (1..=4).contains(&v)));
assert!(thermometers_hold(&solved, &thermos));
}
#[test]
fn a_thermometer_chain_prunes_the_free_path() {
let n = 3u32; let board = vec![0u32; 81];
let thermo = vec![vec![0usize, 1, 2, 3]];
let (mut csp, given) = create_thermo_csp(&board, n, &thermo);
assert!(given.is_empty());
csp.propagate()
.expect("an empty board with one tube is consistent");
let bulb = &csp.variables[0].domain;
assert!(
!bulb.contains(&7),
"bulb kept 7 — the less_than chain did not prune the top"
);
assert!(
!bulb.contains(&9),
"bulb kept 9 — the less_than chain did not prune the top"
);
assert!(bulb.contains(&1), "bulb should still admit 1");
let tip = &csp.variables[3].domain;
assert!(
!tip.contains(&3),
"tip kept 3 — the less_than chain did not prune the bottom"
);
assert!(
!tip.contains(&1),
"tip kept 1 — the less_than chain did not prune the bottom"
);
assert!(tip.contains(&9), "tip should still admit 9");
}