use csp_solver::ordering::Ordering;
use csp_solver::sudoku::{Difficulty, generate_board, measure_difficulty, solve_sudoku};
use csp_solver::{Pruning, SolveConfig};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::Instant;
fn parse_difficulty(s: &str) -> (Difficulty, &'static str) {
match s.to_lowercase().as_str() {
"easy" => (Difficulty::Easy, "easy"),
"medium" => (Difficulty::Medium, "medium"),
"hard" => (Difficulty::Hard, "hard"),
other => panic!("unknown difficulty {other:?} (expected easy|medium|hard)"),
}
}
fn sparse_json(board: &[u32]) -> String {
let entries: Vec<String> = board
.iter()
.enumerate()
.filter(|&(_, &v)| v != 0)
.map(|(i, v)| format!("\"{i}\":{v}"))
.collect();
format!("{{{}}}", entries.join(","))
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 4 {
eprintln!("usage: generate_templates <N> <difficulty> <count> [out_dir]");
std::process::exit(2);
}
let n: u32 = args[1]
.parse()
.expect("N must be an integer (2, 3, 4, or 5)");
let (difficulty, diff_name) = parse_difficulty(&args[2]);
let count: usize = args[3]
.parse()
.expect("count must be a non-negative integer");
let out_root = args
.get(4)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("csp-solver/data/sudoku_puzzles"));
let out_dir = out_root.join(n.to_string()).join(diff_name);
fs::create_dir_all(&out_dir).expect("failed to create output directory");
let verify_config = SolveConfig {
pruning: Pruning::Ac3,
ordering: Ordering::Mrv,
max_solutions: 1,
..Default::default()
};
let m = n * n;
eprintln!(
"generate_templates: N={n} ({m}x{m}) difficulty={diff_name} count={count} -> {}",
out_dir.display()
);
let overall_start = Instant::now();
let mut written = 0usize;
for i in 0..count {
let gen_start = Instant::now();
let puzzle = generate_board(n, difficulty);
let gen_elapsed = gen_start.elapsed();
let solve_start = Instant::now();
let solution = match solve_sudoku(&puzzle, n, &verify_config) {
Some(s) => s,
None => {
eprintln!(
" [{}/{count}] FAILED: generated puzzle did not re-solve (should be \
unreachable — uniqueness was checked during hole-digging)",
i + 1
);
continue;
}
};
let solve_elapsed = solve_start.elapsed();
let measure_start = Instant::now();
let backtracks = if env::var("SKIP_MEASURE").is_ok() {
0
} else {
measure_difficulty(&puzzle, n)
};
let measure_elapsed = measure_start.elapsed();
let json = format!("{{\"puzzle\":{}}}", sparse_json(&puzzle));
let path = out_dir.join(format!("template-{i}.json"));
fs::write(&path, json).expect("failed to write template JSON");
written += 1;
eprintln!(
" [{}/{count}] gen={:>9.1}ms verify_solve={:>7.1}ms measure_difficulty={:>9.1}ms \
backtracks={backtracks:>8} solution_cells={} -> {}",
i + 1,
gen_elapsed.as_secs_f64() * 1000.0,
solve_elapsed.as_secs_f64() * 1000.0,
measure_elapsed.as_secs_f64() * 1000.0,
solution.len(),
path.display()
);
}
eprintln!(
"generate_templates: wrote {written}/{count} template(s) in {:.2}s",
overall_start.elapsed().as_secs_f64()
);
}