From b933f4c2811bf1f16d43ffddcba7e61056b2b3c8 Mon Sep 17 00:00:00 2001 From: Joel Therrien Date: Thu, 24 Sep 2020 15:03:38 -0700 Subject: [PATCH] Difficulty settings now look for a minimum number of complicated Solve Actions --- src/bin/generator.rs | 34 ++++++++-- src/generator.rs | 21 +++--- src/solver.rs | 148 +++++++++++++++++++++++++++++++++---------- 3 files changed, 156 insertions(+), 47 deletions(-) diff --git a/src/bin/generator.rs b/src/bin/generator.rs index 543e1c0..b264610 100644 --- a/src/bin/generator.rs +++ b/src/bin/generator.rs @@ -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 diff --git a/src/generator.rs b/src/generator.rs index 0baf0c8..426af89 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -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 { diff --git a/src/solver.rs b/src/solver.rs index 961a8e7..7974435 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -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>{ // 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){ + fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec) -> 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; + { // 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), @@ -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>, digit_to_remove: u8, initial_line_type: &LineType, initial_line_index: usize) { + fn remove_possibilities_line(line: &Rc>, 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 {