Compare commits

...

2 Commits

Author SHA1 Message Date
9ee76b10fa Add USE_WORKER_THREADS environment var 2021-03-05 12:56:20 -07:00
15b69c510c 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.
2021-03-05 12:32:02 -07:00
6 changed files with 191 additions and 50 deletions

View File

@ -1,5 +1,5 @@
import { gql } from "../mods.js";
import { generate, GenerateArguments } from "../sudoku/index.js";
import { Sudoku, GenerateArguments, SolveArguments } from "../sudoku/index.js";
export const typeDefs = gql`
"""
@ -22,16 +22,22 @@ 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!
}
`;
interface SudokuFuncs {
solve(args: SolveArguments): Promise<Sudoku> | Sudoku;
generate(args: GenerateArguments): Promise<Sudoku> | Sudoku;
}
export const resolvers = {
Query: {
generate: (
obj: any,
{ regionWidth, regionHeight, clues }: GenerateArguments
) => {
return generate(regionWidth, regionHeight, clues);
},
generate: (obj: any, args: GenerateArguments, ctx: SudokuFuncs) =>
ctx.generate(args),
solve: (obj: any, args: SolveArguments, ctx: SudokuFuncs) =>
ctx.solve(args),
},
};

View File

@ -1,6 +1,13 @@
import { Application, bodyParser } from "./mods.js";
import { applyGraphQL } from "./graphql.js";
import { typeDefs, resolvers } from "./graphql/index.js";
import {
initializeWorkers,
solve,
generate,
solveSync,
generateSync,
} from "./sudoku/index.js";
import stoppable from "stoppable";
import cors from "@koa/cors";
@ -31,10 +38,25 @@ async function main() {
);
});
let sudokuFuncs: any;
if (process.env.USE_WORKER_THREADS) {
initializeWorkers();
sudokuFuncs = {
solve,
generate,
};
} else {
sudokuFuncs = {
solve: solveSync,
generate: generateSync,
};
}
applyGraphQL({
app,
typeDefs: typeDefs,
resolvers: resolvers,
context: () => sudokuFuncs,
});
runtime.server = stoppable(

View File

@ -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++;

View File

@ -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,19 +28,41 @@ export type Sudoku = {
cells: Cell[];
};
function getWorker() {
return spawn(new Worker("./worker"));
type SudokuWorker = {
generate: (
regionWidth: number,
regionHeight: number,
clues: number
) => Promise<number[]>;
solve: (
regionWidth: number,
regionHeight: number,
cells: number[]
) => Promise<[boolean, number[]]>;
};
const available: ModuleThread<SudokuWorker>[] = [];
function spawnWorker() {
spawn<SudokuWorker>(new Worker("./worker")).then((worker) =>
available.push(worker)
);
}
const available: any = [];
function initialize() {
export function initializeWorkers() {
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 +82,33 @@ function withTimeout<T>(promise: Promise<T>, ms: number, cb: () => any) {
}).finally(() => clearTimeout(timeout!));
}
export async function generate(
regionWidth: number,
regionHeight: number,
clues: number
): Promise<Sudoku> {
const proxy = available.pop();
if (!proxy) {
throw new Error("No workers available right now. Please try again.");
function workerTaskWithTimeout(
task: (worker: ModuleThread) => Promise<any>,
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);
}
});
}
const puzzle = await withTimeout<number[]>(
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.");
}
export async function generate({
regionWidth,
regionHeight,
clues,
}: GenerateArguments): Promise<Sudoku> {
const puzzle = await workerTaskWithTimeout(
(worker) => worker.generate(regionWidth, regionHeight, clues),
TIMEOUT
);
available.push(proxy);
prettyPrint(regionWidth, regionHeight, puzzle);
return {
regionWidth,
@ -81,3 +117,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<Sudoku> {
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): Sudoku {
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,
};
}

View File

@ -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))
);
}
}

View File

@ -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];
},
});