use clap::{Parser, Subcommand, ValueEnum};
use sodo::{Difficulty, Solver, Sudoku};
use std::{fs, path::PathBuf, process};
#[derive(Parser)]
#[command(name = "sodo", version, about = "Sudoku solver and generator")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
#[command(visible_alias = "s")]
Solve {
puzzle: Option<String>,
#[arg(short, long, conflicts_with = "puzzle")]
file: Option<PathBuf>,
#[arg(short, long, default_value = "9")]
size: usize,
},
#[command(visible_alias = "g")]
Generate {
#[arg(short, long, default_value = "9")]
size: usize,
#[arg(short, long, default_value = "medium")]
difficulty: Level,
},
#[command(visible_alias = "v")]
Validate {
puzzle: String,
#[arg(short, long, default_value = "9")]
size: usize,
#[arg(short, long)]
check: bool,
},
#[command(visible_alias = "h")]
Hint {
puzzle: String,
#[arg(short, long, default_value = "9")]
size: usize,
},
}
#[derive(Clone, ValueEnum)]
enum Level {
Easy,
Medium,
Hard,
Expert,
}
impl From<Level> for Difficulty {
fn from(level: Level) -> Self {
match level {
Level::Easy => Difficulty::Easy,
Level::Medium => Difficulty::Medium,
Level::Hard => Difficulty::Hard,
Level::Expert => Difficulty::Expert,
}
}
}
fn main() {
let cli = Cli::parse();
match cli.command {
Command::Solve { puzzle, file, size } => solve(puzzle, file, size),
Command::Generate { size, difficulty } => generate(size, difficulty.into()),
Command::Validate {
puzzle,
size,
check,
} => validate(&puzzle, size, check),
Command::Hint { puzzle, size } => hint(&puzzle, size),
}
}
fn solve(puzzle: Option<String>, file: Option<PathBuf>, size: usize) {
let input = match (puzzle, file) {
(Some(p), _) => p,
(_, Some(f)) => fs::read_to_string(&f).unwrap_or_else(|e| {
eprintln!("Error reading {}: {e}", f.display());
process::exit(1);
}),
_ => {
eprintln!("Provide puzzle string or --file");
process::exit(1);
}
};
let sudoku = parse(input.trim(), size);
println!("Puzzle:\n{sudoku}");
let mut solver = Solver::new();
match solver.solve_with_stats(sudoku) {
Ok((solution, stats)) => {
println!("Solution:\n{solution}");
println!(
"Stats: {} iters, {} cells, {} backtracks",
stats.iterations, stats.cells_filled, stats.backtracks
);
}
Err(e) => {
eprintln!("Failed: {e}");
process::exit(1);
}
}
}
fn generate(size: usize, difficulty: Difficulty) {
let mut solver = Solver::new();
match solver.generate(size, difficulty) {
Ok(puzzle) => {
println!("{puzzle}");
println!("{}", puzzle.to_string_compact());
}
Err(e) => {
eprintln!("Failed: {e}");
process::exit(1);
}
}
}
fn validate(puzzle: &str, size: usize, check_solvable: bool) {
let sudoku = parse(puzzle, size);
println!("{sudoku}");
if !sudoku.is_valid() {
println!("Invalid!");
process::exit(1);
}
if sudoku.is_complete() {
println!("Valid and complete!");
return;
}
if check_solvable {
let mut solver = Solver::new();
match solver.solve(sudoku) {
Ok(_) => println!("Valid and solvable"),
Err(_) => {
println!("Valid but unsolvable!");
process::exit(1);
}
}
} else {
println!("Valid");
}
}
fn hint(puzzle: &str, size: usize) {
let sudoku = parse(puzzle, size);
let solver = Solver::new();
match solver.hint(&sudoku) {
Some((r, c, v)) => println!("Place {v} at row {}, col {}", r + 1, c + 1),
None => println!("No hint available"),
}
}
fn parse(s: &str, size: usize) -> Sudoku {
Sudoku::from_string(s, size).unwrap_or_else(|e| {
eprintln!("Invalid puzzle: {e}");
process::exit(1)
})
}