Add support for generating multiple puzzles
This commit is contained in:
parent
4c2327fc5d
commit
cffe7b4f47
3 changed files with 157 additions and 130 deletions
|
@ -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,160 +182,154 @@ 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 {
|
||||
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_grid(&grid, &filename, print_possibilities).unwrap();
|
||||
sudoku_solver::pdf::draw_grids(&grids_vec, &filename, print_possibilities).unwrap();
|
||||
println!("Grid saved as pdf to {}", filename);
|
||||
} else {
|
||||
save_grid_csv(&grid, &filename).unwrap();
|
||||
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 grid in grids.iter() {
|
||||
for x in 0..9 {
|
||||
for y in 0..9 {
|
||||
let cell = grid.get(x, y).unwrap();
|
||||
|
@ -347,10 +343,15 @@ fn save_grid_csv(grid: &Grid, filename: &str) -> Result<(), Box<dyn Error>> {
|
|||
if y < 8 {
|
||||
text.push(',');
|
||||
}
|
||||
file.write_all(text.as_bytes())?;
|
||||
//file.write_all(text.as_bytes())?;
|
||||
file.write(text.as_bytes())?;
|
||||
}
|
||||
file.write_all(b"\n")?;
|
||||
file.write(b"\n")?;
|
||||
}
|
||||
file.write(b"\n")?;
|
||||
}
|
||||
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
42
src/pdf.rs
42
src/pdf.rs
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue