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:
parent
98c8cfcd0d
commit
4f2d3a6c58
4 changed files with 96 additions and 47 deletions
|
@ -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"
|
|
||||||
rand_chacha = "0.2.2"
|
|
||||||
printpdf = "0.3.2"
|
printpdf = "0.3.2"
|
||||||
|
|
||||||
|
[dependencies.rand]
|
||||||
|
version = "0.7"
|
||||||
|
features = ["small_rng"]
|
|
@ -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)?;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue