1
0
Fork 0
sudoku_constraint_solver/src/constrain.rs
2023-10-08 23:28:12 +02:00

119 lines
4.1 KiB
Rust

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
}