diff --git a/src/bin/generator.rs b/src/bin/generator.rs index 1562867..c29f169 100644 --- a/src/bin/generator.rs +++ b/src/bin/generator.rs @@ -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 = None; - let mut difficulty = Difficulty::Challenge; + let mut max_attempts: Option = 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 = 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 { 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 { - while num_attempts < max_attempts && !should_stop.load(Ordering::Relaxed) { - let (grid, num_hints, solve_statistics) = + let mut generated_grids: Vec = 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> { +fn save_grids_csv(grids: &Vec, filename: &str) -> Result<(), Box> { // 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(()) } diff --git a/src/generator.rs b/src/generator.rs index 0c383a0..6464173 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -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; diff --git a/src/pdf.rs b/src/pdf.rs index f194553..f2018a4 100644 --- a/src/pdf.rs +++ b/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, filename: &str, print_possibilities: bool, ) -> Result<(), Box> { - 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> { + 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(()) }