119 lines
4.1 KiB
Rust
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
|
|
}
|