make the solver more generic
The solver is now not specific to Sudoku but pretty much any game where a set of square tiles needs to be filled according to a specific rule. Since it is not specific to Sudoku any more, it is a bit less performant, but that is not a big issue in my use case, so is fine.
This commit is contained in:
parent
4e1b5d7b05
commit
38157d7b5e
5 changed files with 388 additions and 166 deletions
|
@ -6,3 +6,5 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "*"
|
||||
simple_logger = "*"
|
112
src/board.rs
Normal file
112
src/board.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
/**
|
||||
Contains the values on a given board as well as a bit of metadata.
|
||||
The actual tiles are stored in a `HashMap` to allow disparate data.
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Board {
|
||||
width: usize,
|
||||
height: usize,
|
||||
num_options: usize,
|
||||
|
||||
tiles: HashMap<(usize, usize), usize>,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
/**
|
||||
Creates a new board.
|
||||
|
||||
Panics if width or height are zero, or there are less than two options.
|
||||
*/
|
||||
pub fn new(width: usize, height: usize, num_options: usize) -> Self {
|
||||
assert!(width != 0);
|
||||
assert!(height != 0);
|
||||
assert!(num_options >= 2);
|
||||
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
num_options,
|
||||
tiles: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
pub fn get_height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
pub fn get_num_options(&self) -> usize {
|
||||
self.num_options
|
||||
}
|
||||
|
||||
/**
|
||||
Set the value of a tile, or removes it if `None` is given.
|
||||
|
||||
Returns the previous value or `None`.
|
||||
|
||||
Panics if `x` or `y` are beyond the boards dimensions or `value` is greater or equal to `num_options`.
|
||||
*/
|
||||
pub fn set_tile(&mut self, x: usize, y: usize, value: Option<usize>) -> Option<usize> {
|
||||
assert!(x < self.width);
|
||||
assert!(y < self.height);
|
||||
|
||||
if let Some(value) = value {
|
||||
assert!(value < self.num_options);
|
||||
self.tiles.insert((x, y), value)
|
||||
} else {
|
||||
self.tiles.remove(&(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Get the value of a tile, or `None` if no value is known.
|
||||
|
||||
Panics if `x` or `y` are beyond the boards dimensions.
|
||||
*/
|
||||
pub fn get_tile(&self, x: usize, y: usize) -> Option<usize> {
|
||||
assert!(x < self.width);
|
||||
assert!(y < self.height);
|
||||
|
||||
self.tiles.get(&(x, y)).copied()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all the values of a column, in ascending y order.
|
||||
If a value in one row is not present, `None` is given instead.
|
||||
*/
|
||||
pub fn get_column(&self, x: usize) -> Vec<Option<usize>> {
|
||||
(0..self.height)
|
||||
.map(|y| self.tiles.get(&(x, y)).copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all the values of a row, in ascending x order.
|
||||
If a value in one column is not present, `None` is given instead.
|
||||
*/
|
||||
pub fn get_row(&self, y: usize) -> Vec<Option<usize>> {
|
||||
(0..self.width)
|
||||
.map(|x| self.tiles.get(&(x, y)).copied())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Board {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for x in 0..self.width {
|
||||
for y in 0..self.height {
|
||||
if let Some(value) = self.tiles.get(&(x, y)) {
|
||||
write!(f, "{value}\t")?;
|
||||
} else {
|
||||
write!(f, "?\t")?;
|
||||
}
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
118
src/constrain.rs
Normal file
118
src/constrain.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
use crate::board::Board;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type BoardCheckFunction = dyn Fn(&Board) -> bool;
|
||||
|
||||
/**
|
||||
Implementation of the actual solving algorithm.
|
||||
|
||||
## Parameters
|
||||
* `board`: The (incomplete) board to be solved, which will be modified to contain the solution.
|
||||
* `check_fn`: A function that, given the board determines whether the board is valid, i.e. the rules of the game.
|
||||
|
||||
## Return value
|
||||
Returns `true` if the board has been solved. The initial given `board` will have been modified to contain it.
|
||||
|
||||
Returns `false` if the rules as defined by `check_fn` do not allow any valid solution of the initial board. The value of `board` may nevertheless have been modified.
|
||||
*/
|
||||
pub fn constrain_board(board: &mut Board, check_fn: &BoardCheckFunction) -> bool {
|
||||
_constrain_board(board, check_fn, None)
|
||||
}
|
||||
|
||||
fn _constrain_board(
|
||||
board: &mut Board,
|
||||
check_fn: &BoardCheckFunction,
|
||||
hints: Option<HashMap<(usize, usize), Vec<usize>>>,
|
||||
) -> bool {
|
||||
let mut anything_changed = true;
|
||||
let mut memo = hints.unwrap_or_default();
|
||||
|
||||
while anything_changed {
|
||||
anything_changed = false;
|
||||
|
||||
for x in 0..board.get_width() {
|
||||
for y in 0..board.get_height() {
|
||||
if board.get_tile(x, y).is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Removing the item from the memo list since it will be replaced later
|
||||
// if it still makes sense to memoize for this tile and this avoids cloning.
|
||||
let mut valid = memo
|
||||
.remove(&(x, y))
|
||||
.unwrap_or_else(|| (0..board.get_num_options()).collect());
|
||||
log::trace!("valid {valid:?}");
|
||||
|
||||
// only keep the options which result in a valid overall board
|
||||
valid.retain(|&opt| {
|
||||
board.set_tile(x, y, Some(opt));
|
||||
|
||||
check_fn(&board)
|
||||
});
|
||||
log::trace!("still valid {valid:?}");
|
||||
|
||||
match valid.len() {
|
||||
0 => {
|
||||
// invalid board
|
||||
board.set_tile(x, y, None);
|
||||
log::error!("no valid tiles at {x},{y}");
|
||||
return false;
|
||||
}
|
||||
1 => {
|
||||
// there is only one option
|
||||
board.set_tile(x, y, valid.pop());
|
||||
anything_changed = true;
|
||||
}
|
||||
_ => {
|
||||
// there are multiple options, restore the board for now and keep trying
|
||||
board.set_tile(x, y, None);
|
||||
memo.insert((x, y), valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After this loop is done, it is guaranteed that memo contains only valid options
|
||||
// because all unfilled cells will have been checked.
|
||||
|
||||
if memo.is_empty() {
|
||||
// There aren't any memoized values, this must mean that there are no empty fields
|
||||
// and we have already solved this board.
|
||||
log::debug!("solved (before recursion)");
|
||||
return true;
|
||||
}
|
||||
|
||||
let old_board = board.clone();
|
||||
|
||||
// find the coordinate with the least amount of possibilities
|
||||
let mut coords = memo
|
||||
.iter()
|
||||
.map(|(key, val)| (*key, val.len()))
|
||||
.collect::<Vec<_>>();
|
||||
coords.sort_unstable_by_key(|&t| t.1);
|
||||
// SAFTEY: can unwrap because `memo` is not empty, so coords will have
|
||||
// at least one entry
|
||||
let best_coord = coords.first().copied().unwrap().0;
|
||||
// SAFETY: can unwrap because we got this coordinate pair from memo
|
||||
let valid = memo.remove(&best_coord).unwrap();
|
||||
|
||||
let (x, y) = best_coord;
|
||||
log::info!("starting recursion at {x},{y}");
|
||||
|
||||
for opt in valid {
|
||||
*board = old_board.clone();
|
||||
|
||||
board.set_tile(x, y, Some(opt));
|
||||
if _constrain_board(board, check_fn, Some(memo.clone())) {
|
||||
// TODO: figure out how to find multiple solutions
|
||||
log::debug!("solved (with recursion)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// if we get here we tried all options and did not find one
|
||||
*board = old_board;
|
||||
log::error!("did not find a valid solution");
|
||||
false
|
||||
}
|
207
src/main.rs
207
src/main.rs
|
@ -1,169 +1,44 @@
|
|||
struct Board([u8; 9 * 9]);
|
||||
struct SuperBoard([bool; 9 * 9 * 9]);
|
||||
|
||||
impl std::fmt::Display for Board {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for i in 0..9 * 9 {
|
||||
if i % 9 > 0 {
|
||||
if i % 3 == 0 {
|
||||
write!(f, "|")?;
|
||||
} else {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
} else if i > 0 && i % (9 * 3) == 0 {
|
||||
writeln!(f, "-----+-----+-----")?;
|
||||
}
|
||||
match self.0[i] {
|
||||
0 => {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
n => {
|
||||
write!(f, "{n}")?;
|
||||
}
|
||||
}
|
||||
if i % 9 == 8 {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SuperBoard {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for j in 0..9 * 9 * 9 {
|
||||
let col = j / 3 % 9;
|
||||
let row = j / (9 * 9);
|
||||
let n = j / 9 / 3 % 3 * 3 + j % 3;
|
||||
|
||||
if j == 0 {
|
||||
// start of grid
|
||||
} else if j % 243 == 0 {
|
||||
writeln!(f, "-----------+-----------+-----------")?;
|
||||
} else if j % 81 == 0 {
|
||||
writeln!(f, "...........|...........|...........")?;
|
||||
} else if j % 27 == 0 {
|
||||
// start of line
|
||||
} else if j % 9 == 0 {
|
||||
write!(f, "|")?;
|
||||
} else if j % 3 == 0 {
|
||||
write!(f, ".")?;
|
||||
}
|
||||
|
||||
let offset = n + (col * 9) + (row * 9 * 9);
|
||||
if self.0[offset] {
|
||||
write!(f, "{}", n + 1)?;
|
||||
} else {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
|
||||
if j % 27 == 26 {
|
||||
// end of line
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Board> for SuperBoard {
|
||||
fn from(board: &Board) -> SuperBoard {
|
||||
let mut super_board = [false; 9 * 9 * 9];
|
||||
|
||||
for (i, &cell) in board.0.iter().enumerate() {
|
||||
let super_i = i * 9;
|
||||
match cell {
|
||||
0 => {
|
||||
for i in 0..9 {
|
||||
super_board[super_i + i] = true;
|
||||
}
|
||||
}
|
||||
n => {
|
||||
super_board[super_i + n as usize - 1] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SuperBoard(super_board)
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
fn constrain(&mut self) -> Result<SuperBoard, ()> {
|
||||
let mut collapsed = self
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &x)| x != 0)
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
let mut super_board = SuperBoard::from(&*self);
|
||||
|
||||
while let Some(i) = collapsed.pop() {
|
||||
let value = self.0[i];
|
||||
assert!(value > 0);
|
||||
let offset = (value - 1) as usize;
|
||||
|
||||
// column
|
||||
let col_start = i % 9;
|
||||
let row_start = i - col_start;
|
||||
let box_start = i / 27 * 27 + i % 9 / 3 * 3;
|
||||
let to_constrain = (col_start..81)
|
||||
.step_by(9)
|
||||
.chain((row_start..81).take(9))
|
||||
.chain((box_start..81).step_by(9).take(3).flat_map(|x| x..x + 3))
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
|
||||
for j in to_constrain.into_iter() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
|
||||
// safe to unwrap because none of the above can ever go outside the predefined bounds
|
||||
let x = &mut super_board.0[j * 9 + offset];
|
||||
if *x {
|
||||
*x = false;
|
||||
|
||||
let cell = &super_board.0[(j * 9)..(j * 9 + 9)];
|
||||
match cell.iter().filter(|&&x| x).count() {
|
||||
0 => return Err(()),
|
||||
1 => {
|
||||
self.0[j] = cell
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, &x)| x)
|
||||
.map(|(i, _)| i + 1)
|
||||
.unwrap() as u8;
|
||||
if !collapsed.contains(&j) {
|
||||
collapsed.push(j);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(super_board)
|
||||
}
|
||||
}
|
||||
pub mod board;
|
||||
mod constrain;
|
||||
mod rules;
|
||||
|
||||
fn main() {
|
||||
let mut board = Board([
|
||||
0, 6, 0, /**/ 0, 0, 1, /**/ 0, 0, 3,
|
||||
8, 0, 0, /**/ 0, 0, 0, /**/ 0, 1, 9,
|
||||
5, 0, 2, /**/ 0, 7, 0, /**/ 8, 4, 0,
|
||||
/**********************************/
|
||||
4, 0, 6, /**/ 9, 0, 2, /**/ 3, 0, 5,
|
||||
0, 0, 0, /**/ 8, 0, 7, /**/ 0, 0, 0,
|
||||
1, 0, 7, /**/ 5, 0, 3, /**/ 4, 0, 8,
|
||||
/**********************************/
|
||||
0, 4, 1, /**/ 0, 9, 0, /**/ 5, 0, 2,
|
||||
9, 2, 0, /**/ 0, 0, 0, /**/ 0, 0, 7,
|
||||
6, 0, 0, /**/ 2, 0, 0, /**/ 0, 9, 0,
|
||||
]);
|
||||
board.constrain().unwrap();
|
||||
println!("{}", board);
|
||||
simple_logger::init_with_level(log::Level::Info).unwrap();
|
||||
|
||||
let mut board = board::Board::new(9, 9, 9);
|
||||
board.set_tile(0, 1, Some(6 -1));
|
||||
board.set_tile(0, 2, Some(8 -1));
|
||||
board.set_tile(0, 4, Some(3 -1));
|
||||
board.set_tile(0, 7, Some(7 -1));
|
||||
board.set_tile(1, 0, Some(2 -1));
|
||||
board.set_tile(1, 3, Some(9 -1));
|
||||
board.set_tile(1, 6, Some(1 -1));
|
||||
board.set_tile(2, 1, Some(4 -1));
|
||||
board.set_tile(2, 4, Some(8 -1));
|
||||
board.set_tile(2, 5, Some(1 -1));
|
||||
board.set_tile(2, 8, Some(3 -1));
|
||||
board.set_tile(3, 2, Some(7 -1));
|
||||
board.set_tile(3, 6, Some(4 -1));
|
||||
board.set_tile(4, 3, Some(8 -1));
|
||||
board.set_tile(4, 4, Some(1 -1));
|
||||
board.set_tile(4, 5, Some(9 -1));
|
||||
board.set_tile(5, 2, Some(9 -1));
|
||||
board.set_tile(5, 6, Some(2 -1));
|
||||
board.set_tile(6, 0, Some(7 -1));
|
||||
board.set_tile(6, 3, Some(3 -1));
|
||||
board.set_tile(6, 4, Some(9 -1));
|
||||
board.set_tile(6, 7, Some(4 -1));
|
||||
board.set_tile(7, 2, Some(2 -1));
|
||||
board.set_tile(7, 5, Some(5 -1));
|
||||
board.set_tile(7, 8, Some(6 -1));
|
||||
board.set_tile(8, 1, Some(3 -1));
|
||||
board.set_tile(8, 4, Some(7 -1));
|
||||
board.set_tile(8, 6, Some(9 -1));
|
||||
board.set_tile(8, 7, Some(5 -1));
|
||||
|
||||
if constrain::constrain_board(&mut board, &rules::valid_board_sudoku) {
|
||||
println!("{board}");
|
||||
} else {
|
||||
eprintln!("could not solve board!");
|
||||
}
|
||||
}
|
||||
|
|
115
src/rules.rs
Normal file
115
src/rules.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use crate::board::Board;
|
||||
|
||||
/**
|
||||
Checks whether a board is valid according to the rules of the game
|
||||
"Unruly" from Simon Tathams Portable Puzzle Collection.
|
||||
|
||||
Only works on boards which have an even number of tiles in width and height,
|
||||
and which have only two options.
|
||||
*/
|
||||
pub fn valid_board_unruly(board: &Board) -> bool {
|
||||
debug_assert!(board.get_num_options() == 2);
|
||||
debug_assert!(board.get_width() % 2 == 0);
|
||||
debug_assert!(board.get_height() % 2 == 0);
|
||||
|
||||
fn valid_window(win: &[Option<usize>]) -> bool {
|
||||
win[0].zip(win[1]).map_or(true, |(x, y)| x != y)
|
||||
|| win[1].zip(win[2]).map_or(true, |(x, y)| x != y)
|
||||
}
|
||||
|
||||
// check each column and row:
|
||||
// * may only contain n/2 number of one option
|
||||
// * may not contain more than 2 consecutives
|
||||
for x in 0..board.get_width() {
|
||||
let col = board.get_column(x);
|
||||
for opt in [0, 1] {
|
||||
if col.iter().filter(|&&x| x == Some(opt)).count() > (board.get_width() / 2) {
|
||||
log::warn!("col {x} invalid: too many of {opt}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !col.windows(3).all(valid_window) {
|
||||
log::warn!("col {x} invalid: invalid window");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0..board.get_height() {
|
||||
let row = board.get_row(y);
|
||||
for opt in [0, 1] {
|
||||
if row.iter().filter(|&&x| x == Some(opt)).count() > (board.get_width() / 2) {
|
||||
log::warn!("row {y} invalid: too many of {opt}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !row.windows(3).all(valid_window) {
|
||||
log::warn!("row {y} invalid: invalid window");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
Checks whether a board is valid according to the rules of ordinary 9x9 Sudoku:
|
||||
Numbers must be unique in row, column and each 3x3 cell.
|
||||
*/
|
||||
pub fn valid_board_sudoku(board: &Board) -> bool {
|
||||
debug_assert!(board.get_num_options() == 9);
|
||||
debug_assert!(board.get_width() == 9);
|
||||
debug_assert!(board.get_height() == 9);
|
||||
|
||||
fn get_cell(board: &Board, start_x: usize, start_y: usize) -> Vec<Option<usize>> {
|
||||
(start_x..start_x + 3)
|
||||
.flat_map(|x| (start_y..start_y + 3).map(|y| (x, y)).collect::<Vec<_>>())
|
||||
.map(|(x, y)| board.get_tile(x, y))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// check columns: must be unique
|
||||
for x in 0..9 {
|
||||
let mut set = HashSet::new();
|
||||
if !board
|
||||
.get_column(x)
|
||||
.iter()
|
||||
.filter_map(|&x| x)
|
||||
.all(|x| set.insert(x))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check rows: must be unique
|
||||
for y in 0..9 {
|
||||
let mut set = HashSet::new();
|
||||
if !board
|
||||
.get_row(y)
|
||||
.iter()
|
||||
.filter_map(|&x| x)
|
||||
.all(|x| set.insert(x))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check 3x3 cells: must be unique
|
||||
for x in [0,3,6] {
|
||||
for y in [0,3,6] {
|
||||
let mut set = HashSet::new();
|
||||
if !get_cell(board, x, y)
|
||||
.iter()
|
||||
.filter_map(|&x| x)
|
||||
.all(|x| set.insert(x))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
Loading…
Reference in a new issue