Difficulty settings now look for a minimum number of complicated Solve Actions

This commit is contained in:
Joel Therrien 2020-09-24 15:03:38 -07:00
parent fa370ed9a9
commit b933f4c281
3 changed files with 156 additions and 47 deletions

View file

@ -3,7 +3,7 @@ use rand::prelude::*;
use sudoku_solver::grid::{Grid, CellValue};
use std::error::Error;
use std::io::Write;
use sudoku_solver::solver::SolveController;
use sudoku_solver::solver::{SolveController, SolveStatistics};
use std::str::FromStr;
#[derive(Clone)] // Needed for argparse
@ -38,6 +38,18 @@ impl Difficulty {
controller
}
fn meets_minimum_requirements(&self, solve_statistics: &SolveStatistics) -> bool {
match self {
Difficulty::Hard => {
(solve_statistics.guesses > 0) && (solve_statistics.possibility_groups > 10) && (solve_statistics.useful_constraints > 10)
}
Difficulty::Medium => {
(solve_statistics.possibility_groups > 10) && (solve_statistics.useful_constraints > 10)
}
Difficulty::Easy => {true} // easy has no minimum
}
}
}
impl FromStr for Difficulty { // Needed for argparse
@ -92,6 +104,7 @@ fn main() {
ap.parse_args_or_exit();
}
/*
if debug {
unsafe {
sudoku_solver::grid::DEBUG = true;
@ -99,6 +112,8 @@ fn main() {
sudoku_solver::generator::DEBUG = true;
}
}
*/
if debug {
println!("Using seed {}", seed);
@ -111,25 +126,34 @@ fn main() {
let mut num_attempts = 0;
let grid = loop {
let (grid, solve_statistics) = loop {
if num_attempts >= max_attempts{
println!("Unable to find a puzzle with only {} hints in {} attempts", max_hints, max_attempts);
return;
}
let (grid, num_hints) = sudoku_solver::generator::generate_grid(&mut rng, &solve_controller);
let (grid, num_hints, solve_statistics) = sudoku_solver::generator::generate_grid(&mut rng, &solve_controller);
num_attempts = num_attempts + 1;
if num_hints <= max_hints {
if difficulty.meets_minimum_requirements(&solve_statistics) && num_hints <= max_hints {
println!("{}", grid);
println!("Puzzle has {} hints", num_hints);
if num_attempts > 1 {
println!("It took {} attempts to find this puzzle.", num_attempts);
}
break grid;
break (grid, solve_statistics);
}
};
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);
}
match filename {
Some(filename) => {
// check if we save to a csv or a pdf

View file

@ -1,5 +1,5 @@
use crate::grid::{Cell, Grid, CellValue, Line};
use crate::solver::{SolveStatus, SolveController, Uniqueness, evaluate_grid_with_solve_controller};
use crate::solver::{SolveStatus, SolveController, Uniqueness, evaluate_grid_with_solve_controller, SolveStatistics};
use std::rc::Rc;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
@ -108,7 +108,7 @@ impl Line {
}
}
pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -> (Grid, i32) {
pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -> (Grid, i32, SolveStatistics) {
let mut grid = generate_completed_grid(rng);
let mut num_hints = 81;
@ -124,6 +124,8 @@ pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -
// Need to randomly reorder non_empty_cells
non_empty_cells.shuffle(rng);
let mut statistics_option = None;
for (_index, cell) in non_empty_cells.iter().enumerate() {
let mut grid_clone = grid.clone();
let cell_clone = grid_clone.get(cell.x, cell.y).unwrap();
@ -132,7 +134,7 @@ pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -
cell_clone.delete_value();
let status = evaluate_grid_with_solve_controller(&mut grid_clone, solve_controller);
let (status, statistics) = evaluate_grid_with_solve_controller(&mut grid_clone, solve_controller);
match status {
SolveStatus::Complete(uniqueness) => {
let uniqueness = uniqueness.unwrap();
@ -147,9 +149,10 @@ pub fn generate_grid(rng: &mut ChaCha8Rng, solve_controller: &SolveController) -
SolveStatus::Unfinished => panic!("evaluate_grid_with_solve_controller should never return UNFINISHED"),
SolveStatus::Invalid => panic!("Removing constraints should not have set the # of solutions to zero")
}
statistics_option = Some(statistics);
}
return (grid, num_hints);
return (grid, num_hints, statistics_option.unwrap());
}
@ -179,7 +182,7 @@ fn generate_completed_grid(rng: &mut ChaCha8Rng) -> Grid {
}
}
let status = evaluate_grid_with_solve_controller(&grid, &solve_controller);
let (status, _statistics) = evaluate_grid_with_solve_controller(&grid, &solve_controller);
match status {
SolveStatus::Complete(uniqueness) => {
let uniqueness = uniqueness.unwrap();
@ -213,7 +216,7 @@ fn generate_completed_grid(rng: &mut ChaCha8Rng) -> Grid {
cell.set(*digit);
let status = evaluate_grid_with_solve_controller(&mut grid_clone, &solve_controller);
let (status, _statistics) = evaluate_grid_with_solve_controller(&mut grid_clone, &solve_controller);
match status {
SolveStatus::Complete(uniqueness) => {
let uniqueness = uniqueness.unwrap();
@ -247,7 +250,7 @@ fn generate_completed_grid(rng: &mut ChaCha8Rng) -> Grid {
#[cfg(test)]
mod tests {
use crate::grid::*;
use crate::solver::{solve_grid_with_solve_controller, SolveController, Uniqueness, SolveStatus};
use crate::solver::{solve_grid_with_solve_controller, SolveController, Uniqueness, SolveStatus, SolveStatistics};
use crate::generator::generate_grid;
use rand_chacha::ChaCha8Rng;
use rand_chacha::rand_core::SeedableRng;
@ -297,7 +300,7 @@ mod tests {
find_possibility_groups: true,
search_useful_constraint: true,
make_guesses: true
});
}, &mut SolveStatistics::new());
assert_eq!(status, SolveStatus::Complete(Some(Uniqueness::NotUnique)));
@ -316,7 +319,7 @@ mod tests {
};
// Note that the puzzle itself doesn't matter
let (grid, _num_hints) = generate_grid(&mut ChaCha8Rng::seed_from_u64(123), &solve_controller);
let (grid, _num_hints, _statistics) = generate_grid(&mut ChaCha8Rng::seed_from_u64(123), &solve_controller);
let mut observed_empty_cell = false;
'outer : for x in 0..9 {

View file

@ -17,6 +17,15 @@ pub enum SolveStatus {
Invalid
}
enum SolveAction{
Single,
HiddenSingle,
PossibilityGroup,
UsefulConstraints,
Guess
}
impl SolveStatus {
fn increment(self, additional_status : SolveStatus) -> SolveStatus {
@ -64,9 +73,7 @@ impl SolveController {
}
fn search_hidden_singles(&self) -> bool {
// search_hidden_singles is a special case of find_possibility_groups, so if find_possibility_groups
// is enabled then it's a waste of resources to keep this on
self.search_hidden_singles && !self.find_possibility_groups
self.search_hidden_singles
}
fn find_possibility_groups(&self) -> bool {
@ -82,6 +89,40 @@ impl SolveController {
}
}
/**
Tracks when we relied on a method to make progress. We'll consider 'relied on' to mean that the method make at least
one change to the line it was originally called on, whether that be setting a value or adjusting the possibilities in a cell.
*/
#[derive(Copy, Clone)]
pub struct SolveStatistics {
pub singles: u32,
pub hidden_singles: u32,
pub possibility_groups: u32,
pub useful_constraints: u32,
pub guesses: u32
}
impl SolveStatistics {
pub(crate) fn new() -> SolveStatistics {
SolveStatistics{
singles: 0,
hidden_singles: 0,
possibility_groups: 0,
useful_constraints: 0,
guesses: 0
}
}
fn increment(&mut self, action: &SolveAction) {
match action {
SolveAction::Single => {self.singles = self.singles + 1}
SolveAction::HiddenSingle => {self.hidden_singles = self.hidden_singles + 1}
SolveAction::PossibilityGroup => {self.possibility_groups = self.possibility_groups + 1}
SolveAction::UsefulConstraints => {self.useful_constraints = self.useful_constraints + 1}
SolveAction::Guess => {self.guesses = self.guesses + 1}
}
}
}
pub fn find_smallest_cell(grid: &Grid) -> Option<Rc<Cell>>{
// Find a cell of smallest size (in terms of possibilities) and make a guess
@ -154,17 +195,17 @@ mod process_possibility_groups {
// See if there's a set of cells with possibilities that exclude those possibilities from other cells.
// Runs recursively on each group to identify all groups in case there's more than 2.
pub fn identify_and_process_possibility_groups(line: &Line){
pub fn identify_and_process_possibility_groups(line: &Line) -> bool{
unsafe {
if super::DEBUG {
println!("Looking for possibility groups on line {:?} {}", line.line_type, line.index);
}
}
bisect_possibility_groups(line, vec![0, 1, 2, 3, 4, 5, 6, 7, 8]);
bisect_possibility_groups(line, vec![0, 1, 2, 3, 4, 5, 6, 7, 8])
}
fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec<usize>){
fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec<usize>) -> bool{
/*
Algorithm -
@ -180,6 +221,8 @@ mod process_possibility_groups {
let mut out_group_indices = Vec::new();
let mut run_recursion = false;
let mut made_change = false;
{
// <Setup>
let mut count = 0;
@ -219,7 +262,7 @@ mod process_possibility_groups {
// No point in continuing.
if faux_line.num_out_group() <= 2 {
return;
return made_change;
}
// A kind of do-while loop
@ -301,6 +344,7 @@ mod process_possibility_groups {
}
if possibilities.len() < starting_possibility_size { // We have a change to make
made_change = true;
let new_value = {
if possibilities.len() == 1 {
CellValue::Fixed(possibilities.pop().unwrap())
@ -331,32 +375,39 @@ mod process_possibility_groups {
bisect_possibility_groups(line, in_group_indices);
bisect_possibility_groups(line, out_group_indices);
}
return made_change;
}
}
// Search for a cell with only one possibility so that we can set it to FIXED
fn search_single_possibility(line: &Line){
fn search_single_possibility(line: &Line) -> bool{
unsafe {
if DEBUG {
println!("search_single_possibility on line {:?} {}", line.line_type, line.index);
}
}
let mut made_change = false;
for (_index, cell) in line.vec.iter().enumerate(){
match cell.get_value_possibilities(){
Some(x) => {
if x.len() == 1 {
let new_value = CellValue::Fixed(x.first().unwrap().clone());
cell.set_value(new_value);
made_change = true;
}
},
None => {}
}
}
return made_change;
}
// Count up how many times each possibility occurs in the Line. If it only occurs once, that's a hidden single that we can set
fn search_hidden_single(line: &Line){
fn search_hidden_single(line: &Line) -> bool{
enum Count {
None,
One(Rc<Cell>),
@ -373,6 +424,8 @@ fn search_hidden_single(line: &Line){
}
}
let mut made_change = false;
let mut counts = [Count::None, Count::None, Count::None, Count::None, Count::None, Count::None, Count::None, Count::None, Count::None];
for (_index, cell) in line.vec.iter().enumerate() {
@ -393,11 +446,13 @@ fn search_hidden_single(line: &Line){
match count {
Count::One(cell) => {
cell.set((digit + 1) as u8);
made_change = true;
},
_ => {}
}
}
return made_change;
}
mod search_useful_constraint{
@ -424,13 +479,15 @@ mod search_useful_constraint{
// I.e. If possibility '1' only occurs in the first row for section 0, then you can remove that possibility
// from row 0 across the other sections. Conversely, if the possibility only occurs in the first section
// for row 0, then you can remove the possibility from the rest of section 0.
pub fn search_useful_constraint(grid: &Grid, line: &Line){
pub fn search_useful_constraint(grid: &Grid, line: &Line) -> bool{
unsafe {
if super::DEBUG {
println!("Searching for a useful constraint on line {:?} {}", line.line_type, line.index);
}
}
let mut made_change = false;
let (check_row, check_column, check_section) = match line.line_type {
LineType::Row => {(false, false, true)},
LineType::Column => {(false, false, true)},
@ -474,29 +531,35 @@ mod search_useful_constraint{
// Check each line and see if we can determine anything
match rows {
PossibilityLines::Unique(index) => {
remove_possibilities_line(grid.rows.get(index).unwrap(), possibility, &line.line_type, line.index);
made_change = made_change |
remove_possibilities_line(grid.rows.get(index).unwrap(), possibility, &line.line_type, line.index);
},
_ => {}
}
match columns {
PossibilityLines::Unique(index) => {
remove_possibilities_line(grid.columns.get(index).unwrap(), possibility, &line.line_type, line.index);
made_change = made_change |
remove_possibilities_line(grid.columns.get(index).unwrap(), possibility, &line.line_type, line.index);
},
_ => {}
}
match sections {
PossibilityLines::Unique(index) => {
remove_possibilities_line(grid.sections.get(index).unwrap(), possibility, &line.line_type, line.index);
made_change = made_change |
remove_possibilities_line(grid.sections.get(index).unwrap(), possibility, &line.line_type, line.index);
},
_ => {}
}
}
return made_change;
}
// initial_line_type and initial_line_index are to identify the cells that should NOT have their possibilities removed
fn remove_possibilities_line(line: &Rc<RefCell<Line>>, digit_to_remove: u8, initial_line_type: &LineType, initial_line_index: usize) {
fn remove_possibilities_line(line: &Rc<RefCell<Line>>, digit_to_remove: u8, initial_line_type: &LineType, initial_line_index: usize) -> bool {
let line = &*(&**line).borrow();
let mut made_change = false;
for (_index, cell) in line.vec.iter().enumerate() {
let new_value = {
@ -538,8 +601,11 @@ mod search_useful_constraint{
};
cell.set_value(new_value);
made_change = true;
}
return made_change;
}
// We detected a useful constraint
@ -563,7 +629,7 @@ mod search_useful_constraint{
}
fn solve_line(grid: &Grid, line: &Line, solve_controller: &SolveController){
fn solve_line(grid: &Grid, line: &Line, solve_controller: &SolveController, solve_statistics: &mut SolveStatistics){
unsafe {
if DEBUG {
println!("Solving {:?} {}", line.line_type, line.index);
@ -578,7 +644,9 @@ fn solve_line(grid: &Grid, line: &Line, solve_controller: &SolveController){
println!("Searching for singles on line {:?} of {}\n{}", line.line_type, line.index, grid);
}
}
search_single_possibility(line);
if search_single_possibility(line) {
solve_statistics.increment(&SolveAction::Single);
}
}
if solve_controller.search_hidden_singles() {
@ -587,7 +655,9 @@ fn solve_line(grid: &Grid, line: &Line, solve_controller: &SolveController){
println!("Searching for hidden singles on line {:?} of {}\n{}", line.line_type, line.index, grid);
}
}
search_hidden_single(line);
if search_hidden_single(line) {
solve_statistics.increment(&SolveAction::HiddenSingle);
}
}
if solve_controller.find_possibility_groups() {
@ -596,7 +666,9 @@ fn solve_line(grid: &Grid, line: &Line, solve_controller: &SolveController){
println!("Searching for possibility groups on line {:?} of {}\n{}", line.line_type, line.index, grid);
}
}
process_possibility_groups::identify_and_process_possibility_groups(line);
if process_possibility_groups::identify_and_process_possibility_groups(line) {
solve_statistics.increment(&SolveAction::PossibilityGroup);
}
}
if solve_controller.search_useful_constraint() {
@ -605,12 +677,14 @@ fn solve_line(grid: &Grid, line: &Line, solve_controller: &SolveController){
println!("Searching for useful constraints on line {:?} of {}\n{}", line.line_type, line.index, grid);
}
}
search_useful_constraint::search_useful_constraint(grid, line);
if search_useful_constraint::search_useful_constraint(grid, line) {
solve_statistics.increment(&SolveAction::UsefulConstraints);
}
}
}
pub fn solve_grid(grid: &mut Grid) -> SolveStatus {
pub fn solve_grid(grid: &mut Grid) -> (SolveStatus, SolveStatistics) {
// By default we enable everything
let solve_controller = SolveController {
determine_uniqueness: true,
@ -621,11 +695,13 @@ pub fn solve_grid(grid: &mut Grid) -> SolveStatus {
make_guesses: true
};
solve_grid_with_solve_controller(grid, &solve_controller)
let mut solve_statistics = SolveStatistics::new();
let solve_status = solve_grid_with_solve_controller(grid, &solve_controller, &mut solve_statistics);
return (solve_status, solve_statistics);
}
pub fn solve_grid_with_solve_controller(grid: &mut Grid, solve_controller: &SolveController) -> SolveStatus{
pub fn solve_grid_with_solve_controller(grid: &mut Grid, solve_controller: &SolveController, solve_statistics: &mut SolveStatistics) -> SolveStatus{
// Code is kind of messy so here it goes - solve_grid first tries to solve without any guesses
// If that's not enough and a guess is required, then solve_grid_guess is called
// solve_grid_guess runs through all the possibilities for the smallest cell, trying to solve them
@ -633,11 +709,11 @@ pub fn solve_grid_with_solve_controller(grid: &mut Grid, solve_controller: &Solv
// solve_grid_no_guess tries to solve without any guesses.
// Of course this is if the solve_controller lets everything be used for solving it
let mut status = solve_grid_no_guess(grid, solve_controller);
let mut status = solve_grid_no_guess(grid, solve_controller, solve_statistics);
status = match status {
SolveStatus::Unfinished => {
if solve_controller.make_guesses() {
solve_grid_guess(grid, solve_controller)
solve_grid_guess(grid, solve_controller, solve_statistics)
} else {
SolveStatus::Complete(Some(Uniqueness::NotUnique)) // solve_grid_no_guess couldn't finish and we can't make guesses, so it's 'not unique' in the sense that we need more guesses
}
@ -649,12 +725,16 @@ pub fn solve_grid_with_solve_controller(grid: &mut Grid, solve_controller: &Solv
}
// Similar to solve_grid_with_solve_controller except that we don't modify the input Grid; we only determine SolveStatus
pub fn evaluate_grid_with_solve_controller(grid: &Grid, solve_controller: &SolveController) -> SolveStatus{
pub fn evaluate_grid_with_solve_controller(grid: &Grid, solve_controller: &SolveController) -> (SolveStatus, SolveStatistics){
let mut mut_grid = grid.clone();
return solve_grid_with_solve_controller(&mut mut_grid, solve_controller);
let mut solve_statistics = SolveStatistics::new();
let solve_status = solve_grid_with_solve_controller(&mut mut_grid, solve_controller, &mut solve_statistics);
return (solve_status, solve_statistics);
}
pub fn solve_grid_no_guess(grid: &mut Grid, solve_controller: &SolveController) -> SolveStatus{
pub fn solve_grid_no_guess(grid: &mut Grid, solve_controller: &SolveController, solve_statistics: &mut SolveStatistics) -> SolveStatus{
loop {
let mut ran_something = false;
@ -662,7 +742,7 @@ pub fn solve_grid_no_guess(grid: &mut Grid, solve_controller: &SolveController)
//println!("Processing row {}", _index);
let line_ref = &*(&**line_ref).borrow();
if line_ref.do_update() {
solve_line(&grid, line_ref, solve_controller);
solve_line(&grid, line_ref, solve_controller, solve_statistics);
ran_something = true;
}
}
@ -670,7 +750,7 @@ pub fn solve_grid_no_guess(grid: &mut Grid, solve_controller: &SolveController)
//println!("Processing column {}", _index);
let line_ref = &*(&**line_ref).borrow();
if line_ref.do_update() {
solve_line(&grid, line_ref, solve_controller);
solve_line(&grid, line_ref, solve_controller, solve_statistics);
ran_something = true;
}
}
@ -678,7 +758,7 @@ pub fn solve_grid_no_guess(grid: &mut Grid, solve_controller: &SolveController)
//println!("Processing section {}", _index);
let line_ref = &*(&**line_ref).borrow();
if line_ref.do_update() {
solve_line(&grid, line_ref, solve_controller);
solve_line(&grid, line_ref, solve_controller, solve_statistics);
ran_something = true;
}
}
@ -717,7 +797,9 @@ pub fn solve_grid_no_guess(grid: &mut Grid, solve_controller: &SolveController)
}
fn solve_grid_guess(grid: &mut Grid, solve_controller: &SolveController) -> SolveStatus{
fn solve_grid_guess(grid: &mut Grid, solve_controller: &SolveController, solve_statistics: &mut SolveStatistics) -> SolveStatus{
solve_statistics.increment(&SolveAction::Guess);
let smallest_cell = find_smallest_cell(grid);
let smallest_cell = match smallest_cell {
Some(cell) => cell,
@ -733,7 +815,7 @@ fn solve_grid_guess(grid: &mut Grid, solve_controller: &SolveController) -> Solv
let mut grid_copy = grid.clone();
grid_copy.get(smallest_cell.x, smallest_cell.y).unwrap().set(digit);
let status = solve_grid_with_solve_controller(&mut grid_copy, solve_controller);
let status = solve_grid_with_solve_controller(&mut grid_copy, solve_controller, solve_statistics);
// Keep a copy of grid_copy in case we later mutate grid with it
match status {