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::process::exit;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{mpsc, Arc};
use std::thread;
use sudoku_solver::grid::{CellValue, Grid};
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
enum Difficulty {
Challenge,
@ -102,9 +92,10 @@ impl FromStr for Difficulty {
fn main() {
let mut debug = false;
let mut max_hints = 81;
let mut max_attempts = 100;
let mut filename: Option<String> = None;
let mut difficulty = Difficulty::Challenge;
let mut max_attempts: Option<usize> = None;
let mut filename= String::new();
let mut number_puzzles: usize = 1;
let mut difficulty = Difficulty::Hard;
let mut threads = 1;
let mut print_possibilities = false;
@ -122,12 +113,19 @@ fn main() {
);
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(
"filename",
argparse::StoreOption,
"Optional filename to store puzzle in as a CSV",
argparse::Store,
"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(
@ -151,9 +149,11 @@ fn main() {
ap.parse_args_or_exit();
}
let max_attempts = max_attempts.unwrap_or(100 * number_puzzles);
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 => {
eprintln!("--threads must be at least 1");
exit(1);
@ -164,14 +164,16 @@ fn main() {
&mut rng,
&difficulty,
&solve_controller,
max_attempts,
max_hints,
&AtomicBool::new(false),
&AtomicI64::new(number_puzzles as i64),
&AtomicI64::new(max_attempts as i64),
debug,
)
}
ComparableOrdering::Greater => run_multi_threaded(
max_attempts,
number_puzzles,
max_hints,
threads,
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 {
println!("Solving this puzzle involves roughly:");
println!("\t{} SINGLE actions", solve_statistics.singles);
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 generated_grids_vec.len() < number_puzzles {
println!("Unable to find {} puzzles in {} tries; instead {} puzzles were found.", number_puzzles, max_attempts, generated_grids_vec.len());
return
}
if let Some(filename) = filename {
// check if we save to a csv or a pdf
if filename.ends_with(".pdf") {
sudoku_solver::pdf::draw_grid(&grid, &filename, print_possibilities).unwrap();
println!("Grid saved as pdf to {}", filename);
} else {
save_grid_csv(&grid, &filename).unwrap();
println!("Grid saved as CSV to {}", filename);
}
let mut grids_vec: Vec<Grid> = Vec::new();
for _ in 0..number_puzzles {
// It may happen that we generated more puzzles than we needed - that's okay, we'll just ignore those
grids_vec.push(generated_grids_vec.pop().unwrap().grid);
}
// 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(
max_attempts: i32,
max_hints: i32,
threads: i32,
max_attempts: usize,
number_puzzles: usize,
max_hints: u64,
threads: u64,
debug: bool,
solve_controller: SolveController,
difficulty: Difficulty,
) -> (Option<(Grid, SolveStatistics, i32)>, i32) {
) -> Vec<GeneratedGrid> {
let mut thread_rng = thread_rng();
let (transmitter, receiver) = mpsc::channel();
let mut remaining_attempts = max_attempts;
let should_stop = AtomicBool::new(false);
let should_stop = Arc::new(should_stop);
let attempts_left = AtomicI64::new(max_attempts as i64);
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 {
let cloned_transmitter = mpsc::Sender::clone(&transmitter);
let mut rng = SmallRng::from_rng(&mut thread_rng).unwrap();
let thread_attempts = remaining_attempts / (threads - i);
remaining_attempts -= thread_attempts;
let should_stop = Arc::clone(&should_stop);
let attempts_left = Arc::clone(&attempts_left);
let puzzles_left = Arc::clone(&puzzles_left);
thread::spawn(move || {
if debug {
println!("Thread {} spawned with {} max attempts", i, thread_attempts);
println!("Thread {} spawned", i);
}
let should_stop = &*should_stop;
let (result, num_attempts) = get_puzzle_matching_conditions(
let found_puzzles = get_puzzle_matching_conditions(
&mut rng,
&difficulty,
&solve_controller,
thread_attempts,
max_hints,
should_stop,
&*puzzles_left,
&*attempts_left,
debug,
);
let mut result_was_some = false;
let result = match result {
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();
let num_puzzles_found = found_puzzles.len();
cloned_transmitter.send(found_puzzles).unwrap();
if debug {
println!(
"Thread {}, terminated having run {} attempts; did send result: {}",
i, num_attempts, result_was_some
"Thread {} terminated having found {} puzzles",
i, num_puzzles_found
);
}
});
}
let mut threads_running = threads;
let mut attempt_count = 0;
let mut result_to_return = None;
let mut all_puzzles_found = Vec::new();
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;
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(
rng: &mut SmallRng,
difficulty: &Difficulty,
solve_controller: &SolveController,
max_attempts: i32,
max_hints: i32,
should_stop: &AtomicBool,
max_hints: u64,
puzzles_left: &AtomicI64,
attempts_left: &AtomicI64,
debug: bool,
) -> (Option<(Grid, SolveStatistics, i32)>, i32) {
let mut num_attempts = 0;
) -> Vec<GeneratedGrid> {
while num_attempts < max_attempts && !should_stop.load(Ordering::Relaxed) {
let (grid, num_hints, solve_statistics) =
let mut generated_grids: Vec<GeneratedGrid> = Vec::new();
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);
num_attempts += 1;
if debug {
println!("Found puzzle with {:#?}", solve_statistics);
println!("Found puzzle with {:#?}", statistics);
}
if difficulty.meets_minimum_requirements(&solve_statistics) && num_hints <= max_hints {
return (Some((grid, solve_statistics, num_hints)), num_attempts);
if difficulty.meets_minimum_requirements(&statistics) && num_hints <= max_hints {
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
let mut file = std::fs::File::create(filename)?;
for x in 0..9 {
for y in 0..9 {
let cell = grid.get(x, y).unwrap();
let value = &*cell.value.borrow();
let digit = match value {
CellValue::Fixed(digit) => *digit,
CellValue::Unknown(_) => 0,
};
for grid in grids.iter() {
for x in 0..9 {
for y in 0..9 {
let cell = grid.get(x, y).unwrap();
let value = &*cell.value.borrow();
let digit = match value {
CellValue::Fixed(digit) => *digit,
CellValue::Unknown(_) => 0,
};
let mut text = digit.to_string();
if y < 8 {
text.push(',');
let mut text = digit.to_string();
if y < 8 {
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(())
}

View file

@ -128,7 +128,7 @@ impl Section {
pub fn generate_grid(
rng: &mut SmallRng,
solve_controller: &SolveController,
) -> (Grid, i32, SolveStatistics) {
) -> (Grid, u64, SolveStatistics) {
let mut grid = generate_completed_grid(rng);
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));
pub fn draw_grid(
grid: &Grid,
pub fn draw_grids(
grids: &Vec<Grid>,
filename: &str,
print_possibilities: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let (doc, page1, layer1) = PdfDocument::new("Sudoku Puzzle", A4.0, A4.1, "Layer 1");
let layer = doc.get_page(page1).get_layer(layer1);
let (doc, page1_index, layer1_index) = PdfDocument::new("Sudoku Puzzle", A4.0, A4.1, "Puzzle 1");
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 possibility_font_size = 12.0;
draw_empty_grid(&layer);
draw_empty_grid(layer);
// x represents position on left-right 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(())
}