Compare commits
3 Commits
master
...
3c6d69c343
Author | SHA1 | Date | |
---|---|---|---|
3c6d69c343 | |||
1ff71d099e | |||
ae9016fbf6 |
@ -1,5 +1,5 @@
|
||||
import { gql } from "../mods.js";
|
||||
import { Sudoku, GenerateArguments, SolveArguments } from "../sudoku/index.js";
|
||||
import { generate, GenerateArguments } from "../sudoku/index.js";
|
||||
|
||||
export const typeDefs = gql`
|
||||
"""
|
||||
@ -22,22 +22,16 @@ 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, args: GenerateArguments, ctx: SudokuFuncs) =>
|
||||
ctx.generate(args),
|
||||
solve: (obj: any, args: SolveArguments, ctx: SudokuFuncs) =>
|
||||
ctx.solve(args),
|
||||
generate: (
|
||||
obj: any,
|
||||
{ regionWidth, regionHeight, clues }: GenerateArguments
|
||||
) => {
|
||||
return generate(regionWidth, regionHeight, clues);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
22
src/main.ts
22
src/main.ts
@ -1,13 +1,6 @@
|
||||
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";
|
||||
@ -38,25 +31,10 @@ 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(
|
||||
|
@ -114,10 +114,10 @@ export class DLX {
|
||||
|
||||
let row = c as DNode;
|
||||
while ((row = row.down) !== c) {
|
||||
// traverse down the rows this column
|
||||
// traverse DOWN the rows this column contained
|
||||
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++;
|
||||
|
@ -1,9 +1,7 @@
|
||||
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;
|
||||
|
||||
@ -15,12 +13,6 @@ export type GenerateArguments = {
|
||||
clues: number;
|
||||
};
|
||||
|
||||
export type SolveArguments = {
|
||||
regionWidth: number;
|
||||
regionHeight: number;
|
||||
cells: Cell[];
|
||||
};
|
||||
|
||||
export type Sudoku = {
|
||||
regionWidth: number;
|
||||
regionHeight: number;
|
||||
@ -28,41 +20,19 @@ export type Sudoku = {
|
||||
cells: Cell[];
|
||||
};
|
||||
|
||||
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)
|
||||
);
|
||||
function getWorker() {
|
||||
return spawn(new Worker("./worker"));
|
||||
}
|
||||
|
||||
export function initializeWorkers() {
|
||||
const available: any = [];
|
||||
|
||||
function initialize() {
|
||||
console.log(`Starting ${WORKERS} worker threads`);
|
||||
for (let n = 0; n < WORKERS; n++) {
|
||||
spawnWorker();
|
||||
getWorker().then((worker) => available.push(worker));
|
||||
}
|
||||
}
|
||||
|
||||
function pickWorker() {
|
||||
const proxy = available.pop();
|
||||
if (!proxy) {
|
||||
throw new Error("No workers available right now. Please try again.");
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
initialize();
|
||||
|
||||
/**
|
||||
* Awaits a promise with a timeout.
|
||||
@ -82,33 +52,27 @@ function withTimeout<T>(promise: Promise<T>, ms: number, cb: () => any) {
|
||||
}).finally(() => clearTimeout(timeout!));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
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.");
|
||||
}
|
||||
|
||||
export async function generate({
|
||||
regionWidth,
|
||||
regionHeight,
|
||||
clues,
|
||||
}: GenerateArguments): Promise<Sudoku> {
|
||||
const puzzle = await workerTaskWithTimeout(
|
||||
(worker) => worker.generate(regionWidth, regionHeight, clues),
|
||||
TIMEOUT
|
||||
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.");
|
||||
}
|
||||
);
|
||||
|
||||
available.push(proxy);
|
||||
prettyPrint(regionWidth, regionHeight, puzzle);
|
||||
return {
|
||||
regionWidth,
|
||||
@ -117,67 +81,3 @@ 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,
|
||||
};
|
||||
}
|
||||
|
@ -141,28 +141,27 @@ export class SudokuMath {
|
||||
return [...firstRow, ...Array(this.values2 - this.values).fill(0)];
|
||||
}
|
||||
|
||||
generateComplete(): [number[], number] {
|
||||
generateComplete() {
|
||||
const result = this._baseBoard();
|
||||
const [header] = this.getDLXHeader(result, true);
|
||||
const dlx = new DLX(header, (solution: DNode[]) => {
|
||||
|
||||
const callback = (solution: DNode[]) => {
|
||||
solution.forEach((node) => {
|
||||
const meta: NodeMeta = node.meta;
|
||||
result[meta.index] = meta.value;
|
||||
});
|
||||
// stop after the first solution
|
||||
// return the first solution
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const dlx = new DLX(header, callback);
|
||||
|
||||
dlx.search();
|
||||
return [result, dlx.updates];
|
||||
return result;
|
||||
}
|
||||
|
||||
generate(clues: number, attempts = Infinity, totalTime = Infinity) {
|
||||
if (clues === 0) {
|
||||
return Array(this.values2).fill(0);
|
||||
}
|
||||
|
||||
let [completed, updates] = this.generateComplete();
|
||||
const completed = this.generateComplete();
|
||||
|
||||
const [header, dlxRows] = this.getDLXHeader(); // complete header - no candidates removed
|
||||
|
||||
@ -177,93 +176,61 @@ export class SudokuMath {
|
||||
candidates[meta.index][meta.value - 1] = node;
|
||||
});
|
||||
|
||||
// board positions which have been removed
|
||||
const removed = new Set<number>();
|
||||
const masked: DNode[] = [];
|
||||
|
||||
const hasOneSolution = () => {
|
||||
solutions = 0;
|
||||
dlx.search();
|
||||
updates += dlx.updates;
|
||||
return solutions === 1;
|
||||
};
|
||||
|
||||
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 nodes = candidates[n];
|
||||
nodes.forEach((node) => {
|
||||
if (node.meta.value !== completed[n]) {
|
||||
masked.push(node);
|
||||
maskRow(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unmask = () => {
|
||||
// unmask all DLX rows
|
||||
while (masked.length > 0) {
|
||||
unmaskRow(masked.pop()!);
|
||||
}
|
||||
};
|
||||
|
||||
const start = Date.now();
|
||||
const elapsed = () => Date.now() - start;
|
||||
|
||||
// masked rows in the order they were masked
|
||||
const masked: DNode[] = [];
|
||||
|
||||
const maskAtIdx = (idx: number) => {
|
||||
const nodes = candidates[idx];
|
||||
nodes.forEach((node) => {
|
||||
if (node.meta.value !== completed[idx]) {
|
||||
masked.push(node);
|
||||
maskRow(node);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removed = new Set<number>();
|
||||
const removeable = Array.from(this.indexes);
|
||||
|
||||
const maskUpto = (n: number) => {
|
||||
for (let j = 0; j < n; j++) {
|
||||
// process up to what we've handled
|
||||
const idx = removeable[j];
|
||||
if (removed.has(idx)) {
|
||||
// if we've removed this cell, do not mask it
|
||||
continue;
|
||||
}
|
||||
// otherwise, we had given up it; mask it leaving the original value
|
||||
// we won't try this cell again
|
||||
maskAtIdx(idx);
|
||||
}
|
||||
|
||||
for (let j = this.values2 - 1; j >= n; j--) {
|
||||
// for all those we haven't handled, mask leaving the original values
|
||||
// for us to to attempt to unmask one cell at a time
|
||||
maskAtIdx(removeable[j]);
|
||||
}
|
||||
};
|
||||
|
||||
const unmaskAll = () => {
|
||||
let node;
|
||||
while ((node = masked.pop())) {
|
||||
unmaskRow(node);
|
||||
}
|
||||
};
|
||||
|
||||
const unmaskNextCell = () => {
|
||||
for (let j = 0; j < this.values - 1; j++) {
|
||||
unmaskRow(masked.pop()!);
|
||||
}
|
||||
};
|
||||
|
||||
let best = 0;
|
||||
const attempt = () => {
|
||||
// attempt to remove cells until 'clues' cells remain
|
||||
attempts--;
|
||||
removed.clear();
|
||||
// attempt remove cells until 'clues' cells remain
|
||||
shuffle(removeable);
|
||||
maskUpto(0);
|
||||
|
||||
for (let i = 0; i < this.values2; i++) {
|
||||
for (let n = 0; n < this.values2; n++) {
|
||||
if (elapsed() > totalTime || this.values2 - removed.size == clues) {
|
||||
break;
|
||||
}
|
||||
unmaskNextCell();
|
||||
|
||||
let toRemove = removeable[n];
|
||||
removed.add(toRemove);
|
||||
mask();
|
||||
|
||||
if (!hasOneSolution()) {
|
||||
// failed attempt. prepare for next
|
||||
unmaskAll();
|
||||
maskUpto(i + 1);
|
||||
continue;
|
||||
removed.delete(toRemove);
|
||||
}
|
||||
|
||||
removed.add(removeable[i]);
|
||||
unmask();
|
||||
}
|
||||
if (removed.size > best) {
|
||||
best = removed.size;
|
||||
}
|
||||
|
||||
unmaskAll();
|
||||
};
|
||||
|
||||
while (
|
||||
@ -273,6 +240,8 @@ 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();
|
||||
}
|
||||
|
||||
@ -282,28 +251,16 @@ export class SudokuMath {
|
||||
return completed;
|
||||
}
|
||||
|
||||
solve(puzzle: Cell[]) {
|
||||
const [header] = this.getDLXHeader(puzzle);
|
||||
let solved = false;
|
||||
solve(existing: Cell[]): void {
|
||||
const [header] = this.getDLXHeader(existing);
|
||||
const callback = (solution: DNode[]) => {
|
||||
solution.forEach((node) => {
|
||||
const meta: NodeMeta = node.meta;
|
||||
puzzle[meta.index] = meta.value;
|
||||
existing[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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
import { SudokuMath } from "./math.js";
|
||||
import { expose } from "threads/worker";
|
||||
|
||||
const maths = {};
|
||||
|
||||
expose({
|
||||
generate(regionWidth, regionHeight, clues) {
|
||||
return SudokuMath.get(regionWidth, regionHeight).generate(clues);
|
||||
},
|
||||
solve(regionWidth, regionHeight, cells) {
|
||||
const result = SudokuMath.get(regionWidth, regionHeight).solve(cells);
|
||||
return [result, cells];
|
||||
const math =
|
||||
maths[`${regionWidth}:${regionHeight}`] ||
|
||||
(maths[`${regionWidth}:${regionHeight}`] = new SudokuMath(
|
||||
regionWidth,
|
||||
regionHeight
|
||||
));
|
||||
return math.generate(clues);
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user