use csp_solver::constraint::{AllDifferent, LambdaConstraint};
use csp_solver::domain::bitset::BitsetDomain;
use csp_solver::ordering::Ordering;
use csp_solver::puzzles::futoshiki::{FutoshikiPuzzle, create_futoshiki_csp};
use csp_solver::{Csp, Pruning, SolveConfig};
const PRUNINGS: [Pruning; 4] = [
Pruning::None,
Pruning::ForwardChecking,
Pruning::Ac3,
Pruning::AcFc,
];
const ORDERINGS: [Ordering; 3] = [Ordering::Chronological, Ordering::FailFirst, Ordering::Mrv];
fn enumerate_cfg(p: Pruning, o: Ordering) -> SolveConfig {
SolveConfig {
pruning: p,
ordering: o,
max_solutions: usize::MAX,
node_budget: Some(5_000_000),
..Default::default()
}
}
fn build_nqueens(n: u32) -> Csp<BitsetDomain> {
let mut csp = Csp::new();
let domain = BitsetDomain::new(0..n);
let vars = csp.add_variables(&domain, n as usize);
csp.add_constraint(AllDifferent::new(vars.clone()));
for i in 0..n {
for j in (i + 1)..n {
let vi = vars[i as usize];
let vj = vars[j as usize];
let diff = j - i;
csp.add_constraint(LambdaConstraint::new(
vec![vi, vj],
move |a: &[Option<u32>]| match (&a[vi as usize], &a[vj as usize]) {
(Some(ri), Some(rj)) => ri.abs_diff(*rj) != diff,
_ => true,
},
format!("diag({i},{j})"),
));
}
}
csp.finalize();
csp
}
fn build_futoshiki(input: &str) -> Csp<BitsetDomain> {
create_futoshiki_csp(&FutoshikiPuzzle::parse(input))
}
fn canonical(sols: &[Vec<u32>]) -> Vec<Vec<u32>> {
let mut v: Vec<Vec<u32>> = sols.to_vec();
v.sort();
v
}
fn assert_set_invariant<F>(name: &str, build: F, expected_count: usize)
where
F: Fn() -> Csp<BitsetDomain>,
{
let mut reference: Option<Vec<Vec<u32>>> = None;
let mut reference_tag = String::new();
for p in PRUNINGS {
for o in ORDERINGS {
let mut csp = build();
let sols = csp.solve(&enumerate_cfg(p, o));
let stats = csp.stats();
assert!(
!stats.budget_exceeded,
"{name}: {p:?}/{o:?} hit the node budget — raise it; this test needs a complete enumeration"
);
let got = canonical(&sols);
assert_eq!(
got.len(),
expected_count,
"{name}: {p:?}/{o:?} enumerated {} solutions, expected {expected_count} \
(solution-set-invariance / enumerate-continuation soundness violation)",
got.len()
);
match &reference {
None => {
reference = Some(got);
reference_tag = format!("{p:?}/{o:?}");
}
Some(reference_set) => {
assert_eq!(
&got, reference_set,
"{name}: {p:?}/{o:?} enumerated a DIFFERENT solution set than \
{reference_tag} — the set must be invariant under pruning/ordering"
);
}
}
}
}
}
#[test]
fn queens8_solution_set_invariant_across_pruning_and_ordering() {
assert_set_invariant("queens8", || build_nqueens(8), 92);
}
#[test]
fn queens6_solution_set_invariant_across_pruning_and_ordering() {
assert_set_invariant("queens6", || build_nqueens(6), 4);
}
#[test]
fn futoshiki_loose_solution_set_invariant_across_pruning_and_ordering() {
assert_set_invariant("futoshiki_loose", || build_futoshiki("4\n\n\n1\n2\n"), 288);
}
#[test]
fn futoshiki_constr_solution_set_invariant_across_pruning_and_ordering() {
assert_set_invariant(
"futoshiki_constr",
|| build_futoshiki("4\n0 5\n1 3\n1\n2\n"),
16,
);
}