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.
This commit is contained in:
parent
4e6cdab9ee
commit
15b69c510c
@ -1,5 +1,10 @@
|
|||||||
import { gql } from "../mods.js";
|
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`
|
export const typeDefs = gql`
|
||||||
"""
|
"""
|
||||||
@ -22,16 +27,15 @@ export const typeDefs = gql`
|
|||||||
type Query {
|
type Query {
|
||||||
"Generates a new sudoku."
|
"Generates a new sudoku."
|
||||||
generate(regionWidth: Int = 3, regionHeight: Int = 3, clues: Int!): 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 = {
|
export const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
generate: (
|
generate: (obj: any, args: GenerateArguments) => generate(args),
|
||||||
obj: any,
|
solve: (obj: any, args: SolveArguments) => solve(args),
|
||||||
{ regionWidth, regionHeight, clues }: GenerateArguments
|
|
||||||
) => {
|
|
||||||
return generate(regionWidth, regionHeight, clues);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -114,10 +114,10 @@ export class DLX {
|
|||||||
|
|
||||||
let row = c as DNode;
|
let row = c as DNode;
|
||||||
while ((row = row.down) !== c) {
|
while ((row = row.down) !== c) {
|
||||||
// traverse DOWN the rows this column contained
|
// traverse down the rows this column
|
||||||
let col = row;
|
let col = row;
|
||||||
while ((col = col.right) !== 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
|
// remove this node from its column, and shrink its column's size
|
||||||
this.updates++;
|
this.updates++;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { spawn, Thread, Worker } from "threads";
|
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 WORKERS from "physical-cpu-count";
|
||||||
import { prettyPrint } from "./util.js";
|
|
||||||
|
|
||||||
const TIMEOUT = 20000;
|
const TIMEOUT = 20000;
|
||||||
|
|
||||||
@ -13,6 +15,12 @@ export type GenerateArguments = {
|
|||||||
clues: number;
|
clues: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SolveArguments = {
|
||||||
|
regionWidth: number;
|
||||||
|
regionHeight: number;
|
||||||
|
cells: Cell[];
|
||||||
|
};
|
||||||
|
|
||||||
export type Sudoku = {
|
export type Sudoku = {
|
||||||
regionWidth: number;
|
regionWidth: number;
|
||||||
regionHeight: number;
|
regionHeight: number;
|
||||||
@ -20,20 +28,43 @@ export type Sudoku = {
|
|||||||
cells: Cell[];
|
cells: Cell[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getWorker() {
|
type SudokuWorker = {
|
||||||
return spawn(new Worker("./worker"));
|
generate: (
|
||||||
}
|
regionWidth: number,
|
||||||
|
regionHeight: number,
|
||||||
|
clues: number
|
||||||
|
) => Promise<number[]>;
|
||||||
|
solve: (
|
||||||
|
regionWidth: number,
|
||||||
|
regionHeight: number,
|
||||||
|
cells: number[]
|
||||||
|
) => Promise<[boolean, number[]]>;
|
||||||
|
};
|
||||||
|
|
||||||
const available: any = [];
|
const available: ModuleThread<SudokuWorker>[] = [];
|
||||||
|
|
||||||
|
function spawnWorker() {
|
||||||
|
spawn<SudokuWorker>(new Worker("./worker")).then((worker) =>
|
||||||
|
available.push(worker)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function initialize() {
|
function initialize() {
|
||||||
console.log(`Starting ${WORKERS} worker threads`);
|
console.log(`Starting ${WORKERS} worker threads`);
|
||||||
for (let n = 0; n < WORKERS; n++) {
|
for (let n = 0; n < WORKERS; n++) {
|
||||||
getWorker().then((worker) => available.push(worker));
|
spawnWorker();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initialize();
|
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.
|
* Awaits a promise with a timeout.
|
||||||
*
|
*
|
||||||
@ -52,27 +83,33 @@ function withTimeout<T>(promise: Promise<T>, ms: number, cb: () => any) {
|
|||||||
}).finally(() => clearTimeout(timeout!));
|
}).finally(() => clearTimeout(timeout!));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generate(
|
function workerTaskWithTimeout(
|
||||||
regionWidth: number,
|
task: (worker: ModuleThread) => Promise<any>,
|
||||||
regionHeight: number,
|
timeout: number
|
||||||
clues: number
|
) {
|
||||||
): Promise<Sudoku> {
|
const worker = pickWorker();
|
||||||
const proxy = available.pop();
|
let timedOut = false;
|
||||||
if (!proxy) {
|
return withTimeout(task(worker), timeout, () => {
|
||||||
throw new Error("No workers available right now. Please try again.");
|
timedOut = true;
|
||||||
|
Thread.terminate(worker);
|
||||||
|
spawnWorker();
|
||||||
|
return new Error("Timed out.");
|
||||||
|
}).finally(() => {
|
||||||
|
if (!timedOut) {
|
||||||
|
available.push(worker);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const puzzle = await withTimeout<number[]>(
|
export async function generate({
|
||||||
proxy.generate(regionWidth, regionHeight, clues),
|
regionWidth,
|
||||||
TIMEOUT,
|
regionHeight,
|
||||||
() => {
|
clues,
|
||||||
Thread.terminate(proxy);
|
}: GenerateArguments): Promise<Sudoku> {
|
||||||
getWorker().then((worker) => available.push(worker));
|
const puzzle = await workerTaskWithTimeout(
|
||||||
return new Error("Timed out. Try reducing the number of clues.");
|
(worker) => worker.generate(regionWidth, regionHeight, clues),
|
||||||
}
|
TIMEOUT
|
||||||
);
|
);
|
||||||
|
|
||||||
available.push(proxy);
|
|
||||||
prettyPrint(regionWidth, regionHeight, puzzle);
|
prettyPrint(regionWidth, regionHeight, puzzle);
|
||||||
return {
|
return {
|
||||||
regionWidth,
|
regionWidth,
|
||||||
@ -81,3 +118,67 @@ export async function generate(
|
|||||||
cells: puzzle,
|
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) {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -236,8 +236,11 @@ export class SudokuMath {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let best = 0;
|
||||||
const attempt = () => {
|
const attempt = () => {
|
||||||
// attempt remove cells until 'clues' cells remain
|
// attempt to remove cells until 'clues' cells remain
|
||||||
|
attempts--;
|
||||||
|
removed.clear();
|
||||||
shuffle(removeable);
|
shuffle(removeable);
|
||||||
maskUpto(0);
|
maskUpto(0);
|
||||||
|
|
||||||
@ -245,7 +248,6 @@ export class SudokuMath {
|
|||||||
if (elapsed() > totalTime || this.values2 - removed.size == clues) {
|
if (elapsed() > totalTime || this.values2 - removed.size == clues) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
unmaskNextCell();
|
unmaskNextCell();
|
||||||
|
|
||||||
if (!hasOneSolution()) {
|
if (!hasOneSolution()) {
|
||||||
@ -257,6 +259,9 @@ export class SudokuMath {
|
|||||||
|
|
||||||
removed.add(removeable[i]);
|
removed.add(removeable[i]);
|
||||||
}
|
}
|
||||||
|
if (removed.size > best) {
|
||||||
|
best = removed.size;
|
||||||
|
}
|
||||||
|
|
||||||
unmaskAll();
|
unmaskAll();
|
||||||
};
|
};
|
||||||
@ -268,28 +273,37 @@ export class SudokuMath {
|
|||||||
) {
|
) {
|
||||||
// try to reach the clue goal up to `attempts` times or as long as
|
// try to reach the clue goal up to `attempts` times or as long as
|
||||||
// elapsed time is less than `totalTime`
|
// elapsed time is less than `totalTime`
|
||||||
attempts--;
|
|
||||||
removed.clear();
|
|
||||||
attempt();
|
attempt();
|
||||||
}
|
}
|
||||||
|
|
||||||
removed.forEach((index) => {
|
removed.forEach((index) => {
|
||||||
completed[index] = 0;
|
completed[index] = 0;
|
||||||
});
|
});
|
||||||
console.log("DLX updates:", updates);
|
|
||||||
return completed;
|
return completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
solve(existing: Cell[]): void {
|
solve(puzzle: Cell[]) {
|
||||||
const [header] = this.getDLXHeader(existing);
|
const [header] = this.getDLXHeader(puzzle);
|
||||||
|
let solved = false;
|
||||||
const callback = (solution: DNode[]) => {
|
const callback = (solution: DNode[]) => {
|
||||||
solution.forEach((node) => {
|
solution.forEach((node) => {
|
||||||
const meta: NodeMeta = node.meta;
|
const meta: NodeMeta = node.meta;
|
||||||
existing[meta.index] = meta.value;
|
puzzle[meta.index] = meta.value;
|
||||||
});
|
});
|
||||||
|
solved = true;
|
||||||
// return the first solution
|
// return the first solution
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
new DLX(header, callback).search();
|
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,13 +1,12 @@
|
|||||||
import { SudokuMath } from "./math.js";
|
import { SudokuMath } from "./math.js";
|
||||||
import { expose } from "threads/worker";
|
import { expose } from "threads/worker";
|
||||||
|
|
||||||
const maths = {};
|
|
||||||
|
|
||||||
expose({
|
expose({
|
||||||
generate(regionWidth, regionHeight, clues) {
|
generate(regionWidth, regionHeight, clues) {
|
||||||
const key = `${regionWidth}:${regionHeight}`;
|
return SudokuMath.get(regionWidth, regionHeight).generate(clues);
|
||||||
const math =
|
},
|
||||||
maths[key] ?? (maths[key] = new SudokuMath(regionWidth, regionHeight));
|
solve(regionWidth, regionHeight, cells) {
|
||||||
return math.generate(clues);
|
const result = SudokuMath.get(regionWidth, regionHeight).solve(cells);
|
||||||
|
return [result, cells];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user