sudoku-api/src/sudoku/math.ts
2021-02-19 23:55:49 -07:00

328 lines
9.2 KiB
TypeScript

import { Cell, Row, Column, Region, Puzzle } from "./index";
import {
clone,
range,
shuffle,
chunkify,
mapArray,
addCellValuesToSet,
} from "./util";
function getTakenValues(region: Region, row: Row, col: Column) {
const filter = new Set<number>();
addCellValuesToSet(filter, region);
addCellValuesToSet(filter, row);
addCellValuesToSet(filter, col);
return filter;
}
export class SudokuMath {
regionWidth: number;
regionHeight: number;
boardWidth: number;
boardHeight: number;
width: number;
height: number;
boardCells: number;
regionCells: number;
legalValues: number[];
_regionsFromIndex: number[];
_rowsFromIndex: number[];
_colsFromIndex: number[];
_rowIndexToRegionIndex: number[];
_colIndexToRegionIndex: number[];
_regionIndexToRowIndex: number[];
_regionIndexToColIndex: number[];
constructor(regionWidth: number, regionHeight: number) {
this.regionWidth = regionWidth;
this.regionHeight = regionHeight;
this.boardWidth = regionHeight;
this.boardHeight = regionWidth;
this.width = regionWidth * this.boardWidth;
this.height = regionHeight * this.boardHeight;
this.boardCells = this.width * this.height;
this.regionCells = regionWidth * regionHeight;
this.legalValues = range(1, this.regionCells);
this._regionsFromIndex = Array(this.boardCells);
this._rowsFromIndex = Array(this.boardCells);
this._colsFromIndex = Array(this.boardCells);
this._rowIndexToRegionIndex = Array(this.boardCells);
this._colIndexToRegionIndex = Array(this.boardCells);
this._regionIndexToRowIndex = Array(this.boardCells);
this._regionIndexToColIndex = Array(this.boardCells);
for (let i = 0; i < this.boardCells; i++) {
this._regionsFromIndex[i] = this._regionFromRegionIndex(i);
const [row, col] = this._rowColFromRegionIndex(i);
this._rowsFromIndex[i] = row;
this._colsFromIndex[i] = col;
const rowIndex = row * this.width + col;
const colIndex = col * this.height + row;
this._rowIndexToRegionIndex[rowIndex] = i;
this._colIndexToRegionIndex[i] = rowIndex;
this._regionIndexToRowIndex[i] = rowIndex;
this._regionIndexToColIndex[colIndex] = i;
}
}
_regionFromRegionIndex(i: number) {
return Math.trunc(i / this.regionCells);
}
regionFromRegionIndex(i: number) {
return this._regionsFromIndex[i];
}
_rowColFromRegionIndex(i: number) {
const region = this.regionFromRegionIndex(i);
const cell = i % this.regionCells;
const regionRow = Math.trunc(region / this.boardWidth);
const regionCol = region % this.boardWidth;
const cellRow = Math.trunc(cell / this.regionWidth);
const cellCol = cell % this.regionWidth;
return [
regionRow * this.regionHeight + cellRow,
regionCol * this.regionWidth + cellCol,
];
}
rowColFromRegionIndex(i: number) {
return [this._rowsFromIndex[i], this._colsFromIndex[i]];
}
regionIndexToRowIndex(i: number) {
return this._regionIndexToRowIndex[i];
}
rowIndexToRegionIndex(i: number) {
return this._rowIndexToRegionIndex[i];
}
regionIndexToColIndex(i: number) {
return this._regionIndexToColIndex[i];
}
colIndexToRegionIndex(i: number) {
return this._colIndexToRegionIndex[i];
}
chunkRegions(cells: Cell[]) {
return chunkify(cells, this.regionCells);
}
regionsToRows(cells: Cell[], split = false) {
const rows = mapArray(cells, this._regionIndexToRowIndex);
return split ? chunkify(rows, this.width) : rows;
}
regionsToCols(cells: Cell[], split = false) {
const cols = mapArray(cells, this._regionIndexToColIndex);
return split ? chunkify(cols, this.height) : cols;
}
rowsToRegions(cells: Cell[], split = false) {
const regions = mapArray(cells, this._rowIndexToRegionIndex);
return split ? chunkify(regions, this.regionCells) : regions;
}
colsToRegions(cells: Cell[], split = false) {
const regions = mapArray(cells, this._colIndexToRegionIndex);
return split ? chunkify(regions, this.regionCells) : regions;
}
getBlankPuzzle(): Puzzle {
return Array(this.boardCells)
.fill(null)
.map((value) => ({ value }));
}
/**
* Returns the remaining legal values given a set of taken values.
*
* @param taken a set of taken values
*/
getLegalValues(taken: Set<number>) {
return this.legalValues.filter((value) => !taken.has(value));
}
/**
* Returns which values are unavailable at a given location, looking at the
* row, column, and region of the given cell index.
*
* @param cell
* @param puzzle
* @param regions
*/
getTakenValues(cell: number, puzzle: Puzzle, regions: Region[]) {
const rows = this.regionsToRows(puzzle, true);
const cols = this.regionsToCols(puzzle, true);
const [row, col] = this.rowColFromRegionIndex(cell);
const region = this.regionFromRegionIndex(cell);
return getTakenValues(regions[region], rows[row], cols[col]);
}
/**
* Returns whether a puzzle has only one solution.
*
* @param puzzle
*/
hasOneSolution(puzzle: Cell[]) {
const optimistic = clone(puzzle);
if (this.optimisticSolver(optimistic)) {
return true;
}
return this.backTrackingSolver(optimistic, 2) === 1;
}
/**
* Generates a single-solution sudoku puzzle with
*
* @param clues the number of cells to have pre-filled
*/
generatePuzzle(clues: number) {
const puzzle = this.getBlankPuzzle();
this.backTrackingSolver(puzzle, 1, shuffle);
if (clues === -1 || clues >= puzzle.length) return puzzle;
const orig = clone(puzzle);
const toRemove = puzzle.length - clues;
let removed: number[] = [];
let removeNext: number[] = shuffle(puzzle.map((_, i) => i));
const remove = () => {
const x = removeNext.shift() as any;
removed.push(x);
puzzle[x].value = null;
};
const replace = () => {
const x = removed.pop() as any;
removeNext.push(x);
puzzle[x].value = orig[x].value;
};
const removeCell = () => {
remove();
if (this.hasOneSolution(puzzle)) {
return true;
}
replace();
return false;
};
let fails = 0;
while (removed.length < toRemove) {
if (!removeCell()) {
fails++;
} else {
console.log(`Removed ${removed.length} cells.`);
fails = 0;
}
if (fails > removeNext.length) {
fails = 0;
console.log("Backstepping..");
Array(removed.length)
.fill(null)
.forEach(() => replace());
shuffle(removeNext);
}
}
return puzzle;
}
/**
* Attempt to solve the puzzle "optimistically". Only sets values which are
* certain, i.e. no guesses are made.
*
* Useful as a first pass.
*
* @param puzzle a region-ordered array of cells (each cell an object with
* a `value` key.
* @returns whether the puzzle was completely solved
*/
optimisticSolver(puzzle: Puzzle) {
const regions = this.chunkRegions(puzzle);
const rows = this.regionsToRows(puzzle, true);
const cols = this.regionsToCols(puzzle, true);
const solve = (): boolean => {
let foundValue = false;
let foundEmpty = false;
for (let i = 0, len = puzzle.length; i < len; i++) {
const cell = puzzle[i];
if (!!cell.value) continue;
foundEmpty = true;
const region = this.regionFromRegionIndex(i);
const [row, col] = this.rowColFromRegionIndex(i);
const taken = getTakenValues(regions[region], rows[row], cols[col]);
if (taken.size === this.regionCells - 1) {
cell.value = this.getLegalValues(taken)[0];
foundValue = true;
}
}
return foundValue && foundEmpty ? solve() : !foundEmpty;
};
return solve();
}
/**
* Backtracking solver. Mutates the puzzle during solve but eventually returns
* it to its initial state.
*
* @param puzzle see optimisticSolver
* @param stopAfter stop looking after this many solutions
* @param guessStrategy a function which takes an array of possible
* values for a cell, and returns the same values (in any order)
* @returns the number of solutions found
*/
backTrackingSolver(
puzzle: Puzzle,
stopAfter: number = -1,
guessStrategy: (values: number[]) => number[] = (values: number[]) => values
) {
const regions = this.chunkRegions(puzzle);
const rows = this.regionsToRows(puzzle, true);
const cols = this.regionsToCols(puzzle, true);
let solutions = 0;
const solve = (): boolean => {
for (let i = 0, len = puzzle.length; i < len; i++) {
const cell = puzzle[i];
if (!cell.value) {
const region = this.regionFromRegionIndex(i);
const [row, col] = this.rowColFromRegionIndex(i);
const avail = guessStrategy(
this.getLegalValues(
getTakenValues(regions[region], rows[row], cols[col])
)
);
for (let j = 0; j < avail.length; j++) {
cell.value = avail[j];
if (solve() && solutions === stopAfter) {
return true;
}
cell.value = null;
}
return false;
}
}
solutions++;
return true;
};
solve();
return solutions;
}
}