From 42e1b72239dd14ff504c9314ca4066af2738d110 Mon Sep 17 00:00:00 2001 From: Joel Therrien Date: Tue, 22 Sep 2020 13:50:38 -0700 Subject: [PATCH 1/3] Add WIP sudoku puzzle generator --- Cargo.toml | 4 +- src/bin/generator.rs | 35 ++++++ src/{main.rs => bin/solver.rs} | 0 src/generator.rs | 219 +++++++++++++++++++++++++++++++++ src/grid.rs | 38 +++--- src/lib.rs | 4 +- src/solver.rs | 128 +++++++++---------- 7 files changed, 342 insertions(+), 86 deletions(-) create mode 100644 src/bin/generator.rs rename src/{main.rs => bin/solver.rs} (100%) create mode 100644 src/generator.rs diff --git a/Cargo.toml b/Cargo.toml index ca88ad2..ce31564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,6 @@ edition = "2018" [dependencies] csv = "1.1.3" -argparse = "0.2.2" \ No newline at end of file +argparse = "0.2.2" +rand = "0.7" +rand_chacha = "0.2.2" \ No newline at end of file diff --git a/src/bin/generator.rs b/src/bin/generator.rs new file mode 100644 index 0000000..0d988b5 --- /dev/null +++ b/src/bin/generator.rs @@ -0,0 +1,35 @@ + +fn main() { + + let mut debug = false; + // Starting default seed will just be based on time + let mut seed = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("Time went backwards").as_secs(); + + { // this block limits scope of borrows by ap.refer() method + let mut ap = argparse::ArgumentParser::new(); + ap.set_description("Generate Sudoku puzzles"); + ap.refer(&mut debug) + .add_option(&["--debug"], argparse::StoreTrue, "Run in debug mode"); + + ap.refer(&mut seed) + .add_option(&["--seed"], argparse::Store, "Provide seed for puzzle generation"); + + ap.parse_args_or_exit(); + } + + if debug { + unsafe { + sudoku_solver::grid::DEBUG = true; + sudoku_solver::solver::DEBUG = true; + } + } + + if debug { + println!("Using seed {}", seed); + } + + let (grid, num_hints) = sudoku_solver::generator::generate_grid(seed); + + println!("{}", grid); + println!("Puzzle has {} hints", num_hints); +} \ No newline at end of file diff --git a/src/main.rs b/src/bin/solver.rs similarity index 100% rename from src/main.rs rename to src/bin/solver.rs diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..2b0e10e --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,219 @@ +use crate::grid::{Cell, Grid, CellValue}; +use crate::solver::{solve_grid_no_guess, SolveStatus, find_smallest_cell}; +use std::rc::Rc; +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; + +// Extension of SolveStatus +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 + // If vector is empty we return an error + + let mut empty_cells = Vec::new(); + for x in 0..9 { + for y in 0..9 { + let cell = self.get(x, y).unwrap(); + let add_cell = { + let cell_value = &*cell.value.borrow(); + match cell_value { // May cause issues with borrow rules + CellValue::Fixed(_) => {false} + CellValue::Unknown(_) => { + true + } + } + }; + if add_cell { + empty_cells.push(cell); + } + } + } + + match empty_cells.iter().choose(rng) { + Some(cell) => Ok(Rc::clone(cell)), + None => Err("Unable to find an empty cell") + } + } +} + +pub fn generate_grid(seed: u64) -> (Grid, i32) { + let mut rng = ChaCha8Rng::seed_from_u64(seed); + + let digit_excluded = rng.gen_range(1, 10); + + let mut num_hints = 0; + 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(); + + for digit in 1..10 { + if digit != digit_excluded { + let cell = grid.get_random_empty_cell(&mut 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;} + GenerateStatus::NotUniqueSolution => {break grid;} + }; + }; + + // Alright, we now have a grid that we can start adding more guesses onto until we find a unique solution + 'outer: loop { + num_hints = num_hints + 1; + let cell = grid.get_random_empty_cell(&mut 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 cell_possibilities = cell.get_value_possibilities().expect("An empty cell has no possibilities"); + + // Let's scramble the order + let cell_possibilities = cell_possibilities.iter().choose_multiple(&mut rng, cell_possibilities.len()); + + for (_index, digit) in cell_possibilities.iter().enumerate() { + if **digit == digit_excluded { + continue; + } + + 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! + return (grid_clone, num_hints); + } + 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"); + num_hints = num_hints - 1; + + } + +} + +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} + }; + + match status { + GenerateStatus::Unfinished => panic!("solve_grid_guess should never return UNFINISHED"), + _ => return status + } +} + +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 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 + } + } + + // 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")} + } + +} \ No newline at end of file diff --git a/src/grid.rs b/src/grid.rs index 04837d9..963dc47 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -6,8 +6,8 @@ pub static mut DEBUG: bool = false; #[derive(Clone, Debug, Eq, PartialEq)] pub enum CellValue { - FIXED(u8), - UNKNOWN(Vec) + Fixed(u8), + Unknown(Vec) } pub struct Cell { @@ -27,7 +27,7 @@ impl Cell { } } - self.value.replace(CellValue::FIXED(digit)); + self.value.replace(CellValue::Fixed(digit)); // We fully expect our row, column, and section to still be here even though the Rust compiler won't guarantee it // Panic-ing if they're not present is perfectly reasonable @@ -53,11 +53,11 @@ impl Cell { pub fn set_value(&self, value: CellValue){ match value { - CellValue::FIXED(digit) => { + CellValue::Fixed(digit) => { self.set(digit); return; }, - CellValue::UNKNOWN(_) => { + CellValue::Unknown(_) => { self.set_value_exact(value); } // continue on } @@ -77,8 +77,8 @@ impl Cell { pub fn get_value_possibilities(&self) -> Option> { let value = &*self.value.borrow(); match value { - CellValue::FIXED(_) => None, - CellValue::UNKNOWN(x) => Some(x.clone()) + CellValue::Fixed(_) => None, + CellValue::Unknown(x) => Some(x.clone()) } } @@ -109,7 +109,7 @@ impl Cell { let value = &*cell.value.borrow(); match value { - CellValue::UNKNOWN(possibilities) => { + CellValue::Unknown(possibilities) => { let mut new_possibilities = possibilities.clone(); match new_possibilities.binary_search(&digit) { @@ -117,7 +117,7 @@ impl Cell { _ => {} }; - Some(CellValue::UNKNOWN(new_possibilities)) + Some(CellValue::Unknown(new_possibilities)) /* if new_possibilities.len() == 1 { let remaining_digit = new_possibilities.first().unwrap().clone(); @@ -128,7 +128,7 @@ impl Cell { Some(CellValue::UNKNOWN(new_possibilities)) }*/ }, - CellValue::FIXED(_) => {None} + CellValue::Fixed(_) => {None} } }; @@ -152,9 +152,9 @@ pub struct Line { #[derive(Debug)] pub enum LineType { - ROW, - COLUMN, - SECTION + Row, + Column, + Section } impl Line { @@ -199,9 +199,9 @@ impl Grid { let mut sections: Vec> = Vec::new(); for i in 0..9 { - rows.push(Rc::new(RefCell::new(Line::new(i, LineType::ROW)))); - columns.push(Rc::new(RefCell::new(Line::new(i, LineType::COLUMN)))); - sections.push(Rc::new(RefCell::new(Line::new(i, LineType::SECTION)))); + rows.push(Rc::new(RefCell::new(Line::new(i, LineType::Row)))); + columns.push(Rc::new(RefCell::new(Line::new(i, LineType::Column)))); + sections.push(Rc::new(RefCell::new(Line::new(i, LineType::Section)))); } for row_index in 0..9 { @@ -229,7 +229,7 @@ impl Grid { let cell = Cell { x: row_index, y: column_index, - value: RefCell::new(CellValue::UNKNOWN(vec![1, 2, 3, 4, 5, 6, 7, 8, 9])), + value: RefCell::new(CellValue::Unknown(vec![1, 2, 3, 4, 5, 6, 7, 8, 9])), row: row_weak, column: column_weak, section: section_weak @@ -325,12 +325,12 @@ impl std::fmt::Display for Grid { match value { - CellValue::FIXED(x) => { + CellValue::Fixed(x) => { row1.push_str(" "); row2.push(' '); row2.push_str(&x.to_string()); row2.push(' '); row3.push_str(" "); }, - CellValue::UNKNOWN(x) => { + CellValue::Unknown(x) => { Grid::process_unknown(&x, 1, &mut row1); Grid::process_unknown(&x, 2, &mut row1); Grid::process_unknown(&x, 3, &mut row1); diff --git a/src/lib.rs b/src/lib.rs index b3e485c..f813ae4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ pub mod grid; - -pub mod solver; \ No newline at end of file +pub mod solver; +pub mod generator; \ No newline at end of file diff --git a/src/solver.rs b/src/solver.rs index 40c4add..5ae6b8b 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -80,14 +80,14 @@ fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec){ let faux_possibilities = { let value = &*cell.value.borrow(); match value { - CellValue::UNKNOWN(possibilities) => { + CellValue::Unknown(possibilities) => { let mut set = HashSet::new(); for (_index, digit) in possibilities.iter().enumerate() { set.insert(digit.clone()); } set }, - CellValue::FIXED(_) => { continue } + CellValue::Fixed(_) => { continue } } }; @@ -164,8 +164,8 @@ fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec){ 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")} + CellValue::Unknown(possibilities) => possibilities.clone(), + CellValue::Fixed(_) => {panic!("Faux_cell shouldn't have linked to fixed cell")} } }; let starting_possibility_size = possibilities.len(); @@ -187,9 +187,9 @@ fn bisect_possibility_groups(line: &Line, cells_of_interest: Vec){ if possibilities.len() < starting_possibility_size { // We have a change to make let new_value = { if possibilities.len() == 1 { - CellValue::FIXED(possibilities.pop().unwrap()) + CellValue::Fixed(possibilities.pop().unwrap()) } else { - CellValue::UNKNOWN(possibilities) + CellValue::Unknown(possibilities) } }; @@ -229,7 +229,7 @@ fn search_single_possibility(line: &Line){ match cell.get_value_possibilities(){ Some(x) => { if x.len() == 1 { - let new_value = CellValue::FIXED(x.first().unwrap().clone()); + let new_value = CellValue::Fixed(x.first().unwrap().clone()); cell.set_value(new_value); } }, @@ -239,15 +239,15 @@ fn search_single_possibility(line: &Line){ } enum PossibilityLines { - UNIQUE(usize), - INVALID, - NONE + Unique(usize), + Invalid, + None } impl PossibilityLines { fn is_invalid(&self) -> bool { match &self { - PossibilityLines::INVALID => true, + PossibilityLines::Invalid => true, _ => false } } @@ -265,15 +265,15 @@ fn search_useful_constraint(grid: &Grid, line: &Line){ } 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)}, + 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}; + 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(); @@ -282,7 +282,7 @@ fn search_useful_constraint(grid: &Grid, line: &Line){ 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 + 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); @@ -290,7 +290,7 @@ fn search_useful_constraint(grid: &Grid, line: &Line){ break; } } - CellValue::UNKNOWN(digits) => { + CellValue::Unknown(digits) => { if digits.contains(&possibility) { rows = process_possibility_line(rows, &cell_ref.row); columns = process_possibility_line(columns, &cell_ref.column); @@ -306,19 +306,19 @@ fn search_useful_constraint(grid: &Grid, line: &Line){ // Check each line and see if we can determine anything match rows { - PossibilityLines::UNIQUE(index) => { + PossibilityLines::Unique(index) => { remove_possibilities_line(grid.rows.get(index).unwrap(), possibility, &line.line_type, line.index); }, _ => {} } match columns { - PossibilityLines::UNIQUE(index) => { + PossibilityLines::Unique(index) => { remove_possibilities_line(grid.columns.get(index).unwrap(), possibility, &line.line_type, line.index); }, _ => {} } match sections { - PossibilityLines::UNIQUE(index) => { + PossibilityLines::Unique(index) => { remove_possibilities_line(grid.sections.get(index).unwrap(), possibility, &line.line_type, line.index); }, _ => {} @@ -335,11 +335,11 @@ fn remove_possibilities_line(line: &Rc>, digit_to_remove: u8, init let new_value = { let value = &*cell.value.borrow(); match value { - CellValue::UNKNOWN(possibilities) => { + CellValue::Unknown(possibilities) => { let parent_line = match initial_line_type { - LineType::ROW => &cell.row, - LineType::COLUMN => &cell.column, - LineType::SECTION => &cell.section + 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(); @@ -359,9 +359,9 @@ fn remove_possibilities_line(line: &Rc>, digit_to_remove: u8, init let new_value; if new_possibilities.len() == 1 { - new_value = CellValue::FIXED(new_possibilities.first().unwrap().clone()); + new_value = CellValue::Fixed(new_possibilities.first().unwrap().clone()); } else { - new_value = CellValue::UNKNOWN(new_possibilities); + new_value = CellValue::Unknown(new_possibilities); } new_value @@ -381,13 +381,13 @@ fn process_possibility_line(possibility_line: PossibilityLines, line: &Weak {PossibilityLines::UNIQUE(line.index)}, - PossibilityLines::INVALID => {possibility_line}, - PossibilityLines::UNIQUE(x) => { + PossibilityLines::None => {PossibilityLines::Unique(line.index)}, + PossibilityLines::Invalid => {possibility_line}, + PossibilityLines::Unique(x) => { if line.index.eq(&x) { possibility_line } else { - PossibilityLines::INVALID + PossibilityLines::Invalid } } } @@ -426,7 +426,7 @@ fn solve_line(grid: &Grid, line: &Line){ } -fn find_smallest_cell(grid: &Grid) -> Option>{ +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 @@ -440,7 +440,7 @@ fn find_smallest_cell(grid: &Grid) -> Option>{ let cell_value = &*cell.value.borrow(); match cell_value { - CellValue::UNKNOWN(possibilities) => { + CellValue::Unknown(possibilities) => { if (possibilities.len() < smallest_size) && (possibilities.len() > 0){ smallest_size = possibilities.len(); smallest_cell = Some(cell_rc); @@ -461,9 +461,9 @@ fn find_smallest_cell(grid: &Grid) -> Option>{ pub enum SolveStatus { - COMPLETE, - UNFINISHED, - INVALID + Complete, + Unfinished, + Invalid } pub fn solve_grid(grid: &mut Grid) -> SolveStatus{ @@ -475,14 +475,14 @@ pub fn solve_grid(grid: &mut Grid) -> SolveStatus{ let mut status = solve_grid_no_guess(grid); status = match status { - SolveStatus::UNFINISHED => { + SolveStatus::Unfinished => { solve_grid_guess(grid) }, _ => {status} }; match status { - SolveStatus::UNFINISHED => panic!("solve_grid_guess should never return UNFINISHED"), + SolveStatus::Unfinished => panic!("solve_grid_guess should never return UNFINISHED"), _ => return status } } @@ -517,7 +517,7 @@ pub fn solve_grid_no_guess(grid: &mut Grid) -> SolveStatus{ } if !ran_something{ // No lines have changed since we last analyzed them - return SolveStatus::UNFINISHED; + return SolveStatus::Unfinished; } // Check if complete or invalid @@ -529,20 +529,20 @@ pub fn solve_grid_no_guess(grid: &mut Grid) -> SolveStatus{ let value = &**(&cell.value.borrow()); match value { - CellValue::UNKNOWN(possibilities) => { + CellValue::Unknown(possibilities) => { appears_complete = false; if possibilities.len() == 0 { - return SolveStatus::INVALID; + return SolveStatus::Invalid; } }, - CellValue::FIXED(_) => {} + CellValue::Fixed(_) => {} } } } if appears_complete { - return SolveStatus::COMPLETE; + return SolveStatus::Complete; } } @@ -552,7 +552,7 @@ fn solve_grid_guess(grid: &mut Grid) -> SolveStatus{ let smallest_cell = find_smallest_cell(grid); let smallest_cell = match smallest_cell { Some(cell) => cell, - None => return SolveStatus::INVALID + None => return SolveStatus::Invalid }; let possibilities = smallest_cell.get_value_possibilities().unwrap(); @@ -562,20 +562,20 @@ fn solve_grid_guess(grid: &mut Grid) -> SolveStatus{ let status = solve_grid(&mut grid_copy); match status { - SolveStatus::COMPLETE => { + SolveStatus::Complete => { grid.clone_from(&grid_copy); - return SolveStatus::COMPLETE; + return SolveStatus::Complete; }, - SolveStatus::UNFINISHED => { + SolveStatus::Unfinished => { panic!("solve_grid should never return UNFINISHED") }, - SolveStatus::INVALID => { + SolveStatus::Invalid => { continue; } } } - return SolveStatus::INVALID; + return SolveStatus::Invalid; } @@ -592,14 +592,14 @@ mod tests { grid.get(0, i).unwrap().set(i as u8 +1); } - assert_eq!(CellValue::UNKNOWN(vec![9]), grid.get(0, 8).unwrap().get_value_copy()); + assert_eq!(CellValue::Unknown(vec![9]), grid.get(0, 8).unwrap().get_value_copy()); let line = grid.rows.first().unwrap(); let line = &*(**line).borrow(); search_single_possibility(line); - assert_eq!(CellValue::FIXED(9), grid.get(0, 8).unwrap().get_value_copy()); + assert_eq!(CellValue::Fixed(9), grid.get(0, 8).unwrap().get_value_copy()); } #[test] @@ -614,14 +614,14 @@ mod tests { grid.get(1, 7).unwrap().set(2); grid.get(1, 8).unwrap().set(3); - assert_eq!(CellValue::UNKNOWN(vec![1, 2, 3, 7, 8, 9]), grid.get(0, 0).unwrap().get_value_copy()); + assert_eq!(CellValue::Unknown(vec![1, 2, 3, 7, 8, 9]), grid.get(0, 0).unwrap().get_value_copy()); let line = grid.rows.first().unwrap(); let line = &*(**line).borrow(); identify_and_process_possibility_groups(line); - assert_eq!(CellValue::UNKNOWN(vec![1, 2, 3]), grid.get(0, 0).unwrap().get_value_copy()); + assert_eq!(CellValue::Unknown(vec![1, 2, 3]), grid.get(0, 0).unwrap().get_value_copy()); } #[test] @@ -638,14 +638,14 @@ mod tests { - assert_eq!(CellValue::UNKNOWN(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]), grid.get(2, 0).unwrap().get_value_copy()); + assert_eq!(CellValue::Unknown(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]), grid.get(2, 0).unwrap().get_value_copy()); let line = grid.rows.first().unwrap(); let line = &*(**line).borrow(); search_useful_constraint(&grid, line); - assert_eq!(CellValue::UNKNOWN(vec![4, 5, 6, 7, 8, 9]), grid.get(2, 0).unwrap().get_value_copy()); + assert_eq!(CellValue::Unknown(vec![4, 5, 6, 7, 8, 9]), grid.get(2, 0).unwrap().get_value_copy()); } @@ -661,15 +661,15 @@ mod tests { grid.get(6, 1).unwrap().set(8); grid.get(8, 2).unwrap().set(7); - grid.get(0, 1).unwrap().set_value(CellValue::UNKNOWN(vec![1, 3, 4, 7, 9])); - grid.get(1, 1).unwrap().set_value(CellValue::UNKNOWN(vec![1, 3, 4, 5, 9])); - grid.get(2, 1).unwrap().set_value(CellValue::UNKNOWN(vec![1, 2])); - grid.get(4, 1).unwrap().set_value(CellValue::UNKNOWN(vec![1, 3, 4, 7])); - grid.get(7, 1).unwrap().set_value(CellValue::UNKNOWN(vec![1, 2, 3, 9])); - grid.get(8, 1).unwrap().set_value(CellValue::UNKNOWN(vec![1, 2, 3, 5, 9])); + grid.get(0, 1).unwrap().set_value(CellValue::Unknown(vec![1, 3, 4, 7, 9])); + grid.get(1, 1).unwrap().set_value(CellValue::Unknown(vec![1, 3, 4, 5, 9])); + grid.get(2, 1).unwrap().set_value(CellValue::Unknown(vec![1, 2])); + grid.get(4, 1).unwrap().set_value(CellValue::Unknown(vec![1, 3, 4, 7])); + grid.get(7, 1).unwrap().set_value(CellValue::Unknown(vec![1, 2, 3, 9])); + grid.get(8, 1).unwrap().set_value(CellValue::Unknown(vec![1, 2, 3, 5, 9])); // 5 is wrongly removed here - grid.get(1, 0).unwrap().set_value(CellValue::UNKNOWN(vec![1, 3, 4, 5, 9])); + grid.get(1, 0).unwrap().set_value(CellValue::Unknown(vec![1, 3, 4, 5, 9])); println!("{}", grid); @@ -678,7 +678,7 @@ mod tests { search_useful_constraint(&grid, line); - assert_eq!(CellValue::UNKNOWN(vec![1, 3, 4, 5, 9]), grid.get(1, 0).unwrap().get_value_copy()); + assert_eq!(CellValue::Unknown(vec![1, 3, 4, 5, 9]), grid.get(1, 0).unwrap().get_value_copy()); } -- 2.45.2 From 3c845aec3f46b4512665f1b52d81dacf78d7a93d Mon Sep 17 00:00:00 2001 From: Joel Therrien Date: Tue, 22 Sep 2020 16:38:38 -0700 Subject: [PATCH 2/3] Slightly improve Sudoku puzzle generator --- src/bin/generator.rs | 1 + src/generator.rs | 140 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 126 insertions(+), 15 deletions(-) diff --git a/src/bin/generator.rs b/src/bin/generator.rs index 0d988b5..7a92715 100644 --- a/src/bin/generator.rs +++ b/src/bin/generator.rs @@ -21,6 +21,7 @@ fn main() { unsafe { sudoku_solver::grid::DEBUG = true; sudoku_solver::solver::DEBUG = true; + sudoku_solver::generator::DEBUG = true; } } diff --git a/src/generator.rs b/src/generator.rs index 2b0e10e..bbe307f 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,9 +1,11 @@ -use crate::grid::{Cell, Grid, CellValue}; +use crate::grid::{Cell, Grid, CellValue, Line}; use crate::solver::{solve_grid_no_guess, SolveStatus, find_smallest_cell}; use std::rc::Rc; use rand::prelude::*; use rand_chacha::ChaCha8Rng; +pub static mut DEBUG : bool = false; + // Extension of SolveStatus pub enum GenerateStatus { UniqueSolution, @@ -79,17 +81,89 @@ impl Grid { } } +impl Cell { + fn delete_value(&self){ + unsafe { + if DEBUG { + println!("Cell {}, {} had its value deleted.", self.x, self.y); + } + } + + self.set_value_exact(CellValue::Unknown(vec![])); // placeholder + + // This will reset all the possibilities for this cell and the ones that might have been limited by this cell + self.section.upgrade().unwrap().borrow().recalculate_and_set_possibilities(); + self.row.upgrade().unwrap().borrow().recalculate_and_set_possibilities(); + self.column.upgrade().unwrap().borrow().recalculate_and_set_possibilities(); + + } + + /** + As part of delete_value, we need to manually recalculate possibilities for not just the cell whose value we deleted, + but also the other empty cells in the same row, column, and section. + */ + fn calculate_possibilities(&self) -> Vec { + // Need to calculate possibilities for this cell + let mut possibilities = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; + fn eliminate_possibilities(possibilities: &mut Vec, line: &Line, cell: &Cell){ + for (_index, other) in line.vec.iter().enumerate(){ + if other.x != cell.x || other.y != cell.y { + let value = &*other.value.borrow(); + match value { + CellValue::Fixed(digit) => { + let location = possibilities.binary_search(digit); + match location { + Ok(location) => { + possibilities.remove(location); + } + Err(_) => {} + } + } + CellValue::Unknown(_) => {} + } + } + } + } + + eliminate_possibilities(&mut possibilities, &self.section.upgrade().unwrap().borrow(), self); + eliminate_possibilities(&mut possibilities, &self.row.upgrade().unwrap().borrow(), self); + eliminate_possibilities(&mut possibilities, &self.column.upgrade().unwrap().borrow(), self); + + return possibilities; + } +} + +impl Line { + fn recalculate_and_set_possibilities(&self) { + for (_index, cell) in self.vec.iter().enumerate() { + let cell = &**cell; + let new_possibilities = { + let cell_value = &*cell.value.borrow(); + match cell_value { + CellValue::Fixed(_) => { continue; } + CellValue::Unknown(_) => { + cell.calculate_possibilities() + } + } + }; + + cell.set_value_exact(CellValue::Unknown(new_possibilities)); + } + } +} + pub fn generate_grid(seed: u64) -> (Grid, i32) { let mut rng = ChaCha8Rng::seed_from_u64(seed); - let digit_excluded = rng.gen_range(1, 10); - - let mut num_hints = 0; + 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 digit_excluded = rng.gen_range(1, 10); for digit in 1..10 { if digit != digit_excluded { @@ -112,29 +186,27 @@ pub fn generate_grid(seed: u64) -> (Grid, i32) { }; // 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(&mut 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 cell_possibilities = cell.get_value_possibilities().expect("An empty cell has no possibilities"); + let mut cell_possibilities = cell.get_value_possibilities().expect("An empty cell has no possibilities"); // Let's scramble the order - let cell_possibilities = cell_possibilities.iter().choose_multiple(&mut rng, cell_possibilities.len()); + cell_possibilities.shuffle(&mut rng); for (_index, digit) in cell_possibilities.iter().enumerate() { - if **digit == digit_excluded { - continue; - } let grid_clone = grid.clone(); let cell = &*grid_clone.get(cell.x, cell.y).unwrap(); - cell.set(**digit); + cell.set(*digit); let status = solve_grid(&grid_clone); match status { GenerateStatus::UniqueSolution => { // We're done! - return (grid_clone, num_hints); + break 'outer grid_clone; } GenerateStatus::Unfinished => { panic!("solve_grid should never return UNFINISHED") @@ -148,15 +220,53 @@ pub fn generate_grid(seed: u64) -> (Grid, i32) { } } - } + }; // 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"); - num_hints = num_hints - 1; + eprint!("No valid hints were found for puzzle\n{} at cell ({}, {})", grid, cell.x, cell.y); + panic!("Unable to continue as puzzle is invalid"); + + }; + + // At this point we have a valid puzzle, but from experience it has way too many guesses, and many of them + // are likely not needed. Let's now try removing a bunch. + let mut non_empty_cells = Vec::new(); + 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(_) => {} + } + } } + // Need to randomly reorder non_empty_cells + non_empty_cells.shuffle(&mut rng); + + for (_index, cell) in non_empty_cells.iter().enumerate() { + let grid_clone = grid.clone(); + let cell_clone = grid_clone.get(cell.x, cell.y).unwrap(); + let cell_clone = &*cell_clone; + + cell_clone.delete_value(); + + let status = solve_grid(&mut grid); + match status { + GenerateStatus::UniqueSolution => { // great; that cell value was not needed + num_hints = num_hints - 1; + grid = grid_clone; + + } + 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;} + }; + } + + return (grid, num_hints); } -- 2.45.2 From 7b881ff1379be605648d5074b7db0b061ff8b605 Mon Sep 17 00:00:00 2001 From: Joel Therrien Date: Tue, 22 Sep 2020 19:30:38 -0700 Subject: [PATCH 3/3] Add ability to specify puzzle difficulty and save result to CSV file --- src/bin/generator.rs | 77 ++++++++++++++++++++++++++++++++++++++++++-- src/generator.rs | 11 +++---- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/bin/generator.rs b/src/bin/generator.rs index 7a92715..fb36309 100644 --- a/src/bin/generator.rs +++ b/src/bin/generator.rs @@ -1,3 +1,8 @@ +use rand_chacha::ChaCha8Rng; +use rand::prelude::*; +use sudoku_solver::grid::{Grid, CellValue}; +use std::error::Error; +use std::io::Write; fn main() { @@ -5,6 +10,10 @@ fn main() { // Starting default seed will just be based on time let mut seed = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("Time went backwards").as_secs(); + let mut max_hints = 81; + let mut max_attempts = 100; + let mut filename : Option = None; + { // this block limits scope of borrows by ap.refer() method let mut ap = argparse::ArgumentParser::new(); ap.set_description("Generate Sudoku puzzles"); @@ -14,6 +23,15 @@ fn main() { ap.refer(&mut seed) .add_option(&["--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"); + + ap.refer(&mut max_attempts) + .add_option(&["--attempts"], argparse::Store, "Number of attempts that will be tried to generate such a puzzle; default is 100"); + + ap.refer(&mut filename) + .add_argument("filename", argparse::StoreOption, "Optional filename to store puzzle in as a CSV"); + ap.parse_args_or_exit(); } @@ -28,9 +46,62 @@ fn main() { if debug { println!("Using seed {}", seed); } + let mut rng = ChaCha8Rng::seed_from_u64(seed); - let (grid, num_hints) = sudoku_solver::generator::generate_grid(seed); + let mut num_attempts = 0; - println!("{}", grid); - println!("Puzzle has {} hints", num_hints); + let grid = 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); + num_attempts = num_attempts + 1; + + if 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; + } + }; + + match filename { + Some(filename) => { + save_grid(&grid, &filename).unwrap(); + println!("Grid saved to {}", filename); + }, + None => {} + } + +} + +fn save_grid(grid: &Grid, filename: &str) -> Result<(), Box>{ + // Not using the csv crate for writing because it's being difficult and won't accept raw integers + let mut file = std::fs::File::create(filename)?; + + for x in 0..9 { + for y in 0..9 { + let cell = grid.get(x, y).unwrap(); + let value = &*cell.value.borrow(); + let digit = + match value { + CellValue::Fixed(digit) => {*digit} + CellValue::Unknown(_) => {0} + }; + + let mut text = digit.to_string(); + if y < 8 { + text.push(','); + } + file.write(text.as_bytes())?; + + } + file.write(b"\n")?; + } + + Ok(()) } \ No newline at end of file diff --git a/src/generator.rs b/src/generator.rs index bbe307f..e35d0d5 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -152,8 +152,7 @@ impl Line { } } -pub fn generate_grid(seed: u64) -> (Grid, i32) { - let mut rng = ChaCha8Rng::seed_from_u64(seed); +pub fn generate_grid(rng: &mut ChaCha8Rng) -> (Grid, i32) { let mut num_hints; let mut grid : Grid = loop { @@ -167,7 +166,7 @@ pub fn generate_grid(seed: u64) -> (Grid, i32) { for digit in 1..10 { if digit != digit_excluded { - let cell = grid.get_random_empty_cell(&mut rng); + let cell = grid.get_random_empty_cell(rng); cell.unwrap().set(digit); num_hints = num_hints + 1; } @@ -189,12 +188,12 @@ pub fn generate_grid(seed: u64) -> (Grid, i32) { grid = 'outer: loop { num_hints = num_hints + 1; - let cell = grid.get_random_empty_cell(&mut rng).unwrap(); // We unwrap because if somehow we're filled each cell without finding a solution, that's reason for a panic + 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(&mut rng); + cell_possibilities.shuffle(rng); for (_index, digit) in cell_possibilities.iter().enumerate() { @@ -244,7 +243,7 @@ pub fn generate_grid(seed: u64) -> (Grid, i32) { } } // Need to randomly reorder non_empty_cells - non_empty_cells.shuffle(&mut rng); + non_empty_cells.shuffle(rng); for (_index, cell) in non_empty_cells.iter().enumerate() { let grid_clone = grid.clone(); -- 2.45.2