mod board;
mod candidates;
mod masks;
mod solution;
mod techniques;
pub use board::Board;
pub use candidates::Candidates;
pub use masks::Masks;
pub use solution::{Solution, SolvePath, SolveStep};
pub use techniques::flags::TechniqueFlags;
use crate::error::RustokuError;
use rand::prelude::SliceRandom;
use rand::rng;
use techniques::TechniquePropagator;
#[derive(Debug, Copy, Clone)]
pub struct Rustoku {
pub board: Board,
pub masks: Masks,
pub candidates: Candidates,
pub techniques: TechniqueFlags,
}
impl Rustoku {
pub fn new(initial_board: Board) -> Result<Self, RustokuError> {
let board = initial_board; let mut masks = Masks::new();
let mut candidates = Candidates::new();
for r in 0..9 {
for c in 0..9 {
let num = board.get(r, c);
if num != 0 {
if !masks.is_safe(r, c, num) {
return Err(RustokuError::DuplicateValues);
}
masks.add_number(r, c, num);
}
}
}
for r in 0..9 {
for c in 0..9 {
if board.is_empty(r, c) {
candidates.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
}
}
}
Ok(Self {
board,
masks,
candidates,
techniques: TechniqueFlags::EASY, })
}
pub fn builder() -> RustokuBuilder {
RustokuBuilder::new()
}
pub fn new_from_str(s: &str) -> Result<Self, RustokuError> {
let board = Board::try_from(s)?;
Self::new(board)
}
pub fn with_techniques(mut self, techniques: TechniqueFlags) -> Self {
self.techniques = techniques;
self
}
fn candidates_from_mask(mask: u16) -> Vec<u8> {
let mut nums = Vec::with_capacity(mask.count_ones() as usize);
for v in 1..=9u8 {
if mask & (1 << (v - 1)) != 0 {
nums.push(v);
}
}
nums
}
fn find_next_empty_cell(&self) -> Option<(usize, usize)> {
let mut min = (10, None); for (r, c) in self.board.iter_empty_cells() {
let count = self.candidates.get(r, c).count_ones() as u8;
if count < min.0 {
min = (count, Some((r, c)));
if count == 1 {
return min.1;
}
}
}
min.1
}
fn place_number(&mut self, r: usize, c: usize, num: u8) {
self.board.set(r, c, num);
self.masks.add_number(r, c, num);
self.candidates
.update_affected_cells_for(r, c, &self.masks, &self.board, Some(num));
}
fn remove_number(&mut self, r: usize, c: usize, num: u8) {
self.board.set(r, c, 0); self.masks.remove_number(r, c, num);
self.candidates
.update_affected_cells(r, c, &self.masks, &self.board);
}
fn solve_until_recursive(
&mut self,
solutions: &mut Vec<Solution>,
path: &mut SolvePath,
bound: usize,
) -> usize {
let Some((r, c)) = self.find_next_empty_cell() else {
solutions.push(Solution {
board: self.board,
solve_path: path.clone(),
});
return 1;
};
let mut count = 0;
let mask = self.candidates.get(r, c);
let mut nums = Self::candidates_from_mask(mask);
nums.shuffle(&mut rng());
for &num in &nums {
if !self.masks.is_safe(r, c, num) {
continue;
}
self.place_number(r, c, num);
let step_number = path.steps.len() as u32;
path.steps.push(SolveStep::Placement {
row: r,
col: c,
value: num,
flags: TechniqueFlags::empty(),
step_number,
candidates_eliminated: 0,
related_cell_count: 0,
difficulty_point: 0,
});
count += self.solve_until_recursive(solutions, path, bound);
path.steps.pop();
self.remove_number(r, c, num);
if bound > 0 && solutions.len() >= bound {
return count;
}
}
count
}
fn techniques_make_valid_changes(&mut self, path: &mut SolvePath) -> bool {
let mut propagator = TechniquePropagator::new(
&mut self.board,
&mut self.masks,
&mut self.candidates,
self.techniques,
);
propagator.propagate_constraints(path, 0)
}
pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
let mut solutions = Vec::new();
let mut path = SolvePath::default();
if !self.techniques_make_valid_changes(&mut path) {
return solutions;
}
self.solve_until_recursive(&mut solutions, &mut path, bound);
solutions
}
pub fn solve_any(&mut self) -> Option<Solution> {
self.solve_until(1).into_iter().next()
}
pub fn solve_all(&mut self) -> Vec<Solution> {
use rayon::prelude::*;
let mut path = SolvePath::default();
if !self.techniques_make_valid_changes(&mut path) {
return Vec::new();
}
if let Some((r, c)) = self.find_next_empty_cell() {
let mask = self.candidates.get(r, c);
let nums = Self::candidates_from_mask(mask);
let initial_path = path.clone();
let chunks: Vec<Vec<Solution>> = nums
.par_iter()
.map(|&num| {
let mut cloned = *self; let mut local_solutions: Vec<Solution> = Vec::new();
let mut local_path = initial_path.clone();
cloned.place_number(r, c, num);
let step_number = local_path.steps.len() as u32;
local_path.steps.push(SolveStep::Placement {
row: r,
col: c,
value: num,
flags: TechniqueFlags::empty(),
step_number,
candidates_eliminated: 0,
related_cell_count: 0,
difficulty_point: 0,
});
cloned.solve_until_recursive(&mut local_solutions, &mut local_path, 0);
local_solutions
})
.collect();
let mut solutions = Vec::new();
for mut s in chunks {
solutions.append(&mut s);
}
solutions
} else {
vec![Solution {
board: self.board,
solve_path: path,
}]
}
}
pub fn is_solved(&self) -> bool {
self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
}
}
pub struct RustokuBuilder {
board: Option<Board>,
techniques: TechniqueFlags,
max_solutions: Option<usize>,
}
impl RustokuBuilder {
pub fn new() -> Self {
RustokuBuilder {
board: None,
techniques: TechniqueFlags::EASY,
max_solutions: None,
}
}
}
impl Default for RustokuBuilder {
fn default() -> Self {
Self::new()
}
}
impl RustokuBuilder {
pub fn board(mut self, board: Board) -> Self {
self.board = Some(board);
self
}
pub fn board_from_str(mut self, s: &str) -> Result<Self, RustokuError> {
let board = Board::try_from(s)?;
self.board = Some(board);
Ok(self)
}
pub fn techniques(mut self, techniques: TechniqueFlags) -> Self {
self.techniques = techniques;
self
}
pub fn max_solutions(mut self, max: usize) -> Self {
self.max_solutions = Some(max);
self
}
pub fn build(self) -> Result<Rustoku, RustokuError> {
let board = self.board.unwrap_or_default();
let mut r = Rustoku::new(board)?;
r.techniques = self.techniques;
Ok(r)
}
}
#[derive(Debug)]
pub struct Solutions {
solver: Rustoku,
path: SolvePath,
stack: Vec<Frame>,
finished: bool,
}
#[derive(Debug)]
struct Frame {
r: usize,
c: usize,
nums: Vec<u8>,
idx: usize,
placed: Option<u8>,
}
impl Solutions {
pub fn from_solver(mut solver: Rustoku) -> Self {
let mut path = SolvePath::default();
let mut finished = false;
if !solver.techniques_make_valid_changes(&mut path) {
finished = true;
}
let mut stack = Vec::new();
if !finished {
if let Some((r, c)) = solver.find_next_empty_cell() {
let mask = solver.candidates.get(r, c);
let mut nums = Rustoku::candidates_from_mask(mask);
nums.shuffle(&mut rng());
stack.push(Frame {
r,
c,
nums,
idx: 0,
placed: None,
});
} else {
}
}
Solutions {
solver,
path,
stack,
finished,
}
}
}
impl Iterator for Solutions {
type Item = Solution;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
loop {
if self.stack.is_empty() {
if let Some((r, c)) = self.solver.find_next_empty_cell() {
let mask = self.solver.candidates.get(r, c);
let mut nums = Rustoku::candidates_from_mask(mask);
nums.shuffle(&mut rng());
self.stack.push(Frame {
r,
c,
nums,
idx: 0,
placed: None,
});
continue;
} else {
let sol = Solution {
board: self.solver.board,
solve_path: self.path.clone(),
};
self.finished = true;
return Some(sol);
}
}
let last_idx = self.stack.len() - 1;
let frame = &mut self.stack[last_idx];
if frame.idx >= frame.nums.len() {
if let Some(num) = frame.placed {
self.solver.remove_number(frame.r, frame.c, num);
self.path.steps.pop();
frame.placed = None;
} else {
self.stack.pop();
}
continue;
}
let num = frame.nums[frame.idx];
frame.idx += 1;
if self.solver.masks.is_safe(frame.r, frame.c, num) {
self.solver.place_number(frame.r, frame.c, num);
let step_number = self.path.steps.len() as u32;
self.path.steps.push(SolveStep::Placement {
row: frame.r,
col: frame.c,
value: num,
flags: TechniqueFlags::empty(),
step_number,
candidates_eliminated: 0,
related_cell_count: 0,
difficulty_point: 0,
});
frame.placed = Some(num);
if let Some((nr, nc)) = self.solver.find_next_empty_cell() {
let mask = self.solver.candidates.get(nr, nc);
let mut nums2 = Rustoku::candidates_from_mask(mask);
nums2.shuffle(&mut rng());
self.stack.push(Frame {
r: nr,
c: nc,
nums: nums2,
idx: 0,
placed: None,
});
continue;
} else {
let solution = Solution {
board: self.solver.board,
solve_path: self.path.clone(),
};
if let Some(pnum) = frame.placed {
self.solver.remove_number(frame.r, frame.c, pnum);
self.path.steps.pop();
frame.placed = None;
}
return Some(solution);
}
}
}
}
}
pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
if !(17..=81).contains(&num_clues) {
return Err(RustokuError::InvalidClueCount);
}
let mut rustoku = Rustoku::new(Board::default())?;
let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
let mut board = solution.board;
let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
cells.shuffle(&mut rng());
let mut clues = 81;
for &(r, c) in &cells {
if clues <= num_clues {
break;
}
let original = board.cells[r][c];
board.cells[r][c] = 0;
if Rustoku::new(board)?.solve_until(2).len() != 1 {
board.cells[r][c] = original; } else {
clues -= 1;
}
}
if Rustoku::new(board)?.solve_until(2).len() != 1 {
return Err(RustokuError::GenerateFailure);
}
Ok(board)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::board::Board;
use crate::error::RustokuError;
use crate::format::format_line;
const UNIQUE_PUZZLE: &str =
"530070000600195000098000060800060003400803001700020006060000280000419005000080079";
const UNIQUE_SOLUTION: &str =
"534678912672195348198342567859761423426853791713924856961537284287419635345286179";
const TWO_PUZZLE: &str =
"295743861431865900876192543387459216612387495549216738763504189928671354154938600";
const SIX_PUZZLE: &str =
"295743001431865900876192543387459216612387495549216738763500000000000000000000000";
#[test]
fn test_builder_and_iterator() {
let board = Board::try_from(UNIQUE_PUZZLE).expect("valid puzzle");
let solver = Rustoku::builder()
.board(board)
.techniques(TechniqueFlags::all())
.build()
.expect("builder build");
let mut sols = Solutions::from_solver(solver);
let first = sols.next();
assert!(first.is_some());
assert!(sols.next().is_none());
}
#[test]
fn test_try_from_with_duplicate_initial_values() {
let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
let rustoku = Rustoku::new(board);
assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
}
#[test]
fn test_solve_any_with_solvable_sudoku() {
let s = UNIQUE_PUZZLE;
let mut rustoku =
Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
assert_eq!(
UNIQUE_SOLUTION,
format_line(&solution.board),
"Solution does not match the expected result"
);
}
#[test]
fn test_solve_any_with_unsolvable_sudoku() {
let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
let solution = rustoku.solve_any();
assert!(
solution.is_none(),
"Expected no solution for this unsolvable puzzle"
);
}
#[test]
fn test_solve_until_with_bound() {
let s = UNIQUE_PUZZLE;
let mut rustoku =
Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
let solutions = rustoku.solve_until(1);
assert_eq!(
1,
solutions.len(),
"Expected exactly one solution with bound = 1"
);
let all_solutions = rustoku.solve_until(0);
assert_eq!(
1,
all_solutions.len(),
"Expected exactly one solution for this board with bound = 0"
);
assert_eq!(
solutions[0].board, all_solutions[0].board,
"Solution with bound = 1 does not match the solution with bound = 0"
);
}
#[test]
fn test_solve_all_with_unique_puzzle() {
let s = UNIQUE_PUZZLE;
let mut rustoku =
Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
let solutions = rustoku.solve_all();
assert_eq!(
1,
solutions.len(),
"Expected a unique solution for the board"
);
}
#[test]
fn test_solve_all_with_two_puzzle() {
let s = TWO_PUZZLE;
let mut rustoku =
Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
let solutions = rustoku.solve_all();
assert_eq!(
2,
solutions.len(),
"Expected two solutions for the given board"
);
}
#[test]
fn test_solve_all_with_six_puzzle() {
let s = SIX_PUZZLE;
let mut rustoku =
Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
let solutions = rustoku.solve_all();
assert_eq!(
6,
solutions.len(),
"Expected one solution for the six puzzle"
);
}
#[test]
fn test_solve_any_with_all_techniques() {
let s = UNIQUE_PUZZLE;
let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
let solution = rustoku
.with_techniques(TechniqueFlags::all())
.solve_any()
.expect("Solving with all techniques failed");
assert_eq!(
UNIQUE_SOLUTION,
format_line(&solution.board),
"Solution does not match the expected result with all techniques"
);
}
#[test]
fn test_solve_all_with_all_techniques() {
let s = TWO_PUZZLE;
let rustoku = Rustoku::new_from_str(s)
.expect("Rustoku creation failed for multi-solution technique test");
let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
assert_eq!(
2,
solutions.len(),
"Expected two solutions for the given board with all techniques"
);
}
#[test]
fn test_generate_with_enough_clues() {
(20..=80).step_by(20).for_each(|num_clues| {
let board = generate_board(num_clues)
.unwrap_or_else(|_| panic!("Board generation failed for {num_clues} clues"));
let mut rustoku =
Rustoku::new(board).expect("Rustoku creation failed from generated board");
let clues_count = board
.cells
.iter()
.flatten()
.filter(|&&cell| cell != 0)
.count();
assert!(
clues_count >= num_clues,
"Expected at least {num_clues} clues, but found {clues_count} clues"
);
let solutions = rustoku.solve_all();
assert_eq!(
1,
solutions.len(),
"Generated puzzle with {num_clues} clues should have a unique solution"
);
})
}
#[test]
fn test_generate_with_too_few_clues() {
let num_clues = 16;
let result = generate_board(num_clues);
assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
}
#[test]
fn test_generate_with_too_many_clues() {
let num_clues = 82;
let result = generate_board(num_clues);
assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
}
#[test]
fn test_is_solved_with_valid_solution() {
let s = UNIQUE_SOLUTION;
let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
}
#[test]
fn test_is_solved_with_unsolved_board() {
let s = UNIQUE_PUZZLE;
let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
assert!(!rustoku.is_solved(), "The board should not be valid");
}
}