Add support for generating multiple puzzles

This commit is contained in:
Joel Therrien 2022-09-04 22:04:56 -07:00
parent 4c2327fc5d
commit cffe7b4f47
3 changed files with 157 additions and 130 deletions

View file

@ -4,22 +4,12 @@ use std::error::Error;
use std::io::Write; use std::io::Write;
use std::process::exit; use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{mpsc, Arc}; use std::sync::{mpsc, Arc};
use std::thread; use std::thread;
use sudoku_solver::grid::{CellValue, Grid}; use sudoku_solver::grid::{CellValue, Grid};
use sudoku_solver::solver::{SolveController, SolveStatistics}; use sudoku_solver::solver::{SolveController, SolveStatistics};
/*
We have to be very careful here because Grid contains lots of Rcs and RefCells which could enable mutability
across multiple threads (with Rcs specifically even just counting the number of active references to the object
involves mutability of the Rc itself). In my specific case with the generator here I know that all those Rcs
and RefCells are fully encapsulated in the one Grid object I'm Sending and will never be accessed again from the thread
that sent them after it's been Sent, so it's safe in this narrowly specific context.
*/
struct SafeGridWrapper(Grid);
unsafe impl Send for SafeGridWrapper {}
#[derive(Clone, Copy)] // Needed for argparse #[derive(Clone, Copy)] // Needed for argparse
enum Difficulty { enum Difficulty {
Challenge, Challenge,
@ -102,9 +92,10 @@ impl FromStr for Difficulty {
fn main() { fn main() {
let mut debug = false; let mut debug = false;
let mut max_hints = 81; let mut max_hints = 81;
let mut max_attempts = 100; let mut max_attempts: Option<usize> = None;
let mut filename: Option<String> = None; let mut filename= String::new();
let mut difficulty = Difficulty::Challenge; let mut number_puzzles: usize = 1;
let mut difficulty = Difficulty::Hard;
let mut threads = 1; let mut threads = 1;
let mut print_possibilities = false; let mut print_possibilities = false;
@ -122,12 +113,19 @@ fn main() {
); );
ap.refer(&mut max_attempts) ap.refer(&mut max_attempts)
.add_option(&["--attempts"], argparse::Store, "Number of puzzles each thread will generate to find an appropriate puzzle; default is 100"); .add_option(&["--attempts"], argparse::StoreOption,
"Number of puzzles attempted to find the appropriate puzzles; default is 100*(num-puzzles)");
ap.refer(&mut filename).add_argument( ap.refer(&mut filename).add_argument(
"filename", "filename",
argparse::StoreOption, argparse::Store,
"Optional filename to store puzzle in as a CSV", "Filename to store puzzle(s) in as a CSV or pdf",
).required();
ap.refer(&mut number_puzzles).add_option(
&["--num-puzzles"],
argparse::Store,
"Number of puzzles to generate; default 1"
); );
ap.refer(&mut difficulty).add_option( ap.refer(&mut difficulty).add_option(
@ -151,9 +149,11 @@ fn main() {
ap.parse_args_or_exit(); ap.parse_args_or_exit();
} }
let max_attempts = max_attempts.unwrap_or(100 * number_puzzles);
let solve_controller = difficulty.map_to_solve_controller(); let solve_controller = difficulty.map_to_solve_controller();
let (result, num_attempts) = match threads.cmp(&1) { let mut generated_grids_vec = match threads.cmp(&1) {
ComparableOrdering::Less => { ComparableOrdering::Less => {
eprintln!("--threads must be at least 1"); eprintln!("--threads must be at least 1");
exit(1); exit(1);
@ -164,14 +164,16 @@ fn main() {
&mut rng, &mut rng,
&difficulty, &difficulty,
&solve_controller, &solve_controller,
max_attempts,
max_hints, max_hints,
&AtomicBool::new(false), &AtomicI64::new(number_puzzles as i64),
&AtomicI64::new(max_attempts as i64),
debug, debug,
) )
} }
ComparableOrdering::Greater => run_multi_threaded( ComparableOrdering::Greater => run_multi_threaded(
max_attempts, max_attempts,
number_puzzles,
max_hints, max_hints,
threads, threads,
debug, debug,
@ -180,177 +182,176 @@ fn main() {
), ),
}; };
let (grid, solve_statistics, num_hints) = match result {
Some(x) => x,
None => {
println!("Unable to find a desired puzzle in {} tries.", num_attempts);
return;
}
};
println!("{}", grid);
println!(
"Puzzle has {} hints and was found in {} attempts.",
num_hints, num_attempts
);
if debug { if generated_grids_vec.len() < number_puzzles {
println!("Solving this puzzle involves roughly:"); println!("Unable to find {} puzzles in {} tries; instead {} puzzles were found.", number_puzzles, max_attempts, generated_grids_vec.len());
println!("\t{} SINGLE actions", solve_statistics.singles); return
println!(
"\t{} HIDDEN_SINGLE actions",
solve_statistics.hidden_singles
);
println!(
"\t{} USEFUL_CONSTRAINT actions",
solve_statistics.useful_constraints
);
println!(
"\t{} POSSIBILITY_GROUP actions",
solve_statistics.possibility_groups
);
println!("\t{} GUESS actions", solve_statistics.guesses);
} }
if let Some(filename) = filename { let mut grids_vec: Vec<Grid> = Vec::new();
// check if we save to a csv or a pdf for _ in 0..number_puzzles {
if filename.ends_with(".pdf") { // It may happen that we generated more puzzles than we needed - that's okay, we'll just ignore those
sudoku_solver::pdf::draw_grid(&grid, &filename, print_possibilities).unwrap(); grids_vec.push(generated_grids_vec.pop().unwrap().grid);
println!("Grid saved as pdf to {}", filename);
} else {
save_grid_csv(&grid, &filename).unwrap();
println!("Grid saved as CSV to {}", filename);
}
} }
// check if we save to a csv or a pdf
if filename.ends_with(".pdf") {
sudoku_solver::pdf::draw_grids(&grids_vec, &filename, print_possibilities).unwrap();
println!("Grid saved as pdf to {}", filename);
} else {
save_grids_csv(&grids_vec, &filename).unwrap();
println!("Grid saved as CSV to {}", filename);
}
} }
struct GeneratedGrid {
grid: Grid,
statistics: SolveStatistics,
num_hints: u64
}
/*
We have to be very careful here because Grid contains lots of Rcs and RefCells which could enable mutability
across multiple threads (with Rcs specifically even just counting the number of active references to the object
involves mutability of the Rc itself). In my specific case with the generator here I know that all those Rcs
and RefCells are fully encapsulated in the one Grid object I'm Sending and will never be accessed again from the thread
that sent them after it's been Sent, so it's safe in this narrowly specific context.
*/
unsafe impl Send for GeneratedGrid {}
fn run_multi_threaded( fn run_multi_threaded(
max_attempts: i32, max_attempts: usize,
max_hints: i32, number_puzzles: usize,
threads: i32, max_hints: u64,
threads: u64,
debug: bool, debug: bool,
solve_controller: SolveController, solve_controller: SolveController,
difficulty: Difficulty, difficulty: Difficulty,
) -> (Option<(Grid, SolveStatistics, i32)>, i32) { ) -> Vec<GeneratedGrid> {
let mut thread_rng = thread_rng(); let mut thread_rng = thread_rng();
let (transmitter, receiver) = mpsc::channel(); let (transmitter, receiver) = mpsc::channel();
let mut remaining_attempts = max_attempts;
let should_stop = AtomicBool::new(false); let attempts_left = AtomicI64::new(max_attempts as i64);
let should_stop = Arc::new(should_stop); let attempts_left = Arc::new(attempts_left);
let puzzles_left = AtomicI64::new(number_puzzles as i64);
let puzzles_left = Arc::new(puzzles_left);
for i in 0..threads { for i in 0..threads {
let cloned_transmitter = mpsc::Sender::clone(&transmitter); let cloned_transmitter = mpsc::Sender::clone(&transmitter);
let mut rng = SmallRng::from_rng(&mut thread_rng).unwrap(); let mut rng = SmallRng::from_rng(&mut thread_rng).unwrap();
let thread_attempts = remaining_attempts / (threads - i); let attempts_left = Arc::clone(&attempts_left);
remaining_attempts -= thread_attempts; let puzzles_left = Arc::clone(&puzzles_left);
let should_stop = Arc::clone(&should_stop);
thread::spawn(move || { thread::spawn(move || {
if debug { if debug {
println!("Thread {} spawned with {} max attempts", i, thread_attempts); println!("Thread {} spawned", i);
} }
let should_stop = &*should_stop; let found_puzzles = get_puzzle_matching_conditions(
let (result, num_attempts) = get_puzzle_matching_conditions(
&mut rng, &mut rng,
&difficulty, &difficulty,
&solve_controller, &solve_controller,
thread_attempts,
max_hints, max_hints,
should_stop, &*puzzles_left,
&*attempts_left,
debug, debug,
); );
let mut result_was_some = false; let num_puzzles_found = found_puzzles.len();
let result = match result { cloned_transmitter.send(found_puzzles).unwrap();
None => None,
Some((grid, solve_statistics, num_hints)) => {
result_was_some = true;
Some((SafeGridWrapper(grid), solve_statistics, num_hints))
}
};
cloned_transmitter.send((result, num_attempts)).unwrap();
if debug { if debug {
println!( println!(
"Thread {}, terminated having run {} attempts; did send result: {}", "Thread {} terminated having found {} puzzles",
i, num_attempts, result_was_some i, num_puzzles_found
); );
} }
}); });
} }
let mut threads_running = threads; let mut threads_running = threads;
let mut attempt_count = 0; let mut all_puzzles_found = Vec::new();
let mut result_to_return = None;
while threads_running > 0 { while threads_running > 0 {
let signal = receiver.recv().unwrap(); // Not sure what errors can result here but they are unexpected and deserve a panic let mut thread_found_puzzles = receiver.recv().unwrap(); // Not sure what errors can result here but they are unexpected and deserve a panic
threads_running -= 1; threads_running -= 1;
all_puzzles_found.append(&mut thread_found_puzzles);
let (result, attempts) = signal;
attempt_count += attempts;
if let Some((safe_grid, solve_statistics, num_hints)) = result {
result_to_return = Some((safe_grid.0, solve_statistics, num_hints));
should_stop.store(true, Ordering::Relaxed);
}
} }
(result_to_return, attempt_count) if debug {
println!("Stopping with {} attempts left and {} puzzles left.", attempts_left.load(Ordering::Relaxed), puzzles_left.load(Ordering::Relaxed));
}
all_puzzles_found
} }
fn get_puzzle_matching_conditions( fn get_puzzle_matching_conditions(
rng: &mut SmallRng, rng: &mut SmallRng,
difficulty: &Difficulty, difficulty: &Difficulty,
solve_controller: &SolveController, solve_controller: &SolveController,
max_attempts: i32, max_hints: u64,
max_hints: i32, puzzles_left: &AtomicI64,
should_stop: &AtomicBool, attempts_left: &AtomicI64,
debug: bool, debug: bool,
) -> (Option<(Grid, SolveStatistics, i32)>, i32) { ) -> Vec<GeneratedGrid> {
let mut num_attempts = 0;
while num_attempts < max_attempts && !should_stop.load(Ordering::Relaxed) { let mut generated_grids: Vec<GeneratedGrid> = Vec::new();
let (grid, num_hints, solve_statistics) =
while attempts_left.fetch_sub(1, Ordering::SeqCst) > 0 && puzzles_left.load(Ordering::SeqCst) > 0 {
let (grid, num_hints, statistics) =
sudoku_solver::generator::generate_grid(rng, &solve_controller); sudoku_solver::generator::generate_grid(rng, &solve_controller);
num_attempts += 1;
if debug { if debug {
println!("Found puzzle with {:#?}", solve_statistics); println!("Found puzzle with {:#?}", statistics);
} }
if difficulty.meets_minimum_requirements(&solve_statistics) && num_hints <= max_hints { if difficulty.meets_minimum_requirements(&statistics) && num_hints <= max_hints {
return (Some((grid, solve_statistics, num_hints)), num_attempts); puzzles_left.fetch_sub(1, Ordering::SeqCst);
generated_grids.push(GeneratedGrid{
grid,
statistics,
num_hints
});
} }
} }
(None, num_attempts) generated_grids
} }
fn save_grid_csv(grid: &Grid, filename: &str) -> Result<(), Box<dyn Error>> { fn save_grids_csv(grids: &Vec<Grid>, filename: &str) -> Result<(), Box<dyn Error>> {
// Not using the csv crate for writing because it's being difficult and won't accept raw integers // Not using the csv crate for writing because it's being difficult and won't accept raw integers
let mut file = std::fs::File::create(filename)?; let mut file = std::fs::File::create(filename)?;
for x in 0..9 { for grid in grids.iter() {
for y in 0..9 { for x in 0..9 {
let cell = grid.get(x, y).unwrap(); for y in 0..9 {
let value = &*cell.value.borrow(); let cell = grid.get(x, y).unwrap();
let digit = match value { let value = &*cell.value.borrow();
CellValue::Fixed(digit) => *digit, let digit = match value {
CellValue::Unknown(_) => 0, CellValue::Fixed(digit) => *digit,
}; CellValue::Unknown(_) => 0,
};
let mut text = digit.to_string(); let mut text = digit.to_string();
if y < 8 { if y < 8 {
text.push(','); text.push(',');
}
//file.write_all(text.as_bytes())?;
file.write(text.as_bytes())?;
} }
file.write_all(text.as_bytes())?; file.write(b"\n")?;
} }
file.write_all(b"\n")?; file.write(b"\n")?;
} }
file.flush()?;
Ok(()) Ok(())
} }

View file

@ -128,7 +128,7 @@ impl Section {
pub fn generate_grid( pub fn generate_grid(
rng: &mut SmallRng, rng: &mut SmallRng,
solve_controller: &SolveController, solve_controller: &SolveController,
) -> (Grid, i32, SolveStatistics) { ) -> (Grid, u64, SolveStatistics) {
let mut grid = generate_completed_grid(rng); let mut grid = generate_completed_grid(rng);
let mut num_hints = 81; let mut num_hints = 81;

View file

@ -9,19 +9,47 @@ const GRID_DIMENSION: f64 = 190.0;
const A4: (Mm, Mm) = (Mm(215.0), Mm(279.0)); const A4: (Mm, Mm) = (Mm(215.0), Mm(279.0));
pub fn draw_grid( pub fn draw_grids(
grid: &Grid, grids: &Vec<Grid>,
filename: &str, filename: &str,
print_possibilities: bool, print_possibilities: bool,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let (doc, page1, layer1) = PdfDocument::new("Sudoku Puzzle", A4.0, A4.1, "Layer 1"); let (doc, page1_index, layer1_index) = PdfDocument::new("Sudoku Puzzle", A4.0, A4.1, "Puzzle 1");
let layer = doc.get_page(page1).get_layer(layer1);
let font = doc.add_builtin_font(BuiltinFont::HelveticaBold)?; let font = doc.add_builtin_font(BuiltinFont::HelveticaBold)?;
for (i, grid) in grids.iter().enumerate() {
let (page_index, layer_index) = match i {
0 => {
(page1_index, layer1_index)
},
_ => {
doc.add_page(A4.0, A4.1, format!("Puzzle {}", i+1))
}
};
let layer = doc.get_page(page_index).get_layer(layer_index);
draw_grid(grid, &layer, print_possibilities, &font)?;
}
doc.save(&mut BufWriter::new(File::create(filename)?))?;
Result::Ok(())
}
pub fn draw_grid(
grid: &Grid,
layer: &PdfLayerReference,
print_possibilities: bool,
font: &IndirectFontRef,
) -> Result<(), Box<dyn std::error::Error>> {
let fixed_value_font_size = 45.0; let fixed_value_font_size = 45.0;
let possibility_font_size = 12.0; let possibility_font_size = 12.0;
draw_empty_grid(&layer); draw_empty_grid(layer);
// x represents position on left-right scale // x represents position on left-right scale
// y represents position on up-down scale // y represents position on up-down scale
@ -65,8 +93,6 @@ pub fn draw_grid(
} }
} }
doc.save(&mut BufWriter::new(File::create(filename)?))?;
Ok(()) Ok(())
} }