From 8f3217f3ccb5f685a338c1cad2b47daf711874d7 Mon Sep 17 00:00:00 2001 From: Matt Low Date: Sun, 21 Feb 2021 20:01:36 -0700 Subject: [PATCH] Implement Algorithm X for solving and generating sudokus --- src/graphql/sudoku.ts | 15 +- src/sudoku/dlx.ts | 195 ++++++++++++++++ src/sudoku/index.ts | 30 +-- src/sudoku/math.ts | 520 +++++++++++++++++++----------------------- src/sudoku/util.ts | 43 +++- src/sudoku/worker.js | 13 +- 6 files changed, 486 insertions(+), 330 deletions(-) create mode 100644 src/sudoku/dlx.ts diff --git a/src/graphql/sudoku.ts b/src/graphql/sudoku.ts index 3dbee07..671f1d3 100644 --- a/src/graphql/sudoku.ts +++ b/src/graphql/sudoku.ts @@ -15,20 +15,11 @@ export const typeDefs = gql` "The height of each region." regionHeight: Int! - "The total number of cells in the board." - cells: Int! - - "The 'raw' board, an array of cells in region-first order." - raw: [Cell!]! + "The number of cells in the board." + size: Int! "The rows of the board, from top to bottom." - rows: [[Cell!]!]! - - "The columns of the board, from left to right." - columns: [[Cell!]!]! - - "The regions of the board, book-ordered." - regions: [[Cell!]!]! + cells: [[Cell!]!]! } type Query { diff --git a/src/sudoku/dlx.ts b/src/sudoku/dlx.ts new file mode 100644 index 0000000..1eb9f49 --- /dev/null +++ b/src/sudoku/dlx.ts @@ -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); + } +} diff --git a/src/sudoku/index.ts b/src/sudoku/index.ts index 19d3807..1b225bd 100644 --- a/src/sudoku/index.ts +++ b/src/sudoku/index.ts @@ -1,15 +1,9 @@ -import { SudokuMath } from "./math"; import { StaticPool, isTimeoutError } from "node-worker-threads-pool"; import WORKERS from "physical-cpu-count"; -const TIMEOUT = 5000; +const TIMEOUT = 20000; 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 = { regionWidth: number; @@ -20,21 +14,16 @@ export type GenerateArguments = { export type Sudoku = { regionWidth: number; regionHeight: number; - cells: number; - raw: Puzzle; - rows: () => Row[]; - columns: () => Column[]; - regions: () => Region[]; + size: number; + cells: Cell[][]; }; -const pool = new StaticPool({ +const pool = new StaticPool({ size: WORKERS, task: "./src/sudoku/worker.js", }); -const _math = new SudokuMath(0, 0); let activeWorkers = 0; - export async function generate( regionWidth: number, regionHeight: number, @@ -46,7 +35,7 @@ export async function generate( try { activeWorkers++; - const [math, puzzle] = await pool.exec( + const puzzle = await pool.exec( { regionWidth, regionHeight, @@ -55,16 +44,11 @@ export async function generate( TIMEOUT ); - Object.assign(_math, math); - return { regionWidth, regionHeight, - cells: math.boardCells, - raw: puzzle, - rows: () => _math.regionsToRows(puzzle, true), - columns: () => _math.regionsToCols(puzzle, true), - regions: () => _math.chunkRegions(puzzle), + size: (regionWidth * regionHeight) ** 2, + cells: puzzle, }; } catch (err) { if (isTimeoutError(err)) { diff --git a/src/sudoku/math.ts b/src/sudoku/math.ts index ba78c32..16e3a36 100644 --- a/src/sudoku/math.ts +++ b/src/sudoku/math.ts @@ -1,326 +1,278 @@ -import { Cell, Row, Column, Region, Puzzle } from "./index"; import { - clone, - range, - shuffle, - chunkify, - mapArray, - addCellValuesToSet, -} from "./util"; + DLX, + DNode, + CNode, + linkNodesLR, + addNodeToColumn, + maskRow, + unmaskRow, +} from "./dlx"; +import { shuffle, range } from "./util"; +import { Cell } from "./index"; -function getTakenValues(region: Region, row: Row, col: Column) { - const filter = new Set(); - addCellValuesToSet(filter, region); - addCellValuesToSet(filter, row); - addCellValuesToSet(filter, col); - return filter; -} +type NodeMeta = { + row: number; + col: number; + region: number; + value: number; +}; + +type BoardInfo = [number, number, number, number, number]; 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[]; + values: number; + values2: number; + + indexes: number[]; + + candidates: BoardInfo[]; 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); + this.values = regionWidth * regionHeight; + this.values2 = this.values * this.values; - for (let i = 0; i < this.boardCells; i++) { - this._regionsFromIndex[i] = this._regionFromRegionIndex(i); + this.indexes = Array(this.values2) + .fill(null) + .map((_, i) => 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; - } + this.candidates = Array.from(Array(this.values ** 3), (_, i) => + this.getRowColRegionValFromCandidate(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; + getConstraintIDs(val: number, row: number, col: number, region: number) { return [ - regionRow * this.regionHeight + cellRow, - regionCol * this.regionWidth + cellCol, + // each cell has a value + 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) { - return [this._rowsFromIndex[i], this._colsFromIndex[i]]; + getRowColRegionValFromCandidate(candidate: number): BoardInfo { + 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) { - 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) { - 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; + _checkInput(cells: Cell[][]) { + if (cells.length !== this.values || cells[0].length !== this.values) { + throw new Error( + "Given cells array does not match regionWidth & regionHeight" + ); } - 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); + // this takes a bit of time and the value may need to be cached + getDLXHeader( + cells: undefined | Cell[][] = undefined, + randomSearch = false + ): [CNode, DNode[]] { + if (cells) this._checkInput(cells); - if (clues === -1 || clues >= puzzle.length) return puzzle; + const header = new CNode(); + header.name = "h"; - const orig = clone(puzzle); + const constraints = new Array(this.values2 * 4) + .fill(null!) + .map((_, i) => { + const column = new CNode(); + column.name = `${i}`; + column.meta = i; + return column; + }); - const toRemove = puzzle.length - clues; - let removed: number[] = []; - let removeNext: number[] = shuffle(puzzle.map((_, i) => i)); + // link together the header and constraint columns + linkNodesLR([header, ...constraints]); - const remove = () => { - const x = removeNext.shift() as any; - removed.push(x); - puzzle[x].value = null; - }; + const candidates = randomSearch + ? shuffle(Array.from(this.candidates)) + : this.candidates; - 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; - 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; + const dlxRows: DNode[] = []; + candidates.forEach(([i, row, col, region, val]) => { + if (cells) { + const exist = cells[row][col].value; + if (exist && exist - 1 !== val) { + // skip candidates matching this constraint's position, but not its value + // the effect is the exisitng value is preserved in the output + return; } } - 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]; } - /** - * 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); + _baseBoard(): Cell[][] { + // return a sudoku board with a random set of values in the first row + // used in generateComplete for small speedup + const firstRow = range(1, this.values + 1); + shuffle(firstRow); + return [ + firstRow.map((value) => ({ value })), + ...Array(this.values - 1) + .fill(null) + .map(() => + Array(this.values) + .fill(0) + .map((val) => ({ value: val > 0 ? val : null })) + ), + ]; + } + + 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; - 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; + const dlx = new DLX(header, () => ++solutions >= 2); + + const candidates: DNode[][][] = Array.from(Array(this.values), () => + Array.from(Array(this.values), () => Array(this.values)) + ); + dlxRows.forEach((node) => { + const meta = node.meta; + candidates[meta.row][meta.col][meta.value - 1] = node; + }); + + // board positions which have been removed, in the order they've been removed + const removed: Set = new Set(); + const masked: DNode[] = []; + + const hasOneSolution = () => { + solutions = 0; + dlx.search(); + return solutions === 1; }; - 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(); } } diff --git a/src/sudoku/util.ts b/src/sudoku/util.ts index 0be8e15..7846663 100644 --- a/src/sudoku/util.ts +++ b/src/sudoku/util.ts @@ -1,14 +1,14 @@ import { Cell } from "./index"; -function randInt(lower: number, upper: number) { - return lower + Math.trunc(Math.random() * (upper - lower + 1)); +export function randInt(lower: number, upper: number) { + return Math.floor(Math.random() * (upper - lower)) + lower; } -export function shuffle(arr: any[]) { +export function shuffle(arr: T[]) { const length = arr.length; - let lastIndex = length - 1; + const last = length; for (let i = 0; i < length; i++) { - const rand = randInt(i, lastIndex); + const rand = randInt(i, last); const tmp = arr[rand]; arr[rand] = arr[i]; @@ -17,8 +17,8 @@ export function shuffle(arr: any[]) { return arr; } -export function chunkify(arr: any[], chunkSize: number) { - const chunks = Array(arr.length / chunkSize); +export function chunkify(arr: T[], chunkSize: number) { + const chunks = Array(arr.length / chunkSize); for (let i = 0, len = chunks.length; i < len; i++) { const start = i * 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) { - return Array(1 + end - start) + return Array(end - start) .fill(null) .map((_, i) => start + i); } @@ -57,3 +57,30 @@ export function addCellValuesToSet(set: Set, cells: Cell[]) { }); 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); + }); +} diff --git a/src/sudoku/worker.js b/src/sudoku/worker.js index 3ad5614..61a0d50 100644 --- a/src/sudoku/worker.js +++ b/src/sudoku/worker.js @@ -1,8 +1,15 @@ const { SudokuMath } = require("./math"); const { parentPort } = require("worker_threads"); +const maths = {}; + parentPort.on("message", ({ regionWidth, regionHeight, clues }) => { - const math = new SudokuMath(regionWidth, regionHeight); - const puzzle = math.generatePuzzle(clues); - parentPort.postMessage([math, puzzle]); + const math = + maths[`${regionWidth}:${regionHeight}`] || + (maths[`${regionWidth}:${regionHeight}`] = new SudokuMath( + regionWidth, + regionHeight + )); + const puzzle = math.generate(clues); + parentPort.postMessage(puzzle); });