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::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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
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));
|
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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue