use std::collections::HashMap;
use pyo3::prelude::*;
use super::config::CancelToken;
use crate::error::CspError;
use crate::ordering::Ordering as RustOrdering;
use crate::puzzles::futoshiki::{
self, FutoshikiPuzzle, create_futoshiki_csp as build_futoshiki_csp,
};
use crate::{Pruning as RustPruning, SolveConfig as RustSolveConfig};
#[pyclass(skip_from_py_object)]
#[derive(Clone)]
pub struct FutoshikiCSP {
puzzle: FutoshikiPuzzle,
max_solutions: usize,
#[pyo3(get)]
board_size: u32,
#[pyo3(get)]
solutions: Vec<HashMap<String, i32>>,
#[pyo3(get)]
backtrack_count: u64,
#[pyo3(get)]
budget_exceeded: bool,
#[pyo3(get)]
cancelled: bool,
}
#[pymethods]
impl FutoshikiCSP {
#[getter]
fn backtracks(&self) -> u64 {
self.backtrack_count
}
#[getter]
fn inequalities(&self) -> Vec<(usize, usize)> {
self.puzzle.inequalities.clone()
}
}
#[pyfunction]
#[pyo3(signature = (board_size, values, inequalities, max_solutions=1))]
pub fn create_futoshiki_csp(
board_size: u32,
values: HashMap<String, i32>,
inequalities: Vec<(usize, usize)>,
max_solutions: usize,
) -> PyResult<FutoshikiCSP> {
let mut fixed_cells: Vec<(usize, u32)> = Vec::new();
for (pos_str, val) in &values {
let pos: usize = pos_str
.parse()
.map_err(|_| CspError::invalid_input(format!("invalid position: {pos_str:?}")))?;
if *val > 0 {
fixed_cells.push((pos, *val as u32));
}
}
let puzzle = FutoshikiPuzzle::from_parts(board_size, fixed_cells, inequalities)?;
Ok(FutoshikiCSP {
puzzle,
max_solutions,
board_size,
solutions: Vec::new(),
backtrack_count: 0,
budget_exceeded: false,
cancelled: false,
})
}
#[pyfunction]
#[pyo3(signature = (csp, cancel=None))]
pub fn solve_futoshiki(
py: Python<'_>,
csp: &mut FutoshikiCSP,
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 puzzle = csp.puzzle.clone();
let (solutions, stats) = py.detach(|| {
let mut rust_csp = build_futoshiki_csp(&puzzle);
let solutions = rust_csp.solve_with_given(&config, &[]);
(solutions, rust_csp.stats().clone())
});
csp.backtrack_count = stats.backtracks;
csp.budget_exceeded = stats.budget_exceeded;
csp.cancelled = stats.cancelled;
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())
}
#[pyclass(skip_from_py_object)]
#[derive(Clone)]
pub struct FutoshikiBoard {
#[pyo3(get)]
values: HashMap<String, i32>,
#[pyo3(get)]
inequalities: Vec<(usize, usize)>,
#[pyo3(get)]
board_size: u32,
}
#[pyfunction]
#[pyo3(signature = (board_size, seed=None))]
pub fn create_random_futoshiki(
py: Python<'_>,
board_size: u32,
seed: Option<u64>,
) -> PyResult<FutoshikiBoard> {
if !(4..=7).contains(&board_size) {
return Err(CspError::invalid_input(format!(
"board_size {board_size} out of range [4, 7] (v1 Futoshiki scope)"
))
.into());
}
let (board, inequalities) = py.detach(|| match seed {
Some(s) => futoshiki::generate_futoshiki_seeded(board_size, s),
None => futoshiki::generate_futoshiki(board_size),
});
let values = board
.into_iter()
.enumerate()
.filter(|&(_, v)| v != 0)
.map(|(i, v)| (i.to_string(), v as i32))
.collect();
Ok(FutoshikiBoard {
values,
inequalities,
board_size,
})
}