|
1 | 1 | use crate::{EMPTY, Sudoku}; |
2 | 2 | use rand::prelude::ThreadRng; |
3 | | -use rand::seq::SliceRandom; |
| 3 | +use rand::{Rng, seq::SliceRandom}; |
| 4 | +use std::fmt::{Display, Formatter}; |
4 | 5 |
|
5 | | -impl Sudoku { |
6 | | - fn count_solutions(sudoku: &mut Sudoku, count: &mut usize, max_count: usize) -> bool { |
7 | | - if *count >= max_count { |
8 | | - return true; // Early return if we already found enough solutions |
| 6 | +#[derive(clap::ValueEnum, Debug, Clone, Copy)] |
| 7 | +pub enum FillAlgorithm { |
| 8 | + DiagonalThinOut, |
| 9 | + Incremental, |
| 10 | +} |
| 11 | + |
| 12 | +impl Display for FillAlgorithm { |
| 13 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| 14 | + match self { |
| 15 | + FillAlgorithm::DiagonalThinOut => write!(f, "diagonal_thin_out"), |
| 16 | + FillAlgorithm::Incremental => write!(f, "incremental"), |
9 | 17 | } |
10 | | - let mut found_empty = false; |
11 | | - let mut empty_row = 0; |
12 | | - let mut empty_col = 0; |
13 | | - 'find_empty: for row in 0..9 { |
14 | | - for col in 0..9 { |
15 | | - if sudoku.board[row][col] == EMPTY { |
16 | | - empty_row = row; |
17 | | - empty_col = col; |
18 | | - found_empty = true; |
19 | | - break 'find_empty; |
| 18 | + } |
| 19 | +} |
| 20 | + |
| 21 | +pub struct SudokuGenerator { |
| 22 | + fill_algorithm: FillAlgorithm, |
| 23 | + max_filled_cells: usize, |
| 24 | + solutions_iter: std::vec::IntoIter<[[u8; 9]; 9]>, |
| 25 | + rng: ThreadRng, |
| 26 | +} |
| 27 | + |
| 28 | +/// A generator for Sudoku puzzles. |
| 29 | +/// This struct implements the Iterator trait, allowing it to be used in a loop |
| 30 | +/// to generate Sudoku puzzles with a specified number of filled cells. |
| 31 | +/// The `max_filled_cells` parameter specifies how many cells should remain filled |
| 32 | +/// at most. The minimum number of cells to fill is 17 (God's Number). |
| 33 | +impl SudokuGenerator { |
| 34 | + pub fn new(fill_algorithm: FillAlgorithm, max_filled_cells: usize) -> Self { |
| 35 | + let mut rng = rand::rng(); |
| 36 | + let solutions = match fill_algorithm { |
| 37 | + FillAlgorithm::DiagonalThinOut => { |
| 38 | + let mut all_digits: Vec<u8> = (1..=9).collect(); |
| 39 | + let mut sudoku = Sudoku::new(); |
| 40 | + // Fill the 3 diagonal boxes (top-left, middle, bottom-right) |
| 41 | + for box_idx in 0..3 { |
| 42 | + let start_row = box_idx * 3; |
| 43 | + let start_col = box_idx * 3; |
| 44 | + all_digits.shuffle(&mut rng); |
| 45 | + for (i, &num) in all_digits.iter().enumerate() { |
| 46 | + sudoku.board[start_row + i / 3][start_col + i % 3] = num; |
| 47 | + } |
20 | 48 | } |
| 49 | + sudoku.all_solutions() |
21 | 50 | } |
22 | | - } |
23 | | - // If no empty cell is found, we have a solution |
24 | | - if !found_empty { |
25 | | - *count += 1; |
26 | | - return *count >= max_count; |
27 | | - } |
28 | | - // Try each possible value |
29 | | - for num in 1..=9 { |
30 | | - if !sudoku.can_place(empty_row, empty_col, num) { |
31 | | - continue; |
32 | | - } |
33 | | - // Place and recurse |
34 | | - sudoku.board[empty_row][empty_col] = num; |
35 | | - if Self::count_solutions(sudoku, count, max_count) { |
36 | | - return true; |
| 51 | + FillAlgorithm::Incremental => { |
| 52 | + if let Some(solution) = Self::generate_incrementally(max_filled_cells) { |
| 53 | + vec![solution.board] |
| 54 | + } else { |
| 55 | + Vec::new() |
| 56 | + } |
37 | 57 | } |
38 | | - // Backtrack |
39 | | - sudoku.board[empty_row][empty_col] = EMPTY; |
| 58 | + }; |
| 59 | + SudokuGenerator { |
| 60 | + fill_algorithm, |
| 61 | + max_filled_cells, |
| 62 | + solutions_iter: solutions.into_iter(), |
| 63 | + rng, |
40 | 64 | } |
41 | | - false |
42 | 65 | } |
| 66 | +} |
43 | 67 |
|
44 | | - /// Generate a Sudoku puzzle by filling cells incrementally. |
45 | | - /// This method fills cells one by one, ensuring that the |
46 | | - /// puzzle has a unique solution. |
47 | | - /// The `filled_cells` parameter specifies how many cells should remain filled. |
48 | | - /// The minimum number of cells to fill is 17. |
49 | | - /// If the puzzle cannot be generated with the specified number of filled cells, |
50 | | - /// it returns `None`. |
51 | | - pub fn generate_incrementally(filled_cells: usize) -> Option<Self> { |
52 | | - let min_cells_to_fill = 17; |
53 | | - let mut rng = rand::rng(); |
54 | | - let mut all_digits: Vec<u8> = (1..=9).collect(); |
55 | | - let mut sudoku = Sudoku::new(); |
56 | | - let mut filled = 0; |
57 | | - let mut positions: Vec<(usize, usize)> = (0..9) |
58 | | - .flat_map(|row| (0..9).map(move |col| (row, col))) |
59 | | - .collect(); |
60 | | - positions.shuffle(&mut rng); |
| 68 | +impl Iterator for SudokuGenerator { |
| 69 | + type Item = Sudoku; |
61 | 70 |
|
62 | | - // Fill cells one by one |
63 | | - while filled < filled_cells.max(min_cells_to_fill) && !positions.is_empty() { |
64 | | - let (row, col) = positions.pop().unwrap(); |
65 | | - if sudoku.board[row][col] != EMPTY { |
66 | | - continue; |
| 71 | + fn next(&mut self) -> Option<Self::Item> { |
| 72 | + match self.fill_algorithm { |
| 73 | + FillAlgorithm::DiagonalThinOut => { |
| 74 | + while let Some(board) = self.solutions_iter.next() { |
| 75 | + let sudoku = Sudoku::from_board(board); |
| 76 | + // Try to reduce this solution |
| 77 | + if let Some(reduced) = self.try_reduce_puzzle(sudoku) { |
| 78 | + return Some(reduced); |
| 79 | + } |
| 80 | + } |
67 | 81 | } |
68 | | - // Shuffle the digits for each attempt |
69 | | - all_digits.shuffle(&mut rng); |
70 | | - // Try placing each digit |
71 | | - for &digit in &all_digits { |
72 | | - if sudoku.can_place(row, col, digit) { |
73 | | - sudoku.board[row][col] = digit; |
74 | | - filled += 1; |
75 | | - break; |
| 82 | + FillAlgorithm::Incremental => { |
| 83 | + if let Some(sudoku) = Self::generate_incrementally(self.max_filled_cells) { |
| 84 | + return Some(sudoku); |
76 | 85 | } |
77 | 86 | } |
78 | 87 | } |
79 | | - if filled == filled_cells.max(min_cells_to_fill) { |
80 | | - sudoku.original_board = sudoku.board; |
81 | | - let mut solution_count = 0; |
82 | | - Self::count_solutions(&mut sudoku, &mut solution_count, 2); |
83 | | - if solution_count != 1 { |
84 | | - return None; |
| 88 | + None |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +impl SudokuGenerator { |
| 93 | + fn try_reduce_puzzle(&mut self, mut sudoku: Sudoku) -> Option<Sudoku> { |
| 94 | + let mut available_cells: Vec<(usize, usize)> = (0..9) |
| 95 | + .flat_map(|row| (0..9).map(move |col| (row, col))) |
| 96 | + .collect(); |
| 97 | + available_cells.shuffle(&mut self.rng); |
| 98 | + let mut filled_cells = 81; |
| 99 | + while let Some((row, col)) = available_cells.pop() { |
| 100 | + let cell = sudoku.board[row][col]; |
| 101 | + sudoku.board[row][col] = EMPTY; |
| 102 | + if Sudoku::has_unique_solution(&sudoku) { |
| 103 | + filled_cells -= 1; |
| 104 | + if filled_cells <= self.max_filled_cells { |
| 105 | + sudoku.original_board = sudoku.board; |
| 106 | + return Some(sudoku); |
| 107 | + } |
| 108 | + } else { |
| 109 | + // Cell removal created a Sudoku with multiple solutions, |
| 110 | + // so we need to backtrack |
| 111 | + sudoku.board[row][col] = cell; |
85 | 112 | } |
86 | | - return Some(sudoku); |
87 | 113 | } |
88 | | - |
89 | 114 | None |
90 | 115 | } |
91 | 116 |
|
92 | | - /// Generate a new Sudoku puzzle with a given number of filled cells. |
93 | | - /// This method fills the diagonal boxes first, then solves the Sudoku, |
94 | | - /// and finally removes cells while ensuring a unique solution. |
| 117 | + /// Generate a Sudoku puzzle by filling cells incrementally. |
| 118 | + /// This method fills cells one by one, ensuring that the |
| 119 | + /// puzzle has a unique solution. |
95 | 120 | /// The `filled_cells` parameter specifies how many cells should remain filled. |
96 | | - pub fn generate_diagonal_fill(filled_cells: usize) -> Option<Self> { |
| 121 | + /// The minimum number of cells to fill is 17. |
| 122 | + /// If the puzzle cannot be generated with the specified number of filled cells, |
| 123 | + /// it returns `None`. |
| 124 | + pub fn generate_incrementally(max_cells_to_fill: usize) -> Option<Sudoku> { |
| 125 | + assert!( |
| 126 | + max_cells_to_fill <= 81, |
| 127 | + "Filled cells must be less than or equal to 81" |
| 128 | + ); |
| 129 | + assert!( |
| 130 | + max_cells_to_fill >= 17, |
| 131 | + "Filled cells must be greater than or equal to 17" |
| 132 | + ); |
97 | 133 | let mut rng: ThreadRng = rand::rng(); |
98 | | - let mut all_digits: Vec<u8> = (1..=9).collect(); |
99 | 134 | let mut sudoku = Sudoku::new(); |
100 | | - // Fill the 3 diagonal boxes (top-left, middle, bottom-right) |
101 | | - for box_idx in 0..3 { |
102 | | - let start_row = box_idx * 3; |
103 | | - let start_col = box_idx * 3; |
104 | | - // Fill the box with a shuffled sequence of 1-9 |
105 | | - all_digits.shuffle(&mut rng); |
106 | | - for (i, &num) in all_digits.iter().enumerate() { |
107 | | - sudoku.board[start_row + i / 3][start_col + i % 3] = num; |
108 | | - } |
109 | | - } |
110 | | - sudoku.solve_by_backtracking(); |
111 | | - // Get all filled cells that haven't been removed yet |
112 | 135 | let mut available_cells: Vec<(usize, usize)> = (0..9) |
113 | 136 | .flat_map(|row| (0..9).map(move |col| (row, col))) |
114 | 137 | .collect(); |
115 | 138 | available_cells.shuffle(&mut rng); |
116 | | - available_cells.truncate(81 - filled_cells); |
117 | | - while let Some((row, col)) = available_cells.pop() { |
118 | | - sudoku.board[row][col] = EMPTY; |
119 | | - // Check if the puzzle still has a unique solution |
120 | | - let mut test_sudoku = sudoku.clone(); |
121 | | - let mut solution_count = 0; |
122 | | - Self::count_solutions(&mut test_sudoku, &mut solution_count, 2); |
123 | | - if solution_count != 1 { |
| 139 | + let mut filled = 0; |
| 140 | + while filled < max_cells_to_fill { |
| 141 | + if let Some((row, col)) = available_cells.pop() { |
| 142 | + let digit = rng.random_range(1..=9); |
| 143 | + if sudoku.can_place(row, col, digit) { |
| 144 | + sudoku.board[row][col] = digit; |
| 145 | + filled += 1; |
| 146 | + } |
| 147 | + } else { |
124 | 148 | return None; |
125 | 149 | } |
126 | 150 | } |
127 | | - // Store the current board state as the original board string |
128 | 151 | sudoku.original_board = sudoku.board; |
129 | | - Some(sudoku) |
| 152 | + Sudoku::has_unique_solution(&sudoku).then_some(sudoku) |
130 | 153 | } |
131 | 154 | } |
0 commit comments