Add support for setting max difficulty.

TODO is to support for min difficulty.
This commit is contained in:
Joel Therrien 2020-09-24 13:48:37 -07:00
parent 6df119d4c9
commit fa370ed9a9
3 changed files with 881 additions and 630 deletions

View file

@ -3,6 +3,59 @@ 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;
use std::str::FromStr;
#[derive(Clone)] // Needed for argparse
enum Difficulty {
Hard,
Medium,
Easy
}
impl Difficulty {
fn map_to_solve_controller(&self) -> SolveController {
let mut controller = SolveController{
determine_uniqueness: true,
search_singles: true,
search_hidden_singles: true,
find_possibility_groups: true,
search_useful_constraint: true,
make_guesses: true
};
match self {
Difficulty::Hard => {} // Do nothing, already hard
Difficulty::Medium => {
controller.make_guesses = false;
},
Difficulty::Easy => {
controller.make_guesses = false;
controller.search_useful_constraint = false;
controller.find_possibility_groups = false;
}
}
controller
}
}
impl FromStr for Difficulty { // Needed for argparse
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("EASY"){
return Ok(Difficulty::Easy);
} else if s.eq_ignore_ascii_case("MEDIUM"){
return Ok(Difficulty::Medium);
} else if s.eq_ignore_ascii_case("HARD"){
return Ok(Difficulty::Hard);
}
return Err(format!("{} is not a valid difficulty", s));
}
}
fn main() { fn main() {
@ -13,6 +66,7 @@ fn main() {
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::Hard;
{ // 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();
@ -21,7 +75,7 @@ fn main() {
.add_option(&["--debug"], argparse::StoreTrue, "Run in debug mode"); .add_option(&["--debug"], argparse::StoreTrue, "Run in debug mode");
ap.refer(&mut seed) ap.refer(&mut seed)
.add_option(&["--seed"], argparse::Store, "Provide seed for puzzle generation"); .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");
@ -32,6 +86,9 @@ fn main() {
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");
ap.refer(&mut difficulty)
.add_option(&["-d", "--difficulty"], argparse::Store, "Max difficulty setting; values are EASY, MEDIUM, or HARD");
ap.parse_args_or_exit(); ap.parse_args_or_exit();
} }
@ -46,6 +103,10 @@ fn main() {
if debug { if debug {
println!("Using seed {}", seed); println!("Using seed {}", seed);
} }
let solve_controller = difficulty.map_to_solve_controller();
let mut rng = ChaCha8Rng::seed_from_u64(seed); let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut num_attempts = 0; let mut num_attempts = 0;
@ -56,7 +117,7 @@ fn main() {
return; return;
} }
let (grid, num_hints) = sudoku_solver::generator::generate_grid(&mut rng); let (grid, num_hints) = sudoku_solver::generator::generate_grid(&mut rng, &solve_controller);
num_attempts = num_attempts + 1; num_attempts = num_attempts + 1;
if num_hints <= max_hints { if num_hints <= max_hints {

View file

@ -1,56 +1,11 @@
use crate::grid::{Cell, Grid, CellValue, Line}; use crate::grid::{Cell, Grid, CellValue, Line};
use crate::solver::{solve_grid_no_guess, SolveStatus, find_smallest_cell}; use crate::solver::{SolveStatus, SolveController, Uniqueness, evaluate_grid_with_solve_controller};
use std::rc::Rc; use std::rc::Rc;
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaCha8Rng; use rand_chacha::ChaCha8Rng;
pub static mut DEBUG : bool = false; pub static mut DEBUG : bool = false;
// Extension of SolveStatus
#[derive(Debug, Eq, PartialEq)]
pub enum GenerateStatus {
UniqueSolution,
Unfinished,
NoSolution,
NotUniqueSolution
}
impl GenerateStatus {
fn increment(self, new_status : GenerateStatus) -> GenerateStatus {
match self {
GenerateStatus::UniqueSolution => {
match new_status {
GenerateStatus::UniqueSolution => GenerateStatus::NotUniqueSolution, // We now have two completes, so the solutions are not unique
GenerateStatus::NoSolution => GenerateStatus::UniqueSolution, // We already have a complete, so no issue with another guess being invalid
GenerateStatus::Unfinished => {panic!("Should not have encountered an UNFINISHED status")},
GenerateStatus::NotUniqueSolution => GenerateStatus::NotUniqueSolution // That solver found multiple solutions so no need to keep checking
}
},
GenerateStatus::Unfinished => {
match new_status {
GenerateStatus::UniqueSolution => GenerateStatus::UniqueSolution,
GenerateStatus::NoSolution => GenerateStatus::Unfinished,
GenerateStatus::Unfinished => {panic!("Should not have encountered an UNFINISHED status")},
GenerateStatus::NotUniqueSolution => GenerateStatus::NotUniqueSolution // That solver found multiple solutions so no need to keep checking
}
},
GenerateStatus::NotUniqueSolution => GenerateStatus::NotUniqueSolution,
GenerateStatus::NoSolution => GenerateStatus::NoSolution // This guess didn't pan out
}
}
}
impl SolveStatus {
fn map_to_generate_status(self) -> GenerateStatus {
match self {
SolveStatus::Complete => {GenerateStatus::UniqueSolution }
SolveStatus::Unfinished => {GenerateStatus::Unfinished }
SolveStatus::Invalid => {GenerateStatus::NoSolution }
}
}
}
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 ChaCha8Rng) -> 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
@ -153,94 +108,17 @@ impl Line {
} }
} }
pub fn generate_grid(rng: &mut ChaCha8Rng) -> (Grid, i32) { pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -> (Grid, i32) {
let mut num_hints; let mut grid = generate_completed_grid(rng);
let mut grid : Grid = loop { let mut num_hints = 81;
// First step; randomly assign 8 different digits to different empty cells and see if there's a possible solution
// We have to ensure that 8 of the digits appear at least once, otherwise the solution can't be unique because you could interchange the two missing digits throughout the puzzle
// We do this in a loop so that if we are really unlucky and our guesses stop there from being any solution, we can easily re-run it
let mut grid = Grid::new();
num_hints = 0;
let digit_excluded = rng.gen_range(1, 10); // We now trim down cells; first going to put them in a vector and shuffle them
for digit in 1..10 {
if digit != digit_excluded {
let cell = grid.get_random_empty_cell(rng);
cell.unwrap().set(digit);
num_hints = num_hints + 1;
}
}
let status = solve_grid(&mut grid);
match status {
GenerateStatus::UniqueSolution => { // very surprising result, given that the smallest puzzles found have 14 guesses
eprintln!("Wow! A puzzle with only 8 guesses have been found");
return (grid, num_hints);
}
GenerateStatus::Unfinished => {panic!("solve_grid should never return UNFINISHED")}
GenerateStatus::NoSolution => {continue;} // unlucky; try again
GenerateStatus::NotUniqueSolution => {break grid;}
};
};
// Alright, we now have a grid that we can start adding more guesses onto until we find a unique solution
grid =
'outer: loop {
num_hints = num_hints + 1;
let cell = grid.get_random_empty_cell(rng).unwrap(); // We unwrap because if somehow we're filled each cell without finding a solution, that's reason for a panic
let cell = &*cell;
let mut cell_possibilities = cell.get_value_possibilities().expect("An empty cell has no possibilities");
// Let's scramble the order
cell_possibilities.shuffle(rng);
for (_index, digit) in cell_possibilities.iter().enumerate() {
let grid_clone = grid.clone();
let cell = &*grid_clone.get(cell.x, cell.y).unwrap();
cell.set(*digit);
let status = solve_grid(&grid_clone);
match status {
GenerateStatus::UniqueSolution => { // We're done!
break 'outer grid_clone;
}
GenerateStatus::Unfinished => {
panic!("solve_grid should never return UNFINISHED")
}
GenerateStatus::NoSolution => { // Try another guess
continue;
}
GenerateStatus::NotUniqueSolution => { // We need more guesses
grid = grid_clone;
continue 'outer;
}
}
};
// If we reach this point in the loop, then none of the possibilities for cell provided any solution
// Which means something serious happened before in the solving process - reason for panic
eprint!("No valid hints were found for puzzle\n{} at cell ({}, {})", grid, cell.x, cell.y);
panic!("Unable to continue as puzzle is invalid");
};
// At this point we have a valid puzzle, but from experience it has way too many guesses, and many of them
// are likely not needed. Let's now try removing a bunch.
let mut non_empty_cells = Vec::new(); let mut non_empty_cells = Vec::new();
for x in 0..9 { for x in 0..9 {
for y in 0..9 { for y in 0..9 {
let cell = grid.get(x, y).unwrap(); let cell = grid.get(x, y).unwrap();
let value = &*cell.value.borrow(); non_empty_cells.push(Rc::clone(&cell));
match value {
CellValue::Fixed(_) => {non_empty_cells.push(Rc::clone(&cell))}
CellValue::Unknown(_) => {}
}
} }
} }
// Need to randomly reorder non_empty_cells // Need to randomly reorder non_empty_cells
@ -253,92 +131,132 @@ pub fn generate_grid(rng: &mut ChaCha8Rng) -> (Grid, i32) {
cell_clone.delete_value(); cell_clone.delete_value();
let status = solve_grid(&mut grid_clone);
match status {
GenerateStatus::UniqueSolution => { // great; that cell value was not needed
num_hints = num_hints - 1;
grid = grid_clone;
let status = evaluate_grid_with_solve_controller(&mut grid_clone, solve_controller);
match status {
SolveStatus::Complete(uniqueness) => {
let uniqueness = uniqueness.unwrap();
match uniqueness {
Uniqueness::Unique => {
num_hints = num_hints - 1;
grid = grid_clone;
}
Uniqueness::NotUnique => continue // We can't remove this cell; continue onto the next one (note that grid hasn't been modified because of solve_controller)
}
} }
GenerateStatus::Unfinished => {panic!("solve_grid should never return UNFINISHED")} SolveStatus::Unfinished => panic!("evaluate_grid_with_solve_controller should never return UNFINISHED"),
GenerateStatus::NoSolution => {panic!("Removing constraints should not have set the # of solutions to zero")} SolveStatus::Invalid => panic!("Removing constraints should not have set the # of solutions to zero")
GenerateStatus::NotUniqueSolution => {continue;} // We can't remove this cell; continue onto the next one (note that grid hasn't been modified) }
};
} }
return (grid, num_hints); return (grid, num_hints);
} }
fn solve_grid(grid: &Grid) -> GenerateStatus{ // 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
// Code is kind of messy so here it goes - solve_grid first tries to solve without any guesses fn generate_completed_grid(rng: &mut ChaCha8Rng) -> Grid {
// If that's not enough and a guess is required, then solve_grid_guess is called let solve_controller = SolveController{
// solve_grid_guess runs through all the possibilities for the smallest cell, trying to solve them determine_uniqueness: true,
// through calling this function. search_singles: true,
// solve_grid_no_guess tries to solve without any guesses. search_hidden_singles: true,
find_possibility_groups: true,
let mut grid = grid.clone(); // We're generating a result and don't want to make changes to our input search_useful_constraint: true,
make_guesses: true
let mut status = solve_grid_no_guess(&mut grid).map_to_generate_status();
status = match status {
GenerateStatus::Unfinished => {
solve_grid_guess(&mut grid)
},
_ => {status}
}; };
match status { let mut grid : Grid = loop {
GenerateStatus::Unfinished => panic!("solve_grid_guess should never return UNFINISHED"), // First step; randomly assign 8 different digits to different empty cells and see if there's a possible solution
_ => return status // We have to ensure that 8 of the digits appear at least once, otherwise the solution can't be unique because you could interchange the two missing digits throughout the puzzle
} // We do this in a loop so that if we are really unlucky and our guesses stop there from being any solution, we can easily re-run it
} let grid = Grid::new();
fn solve_grid_guess(grid: &Grid) -> GenerateStatus{ let digit_excluded = rng.gen_range(1, 10);
let smallest_cell = find_smallest_cell(grid);
let smallest_cell = match smallest_cell {
Some(cell) => cell,
None => return GenerateStatus::NoSolution
};
let possibilities = smallest_cell.get_value_possibilities().unwrap(); for digit in 1..10 {
if digit != digit_excluded {
let mut current_status = GenerateStatus::Unfinished; let cell = grid.get_random_empty_cell(rng);
cell.unwrap().set(digit);
for (_index, &digit) in possibilities.iter().enumerate() { }
let mut grid_copy = grid.clone();
grid_copy.get(smallest_cell.x, smallest_cell.y).unwrap().set(digit);
let status = solve_grid(&mut grid_copy);
current_status = current_status.increment(status);
match current_status {
GenerateStatus::NotUniqueSolution => return GenerateStatus::NotUniqueSolution, // We have our answer; return it
GenerateStatus::UniqueSolution => {continue}, // Still looking to see if solution is unique
GenerateStatus::NoSolution => {panic!("current_status should not be NO_SOLUTION at this point")},
GenerateStatus::Unfinished => {continue} // Still looking for a solution
} }
}
// We've tried all the possibilities for this guess let status = evaluate_grid_with_solve_controller(&grid, &solve_controller);
match current_status { match status {
GenerateStatus::NotUniqueSolution => return current_status, SolveStatus::Complete(uniqueness) => {
GenerateStatus::Unfinished => return GenerateStatus::NoSolution, // nothing panned out; last guess is a bust let uniqueness = uniqueness.unwrap();
GenerateStatus::UniqueSolution => return current_status, // Hey! Looks good! match uniqueness {
GenerateStatus::NoSolution => {panic!("current_status should not be NO_SOLUTION at this point")} Uniqueness::Unique => {
} eprintln!("Wow! A puzzle with only 8 guesses have been found");
return grid;
}
Uniqueness::NotUnique => {break grid;} // What we expect
}
}
SolveStatus::Unfinished => {panic!("evaluate_grid_with_solve_controller should never return UNFINISHED if we are making guesses")}
SolveStatus::Invalid => {continue;} // unlucky; try again
}
};
// Alright, we now have a grid that we can start adding more guesses onto until we find a unique solution
grid =
'outer: loop {
let cell = grid.get_random_empty_cell(rng).unwrap(); // We unwrap because if somehow we're filled each cell without finding a solution, that's reason for a panic
let cell = &*cell;
let mut cell_possibilities = cell.get_value_possibilities().expect("An empty cell has no possibilities");
// Let's scramble the order
cell_possibilities.shuffle(rng);
for (_index, digit) in cell_possibilities.iter().enumerate() {
let mut grid_clone = grid.clone();
let cell = &*grid_clone.get(cell.x, cell.y).unwrap();
cell.set(*digit);
let status = evaluate_grid_with_solve_controller(&mut grid_clone, &solve_controller);
match status {
SolveStatus::Complete(uniqueness) => {
let uniqueness = uniqueness.unwrap();
match uniqueness {
Uniqueness::Unique => {break 'outer grid_clone;} // We're done!
Uniqueness::NotUnique => {// We need more guesses
grid = grid_clone;
continue 'outer;
}
}
}
SolveStatus::Unfinished => panic!("evaluate_grid_with_solve_controller should never return UNFINISHED if making guesses"),
SolveStatus::Invalid => continue // Try another guess
}
};
// If we reach this point in the loop, then none of the possibilities for cell provided any solution
// Which means something serious happened before in the solving process - reason for panic
eprint!("No valid hints were found for puzzle\n{} at cell ({}, {})", grid, cell.x, cell.y);
panic!("Unable to continue as puzzle is invalid");
};
crate::solver::solve_grid(&mut grid);
return grid;
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::grid::*; use crate::grid::*;
use crate::generator::{solve_grid, GenerateStatus}; use crate::solver::{solve_grid_with_solve_controller, SolveController, Uniqueness, SolveStatus};
use crate::generator::generate_grid;
use rand_chacha::ChaCha8Rng;
use rand_chacha::rand_core::SeedableRng;
#[test] #[test]
fn test_unique_detection() { fn test_unique_detection() {
// A puzzle was generated that didn't actually have a unique solution; this is to make sure that the // A puzzle was generated that didn't actually have a unique solution; this is to make sure that the
// modified solving code can actually detect this case // modified solving code can actually detect this case
let grid = Grid::new(); let mut grid = Grid::new();
grid.get(0, 0).unwrap().set(9); grid.get(0, 0).unwrap().set(9);
grid.get(0, 7).unwrap().set(4); grid.get(0, 7).unwrap().set(4);
@ -372,9 +290,51 @@ mod tests {
grid.get(8, 2).unwrap().set(6); grid.get(8, 2).unwrap().set(6);
let status = solve_grid(&grid); let status = solve_grid_with_solve_controller(&mut grid, &SolveController{
determine_uniqueness: true,
search_singles: true,
search_hidden_singles: true,
find_possibility_groups: true,
search_useful_constraint: true,
make_guesses: true
});
assert_eq!(status, GenerateStatus::NotUniqueSolution); assert_eq!(status, SolveStatus::Complete(Some(Uniqueness::NotUnique)));
}
// There was a bug where even though mutate_grid was set to false, the end result was still solved
#[test]
fn ensure_grid_not_complete(){
let solve_controller = SolveController{
determine_uniqueness: true,
search_singles: true,
search_hidden_singles: true,
find_possibility_groups: true,
search_useful_constraint: true,
make_guesses: true
};
// Note that the puzzle itself doesn't matter
let (grid, _num_hints) = generate_grid(&mut ChaCha8Rng::seed_from_u64(123), &solve_controller);
let mut observed_empty_cell = false;
'outer : for x in 0..9 {
for y in 0..9 {
let cell = grid.get(x, y).unwrap();
let value = cell.get_value_copy();
match value {
CellValue::Fixed(_) => {}
CellValue::Unknown(_) => {
observed_empty_cell = true;
break 'outer;
}
}
}
}
assert!(observed_empty_cell);
} }
} }

File diff suppressed because it is too large Load diff