1
0
Fork 0

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:
Johann150 2023-10-08 21:25:56 +02:00
parent 4e1b5d7b05
commit 38157d7b5e
Signed by: Johann150
GPG Key ID: 9EE6577A2A06F8F1
5 changed files with 388 additions and 166 deletions

View File

@ -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
View 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
View 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
}

View File

@ -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
View 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
}