use std::collections::HashMap;
use pyo3::prelude::*;
use pyo3::types::PyType;
use super::config::CancelToken;
use crate::error::CspError;
use crate::ordering::Ordering as RustOrdering;
use crate::sudoku::{self, Difficulty};
use crate::{Pruning as RustPruning, SolveConfig as RustSolveConfig};
#[pyclass(from_py_object)]
#[derive(Clone)]
pub enum SudokuDifficulty {
EASY,
MEDIUM,
HARD,
}
#[pymethods]
impl SudokuDifficulty {
#[classmethod]
#[pyo3(signature = (key, default=None))]
fn get(
_cls: &Bound<'_, PyType>,
key: &str,
default: Option<SudokuDifficulty>,
) -> Option<SudokuDifficulty> {
match key {
"EASY" => Some(SudokuDifficulty::EASY),
"MEDIUM" => Some(SudokuDifficulty::MEDIUM),
"HARD" => Some(SudokuDifficulty::HARD),
_ => default,
}
}
}
impl From<SudokuDifficulty> for Difficulty {
fn from(d: SudokuDifficulty) -> Self {
match d {
SudokuDifficulty::EASY => Difficulty::Easy,
SudokuDifficulty::MEDIUM => Difficulty::Medium,
SudokuDifficulty::HARD => Difficulty::Hard,
}
}
}
#[pyclass(skip_from_py_object)]
#[derive(Clone)]
pub struct SudokuCSP {
board: Vec<u32>,
n: u32,
max_solutions: usize,
#[pyo3(get)]
solutions: Vec<HashMap<String, i32>>,
#[pyo3(get)]
backtrack_count: u64,
budget_exceeded: bool,
_given_values: HashMap<String, i32>,
}
#[pyfunction]
#[pyo3(signature = (N, values, max_solutions=1))]
pub fn create_sudoku_csp(
#[allow(non_snake_case)] N: u32,
values: HashMap<String, i32>,
max_solutions: usize,
) -> PyResult<SudokuCSP> {
let n = N;
let m = n * n;
let total = (m * m) as usize;
let mut board = vec![0u32; total];
let mut given = HashMap::new();
for (pos_str, val) in &values {
let pos: usize = pos_str
.parse()
.map_err(|_| CspError::invalid_input(format!("invalid position: {pos_str:?}")))?;
if pos >= total {
return Err(CspError::invalid_input(format!(
"position {pos} out of range [0, {total})"
))
.into());
}
if *val > 0 {
board[pos] = *val as u32;
given.insert(pos_str.clone(), *val);
}
}
Ok(SudokuCSP {
board,
n,
max_solutions,
solutions: Vec::new(),
backtrack_count: 0,
budget_exceeded: false,
_given_values: given,
})
}
#[pyfunction]
#[pyo3(signature = (csp, cancel=None))]
pub fn solve_sudoku(
py: Python<'_>,
csp: &mut SudokuCSP,
cancel: Option<CancelToken>,
) -> PyResult<bool> {
let config = RustSolveConfig {
pruning: RustPruning::Ac3,
ordering: RustOrdering::Mrv,
max_solutions: csp.max_solutions,
cancel: cancel.as_ref().map(|t| t.inner.clone()),
..Default::default()
};
let board = &csp.board;
let n = csp.n;
let (solutions, stats) = py.detach(|| {
let (mut rust_csp, given) = sudoku::create_sudoku_csp(board, n);
let solutions = rust_csp.solve_with_given(&config, &given);
(solutions, rust_csp.stats().clone())
});
csp.backtrack_count = stats.backtracks;
csp.budget_exceeded = stats.budget_exceeded;
csp.solutions = solutions
.into_iter()
.map(|sol| {
sol.into_iter()
.enumerate()
.map(|(i, v)| (i.to_string(), v as i32))
.collect()
})
.collect();
if csp.solutions.is_empty() && csp.budget_exceeded {
return Err(CspError::BudgetExceeded.into());
}
Ok(!csp.solutions.is_empty())
}
#[pyfunction]
#[pyo3(signature = (N, difficulty=SudokuDifficulty::EASY, templates=None))]
pub fn create_random_board(
py: Python<'_>,
#[allow(non_snake_case)] N: u32,
difficulty: SudokuDifficulty,
templates: Option<Vec<HashMap<String, i32>>>,
) -> PyResult<HashMap<String, i32>> {
let board = py.detach(|| -> Result<Vec<u32>, CspError> {
if let Some(ref tmpls) = templates {
let m = (N * N) as usize;
let total = m * m;
let flat_templates: Vec<Vec<u32>> = tmpls
.iter()
.map(|t| {
let mut flat = vec![0u32; total];
for (k, v) in t {
if let Ok(pos) = k.parse::<usize>()
&& pos < total
{
flat[pos] = *v as u32;
}
}
flat
})
.collect();
Ok(sudoku::generate_board_with_templates(
N,
difficulty.into(),
&flat_templates,
))
} else {
let diff: Difficulty = difficulty.into();
let templates = sudoku::embedded_templates(N, diff);
if templates.is_empty() {
return Err(CspError::invalid_input(format!(
"no embedded template bank for N={N} {diff:?} — tier not \
pregenerated (excised tiers live-generate in the browser)"
)));
}
Ok(sudoku::generate_board_with_templates(N, diff, &templates))
}
})?;
Ok(board
.into_iter()
.enumerate()
.map(|(i, v)| (i.to_string(), v as i32))
.collect())
}