Implement Algorithm X for solving and generating sudokus

This commit is contained in:
Matt Low 2021-02-21 20:01:36 -07:00
parent 7ac3e6ad06
commit 8f3217f3cc
6 changed files with 486 additions and 330 deletions

View File

@ -15,20 +15,11 @@ export const typeDefs = gql`
"The height of each region." "The height of each region."
regionHeight: Int! regionHeight: Int!
"The total number of cells in the board." "The number of cells in the board."
cells: Int! size: Int!
"The 'raw' board, an array of cells in region-first order."
raw: [Cell!]!
"The rows of the board, from top to bottom." "The rows of the board, from top to bottom."
rows: [[Cell!]!]! cells: [[Cell!]!]!
"The columns of the board, from left to right."
columns: [[Cell!]!]!
"The regions of the board, book-ordered."
regions: [[Cell!]!]!
} }
type Query { type Query {

195
src/sudoku/dlx.ts Normal file
View File

@ -0,0 +1,195 @@
export class DNode {
up: DNode;
down: DNode;
left: DNode;
right: DNode;
column: CNode;
meta: any;
constructor(column: CNode) {
this.up = this.down = this.left = this.right = this;
this.column = column;
}
}
export class CNode extends DNode {
left: CNode;
right: CNode;
size: number;
name: string | undefined;
constructor(name = undefined) {
super(null!);
this.column = this;
this.size = 0;
this.name = name;
this.left = this.right = this;
}
}
type SolutionCallback = (output: DNode[]) => boolean;
type ColumnSelector = (header: CNode) => CNode;
function selectColumnSizeHeuristic(header: CNode): CNode {
let minSize = Infinity;
let minColumn: CNode | undefined;
let curColumn = header;
while ((curColumn = curColumn.right) !== header) {
if (curColumn.column.size < minSize) {
minSize = curColumn.column.size;
minColumn = curColumn;
}
}
if (!minColumn) {
throw new Error("minColumn is undefined, this shouldn't be possible.");
}
return minColumn;
}
export function linkNodesLR(nodes: DNode[]) {
// given an array of nodes, link them together left-to-right circularly
let last = nodes[0];
for (let j = 1; j < nodes.length; j++) {
const node = nodes[j];
last.right = node;
node.left = last;
last = node;
}
nodes[0].left = last;
last.right = nodes[0];
return nodes;
}
export function addNodeToColumn(column: CNode, meta: any) {
column.size++;
let node = new DNode(column);
node.down = column; // new last node, points down to column
node.up = column.up; // new last node points up to previous last node
// previous last node points down and column points up to new node,
// repairing the circle
column.up.down = column.up = node;
node.meta = meta;
return node;
}
export function maskRow(node: DNode) {
let row = node;
do {
row.column.size--;
row.down.up = row.up;
row.up.down = row.down;
} while ((row = row.right) !== node);
}
export function unmaskRow(node: DNode) {
let row = node;
do {
row.column.size++;
row.down.up = row.up.down = row;
} while ((row = row.right) !== node);
}
export class DLX {
header: CNode;
updates: number = 0;
callback: SolutionCallback;
columnSelector: ColumnSelector;
constructor(
header: CNode,
callback: SolutionCallback = () => false,
columnSelector: ColumnSelector = selectColumnSizeHeuristic
) {
this.header = header;
this.callback = callback;
this.columnSelector = columnSelector;
}
cover(c: CNode) {
// remove c from column header list
c.right.left = c.left;
c.left.right = c.right;
this.updates++;
let row = c as DNode;
while ((row = row.down) !== c) {
// traverse DOWN the rows this column contained
let col = row;
while ((col = col.right) !== row) {
// traverse the columns of this row (to the RIGHT)
// remove this node from its column, and shrink its column's size
this.updates++;
col.up.down = col.down;
col.down.up = col.up;
col.column.size--;
}
}
}
uncover(c: CNode) {
let row = c as DNode;
while ((row = row.up) !== c) {
// traverse UP the rows this column contained
let col = row;
while ((col = col.left) !== row) {
// traverse the columns of this row (to the LEFT)
// do the inverse of cover()
this.updates++;
col.up.down = col.down.up = col;
col.column.size++;
}
}
// insert c back into column header list
this.updates++;
c.left.right = c.right.left = c;
}
solution: DNode[] = [];
_search(k: number): boolean {
if (this.header.right === this.header) {
return this.callback(this.solution);
}
const column = this.columnSelector(this.header);
this.cover(column);
let complete = false;
let row = column as DNode;
while ((row = row.down) !== column) {
// add this row to the partial solution
this.solution[k] = row;
// cover the columns of every other node on this row
let curCol = row;
while ((curCol = curCol.right) !== row) {
this.cover(curCol.column);
}
complete = this._search(k + 1);
curCol = row;
while ((curCol = curCol.left) !== row) {
this.uncover(curCol.column);
}
if (complete) break;
}
this.uncover(column);
return complete;
}
search() {
this.updates = 0;
this._search(0);
}
}

View File

@ -1,15 +1,9 @@
import { SudokuMath } from "./math";
import { StaticPool, isTimeoutError } from "node-worker-threads-pool"; import { StaticPool, isTimeoutError } from "node-worker-threads-pool";
import WORKERS from "physical-cpu-count"; import WORKERS from "physical-cpu-count";
const TIMEOUT = 5000; const TIMEOUT = 20000;
export type Cell = { value: number | null }; export type Cell = { value: number | null };
export type CellArray = Cell[];
export type Row = CellArray;
export type Column = CellArray;
export type Region = CellArray;
export type Puzzle = CellArray;
export type GenerateArguments = { export type GenerateArguments = {
regionWidth: number; regionWidth: number;
@ -20,21 +14,16 @@ export type GenerateArguments = {
export type Sudoku = { export type Sudoku = {
regionWidth: number; regionWidth: number;
regionHeight: number; regionHeight: number;
cells: number; size: number;
raw: Puzzle; cells: Cell[][];
rows: () => Row[];
columns: () => Column[];
regions: () => Region[];
}; };
const pool = new StaticPool<GenerateArguments, [any, Puzzle]>({ const pool = new StaticPool<GenerateArguments, Cell[][]>({
size: WORKERS, size: WORKERS,
task: "./src/sudoku/worker.js", task: "./src/sudoku/worker.js",
}); });
const _math = new SudokuMath(0, 0);
let activeWorkers = 0; let activeWorkers = 0;
export async function generate( export async function generate(
regionWidth: number, regionWidth: number,
regionHeight: number, regionHeight: number,
@ -46,7 +35,7 @@ export async function generate(
try { try {
activeWorkers++; activeWorkers++;
const [math, puzzle] = await pool.exec( const puzzle = await pool.exec(
{ {
regionWidth, regionWidth,
regionHeight, regionHeight,
@ -55,16 +44,11 @@ export async function generate(
TIMEOUT TIMEOUT
); );
Object.assign(_math, math);
return { return {
regionWidth, regionWidth,
regionHeight, regionHeight,
cells: math.boardCells, size: (regionWidth * regionHeight) ** 2,
raw: puzzle, cells: puzzle,
rows: () => _math.regionsToRows(puzzle, true),
columns: () => _math.regionsToCols(puzzle, true),
regions: () => _math.chunkRegions(puzzle),
}; };
} catch (err) { } catch (err) {
if (isTimeoutError(err)) { if (isTimeoutError(err)) {

View File

@ -1,326 +1,278 @@
import { Cell, Row, Column, Region, Puzzle } from "./index";
import { import {
clone, DLX,
range, DNode,
shuffle, CNode,
chunkify, linkNodesLR,
mapArray, addNodeToColumn,
addCellValuesToSet, maskRow,
} from "./util"; unmaskRow,
} from "./dlx";
import { shuffle, range } from "./util";
import { Cell } from "./index";
function getTakenValues(region: Region, row: Row, col: Column) { type NodeMeta = {
const filter = new Set<number>(); row: number;
addCellValuesToSet(filter, region); col: number;
addCellValuesToSet(filter, row); region: number;
addCellValuesToSet(filter, col); value: number;
return filter; };
}
type BoardInfo = [number, number, number, number, number];
export class SudokuMath { export class SudokuMath {
regionWidth: number; regionWidth: number;
regionHeight: number; regionHeight: number;
boardWidth: number;
boardHeight: number;
width: number;
height: number;
boardCells: number;
regionCells: number;
legalValues: number[];
_regionsFromIndex: number[]; values: number;
_rowsFromIndex: number[]; values2: number;
_colsFromIndex: number[];
_rowIndexToRegionIndex: number[]; indexes: number[];
_colIndexToRegionIndex: number[];
_regionIndexToRowIndex: number[]; candidates: BoardInfo[];
_regionIndexToColIndex: number[];
constructor(regionWidth: number, regionHeight: number) { constructor(regionWidth: number, regionHeight: number) {
this.regionWidth = regionWidth; this.regionWidth = regionWidth;
this.regionHeight = regionHeight; 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.values = regionWidth * regionHeight;
this._rowsFromIndex = Array(this.boardCells); this.values2 = this.values * this.values;
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.indexes = Array(this.values2)
this._regionsFromIndex[i] = this._regionFromRegionIndex(i); .fill(null)
.map((_, i) => i);
const [row, col] = this._rowColFromRegionIndex(i); this.candidates = Array.from(Array(this.values ** 3), (_, i) =>
this._rowsFromIndex[i] = row; this.getRowColRegionValFromCandidate(i)
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) { getConstraintIDs(val: number, row: number, col: number, region: 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 [ return [
regionRow * this.regionHeight + cellRow, // each cell has a value
regionCol * this.regionWidth + cellCol, row * this.values + col,
// each row has one of each value
this.values2 + row * this.values + val,
// each col has one of each value
this.values2 * 2 + col * this.values + val,
// each region has one of each value
this.values2 * 3 + region * this.values + val,
]; ];
} }
rowColFromRegionIndex(i: number) { getRowColRegionValFromCandidate(candidate: number): BoardInfo {
return [this._rowsFromIndex[i], this._colsFromIndex[i]]; const boardIndex = Math.floor(candidate / this.values);
const row = Math.floor(boardIndex / this.values);
const col = boardIndex % this.values;
const region =
Math.floor(row / this.regionHeight) * this.regionHeight +
Math.floor(col / this.regionWidth);
const val = candidate % this.values;
return [candidate, row, col, region, val];
} }
regionIndexToRowIndex(i: number) { _checkInput(cells: Cell[][]) {
return this._regionIndexToRowIndex[i]; if (cells.length !== this.values || cells[0].length !== this.values) {
} throw new Error(
"Given cells array does not match regionWidth & regionHeight"
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;
} }
/** // this takes a bit of time and the value may need to be cached
* Generates a single-solution sudoku puzzle with getDLXHeader(
* cells: undefined | Cell[][] = undefined,
* @param clues the number of cells to have pre-filled randomSearch = false
*/ ): [CNode, DNode[]] {
generatePuzzle(clues: number) { if (cells) this._checkInput(cells);
const puzzle = this.getBlankPuzzle();
this.backTrackingSolver(puzzle, 1, shuffle);
if (clues === -1 || clues >= puzzle.length) return puzzle; const header = new CNode();
header.name = "h";
const orig = clone(puzzle); const constraints = new Array<CNode>(this.values2 * 4)
.fill(null!)
.map((_, i) => {
const column = new CNode();
column.name = `${i}`;
column.meta = i;
return column;
});
const toRemove = puzzle.length - clues; // link together the header and constraint columns
let removed: number[] = []; linkNodesLR([header, ...constraints]);
let removeNext: number[] = shuffle(puzzle.map((_, i) => i));
const remove = () => { const candidates = randomSearch
const x = removeNext.shift() as any; ? shuffle(Array.from(this.candidates))
removed.push(x); : this.candidates;
puzzle[x].value = null;
};
const replace = () => { const dlxRows: DNode[] = [];
const x = removed.pop() as any; candidates.forEach(([i, row, col, region, val]) => {
removeNext.push(x); if (cells) {
puzzle[x].value = orig[x].value; const exist = cells[row][col].value;
}; if (exist && exist - 1 !== val) {
// skip candidates matching this constraint's position, but not its value
const removeCell = () => { // the effect is the exisitng value is preserved in the output
remove(); return;
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;
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(); const meta = { row, col, region, value: val + 1 };
const dlxRow = linkNodesLR(
this.getConstraintIDs(val, row, col, region).map((id) =>
addNodeToColumn(constraints[id], meta)
)
);
dlxRows.push(dlxRow[0]);
});
return [header, dlxRows];
} }
/** _baseBoard(): Cell[][] {
* Backtracking solver. Mutates the puzzle during solve but eventually returns // return a sudoku board with a random set of values in the first row
* it to its initial state. // used in generateComplete for small speedup
* const firstRow = range(1, this.values + 1);
* @param puzzle see optimisticSolver shuffle(firstRow);
* @param stopAfter stop looking after this many solutions return [
* @param guessStrategy a function which takes an array of possible firstRow.map((value) => ({ value })),
* values for a cell, and returns the same values (in any order) ...Array(this.values - 1)
* @returns the number of solutions found .fill(null)
*/ .map(() =>
backTrackingSolver( Array(this.values)
puzzle: Puzzle, .fill(0)
stopAfter: number = -1, .map((val) => ({ value: val > 0 ? val : null }))
guessStrategy: (values: number[]) => number[] = (values: number[]) => values ),
) { ];
const regions = this.chunkRegions(puzzle); }
const rows = this.regionsToRows(puzzle, true);
const cols = this.regionsToCols(puzzle, true); generateComplete() {
const result = this._baseBoard();
const [header] = this.getDLXHeader(result, true);
const callback = (solution: DNode[]) => {
solution.forEach((node) => {
const meta: NodeMeta = node.meta;
result[meta.row][meta.col] = { value: meta.value };
});
// return the first solution
return true;
};
const dlx = new DLX(header, callback);
dlx.search();
return result;
}
generate(clues: number, attempts = Infinity, totalTime = Infinity) {
const completed = this.generateComplete();
const [header, dlxRows] = this.getDLXHeader(); // complete header - no candidates removed
let solutions = 0; let solutions = 0;
const solve = (): boolean => { const dlx = new DLX(header, () => ++solutions >= 2);
for (let i = 0, len = puzzle.length; i < len; i++) {
const cell = puzzle[i]; const candidates: DNode[][][] = Array.from(Array(this.values), () =>
if (!cell.value) { Array.from(Array(this.values), () => Array(this.values))
const region = this.regionFromRegionIndex(i); );
const [row, col] = this.rowColFromRegionIndex(i); dlxRows.forEach((node) => {
const avail = guessStrategy( const meta = node.meta;
this.getLegalValues( candidates[meta.row][meta.col][meta.value - 1] = node;
getTakenValues(regions[region], rows[row], cols[col]) });
)
); // board positions which have been removed, in the order they've been removed
for (let j = 0; j < avail.length; j++) { const removed: Set<number> = new Set();
cell.value = avail[j]; const masked: DNode[] = [];
if (solve() && solutions === stopAfter) {
return true; const hasOneSolution = () => {
} solutions = 0;
cell.value = null; dlx.search();
} return solutions === 1;
return false;
}
}
solutions++;
return true;
}; };
solve(); const mask = () => {
// mask all DLX rows which are nullified by existing values
for (let n = 0; n < this.values2; n++) {
if (removed.has(n)) {
continue;
}
const row = Math.floor(n / this.values);
const col = n % this.values;
const existValue = completed[row][col].value;
const nodes = candidates[row][col];
// console.log(row, col);
// console.log(existValue);
nodes.forEach((node) => {
if (node.meta.value !== existValue) {
// console.log(node.meta);
// console.log("masking node");
masked.push(node);
maskRow(node);
}
});
}
};
return solutions; const unmask = () => {
// unmask all DLX rows
while (masked.length > 0) {
unmaskRow(masked.pop()!);
}
};
const start = Date.now();
const elapsed = () => Date.now() - start;
const removeable = Array.from(this.indexes);
const attempt = () => {
// attempt remove cells until 'clues' cells remain
shuffle(removeable);
for (let n = 0; n < this.values2; n++) {
if (elapsed() > totalTime || this.values2 - removed.size == clues) {
break;
}
let toRemove = removeable[n];
removed.add(toRemove);
mask();
if (!hasOneSolution()) {
removed.delete(toRemove);
}
unmask();
}
};
while (
this.values2 - removed.size > clues &&
attempts > 0 &&
elapsed() < totalTime
) {
// try to reach the clue goal up to `attempts` times or as long as
// elapsed time is less than `totalTime`
attempts--;
removed.clear();
attempt();
}
removed.forEach((index) => {
completed[Math.floor(index / this.values)][
index % this.values
].value = null;
});
return completed;
}
solve(existing: Cell[][]): void {
const [header] = this.getDLXHeader(existing);
const callback = (solution: DNode[]) => {
solution.forEach((node) => {
const meta: NodeMeta = node.meta;
existing[meta.row][meta.col] = { value: meta.value };
});
// return the first solution
return true;
};
new DLX(header, callback).search();
} }
} }

View File

@ -1,14 +1,14 @@
import { Cell } from "./index"; import { Cell } from "./index";
function randInt(lower: number, upper: number) { export function randInt(lower: number, upper: number) {
return lower + Math.trunc(Math.random() * (upper - lower + 1)); return Math.floor(Math.random() * (upper - lower)) + lower;
} }
export function shuffle(arr: any[]) { export function shuffle<T>(arr: T[]) {
const length = arr.length; const length = arr.length;
let lastIndex = length - 1; const last = length;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const rand = randInt(i, lastIndex); const rand = randInt(i, last);
const tmp = arr[rand]; const tmp = arr[rand];
arr[rand] = arr[i]; arr[rand] = arr[i];
@ -17,8 +17,8 @@ export function shuffle(arr: any[]) {
return arr; return arr;
} }
export function chunkify(arr: any[], chunkSize: number) { export function chunkify<T>(arr: T[], chunkSize: number) {
const chunks = Array(arr.length / chunkSize); const chunks = Array<T[]>(arr.length / chunkSize);
for (let i = 0, len = chunks.length; i < len; i++) { for (let i = 0, len = chunks.length; i < len; i++) {
const start = i * chunkSize; const start = i * chunkSize;
chunks[i] = arr.slice(start, start + chunkSize); chunks[i] = arr.slice(start, start + chunkSize);
@ -27,7 +27,7 @@ export function chunkify(arr: any[], chunkSize: number) {
} }
export function range(start: number, end: number) { export function range(start: number, end: number) {
return Array(1 + end - start) return Array(end - start)
.fill(null) .fill(null)
.map((_, i) => start + i); .map((_, i) => start + i);
} }
@ -57,3 +57,30 @@ export function addCellValuesToSet(set: Set<number>, cells: Cell[]) {
}); });
return set; return set;
} }
export function prettyPrint(puzzle: Cell[][]) {
let width = Math.sqrt(puzzle[0].length);
let height = Math.sqrt(puzzle.length);
puzzle.forEach((row, i) => {
let line = "";
row.forEach(({ value: cell }, j) => {
if (j > 0 && j % width == 0) {
line += "| ";
}
line += ((cell ? cell : " ") + " ").padStart(3, " ");
});
if (i > 0 && i % height == 0) {
let divider = "";
row.forEach((_, j) => {
if (j > 0 && j % width == 0) {
divider += " ";
}
divider += "-- ";
});
console.log(divider);
}
console.log(line);
});
}

View File

@ -1,8 +1,15 @@
const { SudokuMath } = require("./math"); const { SudokuMath } = require("./math");
const { parentPort } = require("worker_threads"); const { parentPort } = require("worker_threads");
const maths = {};
parentPort.on("message", ({ regionWidth, regionHeight, clues }) => { parentPort.on("message", ({ regionWidth, regionHeight, clues }) => {
const math = new SudokuMath(regionWidth, regionHeight); const math =
const puzzle = math.generatePuzzle(clues); maths[`${regionWidth}:${regionHeight}`] ||
parentPort.postMessage([math, puzzle]); (maths[`${regionWidth}:${regionHeight}`] = new SudokuMath(
regionWidth,
regionHeight
));
const puzzle = math.generate(clues);
parentPort.postMessage(puzzle);
}); });