From fa370ed9a97852d5123896d4c3d5d83fa9ba319f Mon Sep 17 00:00:00 2001 From: Joel Therrien Date: Thu, 24 Sep 2020 13:48:37 -0700 Subject: [PATCH] Add support for setting max difficulty. TODO is to support for min difficulty. --- src/bin/generator.rs | 67 ++- src/generator.rs | 340 ++++++------- src/solver.rs | 1104 +++++++++++++++++++++++++----------------- 3 files changed, 881 insertions(+), 630 deletions(-) diff --git a/src/bin/generator.rs b/src/bin/generator.rs index 5647d7e..543e1c0 100644 --- a/src/bin/generator.rs +++ b/src/bin/generator.rs @@ -3,6 +3,59 @@ use rand::prelude::*; use sudoku_solver::grid::{Grid, CellValue}; use std::error::Error; 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 { + + 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() { @@ -13,6 +66,7 @@ fn main() { let mut max_hints = 81; let mut max_attempts = 100; let mut filename : Option = None; + let mut difficulty = Difficulty::Hard; { // this block limits scope of borrows by ap.refer() method let mut ap = argparse::ArgumentParser::new(); @@ -21,7 +75,7 @@ fn main() { .add_option(&["--debug"], argparse::StoreTrue, "Run in debug mode"); 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) .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) .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(); } @@ -46,6 +103,10 @@ fn main() { if debug { println!("Using seed {}", seed); } + + let solve_controller = difficulty.map_to_solve_controller(); + + let mut rng = ChaCha8Rng::seed_from_u64(seed); let mut num_attempts = 0; @@ -56,7 +117,7 @@ fn main() { 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; if num_hints <= max_hints { @@ -110,4 +171,4 @@ fn save_grid_csv(grid: &Grid, filename: &str) -> Result<(), Box>{ } Ok(()) -} \ No newline at end of file +} diff --git a/src/generator.rs b/src/generator.rs index 3967936..0baf0c8 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,56 +1,11 @@ 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 rand::prelude::*; use rand_chacha::ChaCha8Rng; 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 { fn get_random_empty_cell(&self, rng : &mut ChaCha8Rng) -> Result, &str> { // 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 : Grid = loop { - // 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 mut grid = generate_completed_grid(rng); + let mut num_hints = 81; - let digit_excluded = rng.gen_range(1, 10); - - 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. + // We now trim down cells; first going to put them in a vector and shuffle them let mut non_empty_cells = Vec::new(); for x in 0..9 { for y in 0..9 { let cell = grid.get(x, y).unwrap(); - let value = &*cell.value.borrow(); - match value { - CellValue::Fixed(_) => {non_empty_cells.push(Rc::clone(&cell))} - CellValue::Unknown(_) => {} - } + non_empty_cells.push(Rc::clone(&cell)); } } // Need to randomly reorder non_empty_cells @@ -253,92 +131,132 @@ pub fn generate_grid(rng: &mut ChaCha8Rng) -> (Grid, i32) { 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")} - GenerateStatus::NoSolution => {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) - }; + 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") + } } return (grid, num_hints); } -fn solve_grid(grid: &Grid) -> GenerateStatus{ - // 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 - // through calling this function. - // solve_grid_no_guess tries to solve without any guesses. - - let mut grid = grid.clone(); // We're generating a result and don't want to make changes to our input - - let mut status = solve_grid_no_guess(&mut grid).map_to_generate_status(); - status = match status { - GenerateStatus::Unfinished => { - solve_grid_guess(&mut grid) - }, - _ => {status} +// 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 { + 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 }; - match status { - GenerateStatus::Unfinished => panic!("solve_grid_guess should never return UNFINISHED"), - _ => return status - } -} + let mut grid : Grid = loop { + // 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 grid = Grid::new(); -fn solve_grid_guess(grid: &Grid) -> GenerateStatus{ - let smallest_cell = find_smallest_cell(grid); - let smallest_cell = match smallest_cell { - Some(cell) => cell, - None => return GenerateStatus::NoSolution - }; + let digit_excluded = rng.gen_range(1, 10); - let possibilities = smallest_cell.get_value_possibilities().unwrap(); - - let mut current_status = GenerateStatus::Unfinished; - - 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 + for digit in 1..10 { + if digit != digit_excluded { + let cell = grid.get_random_empty_cell(rng); + cell.unwrap().set(digit); + } } - } - // We've tried all the possibilities for this guess - match current_status { - GenerateStatus::NotUniqueSolution => return current_status, - GenerateStatus::Unfinished => return GenerateStatus::NoSolution, // nothing panned out; last guess is a bust - GenerateStatus::UniqueSolution => return current_status, // Hey! Looks good! - GenerateStatus::NoSolution => {panic!("current_status should not be NO_SOLUTION at this point")} - } + let status = evaluate_grid_with_solve_controller(&grid, &solve_controller); + match status { + SolveStatus::Complete(uniqueness) => { + let uniqueness = uniqueness.unwrap(); + match uniqueness { + 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)] mod tests { 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] fn test_unique_detection() { // 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 - let grid = Grid::new(); + let mut grid = Grid::new(); grid.get(0, 0).unwrap().set(9); grid.get(0, 7).unwrap().set(4); @@ -372,9 +290,51 @@ mod tests { 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); } } \ No newline at end of file diff --git a/src/solver.rs b/src/solver.rs index 5ae6b8b..961a8e7 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -1,431 +1,88 @@ -use std::rc::{Rc, Weak}; -use std::cell::{RefCell}; -use std::collections::HashSet; - -use crate::grid::{Cell, Line, Grid, CellValue, LineType}; +use std::rc::Rc; +use crate::grid::{Cell, Line, Grid, CellValue}; pub static mut DEBUG: bool = false; -struct FauxCell{ - index: usize, - possibilities: HashSet, - in_group: bool +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Uniqueness { + Unique, + NotUnique } -impl FauxCell { - fn len(&self) -> usize { - self.possibilities.len() - } - - fn remove(&mut self, to_remove: &HashSet){ - to_remove.iter().for_each(|digit| {self.possibilities.remove(digit);}); - } +#[derive(Eq, PartialEq, Debug)] +pub enum SolveStatus { + Complete(Option), + Unfinished, + Invalid } -struct FauxLine (Vec); +impl SolveStatus { -impl FauxLine { - - fn num_in_group(&self) -> usize { - self.0.iter().filter(|faux_cell| faux_cell.in_group).count() - } - - fn num_out_group(&self) -> usize { - self.0.len() - self.num_in_group() - } -} - -// 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. -fn identify_and_process_possibility_groups(line: &Line){ - unsafe { - if 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]); -} - -fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec){ - - /* - Algorithm - - Setup - Let count = 0 - 1. Choose cell with least number of possibilities. Add to in-group. - 2. Add to count the number of possibilities in that cell - 3. Remove the possibilities of that cell from all other out-group cells. - 4. If the number of cells in group == count, finish. - 5. Goto 1 - */ - // For later recursive calls; put here because of scope reasons - let mut in_group_indices = Vec::new(); - let mut out_group_indices = Vec::new(); - let mut run_recursion = false; - - { - // - let mut count = 0; - let mut faux_line = FauxLine(Vec::new()); - - for i in 0..9 { - if !cells_of_interest.contains(&i) { - continue; - } - - let cell = line.get(i).unwrap(); - let cell = Rc::clone(cell); - - let faux_possibilities = { - let value = &*cell.value.borrow(); - match value { - CellValue::Unknown(possibilities) => { - let mut set = HashSet::new(); - for (_index, digit) in possibilities.iter().enumerate() { - set.insert(digit.clone()); - } - set - }, - CellValue::Fixed(_) => { continue } - } - }; - - let faux_cell = FauxCell { - index: i, - possibilities: faux_possibilities, - in_group: false - }; - - faux_line.0.push(faux_cell); - } - // - - // No point in continuing. - if faux_line.num_out_group() <= 2 { - return; - } - - // A kind of do-while loop - loop { - if faux_line.num_out_group() == 0 { - break; - } - - // Step 1 - let mut smallest_cell: Option<&mut FauxCell> = None; - let mut smallest_size = usize::MAX; - - for (_index, cell) in faux_line.0.iter_mut().filter(|faux_cell| !faux_cell.in_group).enumerate() { - if cell.len() < smallest_size { - smallest_size = cell.len(); - smallest_cell = Some(cell); - } - } - - let smallest_cell = smallest_cell.unwrap(); // Safe because we already verified the out-group had members - smallest_cell.in_group = true; - - // Step 2 - count = count + smallest_size; - - - let possibilities_to_remove = smallest_cell.possibilities.clone(); // Necessary because of mutable borrow rules - - // Step 3 - for (_index, cell) in faux_line.0.iter_mut().filter(|faux_cell| !faux_cell.in_group).enumerate() { - cell.remove(&possibilities_to_remove); - } - - // Step 4 (finish condition) - if faux_line.num_in_group() == count { - break; - } - } - - // Now we have to see if this was worth it - if faux_line.num_out_group() > 0 { // Worth it - // We now have two distinct groups and can separate their possibilities - let mut in_group_possibilities = HashSet::new(); - let mut out_group_possibilities = HashSet::new(); - - // Collect the possibilities for each group - for (_index, cell) in faux_line.0.iter().enumerate() { - if cell.in_group { - cell.possibilities.iter().for_each(|digit| {in_group_possibilities.insert(digit.clone());}); + fn increment(self, additional_status : SolveStatus) -> SolveStatus { + match self { + SolveStatus::Complete(uniqueness_option) => { + if uniqueness_option.is_none() { + return SolveStatus::Complete(None); } else { - cell.possibilities.iter().for_each(|digit| {out_group_possibilities.insert(digit.clone());}); - } - } - - // Now to apply this to the real cells - for (_index, faux_cell) in faux_line.0.iter().enumerate() { - let real_cell = line.get(faux_cell.index).unwrap(); - let mut possibilities = { - let value = &*real_cell.value.borrow(); - match value { - CellValue::Unknown(possibilities) => possibilities.clone(), - CellValue::Fixed(_) => {panic!("Faux_cell shouldn't have linked to fixed cell")} - } - }; - let starting_possibility_size = possibilities.len(); - - let possibilities_to_remove = match faux_cell.in_group { - true => &out_group_possibilities, - false => &in_group_possibilities - }; - - for (_i, possibility) in possibilities_to_remove.iter().enumerate() { - match possibilities.binary_search(possibility) { - Ok(x) => { - possibilities.remove(x); - }, - Err(_) => {} - }; - } - - if possibilities.len() < starting_possibility_size { // We have a change to make - let new_value = { - if possibilities.len() == 1 { - CellValue::Fixed(possibilities.pop().unwrap()) - } else { - CellValue::Unknown(possibilities) + match uniqueness_option.unwrap() { + Uniqueness::NotUnique => SolveStatus::Complete(Some(Uniqueness::NotUnique)), + Uniqueness::Unique => match additional_status { + SolveStatus::Complete(_) => SolveStatus::Complete(Some(Uniqueness::NotUnique)), + SolveStatus::Unfinished => SolveStatus::Complete(Some(Uniqueness::Unique)), + SolveStatus::Invalid => SolveStatus::Complete(Some(Uniqueness::Unique)) } - }; - - real_cell.set_value(new_value); - } - } - - // Now finally, it's possible that there were 3 or more groups while this algorithm only identifies 2 - // So we recursively call it but restricted to each of the groups - run_recursion = true; - for (index, cell) in faux_line.0.iter().enumerate() { - if cell.in_group { - in_group_indices.push(index); - } else { - out_group_indices.push(index); - } - } - } - } - - // Out of scope of everything; we need to check again if it was worth it. - if run_recursion { - bisect_possibility_groups(line, in_group_indices); - bisect_possibility_groups(line, out_group_indices); - } -} - -// Search for a cell with only one possibility so that we can set it to FIXED -fn search_single_possibility(line: &Line){ - unsafe { - if DEBUG { - println!("search_single_possibility on line {:?} {}", line.line_type, line.index); - } - } - - 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); - } - }, - None => {} - } - } -} - -enum PossibilityLines { - Unique(usize), - Invalid, - None -} - -impl PossibilityLines { - fn is_invalid(&self) -> bool { - match &self { - PossibilityLines::Invalid => true, - _ => false - } - } -} - -// If all the cells for a particular possibility share a same other Line, they can remove that possibility from other cells in the main line. -// 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. -fn search_useful_constraint(grid: &Grid, line: &Line){ - unsafe { - if DEBUG { - println!("Searching for a useful constraint on line {:?} {}", line.line_type, line.index); - } - } - - let (check_row, check_column, check_section) = match line.line_type { - LineType::Row => {(false, false, true)}, - LineType::Column => {(false, false, true)}, - LineType::Section => {(true, true, false)}, - }; - - for possibility in 0..9 { - let mut rows = match check_row {true => PossibilityLines::None, false => PossibilityLines::Invalid }; - let mut columns = match check_column {true => PossibilityLines::None, false => PossibilityLines::Invalid }; - let mut sections = match check_section {true => PossibilityLines::None, false => PossibilityLines::Invalid }; - - for cell_id in 0..9 { - let cell_ref = line.get(cell_id).unwrap(); - let cell_ref = Rc::clone(cell_ref); - let cell_ref = &*cell_ref; - let value = &*cell_ref.value.borrow(); - - match value { - CellValue::Fixed(x) => { // We can deduce this possibility won't occur elsewhere in our row, so leave for-loop - if possibility.eq(x) { - rows = process_possibility_line(rows, &cell_ref.row); - columns = process_possibility_line(columns, &cell_ref.column); - sections = process_possibility_line(sections, &cell_ref.section); - break; } } - CellValue::Unknown(digits) => { - if digits.contains(&possibility) { - rows = process_possibility_line(rows, &cell_ref.row); - columns = process_possibility_line(columns, &cell_ref.column); - sections = process_possibility_line(sections, &cell_ref.section); - } - } - } - if rows.is_invalid() & columns.is_invalid() & sections.is_invalid() { - break; } - } - - // 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); + SolveStatus::Unfinished => match additional_status { + SolveStatus::Invalid => SolveStatus::Unfinished, + _ => additional_status }, - _ => {} - } - match columns { - PossibilityLines::Unique(index) => { - 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); - }, - _ => {} - } - } - -} - -// 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) { - let line = &*(&**line).borrow(); - - for (_index, cell) in line.vec.iter().enumerate() { - let new_value = { - let value = &*cell.value.borrow(); - match value { - CellValue::Unknown(possibilities) => { - let parent_line = match initial_line_type { - LineType::Row => &cell.row, - LineType::Column => &cell.column, - LineType::Section => &cell.section - }; - let parent_line = &*parent_line.upgrade().unwrap(); - let parent_line = &*parent_line.borrow(); - if parent_line.index == initial_line_index { - // Don't want to apply to this cell - continue; - } - - let new_possibilities = match possibilities.binary_search(&digit_to_remove) { - Ok(x) => { - let mut new_possibilities = possibilities.clone(); - new_possibilities.remove(x); - new_possibilities - }, - Err(_) => { continue; } - }; - - let new_value; - if new_possibilities.len() == 1 { - new_value = CellValue::Fixed(new_possibilities.first().unwrap().clone()); - } else { - new_value = CellValue::Unknown(new_possibilities); - } - - new_value - }, - _ => { continue; } - } - }; - - cell.set_value(new_value); - - } -} - -// We detected -fn process_possibility_line(possibility_line: PossibilityLines, line: &Weak>) -> PossibilityLines { - let line = line.upgrade().unwrap(); - let line = &*(&*line).borrow(); - - match possibility_line { - PossibilityLines::None => {PossibilityLines::Unique(line.index)}, - PossibilityLines::Invalid => {possibility_line}, - PossibilityLines::Unique(x) => { - if line.index.eq(&x) { - possibility_line - } else { - PossibilityLines::Invalid - } + SolveStatus::Invalid => panic!("increment() shouldn't be called on SolveStatus::Invalid") } } } - -fn solve_line(grid: &Grid, line: &Line){ - unsafe { - if DEBUG { - println!("Solving {:?} {}", line.line_type, line.index); - } - } - - line.do_update.replace(false); - - search_single_possibility(line); - unsafe { - if DEBUG { - println!("{}", grid); - } - } - - identify_and_process_possibility_groups(line); - unsafe { - if DEBUG { - println!("{}", grid); - } - } - - search_useful_constraint(grid, line); - unsafe { - if DEBUG { - println!("{}", grid); - } - } - +pub struct SolveController { + pub determine_uniqueness: bool, + pub search_singles: bool, + pub search_hidden_singles: bool, + pub find_possibility_groups: bool, + pub search_useful_constraint: bool, + pub make_guesses: bool, } +impl SolveController { + fn determine_uniqueness(&self) -> bool { + self.determine_uniqueness + } + + fn search_singles(&self) -> bool { + self.search_singles + } + + 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 + } + + fn find_possibility_groups(&self) -> bool { + self.find_possibility_groups + } + + fn search_useful_constraint(&self) -> bool { + self.search_useful_constraint + } + + fn make_guesses(&self) -> bool { + self.make_guesses + } +} + + pub fn find_smallest_cell(grid: &Grid) -> Option>{ // Find a cell of smallest size (in terms of possibilities) and make a guess // Can assume that no cells of only possibility 1 exist @@ -460,34 +117,544 @@ pub fn find_smallest_cell(grid: &Grid) -> Option>{ } -pub enum SolveStatus { - Complete, - Unfinished, - Invalid +// Code for identify_and_process_possibility_groups (it uses it's own structs) +mod process_possibility_groups { + use crate::grid::{Line, CellValue}; + use std::collections::HashSet; + use std::rc::Rc; + + struct FauxCell{ + index: usize, + possibilities: HashSet, + in_group: bool + } + + impl FauxCell { + fn len(&self) -> usize { + self.possibilities.len() + } + + fn remove(&mut self, to_remove: &HashSet){ + to_remove.iter().for_each(|digit| {self.possibilities.remove(digit);}); + } + } + + struct FauxLine (Vec); + + impl FauxLine { + + fn num_in_group(&self) -> usize { + self.0.iter().filter(|faux_cell| faux_cell.in_group).count() + } + + fn num_out_group(&self) -> usize { + self.0.len() - self.num_in_group() + } + } + + // 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){ + 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]); + } + + fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec){ + + /* + Algorithm - + Setup - Let count = 0 + 1. Choose cell with least number of possibilities. Add to in-group. + 2. Add to count the number of possibilities in that cell + 3. Remove the possibilities of that cell from all other out-group cells. + 4. If the number of cells in group == count, finish. + 5. Goto 1 + */ + // For later recursive calls; put here because of scope reasons + let mut in_group_indices = Vec::new(); + let mut out_group_indices = Vec::new(); + let mut run_recursion = false; + + { + // + let mut count = 0; + let mut faux_line = FauxLine(Vec::new()); + + for i in 0..9 { + if !cells_of_interest.contains(&i) { + continue; + } + + let cell = line.get(i).unwrap(); + let cell = Rc::clone(cell); + + let faux_possibilities = { + let value = &*cell.value.borrow(); + match value { + CellValue::Unknown(possibilities) => { + let mut set = HashSet::new(); + for (_index, digit) in possibilities.iter().enumerate() { + set.insert(digit.clone()); + } + set + }, + CellValue::Fixed(_) => { continue } + } + }; + + let faux_cell = FauxCell { + index: i, + possibilities: faux_possibilities, + in_group: false + }; + + faux_line.0.push(faux_cell); + } + // + + // No point in continuing. + if faux_line.num_out_group() <= 2 { + return; + } + + // A kind of do-while loop + loop { + if faux_line.num_out_group() == 0 { + break; + } + + // Step 1 + let mut smallest_cell: Option<&mut FauxCell> = None; + let mut smallest_size = usize::MAX; + + for (_index, cell) in faux_line.0.iter_mut().filter(|faux_cell| !faux_cell.in_group).enumerate() { + if cell.len() < smallest_size { + smallest_size = cell.len(); + smallest_cell = Some(cell); + } + } + + let smallest_cell = smallest_cell.unwrap(); // Safe because we already verified the out-group had members + smallest_cell.in_group = true; + + // Step 2 + count = count + smallest_size; + + + let possibilities_to_remove = smallest_cell.possibilities.clone(); // Necessary because of mutable borrow rules + + // Step 3 + for (_index, cell) in faux_line.0.iter_mut().filter(|faux_cell| !faux_cell.in_group).enumerate() { + cell.remove(&possibilities_to_remove); + } + + // Step 4 (finish condition) + if faux_line.num_in_group() == count { + break; + } + } + + // Now we have to see if this was worth it + if faux_line.num_out_group() > 0 { // Worth it + // We now have two distinct groups and can separate their possibilities + let mut in_group_possibilities = HashSet::new(); + let mut out_group_possibilities = HashSet::new(); + + // Collect the possibilities for each group + for (_index, cell) in faux_line.0.iter().enumerate() { + if cell.in_group { + cell.possibilities.iter().for_each(|digit| {in_group_possibilities.insert(digit.clone());}); + } else { + cell.possibilities.iter().for_each(|digit| {out_group_possibilities.insert(digit.clone());}); + } + } + + // Now to apply this to the real cells + for (_index, faux_cell) in faux_line.0.iter().enumerate() { + let real_cell = line.get(faux_cell.index).unwrap(); + let mut possibilities = { + let value = &*real_cell.value.borrow(); + match value { + CellValue::Unknown(possibilities) => possibilities.clone(), + CellValue::Fixed(_) => {panic!("Faux_cell shouldn't have linked to fixed cell")} + } + }; + let starting_possibility_size = possibilities.len(); + + let possibilities_to_remove = match faux_cell.in_group { + true => &out_group_possibilities, + false => &in_group_possibilities + }; + + for (_i, possibility) in possibilities_to_remove.iter().enumerate() { + match possibilities.binary_search(possibility) { + Ok(x) => { + possibilities.remove(x); + }, + Err(_) => {} + }; + } + + if possibilities.len() < starting_possibility_size { // We have a change to make + let new_value = { + if possibilities.len() == 1 { + CellValue::Fixed(possibilities.pop().unwrap()) + } else { + CellValue::Unknown(possibilities) + } + }; + + real_cell.set_value(new_value); + } + } + + // Now finally, it's possible that there were 3 or more groups while this algorithm only identifies 2 + // So we recursively call it but restricted to each of the groups + run_recursion = true; + for (index, cell) in faux_line.0.iter().enumerate() { + if cell.in_group { + in_group_indices.push(index); + } else { + out_group_indices.push(index); + } + } + } + } + + // Out of scope of everything; we need to check again if it was worth it. + if run_recursion { + bisect_possibility_groups(line, in_group_indices); + bisect_possibility_groups(line, out_group_indices); + } + } } -pub fn solve_grid(grid: &mut Grid) -> SolveStatus{ +// Search for a cell with only one possibility so that we can set it to FIXED +fn search_single_possibility(line: &Line){ + unsafe { + if DEBUG { + println!("search_single_possibility on line {:?} {}", line.line_type, line.index); + } + } + + 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); + } + }, + None => {} + } + } +} + +// 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){ + enum Count { + None, + One(Rc), + Many + }; + + impl Count { + fn increment(&self, cell: Rc) -> Count{ + match self { + Count::None => {Count::One(cell)} + Count::One(_) => {Count::Many} + Count::Many => {Count::Many} + } + } + } + + 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() { + let value = &*cell.value.borrow(); + match value { + CellValue::Unknown(possibilities) => { + for digit in 1..10 { + if possibilities.contains(&(digit as u8)){ + counts[digit-1] = counts[digit-1].increment(Rc::clone(cell)); + } + } + }, + CellValue::Fixed(_) => {} // do nothing + } + } + + for (digit, count) in counts.iter().enumerate() { + match count { + Count::One(cell) => { + cell.set((digit + 1) as u8); + }, + _ => {} + } + + } +} + +mod search_useful_constraint{ + use crate::grid::{Grid, Line, LineType, CellValue}; + use std::rc::{Rc, Weak}; + use std::cell::RefCell; + + enum PossibilityLines { + Unique(usize), + Invalid, + None + } + + impl PossibilityLines { + fn is_invalid(&self) -> bool { + match &self { + PossibilityLines::Invalid => true, + _ => false + } + } + } + + // If all the cells for a particular possibility share a same other Line, they can remove that possibility from other cells in the main line. +// 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){ + unsafe { + if super::DEBUG { + println!("Searching for a useful constraint on line {:?} {}", line.line_type, line.index); + } + } + + let (check_row, check_column, check_section) = match line.line_type { + LineType::Row => {(false, false, true)}, + LineType::Column => {(false, false, true)}, + LineType::Section => {(true, true, false)}, + }; + + for possibility in 0..9 { + let mut rows = match check_row {true => PossibilityLines::None, false => PossibilityLines::Invalid }; + let mut columns = match check_column {true => PossibilityLines::None, false => PossibilityLines::Invalid }; + let mut sections = match check_section {true => PossibilityLines::None, false => PossibilityLines::Invalid }; + + for cell_id in 0..9 { + let cell_ref = line.get(cell_id).unwrap(); + let cell_ref = Rc::clone(cell_ref); + let cell_ref = &*cell_ref; + let value = &*cell_ref.value.borrow(); + + match value { + CellValue::Fixed(x) => { // We can deduce this possibility won't occur elsewhere in our row, so leave for-loop + if possibility.eq(x) { + rows = process_possibility_line(rows, &cell_ref.row); + columns = process_possibility_line(columns, &cell_ref.column); + sections = process_possibility_line(sections, &cell_ref.section); + break; + } + } + CellValue::Unknown(digits) => { + if digits.contains(&possibility) { + rows = process_possibility_line(rows, &cell_ref.row); + columns = process_possibility_line(columns, &cell_ref.column); + sections = process_possibility_line(sections, &cell_ref.section); + } + } + } + + if rows.is_invalid() & columns.is_invalid() & sections.is_invalid() { + break; + } + } + + // 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); + }, + _ => {} + } + match columns { + PossibilityLines::Unique(index) => { + 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); + }, + _ => {} + } + } + + } + + // 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) { + let line = &*(&**line).borrow(); + + for (_index, cell) in line.vec.iter().enumerate() { + let new_value = { + let value = &*cell.value.borrow(); + match value { + CellValue::Unknown(possibilities) => { + let parent_line = match initial_line_type { + LineType::Row => &cell.row, + LineType::Column => &cell.column, + LineType::Section => &cell.section + }; + let parent_line = &*parent_line.upgrade().unwrap(); + let parent_line = &*parent_line.borrow(); + if parent_line.index == initial_line_index { + // Don't want to apply to this cell + continue; + } + + let new_possibilities = match possibilities.binary_search(&digit_to_remove) { + Ok(x) => { + let mut new_possibilities = possibilities.clone(); + new_possibilities.remove(x); + new_possibilities + }, + Err(_) => { continue; } + }; + + let new_value; + if new_possibilities.len() == 1 { + new_value = CellValue::Fixed(new_possibilities.first().unwrap().clone()); + } else { + new_value = CellValue::Unknown(new_possibilities); + } + + new_value + }, + _ => { continue; } + } + }; + + cell.set_value(new_value); + + } + } + + // We detected a useful constraint + fn process_possibility_line(possibility_line: PossibilityLines, line: &Weak>) -> PossibilityLines { + let line = line.upgrade().unwrap(); + let line = &*(&*line).borrow(); + + match possibility_line { + PossibilityLines::None => {PossibilityLines::Unique(line.index)}, + PossibilityLines::Invalid => {possibility_line}, + PossibilityLines::Unique(x) => { + if line.index.eq(&x) { + possibility_line + } else { + PossibilityLines::Invalid + } + } + } + } + +} + + +fn solve_line(grid: &Grid, line: &Line, solve_controller: &SolveController){ + unsafe { + if DEBUG { + println!("Solving {:?} {}", line.line_type, line.index); + } + } + + line.do_update.replace(false); + + if solve_controller.search_singles() { + unsafe { + if DEBUG { + println!("Searching for singles on line {:?} of {}\n{}", line.line_type, line.index, grid); + } + } + search_single_possibility(line); + } + + if solve_controller.search_hidden_singles() { + unsafe { + if DEBUG { + println!("Searching for hidden singles on line {:?} of {}\n{}", line.line_type, line.index, grid); + } + } + search_hidden_single(line); + } + + if solve_controller.find_possibility_groups() { + unsafe { + if DEBUG { + 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 solve_controller.search_useful_constraint() { + unsafe { + if DEBUG { + println!("Searching for useful constraints on line {:?} of {}\n{}", line.line_type, line.index, grid); + } + } + search_useful_constraint::search_useful_constraint(grid, line); + } + +} + +pub fn solve_grid(grid: &mut Grid) -> SolveStatus { + // By default we enable everything + 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 + }; + + solve_grid_with_solve_controller(grid, &solve_controller) + +} + +pub fn solve_grid_with_solve_controller(grid: &mut Grid, solve_controller: &SolveController) -> 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 // through calling this function. // 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); + let mut status = solve_grid_no_guess(grid, solve_controller); status = match status { SolveStatus::Unfinished => { - solve_grid_guess(grid) + if solve_controller.make_guesses() { + solve_grid_guess(grid, solve_controller) + } 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 + } }, _ => {status} }; - match status { - SolveStatus::Unfinished => panic!("solve_grid_guess should never return UNFINISHED"), - _ => return status - } + return status; } -pub fn solve_grid_no_guess(grid: &mut Grid) -> SolveStatus{ +// 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{ + let mut mut_grid = grid.clone(); + return solve_grid_with_solve_controller(&mut mut_grid, solve_controller); +} + +pub fn solve_grid_no_guess(grid: &mut Grid, solve_controller: &SolveController) -> SolveStatus{ loop { let mut ran_something = false; @@ -495,7 +662,7 @@ pub fn solve_grid_no_guess(grid: &mut Grid) -> SolveStatus{ //println!("Processing row {}", _index); let line_ref = &*(&**line_ref).borrow(); if line_ref.do_update() { - solve_line(&grid, line_ref); + solve_line(&grid, line_ref, solve_controller); ran_something = true; } } @@ -503,7 +670,7 @@ pub fn solve_grid_no_guess(grid: &mut Grid) -> SolveStatus{ //println!("Processing column {}", _index); let line_ref = &*(&**line_ref).borrow(); if line_ref.do_update() { - solve_line(&grid, line_ref); + solve_line(&grid, line_ref, solve_controller); ran_something = true; } } @@ -511,7 +678,7 @@ pub fn solve_grid_no_guess(grid: &mut Grid) -> SolveStatus{ //println!("Processing section {}", _index); let line_ref = &*(&**line_ref).borrow(); if line_ref.do_update() { - solve_line(&grid, line_ref); + solve_line(&grid, line_ref, solve_controller); ran_something = true; } } @@ -542,13 +709,15 @@ pub fn solve_grid_no_guess(grid: &mut Grid) -> SolveStatus{ } if appears_complete { - return SolveStatus::Complete; + // Solving by logic rules only implies Uniqueness; + // may be overridden if guesses were made + return SolveStatus::Complete(Some(Uniqueness::Unique)); } } } -fn solve_grid_guess(grid: &mut Grid) -> SolveStatus{ +fn solve_grid_guess(grid: &mut Grid, solve_controller: &SolveController) -> SolveStatus{ let smallest_cell = find_smallest_cell(grid); let smallest_cell = match smallest_cell { Some(cell) => cell, @@ -556,26 +725,60 @@ fn solve_grid_guess(grid: &mut Grid) -> SolveStatus{ }; let possibilities = smallest_cell.get_value_possibilities().unwrap(); + + let mut current_status = SolveStatus::Unfinished; + let mut grid_solution = None; + 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); + let status = solve_grid_with_solve_controller(&mut grid_copy, solve_controller); + // Keep a copy of grid_copy in case we later mutate grid with it match status { - SolveStatus::Complete => { - grid.clone_from(&grid_copy); - return SolveStatus::Complete; + SolveStatus::Complete(_) => { + grid_solution = Some(grid_copy); }, - SolveStatus::Unfinished => { - panic!("solve_grid should never return UNFINISHED") - }, - SolveStatus::Invalid => { - continue; - } + _ => {} } + + current_status = current_status.increment(status); + + match current_status { + SolveStatus::Complete(uniqueness) => { + if !solve_controller.determine_uniqueness() { + current_status = SolveStatus::Complete(None); // be explicit we don't know + break; // no point in continuing + } + + let uniqueness = uniqueness.expect("We're looking for uniqueness and yet an earlier function didn't make a claim"); + match uniqueness { + Uniqueness::Unique => {continue;} // gotta keep on checking + Uniqueness::NotUnique => { + break; // We can stop looking as we already found at least two solutions + } + } + } + SolveStatus::Unfinished => {continue} // Keep looking for a solution + SolveStatus::Invalid => panic!("current_status should not be INVALID at this point") + } + + } - return SolveStatus::Invalid; + // We've finished the for-loop + match current_status { + SolveStatus::Complete(_) => { + grid.clone_from(&grid_solution.expect("grid_solution should have value if we found a solution")); + }, + SolveStatus::Unfinished => { + current_status = SolveStatus::Invalid; // We can now say Invalid + }, + SolveStatus::Invalid => {} + } + + return current_status; } @@ -619,7 +822,7 @@ mod tests { let line = grid.rows.first().unwrap(); let line = &*(**line).borrow(); - identify_and_process_possibility_groups(line); + process_possibility_groups::identify_and_process_possibility_groups(line); assert_eq!(CellValue::Unknown(vec![1, 2, 3]), grid.get(0, 0).unwrap().get_value_copy()); } @@ -643,7 +846,7 @@ mod tests { let line = grid.rows.first().unwrap(); let line = &*(**line).borrow(); - search_useful_constraint(&grid, line); + search_useful_constraint::search_useful_constraint(&grid, line); assert_eq!(CellValue::Unknown(vec![4, 5, 6, 7, 8, 9]), grid.get(2, 0).unwrap().get_value_copy()); } @@ -676,11 +879,38 @@ mod tests { let line = grid.columns.get(1).unwrap(); let line = &*(**line).borrow(); - search_useful_constraint(&grid, line); + search_useful_constraint::search_useful_constraint(&grid, line); assert_eq!(CellValue::Unknown(vec![1, 3, 4, 5, 9]), grid.get(1, 0).unwrap().get_value_copy()); } + #[test] + fn test_hidden_single() { + let grid = Grid::new(); + + // In Row 0 there should be only one spot for 1s and 2s to be set, even though every cell will + // have possibilities for values 3 - 9 + + grid.get(1, 5).unwrap().set(1); + grid.get(2, 6).unwrap().set(1); + grid.get(5, 2).unwrap().set(1); + grid.get(6, 1).unwrap().set(1); + + grid.get(1, 6).unwrap().set(2); + grid.get(2, 5).unwrap().set(2); + grid.get(5, 1).unwrap().set(2); + grid.get(6, 0).unwrap().set(2); + + let first_row = grid.rows.get(0).unwrap(); + let first_row = &*(**first_row).borrow(); + + search_hidden_single(&first_row); + + assert_eq!(CellValue::Fixed(1), grid.get(0, 0).unwrap().get_value_copy()); + assert_eq!(CellValue::Fixed(2), grid.get(0, 2).unwrap().get_value_copy()); + + } + } \ No newline at end of file