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: 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 = 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.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
94                }
95            }
96        }
97
98        Ok(Self {
99            board,
100            masks,
101            candidates,
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.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
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
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    /// Run techniques and check if they make valid changes.
191    fn techniques_make_valid_changes(&mut self, path: &mut SolvePath) -> bool {
192        let mut propagator = TechniquePropagator::new(
193            &mut self.board,
194            &mut self.masks,
195            &mut self.candidates,
196            self.techniques,
197        );
198        propagator.propagate_constraints(path, 0)
199    }
200
201    /// Solves the Sudoku puzzle up to a certain bound, returning solutions with their solve paths.
202    pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
203        let mut solutions = Vec::new();
204        let mut path = SolvePath::default();
205
206        if !self.techniques_make_valid_changes(&mut path) {
207            return solutions;
208        }
209
210        self.solve_until_recursive(&mut solutions, &mut path, bound);
211        solutions
212    }
213
214    /// Attempts to solve the Sudoku puzzle using backtracking with MRV (Minimum Remaining Values).
215    pub fn solve_any(&mut self) -> Option<Solution> {
216        self.solve_until(1).into_iter().next()
217    }
218
219    /// Finds all possible solutions for the Sudoku puzzle.
220    pub fn solve_all(&mut self) -> Vec<Solution> {
221        self.solve_until(0)
222    }
223
224    /// Checks if the Sudoku puzzle is solved correctly.
225    pub fn is_solved(&self) -> bool {
226        self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
227    }
228}
229
230/// Generates a new Sudoku puzzle with a unique solution.
231///
232/// The `num_clues` parameter specifies the desired number of initially
233/// filled cells (clues) in the generated puzzle. Fewer clues generally
234/// result in a harder puzzle. The actual number of clues may be slightly
235/// more than `num_clues` if it's impossible to remove more numbers
236/// while maintaining a unique solution.
237///
238/// # Example
239///
240/// Generate a puzzle with 30 clues:
241/// ```
242/// use rustoku_lib::generate_board;
243/// let puzzle = generate_board(30).unwrap();
244/// assert_eq!(puzzle.cells.len(), 9);
245/// assert_eq!(puzzle.cells[0].len(), 9);
246/// ```
247pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
248    if !(17..=81).contains(&num_clues) {
249        return Err(RustokuError::InvalidClueCount);
250    }
251
252    // Start with a fully solved board
253    let mut rustoku = Rustoku::new(Board::default())?;
254    let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
255    let mut board = solution.board;
256
257    // Shuffle all cell coordinates
258    let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
259    cells.shuffle(&mut rng());
260
261    let mut clues = 81;
262
263    // Remove numbers while maintaining a unique solution
264    for &(r, c) in &cells {
265        if clues <= num_clues {
266            break;
267        }
268
269        let original = board.cells[r][c];
270        board.cells[r][c] = 0;
271
272        if Rustoku::new(board)?.solve_until(2).len() != 1 {
273            board.cells[r][c] = original; // Restore if not unique
274        } else {
275            clues -= 1;
276        }
277    }
278
279    // Check if the generated puzzle has a unique solution
280    if Rustoku::new(board)?.solve_until(2).len() != 1 {
281        // If not unique, return an error
282        return Err(RustokuError::GenerateFailure);
283    }
284
285    Ok(board)
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::core::board::Board;
292    use crate::error::RustokuError;
293    use crate::format::format_line;
294
295    const UNIQUE_PUZZLE: &str =
296        "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
297    const UNIQUE_SOLUTION: &str =
298        "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
299    const TWO_PUZZLE: &str =
300        "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
301    const SIX_PUZZLE: &str =
302        "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
303
304    #[test]
305    fn test_new_with_bytes_and_str() {
306        let board = [
307            [5, 3, 0, 0, 7, 0, 0, 0, 0],
308            [6, 0, 0, 1, 9, 5, 0, 0, 0],
309            [0, 9, 8, 0, 0, 0, 0, 6, 0],
310            [8, 0, 0, 0, 6, 0, 0, 0, 3],
311            [4, 0, 0, 8, 0, 3, 0, 0, 1],
312            [7, 0, 0, 0, 2, 0, 0, 0, 6],
313            [0, 6, 0, 0, 0, 0, 2, 8, 0],
314            [0, 0, 0, 4, 1, 9, 0, 0, 5],
315            [0, 0, 0, 0, 8, 0, 0, 7, 9],
316        ];
317
318        let flat_bytes: [u8; 81] = board
319            .concat()
320            .try_into()
321            .expect("Concat board to bytes failed");
322        let board_str: String = flat_bytes.iter().map(|&b| (b + b'0') as char).collect();
323
324        let board_from_new = Board::new(board);
325        let board_from_bytes = Board::try_from(flat_bytes).expect("Board from flat bytes failed");
326        let board_from_str = Board::try_from(board_str.as_str()).expect("Board from string failed");
327
328        assert_eq!(board_from_new, board_from_bytes);
329        assert_eq!(board_from_new, board_from_str);
330        assert_eq!(board_from_bytes, board_from_str);
331    }
332
333    #[test]
334    fn test_try_from_with_valid_input() {
335        let rustoku = Board::try_from(UNIQUE_PUZZLE);
336        assert!(rustoku.is_ok());
337    }
338
339    #[test]
340    fn test_try_from_with_invalid_length() {
341        let s = "530070000"; // Too short
342        let rustoku = Board::try_from(s);
343        assert!(matches!(rustoku, Err(RustokuError::InvalidInputLength)));
344    }
345
346    #[test]
347    fn test_try_from_with_invalid_character() {
348        let s = "53007000060019500009800006080006000340080300170002000606000028000041900500008007X"; // 'X'
349        let rustoku = Board::try_from(s);
350        assert!(matches!(rustoku, Err(RustokuError::InvalidInputCharacter)));
351    }
352
353    #[test]
354    fn test_try_from_with_duplicate_initial_values() {
355        let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
356        let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
357        let rustoku = Rustoku::new(board);
358        assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
359    }
360
361    #[test]
362    fn test_solve_any_with_solvable_sudoku() {
363        let s = UNIQUE_PUZZLE;
364        let mut rustoku =
365            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
366        let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
367
368        assert_eq!(
369            UNIQUE_SOLUTION,
370            format_line(&solution.board),
371            "Solution does not match the expected result"
372        );
373    }
374
375    #[test]
376    fn test_solve_any_with_unsolvable_sudoku() {
377        let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
378        let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
379        let solution = rustoku.solve_any();
380        assert!(
381            solution.is_none(),
382            "Expected no solution for this unsolvable puzzle"
383        );
384    }
385
386    #[test]
387    fn test_solve_until_with_bound() {
388        let s = UNIQUE_PUZZLE;
389        let mut rustoku =
390            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
391
392        let solutions = rustoku.solve_until(1);
393        assert_eq!(
394            1,
395            solutions.len(),
396            "Expected exactly one solution with bound = 1"
397        );
398
399        let all_solutions = rustoku.solve_until(0);
400        assert_eq!(
401            1,
402            all_solutions.len(),
403            "Expected exactly one solution for this board with bound = 0"
404        );
405
406        assert_eq!(
407            solutions[0].board, all_solutions[0].board,
408            "Solution with bound = 1 does not match the solution with bound = 0"
409        );
410    }
411
412    #[test]
413    fn test_solve_all_with_unique_puzzle() {
414        let s = UNIQUE_PUZZLE;
415        let mut rustoku =
416            Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
417        let solutions = rustoku.solve_all();
418        assert_eq!(
419            1,
420            solutions.len(),
421            "Expected a unique solution for the board"
422        );
423    }
424
425    #[test]
426    fn test_solve_all_with_two_puzzle() {
427        let s = TWO_PUZZLE;
428        let mut rustoku =
429            Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
430        let solutions = rustoku.solve_all();
431        assert_eq!(
432            2,
433            solutions.len(),
434            "Expected two solutions for the given board"
435        );
436    }
437
438    #[test]
439    fn test_solve_all_with_six_puzzle() {
440        let s = SIX_PUZZLE;
441        let mut rustoku =
442            Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
443        let solutions = rustoku.solve_all();
444        assert_eq!(
445            6,
446            solutions.len(),
447            "Expected one solution for the six puzzle"
448        );
449    }
450
451    #[test]
452    fn test_solve_any_with_all_techniques() {
453        let s = UNIQUE_PUZZLE;
454        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
455        let solution = rustoku
456            .with_techniques(TechniqueFlags::all())
457            .solve_any()
458            .expect("Solving with all techniques failed");
459
460        assert_eq!(
461            UNIQUE_SOLUTION,
462            format_line(&solution.board),
463            "Solution does not match the expected result with all techniques"
464        );
465    }
466
467    #[test]
468    fn test_solve_all_with_all_techniques() {
469        let s = TWO_PUZZLE;
470        let rustoku = Rustoku::new_from_str(s)
471            .expect("Rustoku creation failed for multi-solution technique test");
472        let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
473
474        assert_eq!(
475            2,
476            solutions.len(),
477            "Expected two solutions for the given board with all techniques"
478        );
479    }
480
481    #[test]
482    fn test_generate_with_enough_clues() {
483        (20..=80).step_by(20).for_each(|num_clues| {
484            let board = generate_board(num_clues)
485                .unwrap_or_else(|_| panic!("Board generation failed for {} clues", num_clues));
486            let mut rustoku =
487                Rustoku::new(board).expect("Rustoku creation failed from generated board");
488            let clues_count = board
489                .cells
490                .iter()
491                .flatten()
492                .filter(|&&cell| cell != 0)
493                .count();
494            assert!(
495                clues_count >= num_clues,
496                "Expected at least {} clues, but found {} clues",
497                num_clues,
498                clues_count
499            );
500
501            let solutions = rustoku.solve_all();
502            assert_eq!(
503                1,
504                solutions.len(),
505                "Generated puzzle with {} clues should have a unique solution",
506                num_clues
507            );
508        })
509    }
510
511    #[test]
512    fn test_generate_with_too_few_clues() {
513        let num_clues = 16;
514        let result = generate_board(num_clues);
515        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
516    }
517
518    #[test]
519    fn test_generate_with_too_many_clues() {
520        let num_clues = 82;
521        let result = generate_board(num_clues);
522        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
523    }
524
525    #[test]
526    fn test_is_solved_with_valid_solution() {
527        let s = UNIQUE_SOLUTION;
528        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
529        assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
530    }
531
532    #[test]
533    fn test_is_solved_with_unsolved_board() {
534        let s = UNIQUE_PUZZLE;
535        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
536        assert!(!rustoku.is_solved(), "The board should not be valid");
537    }
538
539    struct TechniqueTestCase<'a> {
540        name: &'a str,
541        trigger_string: &'a str,
542        technique_flag: TechniqueFlags,
543    }
544
545    #[test]
546    fn test_each_technique_makes_valid_changes() {
547        let test_cases = vec![
548            // Last digit is empty, no other option exists
549            TechniqueTestCase {
550                name: "Naked Singles",
551                trigger_string: "385421967194756328627983145571892634839645271246137589462579813918364752753218490",
552                technique_flag: TechniqueFlags::NAKED_SINGLES,
553            },
554            // https://hodoku.sourceforge.net/en/show_example.php?file=h101&tech=Hidden+Single
555            TechniqueTestCase {
556                name: "Hidden Singles",
557                trigger_string: "008007000016083000000000051107290000000000000000046307290000000000860140000300700",
558                technique_flag: TechniqueFlags::HIDDEN_SINGLES,
559            },
560            // https://hodoku.sourceforge.net/en/show_example.php?file=n201&tech=Naked+Pair
561            TechniqueTestCase {
562                name: "Naked Pairs",
563                trigger_string: "700009030000105006400260009002083951007000000005600000000000003100000060000004010",
564                technique_flag: TechniqueFlags::NAKED_PAIRS,
565            },
566            // https://hodoku.sourceforge.net/en/show_example.php?file=h201&tech=Hidden+Pair
567            TechniqueTestCase {
568                name: "Hidden Pairs",
569                trigger_string: "000032000000000000007600914096000800005008000030040005050200000700000560904010000",
570                technique_flag: TechniqueFlags::HIDDEN_PAIRS,
571            },
572            // https://hodoku.sourceforge.net/en/show_example.php?file=lc101&tech=Locked+Candidates+Type+1+%28Pointing%29
573            TechniqueTestCase {
574                name: "Locked Candidates",
575                trigger_string: "984000000000500040000000002006097200003002000000000010005060003407051890030009700",
576                technique_flag: TechniqueFlags::LOCKED_CANDIDATES,
577            },
578            // https://hodoku.sourceforge.net/en/show_example.php?file=bf201&tech=X-Wing
579            TechniqueTestCase {
580                name: "X-Wing",
581                trigger_string: "000000000760003002002640009403900070000004903005000020010560000370090041000000060",
582                technique_flag: TechniqueFlags::XWING,
583            },
584        ];
585
586        for test_case in test_cases {
587            let rustoku = Rustoku::new_from_str(test_case.trigger_string)
588                .expect(&format!("Rustoku creation failed for '{}'", test_case.name));
589            assert!(
590                rustoku
591                    .with_techniques(test_case.technique_flag)
592                    .techniques_make_valid_changes(&mut SolvePath::default()),
593                "The board should change for '{}'",
594                test_case.name
595            )
596        }
597    }
598}