Skip to content

Commit 7986b25

Browse files
committed
Generator is much more efficient.
1 parent 4a1f66d commit 7986b25

File tree

4 files changed

+271
-118
lines changed

4 files changed

+271
-118
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ categories = ["games"]
1111

1212
[dependencies]
1313
rand = { version = "^0.9" }
14+
clap = { version = "^4.5", features = ["derive"] }
1415
eframe = { version = "^0.31", features = [
1516
"persistence",
1617
"glow",

src/gen/gen.rs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,57 @@
1-
use rate_my_sudoku::Sudoku;
1+
use clap::Parser;
2+
use rate_my_sudoku::generator::{FillAlgorithm, SudokuGenerator};
23
use std::io::Write;
34
use std::sync::mpsc;
45
use std::thread;
56

6-
fn main() -> Result<(), Box<dyn std::error::Error>> {
7-
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
8-
.format_timestamp(None)
9-
.format_target(false)
10-
.init();
7+
#[derive(clap::Parser, Debug)]
8+
#[command(name = "sudokugen", version = "0.1.0", about = "Generate Sudokus")]
9+
struct Cli {
10+
#[arg(short, long, default_value_t = FillAlgorithm::DiagonalThinOut, help = "Algorithm to use for generating Sudoku puzzles")]
11+
algorithm: FillAlgorithm,
12+
#[arg(
13+
short,
14+
long,
15+
default_value_t = 24,
16+
value_name = "N",
17+
help = "Number of filled cells in the Sudoku puzzle"
18+
)]
19+
max_filled_cells: usize,
20+
#[arg(short, long, help = "Number of threads to use for generation")]
21+
num_threads: Option<usize>,
22+
#[arg(short, long, help = "Enable logging")]
23+
logging: Option<String>,
24+
}
1125

12-
let default_filled_cells: usize = 20;
13-
let args: Vec<String> = std::env::args().collect();
14-
let filled_cells = if args.len() > 1 {
15-
args[1].parse::<usize>().unwrap_or(default_filled_cells)
16-
} else {
17-
default_filled_cells
26+
fn main() -> Result<(), Box<dyn std::error::Error>> {
27+
let cli = Cli::parse();
28+
if let Some(ref filter) = cli.logging {
29+
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(filter))
30+
.format_timestamp(None)
31+
.format_target(false)
32+
.init();
1833
};
19-
let thread_count = num_cpus::get();
34+
let max_filled_cells = cli.max_filled_cells;
35+
let fill_algorithm = cli.algorithm;
36+
let thread_count = match cli.num_threads {
37+
Some(num_threads) => num_threads,
38+
None => num_cpus::get(),
39+
};
40+
log::info!(
41+
"Starting Sudoku generation with {} threads using the fill algorithm {} for a maximum of {} filled cells ...",
42+
thread_count,
43+
fill_algorithm,
44+
max_filled_cells
45+
);
46+
2047
let (tx, rx) = mpsc::channel();
2148
let stdout_mutex = std::sync::Mutex::new(());
22-
2349
for _ in 0..thread_count {
2450
let tx = tx.clone();
2551
thread::spawn(move || {
2652
loop {
27-
if let Some(sudoku) = Sudoku::generate_incrementally(filled_cells) {
53+
let generator = SudokuGenerator::new(fill_algorithm, max_filled_cells);
54+
for sudoku in generator {
2855
let sudoku_string = sudoku.to_board_string();
2956
let mut computer_sudoku = sudoku.clone();
3057
let mut sudoku = sudoku;
@@ -39,7 +66,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3966
);
4067
}
4168
} else {
42-
tx.send((-1.0, sudoku_string)).unwrap();
69+
tx.send((f64::INFINITY, sudoku_string)).unwrap();
4370
}
4471
}
4572
}
@@ -52,7 +79,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
5279
// Print results from the channel
5380
while let Ok((difficulty, sudoku_string)) = rx.recv() {
5481
let _guard = stdout_mutex.lock().unwrap();
55-
if difficulty > 0.0 {
82+
if difficulty != f64::INFINITY {
5683
println!("{:6.2} {}", difficulty, sudoku_string);
5784
} else {
5885
println!(" ? {}", sudoku_string);

src/generator.rs

Lines changed: 121 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,154 @@
11
use crate::{EMPTY, Sudoku};
22
use rand::prelude::ThreadRng;
3-
use rand::seq::SliceRandom;
3+
use rand::{Rng, seq::SliceRandom};
4+
use std::fmt::{Display, Formatter};
45

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"),
917
}
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+
}
2048
}
49+
sudoku.all_solutions()
2150
}
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+
}
3757
}
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,
4064
}
41-
false
4265
}
66+
}
4367

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;
6170

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+
}
6781
}
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);
7685
}
7786
}
7887
}
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;
85112
}
86-
return Some(sudoku);
87113
}
88-
89114
None
90115
}
91116

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.
95120
/// 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+
);
97133
let mut rng: ThreadRng = rand::rng();
98-
let mut all_digits: Vec<u8> = (1..=9).collect();
99134
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
112135
let mut available_cells: Vec<(usize, usize)> = (0..9)
113136
.flat_map(|row| (0..9).map(move |col| (row, col)))
114137
.collect();
115138
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 {
124148
return None;
125149
}
126150
}
127-
// Store the current board state as the original board string
128151
sudoku.original_board = sudoku.board;
129-
Some(sudoku)
152+
Sudoku::has_unique_solution(&sudoku).then_some(sudoku)
130153
}
131154
}

0 commit comments

Comments
 (0)