rustoku_lib/core/
mod.rs

1//! Core module for the Rustoku solver and generator.
2//!
3//! This module includes the `Rustoku` struct for representing and solving Sudoku puzzles using
4//! backtracking and Minimum Remaining Values (MRV). It provides functionality for solving
5//! puzzles and checking solutions.
6//!
7//! This module also includes a function to generate new Sudoku puzzles with a specified
8//! number of clues, ensuring that the generated puzzle has a unique solution.
9
10mod board;
11mod candidates;
12mod masks;
13mod solution;
14mod techniques;
15
16pub use board::Board;
17pub use solution::{Solution, SolvePath, SolveStep};
18pub use techniques::flags::TechniqueFlags;
19
20use crate::error::RustokuError;
21use candidates::Candidates;
22use masks::Masks;
23use rand::prelude::SliceRandom;
24use rand::rng;
25use techniques::TechniquePropagator;
26
27/// Solver primitive that uses backtracking and bitmasking for constraints.
28///
29/// This struct supports the ability to:
30/// - Initialize from a 2D array, a flat byte array, or a string representation
31/// - Solve a Sudoku puzzle using backtracking with Minimum Remaining Values (MRV)
32/// - Generate a Sudoku puzzle with a unique solution based on the number of clues specified
33/// - Check if a Sudoku puzzle is solved correctly
34///
35/// # Examples
36///
37/// Solve a Sudoku puzzle:
38/// ```
39/// use rustoku_lib::Rustoku;
40/// let puzzle = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
41/// let mut rustoku = Rustoku::new_from_str(puzzle).unwrap();
42/// assert!(rustoku.solve_any().is_some());
43/// ```
44///
45/// Generate a Sudoku puzzle:
46/// ```
47/// use rustoku_lib::{Rustoku, generate_board};
48/// let board = generate_board(30).unwrap();
49/// let solution = Rustoku::new(board).unwrap().solve_all();
50/// assert_eq!(solution.len(), 1);
51/// ```
52///
53/// Check if a Sudoku puzzle is solved:
54/// ```
55/// use rustoku_lib::Rustoku;
56/// let puzzle = "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
57/// let rustoku = Rustoku::new_from_str(puzzle).unwrap();
58/// assert!(rustoku.is_solved());
59/// ```
60#[derive(Debug, Copy, Clone)]
61pub struct Rustoku {
62    /// The current state of the Sudoku board.
63    pub board: Board,
64    masks: Masks,
65    candidates_cache: Candidates,
66    techniques: TechniqueFlags,
67}
68
69impl Rustoku {
70    /// Constructs a new `Rustoku` instance from an initial `Board`.
71    pub fn new(initial_board: Board) -> Result<Self, RustokuError> {
72        let board = initial_board; // Now takes a Board directly
73        let mut masks = Masks::new();
74        let mut candidates_cache = Candidates::new();
75
76        // Initialize masks and check for duplicates based on the provided board
77        for r in 0..9 {
78            for c in 0..9 {
79                let num = board.get(r, c);
80                if num != 0 {
81                    if !masks.is_safe(r, c, num) {
82                        return Err(RustokuError::DuplicateValues);
83                    }
84                    masks.add_number(r, c, num);
85                }
86            }
87        }
88
89        // Initialize the candidates cache for empty cells based on initial masks and board
90        for r in 0..9 {
91            for c in 0..9 {
92                if board.is_empty(r, c) {
93                    candidates_cache.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
94                }
95            }
96        }
97
98        Ok(Self {
99            board,
100            masks,
101            candidates_cache,
102            techniques: TechniqueFlags::EASY, // Default
103        })
104    }
105
106    /// Constructs a new `Rustoku` instance from a string representation of the board.
107    pub fn new_from_str(s: &str) -> Result<Self, RustokuError> {
108        let board = Board::try_from(s)?;
109        Self::new(board)
110    }
111
112    /// Returns the existing Rustoku instance, with modified techniques.
113    pub fn with_techniques(mut self, techniques: TechniqueFlags) -> Self {
114        self.techniques = techniques;
115        self
116    }
117
118    /// Helper for solver to find the next empty cell (MRV).
119    fn find_next_empty_cell(&self) -> Option<(usize, usize)> {
120        let mut min = (10, None); // Min candidates, (r, c)
121        for (r, c) in self.board.iter_empty_cells() {
122            let count = self.candidates_cache.get(r, c).count_ones() as u8;
123            if count < min.0 {
124                min = (count, Some((r, c)));
125                if count == 1 {
126                    return min.1;
127                }
128            }
129        }
130        min.1
131    }
132
133    /// Place and remove operations for the solver, updated to use the new structs.
134    fn place_number(&mut self, r: usize, c: usize, num: u8) {
135        self.board.set(r, c, num);
136        self.masks.add_number(r, c, num);
137        self.candidates_cache
138            .update_affected_cells(r, c, &self.masks, &self.board);
139    }
140
141    /// Remove a number from the board and update masks and candidates.
142    fn remove_number(&mut self, r: usize, c: usize, num: u8) {
143        self.board.set(r, c, 0); // Set back to empty
144        self.masks.remove_number(r, c, num);
145        self.candidates_cache
146            .update_affected_cells(r, c, &self.masks, &self.board);
147        // Note: `update_affected_cells` will recalculate candidates for the removed cell.
148    }
149
150    /// Recursive function to solve the Sudoku puzzle with backtracking.
151    fn solve_until_recursive(
152        &mut self,
153        solutions: &mut Vec<Solution>,
154        path: &mut SolvePath,
155        bound: usize,
156    ) -> usize {
157        if let Some((r, c)) = self.find_next_empty_cell() {
158            let mut count = 0;
159            let mut nums: Vec<u8> = (1..=9).collect();
160            nums.shuffle(&mut rng());
161
162            for &num in &nums {
163                if self.masks.is_safe(r, c, num) {
164                    self.place_number(r, c, num);
165                    path.steps.push(SolveStep::Placement {
166                        row: r,
167                        col: c,
168                        value: num,
169                        flags: TechniqueFlags::empty(),
170                    });
171                    count += self.solve_until_recursive(solutions, path, bound);
172                    path.steps.pop();
173                    self.remove_number(r, c, num);
174
175                    if bound > 0 && solutions.len() >= bound {
176                        return count;
177                    }
178                }
179            }
180            count
181        } else {
182            solutions.push(Solution {
183                board: self.board,
184                solve_path: path.clone(),
185            });
186            1
187        }
188    }
189
190    /// Solves the Sudoku puzzle up to a certain bound, returning solutions with their solve paths.
191    pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
192        let mut solutions = Vec::new();
193        let mut path = SolvePath::default();
194
195        let mut propagator = TechniquePropagator::new(
196            &mut self.board,
197            &mut self.masks,
198            &mut self.candidates_cache,
199            self.techniques,
200        );
201
202        if !propagator.propagate_constraints(&mut path, 0) {
203            return solutions; // Early exit if initial constraints are inconsistent
204        }
205
206        self.solve_until_recursive(&mut solutions, &mut path, bound);
207        solutions
208    }
209
210    /// Attempts to solve the Sudoku puzzle using backtracking with MRV (Minimum Remaining Values).
211    pub fn solve_any(&mut self) -> Option<Solution> {
212        self.solve_until(1).into_iter().next()
213    }
214
215    /// Finds all possible solutions for the Sudoku puzzle.
216    pub fn solve_all(&mut self) -> Vec<Solution> {
217        self.solve_until(0)
218    }
219
220    /// Checks if the Sudoku puzzle is solved correctly.
221    pub fn is_solved(&self) -> bool {
222        self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
223    }
224}
225
226/// Generates a new Sudoku puzzle with a unique solution.
227///
228/// The `num_clues` parameter specifies the desired number of initially
229/// filled cells (clues) in the generated puzzle. Fewer clues generally
230/// result in a harder puzzle. The actual number of clues may be slightly
231/// more than `num_clues` if it's impossible to remove more numbers
232/// while maintaining a unique solution.
233///
234/// # Example
235///
236/// Generate a puzzle with 30 clues:
237/// ```
238/// use rustoku_lib::generate_board;
239/// let puzzle = generate_board(30).unwrap();
240/// assert_eq!(puzzle.cells.len(), 9);
241/// assert_eq!(puzzle.cells[0].len(), 9);
242/// ```
243pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
244    if !(17..=81).contains(&num_clues) {
245        return Err(RustokuError::InvalidClueCount);
246    }
247
248    // Start with a fully solved board
249    let mut rustoku = Rustoku::new(Board::empty())?;
250    let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
251    let mut board = solution.board;
252
253    // Shuffle all cell coordinates
254    let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
255    cells.shuffle(&mut rng());
256
257    let mut clues = 81;
258
259    // Remove numbers while maintaining a unique solution
260    for &(r, c) in &cells {
261        if clues <= num_clues {
262            break;
263        }
264
265        let original = board.cells[r][c];
266        board.cells[r][c] = 0;
267
268        if Rustoku::new(board)?.solve_until(2).len() != 1 {
269            board.cells[r][c] = original; // Restore if not unique
270        } else {
271            clues -= 1;
272        }
273    }
274
275    // Check if the generated puzzle has a unique solution
276    if Rustoku::new(board)?.solve_until(2).len() != 1 {
277        // If not unique, return an error
278        return Err(RustokuError::GenerateFailure);
279    }
280
281    Ok(board)
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::core::board::Board;
288    use crate::error::RustokuError;
289    use crate::format::format_line;
290
291    const UNIQUE_PUZZLE: &str =
292        "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
293    const UNIQUE_SOLUTION: &str =
294        "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
295    const TWO_PUZZLE: &str =
296        "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
297    const SIX_PUZZLE: &str =
298        "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
299
300    #[test]
301    fn test_new_with_bytes_and_str() {
302        let board = [
303            [5, 3, 0, 0, 7, 0, 0, 0, 0],
304            [6, 0, 0, 1, 9, 5, 0, 0, 0],
305            [0, 9, 8, 0, 0, 0, 0, 6, 0],
306            [8, 0, 0, 0, 6, 0, 0, 0, 3],
307            [4, 0, 0, 8, 0, 3, 0, 0, 1],
308            [7, 0, 0, 0, 2, 0, 0, 0, 6],
309            [0, 6, 0, 0, 0, 0, 2, 8, 0],
310            [0, 0, 0, 4, 1, 9, 0, 0, 5],
311            [0, 0, 0, 0, 8, 0, 0, 7, 9],
312        ];
313
314        let flat_bytes: [u8; 81] = board
315            .concat()
316            .try_into()
317            .expect("Concat board to bytes failed");
318        let board_str: String = flat_bytes.iter().map(|&b| (b + b'0') as char).collect();
319
320        let board_from_new = Board::new(board);
321        let board_from_bytes = Board::try_from(flat_bytes).expect("Board from flat bytes failed");
322        let board_from_str = Board::try_from(board_str.as_str()).expect("Board from string failed");
323
324        assert_eq!(board_from_new, board_from_bytes);
325        assert_eq!(board_from_new, board_from_str);
326        assert_eq!(board_from_bytes, board_from_str);
327    }
328
329    #[test]
330    fn test_try_from_with_valid_input() {
331        let rustoku = Board::try_from(UNIQUE_PUZZLE);
332        assert!(rustoku.is_ok());
333    }
334
335    #[test]
336    fn test_try_from_with_invalid_length() {
337        let s = "530070000"; // Too short
338        let rustoku = Board::try_from(s);
339        assert!(matches!(rustoku, Err(RustokuError::InvalidInputLength)));
340    }
341
342    #[test]
343    fn test_try_from_with_invalid_character() {
344        let s = "53007000060019500009800006080006000340080300170002000606000028000041900500008007X"; // 'X'
345        let rustoku = Board::try_from(s);
346        assert!(matches!(rustoku, Err(RustokuError::InvalidInputCharacter)));
347    }
348
349    #[test]
350    fn test_try_from_with_duplicate_initial_values() {
351        let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
352        let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
353        let rustoku = Rustoku::new(board);
354        assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
355    }
356
357    #[test]
358    fn test_solve_any_with_solvable_sudoku() {
359        let s = UNIQUE_PUZZLE;
360        let mut rustoku =
361            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
362        let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
363
364        assert_eq!(
365            UNIQUE_SOLUTION,
366            format_line(&solution.board.cells),
367            "Solution does not match the expected result"
368        );
369    }
370
371    #[test]
372    fn test_solve_any_with_unsolvable_sudoku() {
373        let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
374        let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
375        let solution = rustoku.solve_any();
376        assert!(
377            solution.is_none(),
378            "Expected no solution for this unsolvable puzzle"
379        );
380    }
381
382    #[test]
383    fn test_solve_until_with_bound() {
384        let s = UNIQUE_PUZZLE;
385        let mut rustoku =
386            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
387
388        let solutions = rustoku.solve_until(1);
389        assert_eq!(
390            1,
391            solutions.len(),
392            "Expected exactly one solution with bound = 1"
393        );
394
395        let all_solutions = rustoku.solve_until(0);
396        assert_eq!(
397            1,
398            all_solutions.len(),
399            "Expected exactly one solution for this board with bound = 0"
400        );
401
402        assert_eq!(
403            solutions[0].board, all_solutions[0].board,
404            "Solution with bound = 1 does not match the solution with bound = 0"
405        );
406    }
407
408    #[test]
409    fn test_solve_all_with_unique_puzzle() {
410        let s = UNIQUE_PUZZLE;
411        let mut rustoku =
412            Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
413        let solutions = rustoku.solve_all();
414        assert_eq!(
415            1,
416            solutions.len(),
417            "Expected a unique solution for the board"
418        );
419    }
420
421    #[test]
422    fn test_solve_all_with_two_puzzle() {
423        let s = TWO_PUZZLE;
424        let mut rustoku =
425            Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
426        let solutions = rustoku.solve_all();
427        assert_eq!(
428            2,
429            solutions.len(),
430            "Expected two solutions for the given board"
431        );
432    }
433
434    #[test]
435    fn test_solve_all_with_six_puzzle() {
436        let s = SIX_PUZZLE;
437        let mut rustoku =
438            Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
439        let solutions = rustoku.solve_all();
440        assert_eq!(
441            6,
442            solutions.len(),
443            "Expected one solution for the six puzzle"
444        );
445    }
446
447    #[test]
448    fn test_solve_any_with_all_techniques() {
449        let s = UNIQUE_PUZZLE;
450        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
451        let solution = rustoku
452            .with_techniques(TechniqueFlags::all())
453            .solve_any()
454            .expect("Solving with all techniques failed");
455
456        assert_eq!(
457            UNIQUE_SOLUTION,
458            format_line(&solution.board.cells),
459            "Solution does not match the expected result with all techniques"
460        );
461    }
462
463    #[test]
464    fn test_solve_all_with_all_techniques() {
465        let s = TWO_PUZZLE;
466        let rustoku = Rustoku::new_from_str(s)
467            .expect("Rustoku creation failed for multi-solution technique test");
468        let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
469
470        assert_eq!(
471            2,
472            solutions.len(),
473            "Expected two solutions for the given board with all techniques"
474        );
475    }
476
477    #[test]
478    fn test_generate_with_enough_clues() {
479        (20..=80).step_by(20).for_each(|num_clues| {
480            let board = generate_board(num_clues)
481                .unwrap_or_else(|_| panic!("Board generation failed for {} clues", num_clues));
482            let mut rustoku =
483                Rustoku::new(board).expect("Rustoku creation failed from generated board");
484            let clues_count = board
485                .cells
486                .iter()
487                .flatten()
488                .filter(|&&cell| cell != 0)
489                .count();
490            assert!(
491                clues_count >= num_clues,
492                "Expected at least {} clues, but found {} clues",
493                num_clues,
494                clues_count
495            );
496
497            let solutions = rustoku.solve_all();
498            assert_eq!(
499                1,
500                solutions.len(),
501                "Generated puzzle with {} clues should have a unique solution",
502                num_clues
503            );
504        })
505    }
506
507    #[test]
508    fn test_generate_with_too_few_clues() {
509        let num_clues = 16;
510        let result = generate_board(num_clues);
511        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
512    }
513
514    #[test]
515    fn test_generate_with_too_many_clues() {
516        let num_clues = 82;
517        let result = generate_board(num_clues);
518        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
519    }
520
521    #[test]
522    fn test_is_solved_with_valid_solution() {
523        let s = UNIQUE_SOLUTION;
524        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
525        assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
526    }
527
528    #[test]
529    fn test_is_solved_with_unsolved_board() {
530        let s = UNIQUE_PUZZLE;
531        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
532        assert!(!rustoku.is_solved(), "The board should not be valid");
533    }
534}