From 15b69c510c6d5d9f810a3d1901b546b236e71fcc Mon Sep 17 00:00:00 2001 From: Matt Low Date: Fri, 5 Mar 2021 12:31:47 -0700 Subject: [PATCH] Add solve endpoint Also implement currently unused solveSync and generateSync methods, useful for testing/debugging. Add static get() to SudokuMath which returns cached SudokuMath instances. --- src/graphql/sudoku.ts | 18 +++-- src/sudoku/dlx.ts | 4 +- src/sudoku/index.ts | 151 +++++++++++++++++++++++++++++++++++------- src/sudoku/math.ts | 30 ++++++--- src/sudoku/worker.js | 11 ++- 5 files changed, 166 insertions(+), 48 deletions(-) diff --git a/src/graphql/sudoku.ts b/src/graphql/sudoku.ts index 8cecf9a..81f7373 100644 --- a/src/graphql/sudoku.ts +++ b/src/graphql/sudoku.ts @@ -1,5 +1,10 @@ import { gql } from "../mods.js"; -import { generate, GenerateArguments } from "../sudoku/index.js"; +import { + solve, + generate, + GenerateArguments, + SolveArguments, +} from "../sudoku/index.js"; export const typeDefs = gql` """ @@ -22,16 +27,15 @@ export const typeDefs = gql` type Query { "Generates a new sudoku." generate(regionWidth: Int = 3, regionHeight: Int = 3, clues: Int!): Sudoku! + + "Solves the given sudoku." + solve(regionWidth: Int = 3, regionHeight: Int = 3, cells: [Int!]!): Sudoku! } `; export const resolvers = { Query: { - generate: ( - obj: any, - { regionWidth, regionHeight, clues }: GenerateArguments - ) => { - return generate(regionWidth, regionHeight, clues); - }, + generate: (obj: any, args: GenerateArguments) => generate(args), + solve: (obj: any, args: SolveArguments) => solve(args), }, }; diff --git a/src/sudoku/dlx.ts b/src/sudoku/dlx.ts index dd076e2..3e92024 100644 --- a/src/sudoku/dlx.ts +++ b/src/sudoku/dlx.ts @@ -114,10 +114,10 @@ export class DLX { let row = c as DNode; while ((row = row.down) !== c) { - // traverse DOWN the rows this column contained + // traverse down the rows this column let col = row; while ((col = col.right) !== row) { - // traverse the columns of this row (to the RIGHT) + // traverse the columns of this row to the right // remove this node from its column, and shrink its column's size this.updates++; diff --git a/src/sudoku/index.ts b/src/sudoku/index.ts index d7549af..056c2f9 100644 --- a/src/sudoku/index.ts +++ b/src/sudoku/index.ts @@ -1,7 +1,9 @@ import { spawn, Thread, Worker } from "threads"; +import type { ModuleThread } from "threads"; +import { SudokuMath } from "./math.js"; +import { prettyPrint } from "./util.js"; import WORKERS from "physical-cpu-count"; -import { prettyPrint } from "./util.js"; const TIMEOUT = 20000; @@ -13,6 +15,12 @@ export type GenerateArguments = { clues: number; }; +export type SolveArguments = { + regionWidth: number; + regionHeight: number; + cells: Cell[]; +}; + export type Sudoku = { regionWidth: number; regionHeight: number; @@ -20,20 +28,43 @@ export type Sudoku = { cells: Cell[]; }; -function getWorker() { - return spawn(new Worker("./worker")); -} +type SudokuWorker = { + generate: ( + regionWidth: number, + regionHeight: number, + clues: number + ) => Promise; + solve: ( + regionWidth: number, + regionHeight: number, + cells: number[] + ) => Promise<[boolean, number[]]>; +}; -const available: any = []; +const available: ModuleThread[] = []; + +function spawnWorker() { + spawn(new Worker("./worker")).then((worker) => + available.push(worker) + ); +} function initialize() { console.log(`Starting ${WORKERS} worker threads`); for (let n = 0; n < WORKERS; n++) { - getWorker().then((worker) => available.push(worker)); + spawnWorker(); } } initialize(); +function pickWorker() { + const proxy = available.pop(); + if (!proxy) { + throw new Error("No workers available right now. Please try again."); + } + return proxy; +} + /** * Awaits a promise with a timeout. * @@ -52,27 +83,33 @@ function withTimeout(promise: Promise, ms: number, cb: () => any) { }).finally(() => clearTimeout(timeout!)); } -export async function generate( - regionWidth: number, - regionHeight: number, - clues: number -): Promise { - const proxy = available.pop(); - if (!proxy) { - throw new Error("No workers available right now. Please try again."); - } - - const puzzle = await withTimeout( - proxy.generate(regionWidth, regionHeight, clues), - TIMEOUT, - () => { - Thread.terminate(proxy); - getWorker().then((worker) => available.push(worker)); - return new Error("Timed out. Try reducing the number of clues."); +function workerTaskWithTimeout( + task: (worker: ModuleThread) => Promise, + timeout: number +) { + const worker = pickWorker(); + let timedOut = false; + return withTimeout(task(worker), timeout, () => { + timedOut = true; + Thread.terminate(worker); + spawnWorker(); + return new Error("Timed out."); + }).finally(() => { + if (!timedOut) { + available.push(worker); } - ); + }); +} - available.push(proxy); +export async function generate({ + regionWidth, + regionHeight, + clues, +}: GenerateArguments): Promise { + const puzzle = await workerTaskWithTimeout( + (worker) => worker.generate(regionWidth, regionHeight, clues), + TIMEOUT + ); prettyPrint(regionWidth, regionHeight, puzzle); return { regionWidth, @@ -81,3 +118,67 @@ export async function generate( cells: puzzle, }; } + +export function generateSync({ + regionWidth, + regionHeight, + clues, +}: GenerateArguments): Sudoku { + const math = SudokuMath.get(regionWidth, regionHeight); + const cells = math.generate(clues, 1, TIMEOUT); + prettyPrint(regionWidth, regionHeight, cells); + return { + regionWidth, + regionHeight, + size: (regionWidth * regionHeight) ** 2, + cells, + }; +} + +export async function solve({ + regionWidth, + regionHeight, + cells, +}: SolveArguments): Promise { + const size = (regionWidth * regionHeight) ** 2; + if (size !== cells.length) { + throw new Error( + "The given region dimensions do not align with the number of cells." + ); + } + const [solved, result] = await workerTaskWithTimeout( + (proxy) => proxy.solve(regionWidth, regionHeight, cells), + TIMEOUT + ); + if (!solved) { + throw new Error("The given puzzle has no solution."); + } + return { + regionWidth, + regionHeight, + size, + cells: result, + }; +} + +export function solveSync({ + regionWidth, + regionHeight, + cells, +}: SolveArguments) { + const size = (regionWidth * regionHeight) ** 2; + if (size !== cells.length) { + throw new Error( + "The given region dimensions do not align with the number of cells." + ); + } + if (!SudokuMath.get(regionWidth, regionHeight).solve(cells)) { + throw new Error("The given puzzle has no solution."); + } + return { + regionWidth, + regionHeight, + size, + cells, + }; +} diff --git a/src/sudoku/math.ts b/src/sudoku/math.ts index fdfe125..ff6e3df 100644 --- a/src/sudoku/math.ts +++ b/src/sudoku/math.ts @@ -236,8 +236,11 @@ export class SudokuMath { } }; + let best = 0; const attempt = () => { - // attempt remove cells until 'clues' cells remain + // attempt to remove cells until 'clues' cells remain + attempts--; + removed.clear(); shuffle(removeable); maskUpto(0); @@ -245,7 +248,6 @@ export class SudokuMath { if (elapsed() > totalTime || this.values2 - removed.size == clues) { break; } - unmaskNextCell(); if (!hasOneSolution()) { @@ -257,6 +259,9 @@ export class SudokuMath { removed.add(removeable[i]); } + if (removed.size > best) { + best = removed.size; + } unmaskAll(); }; @@ -268,28 +273,37 @@ export class SudokuMath { ) { // 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[index] = 0; }); - console.log("DLX updates:", updates); return completed; } - solve(existing: Cell[]): void { - const [header] = this.getDLXHeader(existing); + solve(puzzle: Cell[]) { + const [header] = this.getDLXHeader(puzzle); + let solved = false; const callback = (solution: DNode[]) => { solution.forEach((node) => { const meta: NodeMeta = node.meta; - existing[meta.index] = meta.value; + puzzle[meta.index] = meta.value; }); + solved = true; // return the first solution return true; }; new DLX(header, callback).search(); + return solved; + } + + static maths: { [key: string]: SudokuMath } = {}; + static get(regionWidth: number, regionHeight: number) { + const key = `${regionWidth}:${regionHeight}`; + return ( + SudokuMath.maths[key] ?? + (SudokuMath.maths[key] = new SudokuMath(regionWidth, regionHeight)) + ); } } diff --git a/src/sudoku/worker.js b/src/sudoku/worker.js index a9db9b7..e0a7798 100644 --- a/src/sudoku/worker.js +++ b/src/sudoku/worker.js @@ -1,13 +1,12 @@ import { SudokuMath } from "./math.js"; import { expose } from "threads/worker"; -const maths = {}; - expose({ generate(regionWidth, regionHeight, clues) { - const key = `${regionWidth}:${regionHeight}`; - const math = - maths[key] ?? (maths[key] = new SudokuMath(regionWidth, regionHeight)); - return math.generate(clues); + return SudokuMath.get(regionWidth, regionHeight).generate(clues); + }, + solve(regionWidth, regionHeight, cells) { + const result = SudokuMath.get(regionWidth, regionHeight).solve(cells); + return [result, cells]; }, });