Slightly improve Sudoku puzzle generator

This commit is contained in:
Joel Therrien 2020-09-22 16:38:38 -07:00
parent 42e1b72239
commit 3c845aec3f
2 changed files with 126 additions and 15 deletions

View file

@ -21,6 +21,7 @@ fn main() {
unsafe { unsafe {
sudoku_solver::grid::DEBUG = true; sudoku_solver::grid::DEBUG = true;
sudoku_solver::solver::DEBUG = true; sudoku_solver::solver::DEBUG = true;
sudoku_solver::generator::DEBUG = true;
} }
} }

View file

@ -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 crate::solver::{solve_grid_no_guess, SolveStatus, find_smallest_cell};
use std::rc::Rc; use std::rc::Rc;
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaCha8Rng; use rand_chacha::ChaCha8Rng;
pub static mut DEBUG : bool = false;
// Extension of SolveStatus // Extension of SolveStatus
pub enum GenerateStatus { pub enum GenerateStatus {
UniqueSolution, 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<u8> {
// 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<u8>, 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) { pub fn generate_grid(seed: u64) -> (Grid, i32) {
let mut rng = ChaCha8Rng::seed_from_u64(seed); let mut rng = ChaCha8Rng::seed_from_u64(seed);
let digit_excluded = rng.gen_range(1, 10); let mut num_hints;
let mut num_hints = 0;
let mut grid : Grid = loop { let mut grid : Grid = loop {
// First step; randomly assign 8 different digits to different empty cells and see if there's a possible solution // 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 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 // 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(); let mut grid = Grid::new();
num_hints = 0;
let digit_excluded = rng.gen_range(1, 10);
for digit in 1..10 { for digit in 1..10 {
if digit != digit_excluded { 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 // Alright, we now have a grid that we can start adding more guesses onto until we find a unique solution
grid =
'outer: loop { 'outer: loop {
num_hints = num_hints + 1; 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(&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 = &*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'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() { for (_index, digit) in cell_possibilities.iter().enumerate() {
if **digit == digit_excluded {
continue;
}
let grid_clone = grid.clone(); let grid_clone = grid.clone();
let cell = &*grid_clone.get(cell.x, cell.y).unwrap(); let cell = &*grid_clone.get(cell.x, cell.y).unwrap();
cell.set(**digit); cell.set(*digit);
let status = solve_grid(&grid_clone); let status = solve_grid(&grid_clone);
match status { match status {
GenerateStatus::UniqueSolution => { // We're done! GenerateStatus::UniqueSolution => { // We're done!
return (grid_clone, num_hints); break 'outer grid_clone;
} }
GenerateStatus::Unfinished => { GenerateStatus::Unfinished => {
panic!("solve_grid should never return 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 // 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 // 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); eprint!("No valid hints were found for puzzle\n{} at cell ({}, {})", grid, cell.x, cell.y);
//panic!("Unable to continue as puzzle is invalid"); 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; 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);
} }