Add WIP support for multi-threaded generation of puzzles

There is a bug where if a puzzle isn't found, the program never
returns; hence this is WIP.
This commit is contained in:
Joel Therrien 2020-09-26 14:46:22 -07:00
parent 98c8cfcd0d
commit 4f2d3a6c58
4 changed files with 96 additions and 47 deletions

View file

@ -9,6 +9,8 @@ edition = "2018"
[dependencies] [dependencies]
csv = "1.1.3" csv = "1.1.3"
argparse = "0.2.2" argparse = "0.2.2"
rand = "0.7" printpdf = "0.3.2"
rand_chacha = "0.2.2"
printpdf = "0.3.2" [dependencies.rand]
version = "0.7"
features = ["small_rng"]

View file

@ -1,12 +1,24 @@
use rand_chacha::ChaCha8Rng;
use rand::prelude::*; use rand::prelude::*;
use sudoku_solver::grid::{Grid, CellValue}; use sudoku_solver::grid::{Grid, CellValue};
use std::error::Error; use std::error::Error;
use std::io::Write; use std::io::Write;
use sudoku_solver::solver::{SolveController, SolveStatistics}; use sudoku_solver::solver::{SolveController, SolveStatistics};
use std::str::FromStr; use std::str::FromStr;
use std::sync::{mpsc};
use std::process::exit;
use std::thread;
#[derive(Clone)] // Needed for argparse /*
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 { enum Difficulty {
Challenge, Challenge,
Hard, Hard,
@ -81,13 +93,11 @@ impl FromStr for Difficulty { // Needed for argparse
fn main() { fn main() {
let mut debug = false; let mut debug = false;
// Starting default seed will just be based on time
let mut seed = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("Time went backwards").as_secs();
let mut max_hints = 81; let mut max_hints = 81;
let mut max_attempts = 100; let mut max_attempts = 100;
let mut filename : Option<String> = None; let mut filename : Option<String> = None;
let mut difficulty = Difficulty::Challenge; let mut difficulty = Difficulty::Challenge;
let mut threads = 1;
{ // this block limits scope of borrows by ap.refer() method { // this block limits scope of borrows by ap.refer() method
let mut ap = argparse::ArgumentParser::new(); let mut ap = argparse::ArgumentParser::new();
@ -95,14 +105,11 @@ fn main() {
ap.refer(&mut debug) ap.refer(&mut debug)
.add_option(&["--debug"], argparse::StoreTrue, "Run in debug mode"); .add_option(&["--debug"], argparse::StoreTrue, "Run in debug mode");
ap.refer(&mut seed)
.add_option(&["-s", "--seed"], argparse::Store, "Provide seed for puzzle generation");
ap.refer(&mut max_hints) ap.refer(&mut max_hints)
.add_option(&["--hints"], argparse::Store, "Only return a puzzle with less than or equal to this number of hints"); .add_option(&["--hints"], argparse::Store, "Only return a puzzle with less than or equal to this number of hints");
ap.refer(&mut max_attempts) ap.refer(&mut max_attempts)
.add_option(&["--attempts"], argparse::Store, "Number of attempts that will be tried to generate such a puzzle; default is 100"); .add_option(&["--attempts"], argparse::Store, "Number of puzzles each thread will generate to find an appropriate puzzle; default is 100");
ap.refer(&mut filename) ap.refer(&mut filename)
.add_argument("filename", argparse::StoreOption, "Optional filename to store puzzle in as a CSV"); .add_argument("filename", argparse::StoreOption, "Optional filename to store puzzle in as a CSV");
@ -110,50 +117,73 @@ fn main() {
ap.refer(&mut difficulty) ap.refer(&mut difficulty)
.add_option(&["-d", "--difficulty"], argparse::Store, "Max difficulty setting; values are EASY, MEDIUM, HARD, or CHALLENGE"); .add_option(&["-d", "--difficulty"], argparse::Store, "Max difficulty setting; values are EASY, MEDIUM, HARD, or CHALLENGE");
ap.refer(&mut threads)
.add_option(&["--threads"], argparse::Store, "Number of threads to use when generating possible puzzles");
ap.parse_args_or_exit(); ap.parse_args_or_exit();
} }
/*
if debug {
unsafe {
sudoku_solver::grid::DEBUG = true;
sudoku_solver::solver::DEBUG = true;
sudoku_solver::generator::DEBUG = true;
}
}
*/
if debug {
println!("Using seed {}", seed);
}
let solve_controller = difficulty.map_to_solve_controller(); let solve_controller = difficulty.map_to_solve_controller();
let (grid, solve_statistics, num_hints) =
if threads < 1 {
eprintln!("--threads must be at least 1");
exit(1);
} else if threads == 1 {
let mut rng = SmallRng::from_entropy();
let result = get_puzzle_matching_conditions(&mut rng, &difficulty, &solve_controller, max_attempts, max_hints);
match result {
Some(x) => x,
None => {
eprintln!("Unable to find an appropriate puzzle in the required amount of attempts");
exit(1);
}
}
} else {
let mut thread_rng = thread_rng();
let (transmitter, receiver) = mpsc::channel();
let mut rng = ChaCha8Rng::seed_from_u64(seed); for _i in 0..threads {
let cloned_transmitter = mpsc::Sender::clone(&transmitter);
let mut rng = SmallRng::from_rng(&mut thread_rng).unwrap();
let mut num_attempts = 0; thread::spawn(move || {
if debug {
println!("Thread spawned");
}
let (grid, solve_statistics) = loop { let result = get_puzzle_matching_conditions(&mut rng, &difficulty, &solve_controller, max_attempts, max_hints);
if num_attempts >= max_attempts{ match result {
println!("Unable to find a puzzle with only {} hints in {} attempts", max_hints, max_attempts); Some((grid, solve_statistics, num_hints)) => {
return; cloned_transmitter.send((SafeGridWrapper(grid), solve_statistics, num_hints)).unwrap();
},
None => {}
};
if debug {
println!("Thread terminated");
}
});
} }
let (grid, num_hints, solve_statistics) = sudoku_solver::generator::generate_grid(&mut rng, &solve_controller); // TODO - fix bug where recv doesn't return if no Grid is found by any threads!
num_attempts = num_attempts + 1; match receiver.recv() {
Ok((grid, solve_statistics, num_hints)) => (grid.0, solve_statistics, num_hints),
Err(e) => {
eprintln!("Unable to find an appropriate puzzle in the required amount of attempts");
if debug {
eprintln!("Error returned: {:?}", e);
}
if difficulty.meets_minimum_requirements(&solve_statistics) && num_hints <= max_hints { exit(1);
println!("{}", grid);
println!("Puzzle has {} hints", num_hints);
if num_attempts > 1 {
println!("It took {} attempts to find this puzzle.", num_attempts);
} }
break (grid, solve_statistics);
} }
}; };
println!("{}", grid);
println!("Puzzle has {} hints", num_hints);
if debug { if debug {
println!("Solving this puzzle involves roughly:"); println!("Solving this puzzle involves roughly:");
println!("\t{} SINGLE actions", solve_statistics.singles); println!("\t{} SINGLE actions", solve_statistics.singles);
@ -179,6 +209,23 @@ fn main() {
} }
fn get_puzzle_matching_conditions(rng: &mut SmallRng, difficulty: &Difficulty, solve_controller: &SolveController, max_attempts: i32, max_hints: i32) -> Option<(Grid, SolveStatistics, i32)>{
let mut num_attempts = 0;
loop {
if num_attempts >= max_attempts {
return None;
}
let (grid, num_hints, solve_statistics) = sudoku_solver::generator::generate_grid(rng, &solve_controller);
num_attempts += 1;
if difficulty.meets_minimum_requirements(&solve_statistics) && num_hints <= max_hints {
return Some((grid, solve_statistics, num_hints));
}
}
}
fn save_grid_csv(grid: &Grid, filename: &str) -> Result<(), Box<dyn Error>>{ fn save_grid_csv(grid: &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)?;

View file

@ -2,12 +2,11 @@ use crate::grid::{Cell, Grid, CellValue, Line};
use crate::solver::{SolveStatus, SolveController, Uniqueness, evaluate_grid_with_solve_controller, SolveStatistics}; use crate::solver::{SolveStatus, SolveController, Uniqueness, evaluate_grid_with_solve_controller, SolveStatistics};
use std::rc::Rc; use std::rc::Rc;
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
pub static mut DEBUG : bool = false; pub static mut DEBUG : bool = false;
impl Grid { impl Grid {
fn get_random_empty_cell(&self, rng : &mut ChaCha8Rng) -> Result<Rc<Cell>, &str> { fn get_random_empty_cell(&self, rng : &mut SmallRng) -> Result<Rc<Cell>, &str> {
// Idea - put all empty cells into a vector and choose one at random // Idea - put all empty cells into a vector and choose one at random
// If vector is empty we return an error // If vector is empty we return an error
@ -108,7 +107,7 @@ impl Line {
} }
} }
pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -> (Grid, i32, SolveStatistics) { pub fn generate_grid(rng: &mut SmallRng, solve_controller: &SolveController) -> (Grid, i32, 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;
@ -157,7 +156,7 @@ pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -
} }
// We generate a completed grid with no mind for difficulty; afterward generate_puzzle will take out as many fields as it can with regards to the difficulty // We generate a completed grid with no mind for difficulty; afterward generate_puzzle will take out as many fields as it can with regards to the difficulty
fn generate_completed_grid(rng: &mut ChaCha8Rng) -> Grid { fn generate_completed_grid(rng: &mut SmallRng) -> Grid {
let solve_controller = SolveController{ let solve_controller = SolveController{
determine_uniqueness: true, determine_uniqueness: true,
search_singles: true, search_singles: true,
@ -252,7 +251,7 @@ mod tests {
use crate::grid::*; use crate::grid::*;
use crate::solver::{solve_grid_with_solve_controller, SolveController, Uniqueness, SolveStatus, SolveStatistics}; use crate::solver::{solve_grid_with_solve_controller, SolveController, Uniqueness, SolveStatus, SolveStatistics};
use crate::generator::generate_grid; use crate::generator::generate_grid;
use rand_chacha::ChaCha8Rng; use rand_chacha::SmallRng;
use rand_chacha::rand_core::SeedableRng; use rand_chacha::rand_core::SeedableRng;
#[test] #[test]
@ -319,7 +318,7 @@ mod tests {
}; };
// Note that the puzzle itself doesn't matter // Note that the puzzle itself doesn't matter
let (grid, _num_hints, _statistics) = generate_grid(&mut ChaCha8Rng::seed_from_u64(123), &solve_controller); let (grid, _num_hints, _statistics) = generate_grid(&mut SmallRng::seed_from_u64(123), &solve_controller);
let mut observed_empty_cell = false; let mut observed_empty_cell = false;
'outer : for x in 0..9 { 'outer : for x in 0..9 {

View file

@ -54,6 +54,7 @@ impl SolveStatus {
} }
} }
#[derive(Clone, Copy)]
pub struct SolveController { pub struct SolveController {
pub determine_uniqueness: bool, pub determine_uniqueness: bool,
pub search_singles: bool, pub search_singles: bool,