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 candidates::Candidates;
18pub use masks::Masks;
19pub use solution::{Solution, SolvePath, SolveStep};
20pub use techniques::flags::TechniqueFlags;
21
22use crate::error::RustokuError;
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    /// Bitmasks that check if a cell is safe in a row, column and box.
65    pub masks: Masks,
66    /// Candidate cache from computing the bitmasks.
67    pub candidates: Candidates,
68    /// Techniques used during the initial phase of solving.
69    pub techniques: TechniqueFlags,
70}
71
72impl Rustoku {
73    /// Constructs a new `Rustoku` instance from an initial `Board`.
74    pub fn new(initial_board: Board) -> Result<Self, RustokuError> {
75        let board = initial_board; // Now takes a Board directly
76        let mut masks = Masks::new();
77        let mut candidates = Candidates::new();
78
79        // Initialize masks and check for duplicates based on the provided board
80        for r in 0..9 {
81            for c in 0..9 {
82                let num = board.get(r, c);
83                if num != 0 {
84                    if !masks.is_safe(r, c, num) {
85                        return Err(RustokuError::DuplicateValues);
86                    }
87                    masks.add_number(r, c, num);
88                }
89            }
90        }
91
92        // Initialize the candidates cache for empty cells based on initial masks and board
93        for r in 0..9 {
94            for c in 0..9 {
95                if board.is_empty(r, c) {
96                    candidates.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
97                }
98            }
99        }
100
101        Ok(Self {
102            board,
103            masks,
104            candidates,
105            techniques: TechniqueFlags::EASY, // Default
106        })
107    }
108
109    /// Constructs a new `Rustoku` instance from a string representation of the board.
110    pub fn new_from_str(s: &str) -> Result<Self, RustokuError> {
111        let board = Board::try_from(s)?;
112        Self::new(board)
113    }
114
115    /// Returns the existing Rustoku instance, with modified techniques.
116    pub fn with_techniques(mut self, techniques: TechniqueFlags) -> Self {
117        self.techniques = techniques;
118        self
119    }
120
121    /// Helper for solver to find the next empty cell (MRV).
122    fn find_next_empty_cell(&self) -> Option<(usize, usize)> {
123        let mut min = (10, None); // Min candidates, (r, c)
124        for (r, c) in self.board.iter_empty_cells() {
125            let count = self.candidates.get(r, c).count_ones() as u8;
126            if count < min.0 {
127                min = (count, Some((r, c)));
128                if count == 1 {
129                    return min.1;
130                }
131            }
132        }
133        min.1
134    }
135
136    /// Place and remove operations for the solver, updated to use the new structs.
137    fn place_number(&mut self, r: usize, c: usize, num: u8) {
138        self.board.set(r, c, num);
139        self.masks.add_number(r, c, num);
140        self.candidates
141            .update_affected_cells(r, c, &self.masks, &self.board);
142    }
143
144    /// Remove a number from the board and update masks and candidates.
145    fn remove_number(&mut self, r: usize, c: usize, num: u8) {
146        self.board.set(r, c, 0); // Set back to empty
147        self.masks.remove_number(r, c, num);
148        self.candidates
149            .update_affected_cells(r, c, &self.masks, &self.board);
150        // Note: `update_affected_cells` will recalculate candidates for the removed cell.
151    }
152
153    /// Recursive function to solve the Sudoku puzzle with backtracking.
154    fn solve_until_recursive(
155        &mut self,
156        solutions: &mut Vec<Solution>,
157        path: &mut SolvePath,
158        bound: usize,
159    ) -> usize {
160        if let Some((r, c)) = self.find_next_empty_cell() {
161            let mut count = 0;
162            let mut nums: Vec<u8> = (1..=9).collect();
163            nums.shuffle(&mut rng());
164
165            for &num in &nums {
166                if self.masks.is_safe(r, c, num) {
167                    self.place_number(r, c, num);
168                    path.steps.push(SolveStep::Placement {
169                        row: r,
170                        col: c,
171                        value: num,
172                        flags: TechniqueFlags::empty(),
173                    });
174                    count += self.solve_until_recursive(solutions, path, bound);
175                    path.steps.pop();
176                    self.remove_number(r, c, num);
177
178                    if bound > 0 && solutions.len() >= bound {
179                        return count;
180                    }
181                }
182            }
183            count
184        } else {
185            solutions.push(Solution {
186                board: self.board,
187                solve_path: path.clone(),
188            });
189            1
190        }
191    }
192
193    /// Run techniques and check if they make valid changes.
194    fn techniques_make_valid_changes(&mut self, path: &mut SolvePath) -> bool {
195        let mut propagator = TechniquePropagator::new(
196            &mut self.board,
197            &mut self.masks,
198            &mut self.candidates,
199            self.techniques,
200        );
201        propagator.propagate_constraints(path, 0)
202    }
203
204    /// Solves the Sudoku puzzle up to a certain bound, returning solutions with their solve paths.
205    pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
206        let mut solutions = Vec::new();
207        let mut path = SolvePath::default();
208
209        if !self.techniques_make_valid_changes(&mut path) {
210            return solutions;
211        }
212
213        self.solve_until_recursive(&mut solutions, &mut path, bound);
214        solutions
215    }
216
217    /// Attempts to solve the Sudoku puzzle using backtracking with MRV (Minimum Remaining Values).
218    pub fn solve_any(&mut self) -> Option<Solution> {
219        self.solve_until(1).into_iter().next()
220    }
221
222    /// Finds all possible solutions for the Sudoku puzzle.
223    pub fn solve_all(&mut self) -> Vec<Solution> {
224        self.solve_until(0)
225    }
226
227    /// Checks if the Sudoku puzzle is solved correctly.
228    pub fn is_solved(&self) -> bool {
229        self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
230    }
231}
232
233/// Generates a new Sudoku puzzle with a unique solution.
234///
235/// The `num_clues` parameter specifies the desired number of initially
236/// filled cells (clues) in the generated puzzle. Fewer clues generally
237/// result in a harder puzzle. The actual number of clues may be slightly
238/// more than `num_clues` if it's impossible to remove more numbers
239/// while maintaining a unique solution.
240///
241/// # Example
242///
243/// Generate a puzzle with 30 clues:
244/// ```
245/// use rustoku_lib::generate_board;
246/// let puzzle = generate_board(30);
247/// assert!(puzzle.is_ok());
248/// ```
249pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
250    if !(17..=81).contains(&num_clues) {
251        return Err(RustokuError::InvalidClueCount);
252    }
253
254    // Start with a fully solved board
255    let mut rustoku = Rustoku::new(Board::default())?;
256    let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
257    let mut board = solution.board;
258
259    // Shuffle all cell coordinates
260    let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
261    cells.shuffle(&mut rng());
262
263    let mut clues = 81;
264
265    // Remove numbers while maintaining a unique solution
266    for &(r, c) in &cells {
267        if clues <= num_clues {
268            break;
269        }
270
271        let original = board.cells[r][c];
272        board.cells[r][c] = 0;
273
274        if Rustoku::new(board)?.solve_until(2).len() != 1 {
275            board.cells[r][c] = original; // Restore if not unique
276        } else {
277            clues -= 1;
278        }
279    }
280
281    // Check if the generated puzzle has a unique solution
282    if Rustoku::new(board)?.solve_until(2).len() != 1 {
283        // If not unique, return an error
284        return Err(RustokuError::GenerateFailure);
285    }
286
287    Ok(board)
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::core::board::Board;
294    use crate::error::RustokuError;
295    use crate::format::format_line;
296
297    const UNIQUE_PUZZLE: &str =
298        "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
299    const UNIQUE_SOLUTION: &str =
300        "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
301    const TWO_PUZZLE: &str =
302        "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
303    const SIX_PUZZLE: &str =
304        "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
305
306    #[test]
307    fn test_new_with_bytes_and_str() {
308        let board = [
309            [5, 3, 0, 0, 7, 0, 0, 0, 0],
310            [6, 0, 0, 1, 9, 5, 0, 0, 0],
311            [0, 9, 8, 0, 0, 0, 0, 6, 0],
312            [8, 0, 0, 0, 6, 0, 0, 0, 3],
313            [4, 0, 0, 8, 0, 3, 0, 0, 1],
314            [7, 0, 0, 0, 2, 0, 0, 0, 6],
315            [0, 6, 0, 0, 0, 0, 2, 8, 0],
316            [0, 0, 0, 4, 1, 9, 0, 0, 5],
317            [0, 0, 0, 0, 8, 0, 0, 7, 9],
318        ];
319
320        let flat_bytes: [u8; 81] = board
321            .concat()
322            .try_into()
323            .expect("Concat board to bytes failed");
324        let board_str: String = flat_bytes.iter().map(|&b| (b + b'0') as char).collect();
325
326        let board_from_new = Board::new(board);
327        let board_from_bytes = Board::try_from(flat_bytes).expect("Board from flat bytes failed");
328        let board_from_str = Board::try_from(board_str.as_str()).expect("Board from string failed");
329
330        assert_eq!(board_from_new, board_from_bytes);
331        assert_eq!(board_from_new, board_from_str);
332        assert_eq!(board_from_bytes, board_from_str);
333    }
334
335    #[test]
336    fn test_try_from_with_valid_input() {
337        let rustoku = Board::try_from(UNIQUE_PUZZLE);
338        assert!(rustoku.is_ok());
339    }
340
341    #[test]
342    fn test_try_from_with_invalid_length() {
343        let s = "530070000"; // Too short
344        let rustoku = Board::try_from(s);
345        assert!(matches!(rustoku, Err(RustokuError::InvalidInputLength)));
346    }
347
348    #[test]
349    fn test_try_from_with_invalid_character() {
350        let s = "53007000060019500009800006080006000340080300170002000606000028000041900500008007X"; // 'X'
351        let rustoku = Board::try_from(s);
352        assert!(matches!(rustoku, Err(RustokuError::InvalidInputCharacter)));
353    }
354
355    #[test]
356    fn test_try_from_with_duplicate_initial_values() {
357        let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
358        let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
359        let rustoku = Rustoku::new(board);
360        assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
361    }
362
363    #[test]
364    fn test_solve_any_with_solvable_sudoku() {
365        let s = UNIQUE_PUZZLE;
366        let mut rustoku =
367            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
368        let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
369
370        assert_eq!(
371            UNIQUE_SOLUTION,
372            format_line(&solution.board),
373            "Solution does not match the expected result"
374        );
375    }
376
377    #[test]
378    fn test_solve_any_with_unsolvable_sudoku() {
379        let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
380        let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
381        let solution = rustoku.solve_any();
382        assert!(
383            solution.is_none(),
384            "Expected no solution for this unsolvable puzzle"
385        );
386    }
387
388    #[test]
389    fn test_solve_until_with_bound() {
390        let s = UNIQUE_PUZZLE;
391        let mut rustoku =
392            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
393
394        let solutions = rustoku.solve_until(1);
395        assert_eq!(
396            1,
397            solutions.len(),
398            "Expected exactly one solution with bound = 1"
399        );
400
401        let all_solutions = rustoku.solve_until(0);
402        assert_eq!(
403            1,
404            all_solutions.len(),
405            "Expected exactly one solution for this board with bound = 0"
406        );
407
408        assert_eq!(
409            solutions[0].board, all_solutions[0].board,
410            "Solution with bound = 1 does not match the solution with bound = 0"
411        );
412    }
413
414    #[test]
415    fn test_solve_all_with_unique_puzzle() {
416        let s = UNIQUE_PUZZLE;
417        let mut rustoku =
418            Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
419        let solutions = rustoku.solve_all();
420        assert_eq!(
421            1,
422            solutions.len(),
423            "Expected a unique solution for the board"
424        );
425    }
426
427    #[test]
428    fn test_solve_all_with_two_puzzle() {
429        let s = TWO_PUZZLE;
430        let mut rustoku =
431            Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
432        let solutions = rustoku.solve_all();
433        assert_eq!(
434            2,
435            solutions.len(),
436            "Expected two solutions for the given board"
437        );
438    }
439
440    #[test]
441    fn test_solve_all_with_six_puzzle() {
442        let s = SIX_PUZZLE;
443        let mut rustoku =
444            Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
445        let solutions = rustoku.solve_all();
446        assert_eq!(
447            6,
448            solutions.len(),
449            "Expected one solution for the six puzzle"
450        );
451    }
452
453    #[test]
454    fn test_solve_any_with_all_techniques() {
455        let s = UNIQUE_PUZZLE;
456        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
457        let solution = rustoku
458            .with_techniques(TechniqueFlags::all())
459            .solve_any()
460            .expect("Solving with all techniques failed");
461
462        assert_eq!(
463            UNIQUE_SOLUTION,
464            format_line(&solution.board),
465            "Solution does not match the expected result with all techniques"
466        );
467    }
468
469    #[test]
470    fn test_solve_all_with_all_techniques() {
471        let s = TWO_PUZZLE;
472        let rustoku = Rustoku::new_from_str(s)
473            .expect("Rustoku creation failed for multi-solution technique test");
474        let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
475
476        assert_eq!(
477            2,
478            solutions.len(),
479            "Expected two solutions for the given board with all techniques"
480        );
481    }
482
483    #[test]
484    fn test_generate_with_enough_clues() {
485        (20..=80).step_by(20).for_each(|num_clues| {
486            let board = generate_board(num_clues)
487                .unwrap_or_else(|_| panic!("Board generation failed for {} clues", num_clues));
488            let mut rustoku =
489                Rustoku::new(board).expect("Rustoku creation failed from generated board");
490            let clues_count = board
491                .cells
492                .iter()
493                .flatten()
494                .filter(|&&cell| cell != 0)
495                .count();
496            assert!(
497                clues_count >= num_clues,
498                "Expected at least {} clues, but found {} clues",
499                num_clues,
500                clues_count
501            );
502
503            let solutions = rustoku.solve_all();
504            assert_eq!(
505                1,
506                solutions.len(),
507                "Generated puzzle with {} clues should have a unique solution",
508                num_clues
509            );
510        })
511    }
512
513    #[test]
514    fn test_generate_with_too_few_clues() {
515        let num_clues = 16;
516        let result = generate_board(num_clues);
517        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
518    }
519
520    #[test]
521    fn test_generate_with_too_many_clues() {
522        let num_clues = 82;
523        let result = generate_board(num_clues);
524        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
525    }
526
527    #[test]
528    fn test_is_solved_with_valid_solution() {
529        let s = UNIQUE_SOLUTION;
530        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
531        assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
532    }
533
534    #[test]
535    fn test_is_solved_with_unsolved_board() {
536        let s = UNIQUE_PUZZLE;
537        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
538        assert!(!rustoku.is_solved(), "The board should not be valid");
539    }
540
541    struct TechniqueTestCase<'a> {
542        name: &'a str,
543        trigger_string: &'a str,
544        technique_flag: TechniqueFlags,
545    }
546
547    #[test]
548    fn test_each_technique_makes_valid_changes() {
549        let test_cases = vec![
550            // Last digit is empty, no other option exists
551            TechniqueTestCase {
552                name: "Naked Singles",
553                trigger_string: "385421967194756328627983145571892634839645271246137589462579813918364752753218490",
554                technique_flag: TechniqueFlags::NAKED_SINGLES,
555            },
556            // https://hodoku.sourceforge.net/en/show_example.php?file=h101&tech=Hidden+Single
557            TechniqueTestCase {
558                name: "Hidden Singles",
559                trigger_string: "008007000016083000000000051107290000000000000000046307290000000000860140000300700",
560                technique_flag: TechniqueFlags::HIDDEN_SINGLES,
561            },
562            // https://hodoku.sourceforge.net/en/show_example.php?file=n201&tech=Naked+Pair
563            TechniqueTestCase {
564                name: "Naked Pairs",
565                trigger_string: "700009030000105006400260009002083951007000000005600000000000003100000060000004010",
566                technique_flag: TechniqueFlags::NAKED_PAIRS,
567            },
568            // https://hodoku.sourceforge.net/en/show_example.php?file=h201&tech=Hidden+Pair
569            TechniqueTestCase {
570                name: "Hidden Pairs",
571                trigger_string: "000032000000000000007600914096000800005008000030040005050200000700000560904010000",
572                technique_flag: TechniqueFlags::HIDDEN_PAIRS,
573            },
574            // https://hodoku.sourceforge.net/en/show_example.php?file=lc101&tech=Locked+Candidates+Type+1+%28Pointing%29
575            TechniqueTestCase {
576                name: "Locked Candidates",
577                trigger_string: "984000000000500040000000002006097200003002000000000010005060003407051890030009700",
578                technique_flag: TechniqueFlags::LOCKED_CANDIDATES,
579            },
580            // https://hodoku.sourceforge.net/en/show_example.php?file=bf201&tech=X-Wing
581            TechniqueTestCase {
582                name: "X-Wing",
583                trigger_string: "000000000760003002002640009403900070000004903005000020010560000370090041000000060",
584                technique_flag: TechniqueFlags::XWING,
585            },
586        ];
587
588        for test_case in test_cases {
589            let rustoku = Rustoku::new_from_str(test_case.trigger_string)
590                .expect(&format!("Rustoku creation failed for '{}'", test_case.name));
591            assert!(
592                rustoku
593                    .with_techniques(test_case.technique_flag)
594                    .techniques_make_valid_changes(&mut SolvePath::default()),
595                "The board should change for '{}'",
596                test_case.name
597            )
598        }
599    }
600}