Skip to main content

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::{Difficulty, TechniqueFlags};
21
22use crate::error::RustokuError;
23use rand::prelude::SliceRandom;
24use rand::rng;
25use std::collections::HashSet;
26use techniques::TechniquePropagator;
27
28/// Represents the type of symmetry to apply during board generation.
29#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
30pub enum Symmetry {
31    /// No symmetry (clues placed randomly).
32    #[default]
33    None,
34    /// 180-degree rotational (point) symmetry.
35    Rotational180,
36    /// 90-degree rotational symmetry.
37    Rotational90,
38    /// Mirror symmetry across the vertical center line.
39    MirrorVertical,
40    /// Mirror symmetry across the horizontal center line.
41    MirrorHorizontal,
42    /// Mirror symmetry across the main diagonal (top-left to bottom-right).
43    MirrorDiagonal,
44}
45
46impl Symmetry {
47    /// Returns the symmetric partners for a given cell (r, c).
48    pub fn get_partners(&self, r: usize, c: usize) -> Vec<(usize, usize)> {
49        let mut partners = HashSet::new();
50        partners.insert((r, c));
51
52        match self {
53            Symmetry::None => {}
54            Symmetry::Rotational180 => {
55                partners.insert((8 - r, 8 - c));
56            }
57            Symmetry::Rotational90 => {
58                partners.insert((c, 8 - r));
59                partners.insert((8 - r, 8 - c));
60                partners.insert((8 - c, r));
61            }
62            Symmetry::MirrorVertical => {
63                partners.insert((r, 8 - c));
64            }
65            Symmetry::MirrorHorizontal => {
66                partners.insert((8 - r, c));
67            }
68            Symmetry::MirrorDiagonal => {
69                partners.insert((c, r));
70            }
71        }
72
73        partners.into_iter().collect()
74    }
75}
76
77/// A builder for generating Sudoku puzzles with various constraints and properties.
78///
79/// `BoardGenerator` provides a unified interface for creating puzzles with specific
80/// clue counts, symmetry types, and difficulty levels.
81///
82/// # Example
83///
84/// ```
85/// use rustoku_lib::{BoardGenerator, Symmetry};
86///
87/// let board = BoardGenerator::new()
88///     .clues(25)
89///     .symmetry(Symmetry::Rotational180)
90///     .generate();
91///
92/// assert!(board.is_ok());
93/// ```
94#[derive(Debug, Clone, Copy)]
95pub struct BoardGenerator {
96    num_clues: usize,
97    symmetry: Symmetry,
98    difficulty: Option<Difficulty>,
99    max_attempts: usize,
100}
101
102impl Default for BoardGenerator {
103    fn default() -> Self {
104        Self {
105            num_clues: 30,
106            symmetry: Symmetry::None,
107            difficulty: None,
108            max_attempts: 1,
109        }
110    }
111}
112
113impl BoardGenerator {
114    /// Creates a new `BoardGenerator` with default settings (30 clues, no symmetry).
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Sets the target number of clues for the generated puzzle.
120    pub fn clues(mut self, num_clues: usize) -> Self {
121        self.num_clues = num_clues;
122        self
123    }
124
125    /// Sets the type of symmetry to apply to the generated puzzle.
126    pub fn symmetry(mut self, symmetry: Symmetry) -> Self {
127        self.symmetry = symmetry;
128        self
129    }
130
131    /// Sets the target difficulty for the generated puzzle.
132    ///
133    /// If a difficulty is set, the generator will attempt to find a puzzle
134    /// of that exact difficulty by repeatedly generating candidates.
135    pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
136        self.difficulty = Some(difficulty);
137        self
138    }
139
140    /// Sets the maximum number of attempts when generating a puzzle with a specific difficulty.
141    pub fn max_attempts(mut self, max_attempts: usize) -> Self {
142        self.max_attempts = max_attempts;
143        self
144    }
145
146    /// Generates a new Sudoku puzzle based on the current configuration.
147    pub fn generate(&self) -> Result<Board, RustokuError> {
148        if let Some(target_difficulty) = self.difficulty {
149            self.generate_with_difficulty(target_difficulty)
150        } else {
151            self.generate_single()
152        }
153    }
154
155    fn generate_single(&self) -> Result<Board, RustokuError> {
156        if !(17..=81).contains(&self.num_clues) {
157            return Err(RustokuError::InvalidClueCount);
158        }
159
160        // Start with a fully solved board
161        let mut rustoku = Rustoku::new(Board::default())?;
162        let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
163        let mut board = solution.board;
164
165        // Collect all unique symmetric groups
166        let mut visited = [[false; 9]; 9];
167        let mut groups = Vec::new();
168
169        // Iterate in a deterministic but shuffled order to ensure variety
170        let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
171        cells.shuffle(&mut rng());
172
173        for (r, c) in cells {
174            if !visited[r][c] {
175                let partners = self.symmetry.get_partners(r, c);
176                for &(pr, pc) in &partners {
177                    visited[pr][pc] = true;
178                }
179                groups.push(partners);
180            }
181        }
182
183        // Re-shuffle groups to ensure we don't always try to remove the same regions first
184        groups.shuffle(&mut rng());
185
186        let mut clues = 81;
187
188        // Remove numbers while maintaining a unique solution
189        for group in groups {
190            if clues <= self.num_clues {
191                break;
192            }
193
194            // Potential clues to remove (only those currently filled)
195            let mut group_clues = Vec::new();
196            for &(r, c) in &group {
197                let val = board.cells[r][c];
198                if val != 0 {
199                    group_clues.push((r, c, val));
200                }
201            }
202
203            if group_clues.is_empty() {
204                continue;
205            }
206
207            // Temporarily remove the entire group
208            for &(r, c, _) in &group_clues {
209                board.cells[r][c] = 0;
210            }
211
212            if Rustoku::new(board)?.solve_until(2).len() != 1 {
213                // Restore if not unique
214                for &(r, c, val) in &group_clues {
215                    board.cells[r][c] = val;
216                }
217            } else {
218                clues -= group_clues.len();
219            }
220        }
221
222        // Final safety check
223        if Rustoku::new(board)?.solve_until(2).len() != 1 {
224            return Err(RustokuError::GenerateFailure);
225        }
226
227        Ok(board)
228    }
229
230    fn generate_with_difficulty(
231        &self,
232        target_difficulty: Difficulty,
233    ) -> Result<Board, RustokuError> {
234        use rand::RngExt;
235
236        for _ in 0..self.max_attempts {
237            // Ignore num_clues if difficulty is set, and use a range appropriate for the level
238            let clues = match target_difficulty {
239                Difficulty::Easy => rand::rng().random_range(34..=42),
240                Difficulty::Medium => rand::rng().random_range(28..=34),
241                Difficulty::Hard => rand::rng().random_range(22..=28),
242                Difficulty::Expert => rand::rng().random_range(17..=22),
243            };
244
245            // Generate a uniquely solvable board with symmetry
246            let mut sub_generator = *self;
247            sub_generator.num_clues = clues;
248            sub_generator.difficulty = None; // Avoid recursion
249
250            if let Ok(board) = sub_generator.generate_single() {
251                let mut rustoku = Rustoku::builder()
252                    .board(board)
253                    .techniques(TechniqueFlags::all())
254                    .build()?;
255
256                // Check if human techniques can fully solve the board
257                let solutions = rustoku.solve_all();
258                if solutions.len() == 1 {
259                    let solution = &solutions[0];
260
261                    // Inspect the solve path to find the highest difficulty technique used
262                    let mut max_difficulty = Difficulty::Easy;
263                    let mut required_guessing = false;
264
265                    for step in &solution.solve_path.steps {
266                        match step {
267                            SolveStep::Placement { flags, .. } => {
268                                if flags.is_empty() {
269                                    required_guessing = true;
270                                    break;
271                                }
272                                let step_diff = flags.difficulty();
273                                if step_diff > max_difficulty {
274                                    max_difficulty = step_diff;
275                                }
276                            }
277                            SolveStep::CandidateElimination { flags, .. } => {
278                                if !flags.is_empty() {
279                                    let step_diff = flags.difficulty();
280                                    if step_diff > max_difficulty {
281                                        max_difficulty = step_diff;
282                                    }
283                                }
284                            }
285                        }
286                    }
287
288                    if !required_guessing && max_difficulty == target_difficulty {
289                        return Ok(board);
290                    }
291                }
292            }
293        }
294
295        Err(RustokuError::GenerateFailure)
296    }
297}
298
299/// Solver primitive that uses backtracking and bitmasking for constraints.
300///
301/// This struct supports the ability to:
302/// - Initialize from a 2D array, a flat byte array, or a string representation
303/// - Solve a Sudoku puzzle using backtracking with Minimum Remaining Values (MRV)
304/// - Generate a Sudoku puzzle with a unique solution based on the number of clues specified
305/// - Check if a Sudoku puzzle is solved correctly
306///
307/// # Examples
308///
309/// Solve a Sudoku puzzle:
310/// ```
311/// use rustoku_lib::Rustoku;
312/// let puzzle = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
313/// let mut rustoku = Rustoku::new_from_str(puzzle).unwrap();
314/// assert!(rustoku.solve_any().is_some());
315/// ```
316///
317/// Generate a Sudoku puzzle:
318/// ```
319/// use rustoku_lib::{Rustoku, generate_board};
320/// let board = generate_board(30).unwrap();
321/// let solution = Rustoku::new(board).unwrap().solve_all();
322/// assert_eq!(solution.len(), 1);
323/// ```
324///
325/// Check if a Sudoku puzzle is solved:
326/// ```
327/// use rustoku_lib::Rustoku;
328/// let puzzle = "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
329/// let rustoku = Rustoku::new_from_str(puzzle).unwrap();
330/// assert!(rustoku.is_solved());
331/// ```
332#[derive(Debug, Copy, Clone)]
333pub struct Rustoku {
334    /// The current state of the Sudoku board.
335    pub board: Board,
336    /// Bitmasks that check if a cell is safe in a row, column and box.
337    pub masks: Masks,
338    /// Candidate cache from computing the bitmasks.
339    pub candidates: Candidates,
340    /// Techniques used during the initial phase of solving.
341    pub techniques: TechniqueFlags,
342}
343
344impl Rustoku {
345    /// Constructs a new `Rustoku` instance from an initial `Board`.
346    pub fn new(initial_board: Board) -> Result<Self, RustokuError> {
347        let board = initial_board; // Now takes a Board directly
348        let mut masks = Masks::new();
349        let mut candidates = Candidates::new();
350
351        // Initialize masks and check for duplicates based on the provided board
352        for r in 0..9 {
353            for c in 0..9 {
354                let num = board.get(r, c);
355                if num != 0 {
356                    if !masks.is_safe(r, c, num) {
357                        return Err(RustokuError::DuplicateValues);
358                    }
359                    masks.add_number(r, c, num);
360                }
361            }
362        }
363
364        // Initialize the candidates cache for empty cells based on initial masks and board
365        for r in 0..9 {
366            for c in 0..9 {
367                if board.is_empty(r, c) {
368                    candidates.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
369                }
370            }
371        }
372
373        Ok(Self {
374            board,
375            masks,
376            candidates,
377            techniques: TechniqueFlags::EASY, // Default
378        })
379    }
380
381    /// Start building a configured `Rustoku` via a builder pattern.
382    pub fn builder() -> RustokuBuilder {
383        RustokuBuilder::new()
384    }
385
386    /// Constructs a new `Rustoku` instance from a string representation of the board.
387    pub fn new_from_str(s: &str) -> Result<Self, RustokuError> {
388        let board = Board::try_from(s)?;
389        Self::new(board)
390    }
391
392    /// Returns the existing Rustoku instance, with modified techniques.
393    pub fn with_techniques(mut self, techniques: TechniqueFlags) -> Self {
394        self.techniques = techniques;
395        self
396    }
397
398    pub(crate) fn candidate_grid_snapshot(&self) -> Vec<Vec<Vec<u8>>> {
399        (0..9)
400            .map(|r| {
401                (0..9)
402                    .map(|c| {
403                        if self.board.get(r, c) != 0 {
404                            vec![]
405                        } else {
406                            self.candidates.get_candidates(r, c)
407                        }
408                    })
409                    .collect()
410            })
411            .collect()
412    }
413
414    pub(crate) fn apply_trace_step(&mut self, step: &SolveStep) {
415        match *step {
416            SolveStep::Placement {
417                row, col, value, ..
418            } => {
419                self.place_number(row, col, value);
420            }
421            SolveStep::CandidateElimination {
422                row, col, value, ..
423            } => {
424                let initial_mask = self.candidates.get(row, col);
425                let refined_mask = initial_mask & !(1 << (value - 1));
426                self.candidates.set(row, col, refined_mask);
427            }
428        }
429    }
430
431    /// Extracts candidate numbers (1-9) from a bitmask into a Vec.
432    fn candidates_from_mask(mask: u16) -> Vec<u8> {
433        let mut nums = Vec::with_capacity(mask.count_ones() as usize);
434        for v in 1..=9u8 {
435            if mask & (1 << (v - 1)) != 0 {
436                nums.push(v);
437            }
438        }
439        nums
440    }
441
442    /// Helper for solver to find the next empty cell (MRV).
443    #[inline]
444    fn find_next_empty_cell(&self) -> Option<(usize, usize)> {
445        let mut min = (10, None); // Min candidates, (r, c)
446        for (r, c) in self.board.iter_empty_cells() {
447            let count = self.candidates.get(r, c).count_ones() as u8;
448            if count < min.0 {
449                min = (count, Some((r, c)));
450                if count == 1 {
451                    return min.1;
452                }
453            }
454        }
455        min.1
456    }
457
458    /// Place and remove operations for the solver, updated to use the new structs.
459    #[inline]
460    fn place_number(&mut self, r: usize, c: usize, num: u8) {
461        self.board.set(r, c, num);
462        self.masks.add_number(r, c, num);
463        self.candidates
464            .update_affected_cells_for(r, c, &self.masks, &self.board, Some(num));
465    }
466
467    /// Remove a number from the board and update masks and candidates.
468    #[inline]
469    fn remove_number(&mut self, r: usize, c: usize, num: u8) {
470        self.board.set(r, c, 0); // Set back to empty
471        self.masks.remove_number(r, c, num);
472        self.candidates
473            .update_affected_cells(r, c, &self.masks, &self.board);
474        // Note: `update_affected_cells` will recalculate candidates for the removed cell.
475    }
476
477    /// Recursive function to solve the Sudoku puzzle with backtracking.
478    fn solve_until_recursive(
479        &mut self,
480        solutions: &mut Vec<Solution>,
481        path: &mut SolvePath,
482        bound: usize,
483    ) -> usize {
484        // Early return for base case: no empty cells means puzzle is solved
485        let Some((r, c)) = self.find_next_empty_cell() else {
486            solutions.push(Solution {
487                board: self.board,
488                solve_path: path.clone(),
489            });
490            return 1;
491        };
492
493        let mut count = 0;
494        // Use the candidate cache to only iterate valid candidates
495        let mask = self.candidates.get(r, c);
496        let mut nums = Self::candidates_from_mask(mask);
497        nums.shuffle(&mut rng());
498
499        for &num in &nums {
500            if !self.masks.is_safe(r, c, num) {
501                continue;
502            }
503
504            self.place_number(r, c, num);
505            let step_number = path.steps.len() as u32;
506            path.steps.push(SolveStep::Placement {
507                row: r,
508                col: c,
509                value: num,
510                flags: TechniqueFlags::empty(),
511                step_number,
512                candidates_eliminated: 0,
513                related_cell_count: 0,
514                difficulty_point: 0,
515            });
516
517            count += self.solve_until_recursive(solutions, path, bound);
518            path.steps.pop();
519            self.remove_number(r, c, num);
520
521            // Early return if we've found enough solutions
522            if bound > 0 && solutions.len() >= bound {
523                return count;
524            }
525        }
526
527        count
528    }
529
530    /// Run techniques and check if they make valid changes.
531    fn techniques_make_valid_changes(&mut self, path: &mut SolvePath) -> bool {
532        let mut propagator = TechniquePropagator::new(
533            &mut self.board,
534            &mut self.masks,
535            &mut self.candidates,
536            self.techniques,
537        );
538        propagator.propagate_constraints(path, 0)
539    }
540
541    /// Solves the Sudoku puzzle up to a certain bound, returning solutions with their solve paths.
542    pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
543        let mut solutions = Vec::new();
544        let mut path = SolvePath::default();
545
546        if !self.techniques_make_valid_changes(&mut path) {
547            return solutions;
548        }
549
550        self.solve_until_recursive(&mut solutions, &mut path, bound);
551        solutions
552    }
553
554    /// Attempts to solve the Sudoku puzzle using backtracking with MRV (Minimum Remaining Values).
555    pub fn solve_any(&mut self) -> Option<Solution> {
556        self.solve_until(1).into_iter().next()
557    }
558
559    /// Finds all possible solutions for the Sudoku puzzle.
560    pub fn solve_all(&mut self) -> Vec<Solution> {
561        use rayon::prelude::*;
562
563        // Run technique propagation once on the current solver state.
564        let mut path = SolvePath::default();
565        if !self.techniques_make_valid_changes(&mut path) {
566            return Vec::new();
567        }
568
569        // If there is at least one empty cell, split work by the first MRV cell's candidates.
570        if let Some((r, c)) = self.find_next_empty_cell() {
571            let mask = self.candidates.get(r, c);
572            let nums = Self::candidates_from_mask(mask);
573
574            let initial_path = path.clone();
575
576            // Parallelize each top-level candidate branch.
577            let chunks: Vec<Vec<Solution>> = nums
578                .par_iter()
579                .map(|&num| {
580                    let mut cloned = *self; // Rustoku is Copy/Clone
581                    let mut local_solutions: Vec<Solution> = Vec::new();
582                    let mut local_path = initial_path.clone();
583
584                    // Place the candidate and record the placement in the path.
585                    cloned.place_number(r, c, num);
586                    let step_number = local_path.steps.len() as u32;
587                    local_path.steps.push(SolveStep::Placement {
588                        row: r,
589                        col: c,
590                        value: num,
591                        flags: TechniqueFlags::empty(),
592                        step_number,
593                        candidates_eliminated: 0,
594                        related_cell_count: 0,
595                        difficulty_point: 0,
596                    });
597
598                    // Continue DFS from this state without re-running the propagator.
599                    cloned.solve_until_recursive(&mut local_solutions, &mut local_path, 0);
600                    local_solutions
601                })
602                .collect();
603
604            // Flatten results
605            let mut solutions = Vec::new();
606            for mut s in chunks {
607                solutions.append(&mut s);
608            }
609            solutions
610        } else {
611            // Already solved after propagation
612            vec![Solution {
613                board: self.board,
614                solve_path: path,
615            }]
616        }
617    }
618
619    /// Checks if the Sudoku puzzle is solved correctly.
620    pub fn is_solved(&self) -> bool {
621        self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
622    }
623}
624
625/// A simple builder for constructing `Rustoku` with fluent configuration.
626pub struct RustokuBuilder {
627    board: Option<Board>,
628    techniques: TechniqueFlags,
629    max_solutions: Option<usize>,
630}
631
632impl RustokuBuilder {
633    /// Create a new builder with reasonable defaults.
634    pub fn new() -> Self {
635        RustokuBuilder {
636            board: None,
637            techniques: TechniqueFlags::EASY,
638            max_solutions: None,
639        }
640    }
641}
642
643impl Default for RustokuBuilder {
644    fn default() -> Self {
645        Self::new()
646    }
647}
648
649impl RustokuBuilder {
650    /// Provide the initial `Board` for the solver.
651    pub fn board(mut self, board: Board) -> Self {
652        self.board = Some(board);
653        self
654    }
655
656    /// Provide the initial board as a string (convenience).
657    pub fn board_from_str(mut self, s: &str) -> Result<Self, RustokuError> {
658        let board = Board::try_from(s)?;
659        self.board = Some(board);
660        Ok(self)
661    }
662
663    /// Configure which techniques the solver should use.
664    pub fn techniques(mut self, techniques: TechniqueFlags) -> Self {
665        self.techniques = techniques;
666        self
667    }
668
669    /// Optionally hint the builder with a maximum number of solutions.
670    pub fn max_solutions(mut self, max: usize) -> Self {
671        self.max_solutions = Some(max);
672        self
673    }
674
675    /// Finalize the builder and construct the `Rustoku` instance.
676    pub fn build(self) -> Result<Rustoku, RustokuError> {
677        let board = self.board.unwrap_or_default();
678        let mut r = Rustoku::new(board)?;
679        r.techniques = self.techniques;
680        // If the user provided a max_solutions hint, we store it in techniques as not applicable
681        // for now; the builder primarily configures creation state.
682        Ok(r)
683    }
684}
685
686/// Lazy iterator wrapper for solutions. Uses an explicit DFS stack and yields
687/// solutions one-by-one without computing them all up-front.
688#[derive(Debug)]
689pub struct Solutions {
690    solver: Rustoku,
691    path: SolvePath,
692    stack: Vec<Frame>,
693    finished: bool,
694}
695
696#[derive(Debug)]
697struct Frame {
698    r: usize,
699    c: usize,
700    nums: Vec<u8>,
701    idx: usize,
702    placed: Option<u8>,
703}
704
705impl Solutions {
706    /// Construct a `Solutions` iterator from an existing `Rustoku` solver.
707    /// This will run the technique propagator once before starting DFS.
708    pub fn from_solver(mut solver: Rustoku) -> Self {
709        let mut path = SolvePath::default();
710        let mut finished = false;
711
712        if !solver.techniques_make_valid_changes(&mut path) {
713            finished = true;
714        }
715
716        let mut stack = Vec::new();
717        if !finished {
718            if let Some((r, c)) = solver.find_next_empty_cell() {
719                let mask = solver.candidates.get(r, c);
720                let mut nums = Rustoku::candidates_from_mask(mask);
721                nums.shuffle(&mut rng());
722                stack.push(Frame {
723                    r,
724                    c,
725                    nums,
726                    idx: 0,
727                    placed: None,
728                });
729            } else {
730                // Already solved; leave stack empty and let next() yield the board once
731            }
732        }
733
734        Solutions {
735            solver,
736            path,
737            stack,
738            finished,
739        }
740    }
741}
742
743impl Iterator for Solutions {
744    type Item = Solution;
745
746    fn next(&mut self) -> Option<Self::Item> {
747        if self.finished {
748            return None;
749        }
750
751        loop {
752            // If stack is empty, check if there are any empty cells left
753            if self.stack.is_empty() {
754                if let Some((r, c)) = self.solver.find_next_empty_cell() {
755                    let mask = self.solver.candidates.get(r, c);
756                    let mut nums = Rustoku::candidates_from_mask(mask);
757                    nums.shuffle(&mut rng());
758                    self.stack.push(Frame {
759                        r,
760                        c,
761                        nums,
762                        idx: 0,
763                        placed: None,
764                    });
765                    continue;
766                } else {
767                    // No empty cells -> current board is a solution
768                    let sol = Solution {
769                        board: self.solver.board,
770                        solve_path: self.path.clone(),
771                    };
772                    self.finished = true;
773                    return Some(sol);
774                }
775            }
776
777            let last_idx = self.stack.len() - 1;
778            let frame = &mut self.stack[last_idx];
779
780            // If we've exhausted candidates for this frame
781            if frame.idx >= frame.nums.len() {
782                if let Some(num) = frame.placed {
783                    // remove the previously placed number
784                    self.solver.remove_number(frame.r, frame.c, num);
785                    self.path.steps.pop();
786                    frame.placed = None;
787                } else {
788                    // No placement was made for this frame; pop it and continue
789                    self.stack.pop();
790                }
791                continue;
792            }
793
794            let num = frame.nums[frame.idx];
795            frame.idx += 1;
796
797            if self.solver.masks.is_safe(frame.r, frame.c, num) {
798                // place and record
799                self.solver.place_number(frame.r, frame.c, num);
800                let step_number = self.path.steps.len() as u32;
801                self.path.steps.push(SolveStep::Placement {
802                    row: frame.r,
803                    col: frame.c,
804                    value: num,
805                    flags: TechniqueFlags::empty(),
806                    step_number,
807                    candidates_eliminated: 0,
808                    related_cell_count: 0,
809                    difficulty_point: 0,
810                });
811                frame.placed = Some(num);
812
813                // Find next empty cell after this placement
814                if let Some((nr, nc)) = self.solver.find_next_empty_cell() {
815                    let mask = self.solver.candidates.get(nr, nc);
816                    let mut nums2 = Rustoku::candidates_from_mask(mask);
817                    nums2.shuffle(&mut rng());
818                    self.stack.push(Frame {
819                        r: nr,
820                        c: nc,
821                        nums: nums2,
822                        idx: 0,
823                        placed: None,
824                    });
825                    continue;
826                } else {
827                    // Found a solution. Capture it, then backtrack one placement so iteration can continue.
828                    let solution = Solution {
829                        board: self.solver.board,
830                        solve_path: self.path.clone(),
831                    };
832                    // Backtrack the placement we just made on this frame
833                    if let Some(pnum) = frame.placed {
834                        self.solver.remove_number(frame.r, frame.c, pnum);
835                        self.path.steps.pop();
836                        frame.placed = None;
837                    }
838                    return Some(solution);
839                }
840            }
841            // else try next candidate
842        }
843    }
844}
845
846/// Generates a new Sudoku puzzle with a unique solution and specified symmetry.
847///
848/// The `num_clues` parameter specifies the desired number of initially
849/// filled cells (clues) in the generated puzzle. Fewer clues generally
850/// result in a harder puzzle. The actual number of clues may be slightly
851/// more than `num_clues` if it's impossible to remove more numbers
852/// while maintaining a unique solution.
853///
854/// # Example
855///
856/// ```
857/// use rustoku_lib::generate_board;
858/// let puzzle = generate_board(30);
859/// assert!(puzzle.is_ok());
860/// ```
861/// Generates a new Sudoku puzzle with a unique solution.
862///
863/// This is a convenience shim for `BoardGenerator::new().clues(num_clues).generate()`.
864pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
865    BoardGenerator::new().clues(num_clues).generate()
866}
867
868/// Generates a new Sudoku puzzle that matches a specific difficulty level.
869///
870/// This is a convenience shim for `BoardGenerator::new().difficulty(difficulty).max_attempts(max_attempts).generate()`.
871pub fn generate_board_by_difficulty(
872    difficulty: Difficulty,
873    max_attempts: usize,
874) -> Result<Board, RustokuError> {
875    BoardGenerator::new()
876        .difficulty(difficulty)
877        .max_attempts(max_attempts)
878        .generate()
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use crate::core::board::Board;
885    use crate::core::techniques::flags::Difficulty;
886    use crate::error::RustokuError;
887    use crate::format::format_line;
888
889    const UNIQUE_PUZZLE: &str =
890        "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
891    const UNIQUE_SOLUTION: &str =
892        "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
893    const TWO_PUZZLE: &str =
894        "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
895    const SIX_PUZZLE: &str =
896        "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
897
898    #[test]
899    fn test_builder_and_iterator() {
900        let board = Board::try_from(UNIQUE_PUZZLE).expect("valid puzzle");
901        let solver = Rustoku::builder()
902            .board(board)
903            .techniques(TechniqueFlags::all())
904            .build()
905            .expect("builder build");
906
907        // Using the iterator wrapper (eager compute, lazy yield)
908        let mut sols = Solutions::from_solver(solver);
909        let first = sols.next();
910        assert!(first.is_some());
911        // For unique puzzle, there should be exactly one solution
912        assert!(sols.next().is_none());
913    }
914
915    #[test]
916    fn test_try_from_with_duplicate_initial_values() {
917        let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
918        let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
919        let rustoku = Rustoku::new(board);
920        assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
921    }
922
923    #[test]
924    fn test_solve_any_with_solvable_sudoku() {
925        let s = UNIQUE_PUZZLE;
926        let mut rustoku =
927            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
928        let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
929
930        assert_eq!(
931            UNIQUE_SOLUTION,
932            format_line(&solution.board),
933            "Solution does not match the expected result"
934        );
935    }
936
937    #[test]
938    fn test_solve_any_with_unsolvable_sudoku() {
939        let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
940        let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
941        let solution = rustoku.solve_any();
942        assert!(
943            solution.is_none(),
944            "Expected no solution for this unsolvable puzzle"
945        );
946    }
947
948    #[test]
949    fn test_solve_until_with_bound() {
950        let s = UNIQUE_PUZZLE;
951        let mut rustoku =
952            Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
953
954        let solutions = rustoku.solve_until(1);
955        assert_eq!(
956            1,
957            solutions.len(),
958            "Expected exactly one solution with bound = 1"
959        );
960
961        let all_solutions = rustoku.solve_until(0);
962        assert_eq!(
963            1,
964            all_solutions.len(),
965            "Expected exactly one solution for this board with bound = 0"
966        );
967
968        assert_eq!(
969            solutions[0].board, all_solutions[0].board,
970            "Solution with bound = 1 does not match the solution with bound = 0"
971        );
972    }
973
974    #[test]
975    fn test_solve_all_with_unique_puzzle() {
976        let s = UNIQUE_PUZZLE;
977        let mut rustoku =
978            Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
979        let solutions = rustoku.solve_all();
980        assert_eq!(
981            1,
982            solutions.len(),
983            "Expected a unique solution for the board"
984        );
985    }
986
987    #[test]
988    fn test_solve_all_with_two_puzzle() {
989        let s = TWO_PUZZLE;
990        let mut rustoku =
991            Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
992        let solutions = rustoku.solve_all();
993        assert_eq!(
994            2,
995            solutions.len(),
996            "Expected two solutions for the given board"
997        );
998    }
999
1000    #[test]
1001    fn test_solve_all_with_six_puzzle() {
1002        let s = SIX_PUZZLE;
1003        let mut rustoku =
1004            Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
1005        let solutions = rustoku.solve_all();
1006        assert_eq!(
1007            6,
1008            solutions.len(),
1009            "Expected one solution for the six puzzle"
1010        );
1011    }
1012
1013    #[test]
1014    fn test_solve_any_with_all_techniques() {
1015        let s = UNIQUE_PUZZLE;
1016        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
1017        let solution = rustoku
1018            .with_techniques(TechniqueFlags::all())
1019            .solve_any()
1020            .expect("Solving with all techniques failed");
1021
1022        assert_eq!(
1023            UNIQUE_SOLUTION,
1024            format_line(&solution.board),
1025            "Solution does not match the expected result with all techniques"
1026        );
1027    }
1028
1029    #[test]
1030    fn test_solve_all_with_all_techniques() {
1031        let s = TWO_PUZZLE;
1032        let rustoku = Rustoku::new_from_str(s)
1033            .expect("Rustoku creation failed for multi-solution technique test");
1034        let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
1035
1036        assert_eq!(
1037            2,
1038            solutions.len(),
1039            "Expected two solutions for the given board with all techniques"
1040        );
1041    }
1042
1043    #[test]
1044    fn test_generate_with_enough_clues() {
1045        (20..=80).step_by(20).for_each(|num_clues| {
1046            let board = generate_board(num_clues)
1047                .expect("Board generation failed - check clue count is between 17 and 81");
1048            let mut rustoku =
1049                Rustoku::new(board).expect("Rustoku creation failed from generated board");
1050            let clues_count = board
1051                .cells
1052                .iter()
1053                .flatten()
1054                .filter(|&&cell| cell != 0)
1055                .count();
1056            assert!(
1057                clues_count >= num_clues,
1058                "Expected at least {num_clues} clues, but found {clues_count} clues"
1059            );
1060
1061            let solutions = rustoku.solve_all();
1062            assert_eq!(
1063                1,
1064                solutions.len(),
1065                "Generated puzzle with {num_clues} clues should have a unique solution"
1066            );
1067        })
1068    }
1069
1070    #[test]
1071    fn test_generate_with_too_few_clues() {
1072        let num_clues = 16;
1073        let result = generate_board(num_clues);
1074        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
1075    }
1076
1077    #[test]
1078    fn test_generate_with_too_many_clues() {
1079        let num_clues = 82;
1080        let result = generate_board(num_clues);
1081        assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
1082    }
1083
1084    #[test]
1085    fn test_is_solved_with_valid_solution() {
1086        let s = UNIQUE_SOLUTION;
1087        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
1088        assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
1089    }
1090
1091    #[test]
1092    fn test_is_solved_with_unsolved_board() {
1093        let s = UNIQUE_PUZZLE;
1094        let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
1095        assert!(!rustoku.is_solved(), "The board should not be valid");
1096    }
1097
1098    #[test]
1099    fn test_generate_by_difficulty_easy() {
1100        let board = generate_board_by_difficulty(Difficulty::Easy, 100)
1101            .expect("Failed to generate an Easy puzzle within 100 attempts");
1102
1103        let mut rustoku = Rustoku::builder()
1104            .board(board)
1105            .techniques(TechniqueFlags::all())
1106            .build()
1107            .unwrap();
1108
1109        let solutions = rustoku.solve_all();
1110        assert_eq!(solutions.len(), 1);
1111
1112        let mut required_guessing = false;
1113        let mut max_difficulty = Difficulty::Easy;
1114
1115        for step in &solutions[0].solve_path.steps {
1116            match step {
1117                crate::core::solution::SolveStep::Placement { flags, .. } => {
1118                    if flags.is_empty() {
1119                        required_guessing = true;
1120                    }
1121                    if flags.difficulty() > max_difficulty {
1122                        max_difficulty = flags.difficulty();
1123                    }
1124                }
1125                crate::core::solution::SolveStep::CandidateElimination { flags, .. } => {
1126                    if flags.difficulty() > max_difficulty {
1127                        max_difficulty = flags.difficulty();
1128                    }
1129                }
1130            }
1131        }
1132
1133        assert!(
1134            !required_guessing,
1135            "Easy puzzle should not require guessing"
1136        );
1137        assert_eq!(
1138            max_difficulty,
1139            Difficulty::Easy,
1140            "Puzzle exceeded target difficulty"
1141        );
1142    }
1143
1144    #[test]
1145    fn test_generate_by_difficulty_hard() {
1146        let board = generate_board_by_difficulty(Difficulty::Hard, 1000)
1147            .expect("Failed to generate a Hard puzzle within 1000 attempts");
1148
1149        let mut rustoku = Rustoku::builder()
1150            .board(board)
1151            .techniques(TechniqueFlags::all())
1152            .build()
1153            .unwrap();
1154
1155        let solutions = rustoku.solve_all();
1156        assert_eq!(solutions.len(), 1);
1157
1158        let mut required_guessing = false;
1159        let mut max_difficulty = Difficulty::Easy;
1160
1161        for step in &solutions[0].solve_path.steps {
1162            match step {
1163                crate::core::solution::SolveStep::Placement { flags, .. } => {
1164                    if flags.is_empty() {
1165                        required_guessing = true;
1166                    }
1167                    if flags.difficulty() > max_difficulty {
1168                        max_difficulty = flags.difficulty();
1169                    }
1170                }
1171                crate::core::solution::SolveStep::CandidateElimination { flags, .. } => {
1172                    if flags.difficulty() > max_difficulty {
1173                        max_difficulty = flags.difficulty();
1174                    }
1175                }
1176            }
1177        }
1178
1179        assert!(
1180            !required_guessing,
1181            "Hard puzzle should not require guessing"
1182        );
1183        assert_eq!(
1184            max_difficulty,
1185            Difficulty::Hard,
1186            "Puzzle exceeded target difficulty"
1187        );
1188    }
1189}