Compare commits
8 Commits
5ccbb2ef1c
...
master
Author | SHA1 | Date | |
---|---|---|---|
9ee76b10fa | |||
15b69c510c | |||
4e6cdab9ee | |||
8526dcd083 | |||
938954d621 | |||
058dc13c1c | |||
18e468b17f | |||
24d8ab6763 |
11
Dockerfile
11
Dockerfile
@ -1,7 +1,6 @@
|
|||||||
# dev stage
|
# dev stage
|
||||||
FROM node:14-alpine as dev
|
FROM node:14-alpine as dev
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk update && apk add --no-cache python3 make gcc g++
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -14,21 +13,13 @@ RUN npx tsc && npm prune --production
|
|||||||
FROM node:14-alpine
|
FROM node:14-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN printf "%b" '#!'"/bin/sh\n\
|
RUN apk add --update --no-cache util-linux
|
||||||
set -e\n\
|
|
||||||
if [ ! -z \"\$RUN_MIGRATIONS\" ]; then\n\
|
|
||||||
echo \"Running migrations.\"\n\
|
|
||||||
npm run knex:migrate:latest\n\
|
|
||||||
fi\n\
|
|
||||||
exec \"\$@\"\n" > docker-entrypoint.sh && chmod +x docker-entrypoint.sh
|
|
||||||
|
|
||||||
# Copy over production modules and dist folder
|
# Copy over production modules and dist folder
|
||||||
COPY --from=build /app/package*.json ./
|
COPY --from=build /app/package*.json ./
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
COPY --from=build /app/dist ./dist
|
COPY --from=build /app/dist ./dist
|
||||||
COPY --from=build /app/db ./db
|
|
||||||
|
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
|
|
||||||
ENTRYPOINT [ "./docker-entrypoint.sh" ]
|
|
||||||
CMD [ "node", "dist/main.js" ]
|
CMD [ "node", "dist/main.js" ]
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
RouterContext,
|
RouterContext,
|
||||||
graphql,
|
graphql,
|
||||||
makeExecutableSchema,
|
makeExecutableSchema,
|
||||||
} from "./mods";
|
} from "./mods.js";
|
||||||
|
|
||||||
export interface ResolversProps {
|
export interface ResolversProps {
|
||||||
Query?: any;
|
Query?: any;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { mergeTypeDefs, mergeResolvers } from "../mods";
|
import { mergeTypeDefs, mergeResolvers } from "../mods.js";
|
||||||
|
import * as sudoku from "./sudoku.js";
|
||||||
|
|
||||||
const modules = [require("./sudoku")];
|
const modules = [sudoku];
|
||||||
|
|
||||||
export const typeDefs = mergeTypeDefs(modules.map((mod) => mod.typeDefs));
|
export const typeDefs = mergeTypeDefs(modules.map((mod) => mod.typeDefs));
|
||||||
export const resolvers = mergeResolvers(modules.map((mod) => mod.resolvers));
|
export const resolvers = mergeResolvers(modules.map((mod) => mod.resolvers));
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { gql } from "../mods";
|
import { gql } from "../mods.js";
|
||||||
import { generate, GenerateArguments } from "../sudoku/index";
|
import { Sudoku, GenerateArguments, SolveArguments } from "../sudoku/index.js";
|
||||||
|
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
"""
|
"""
|
||||||
@ -22,16 +22,22 @@ 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!
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
interface SudokuFuncs {
|
||||||
|
solve(args: SolveArguments): Promise<Sudoku> | Sudoku;
|
||||||
|
generate(args: GenerateArguments): Promise<Sudoku> | Sudoku;
|
||||||
|
}
|
||||||
|
|
||||||
export const resolvers = {
|
export const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
generate: (
|
generate: (obj: any, args: GenerateArguments, ctx: SudokuFuncs) =>
|
||||||
obj: any,
|
ctx.generate(args),
|
||||||
{ regionWidth, regionHeight, clues }: GenerateArguments
|
solve: (obj: any, args: SolveArguments, ctx: SudokuFuncs) =>
|
||||||
) => {
|
ctx.solve(args),
|
||||||
return generate(regionWidth, regionHeight, clues);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
28
src/main.ts
28
src/main.ts
@ -1,6 +1,13 @@
|
|||||||
import { Application, bodyParser } from "./mods";
|
import { Application, bodyParser } from "./mods.js";
|
||||||
import { applyGraphQL } from "./graphql";
|
import { applyGraphQL } from "./graphql.js";
|
||||||
import { typeDefs, resolvers } from "./graphql/index";
|
import { typeDefs, resolvers } from "./graphql/index.js";
|
||||||
|
import {
|
||||||
|
initializeWorkers,
|
||||||
|
solve,
|
||||||
|
generate,
|
||||||
|
solveSync,
|
||||||
|
generateSync,
|
||||||
|
} from "./sudoku/index.js";
|
||||||
import stoppable from "stoppable";
|
import stoppable from "stoppable";
|
||||||
|
|
||||||
import cors from "@koa/cors";
|
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({
|
applyGraphQL({
|
||||||
app,
|
app,
|
||||||
typeDefs: typeDefs,
|
typeDefs: typeDefs,
|
||||||
resolvers: resolvers,
|
resolvers: resolvers,
|
||||||
|
context: () => sudokuFuncs,
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime.server = stoppable(
|
runtime.server = stoppable(
|
||||||
|
@ -33,18 +33,13 @@ type SolutionCallback = (output: DNode[]) => boolean;
|
|||||||
type ColumnSelector = (header: CNode) => CNode;
|
type ColumnSelector = (header: CNode) => CNode;
|
||||||
|
|
||||||
function selectColumnSizeHeuristic(header: CNode): CNode {
|
function selectColumnSizeHeuristic(header: CNode): CNode {
|
||||||
let minSize = Infinity;
|
let minColumn = header.right;
|
||||||
let minColumn: CNode | undefined;
|
let curColumn = minColumn;
|
||||||
let curColumn = header;
|
|
||||||
while ((curColumn = curColumn.right) !== header) {
|
while ((curColumn = curColumn.right) !== header) {
|
||||||
if (curColumn.column.size < minSize) {
|
if (curColumn.size < minColumn.size) {
|
||||||
minSize = curColumn.column.size;
|
|
||||||
minColumn = curColumn;
|
minColumn = curColumn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!minColumn) {
|
|
||||||
throw new Error("minColumn is undefined, this shouldn't be possible.");
|
|
||||||
}
|
|
||||||
return minColumn;
|
return minColumn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,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,10 @@
|
|||||||
import { StaticPool, isTimeoutError } from "node-worker-threads-pool";
|
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";
|
|
||||||
const TIMEOUT = 20000;
|
const TIMEOUT = 20000;
|
||||||
|
|
||||||
export type Cell = number;
|
export type Cell = number;
|
||||||
@ -12,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;
|
||||||
@ -19,46 +28,156 @@ export type Sudoku = {
|
|||||||
cells: Cell[];
|
cells: Cell[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const pool = new StaticPool<GenerateArguments, Cell[]>({
|
type SudokuWorker = {
|
||||||
size: WORKERS,
|
generate: (
|
||||||
task: "./src/sudoku/worker.js",
|
|
||||||
});
|
|
||||||
|
|
||||||
let activeWorkers = 0;
|
|
||||||
export async function generate(
|
|
||||||
regionWidth: number,
|
regionWidth: number,
|
||||||
regionHeight: number,
|
regionHeight: number,
|
||||||
clues: number
|
clues: number
|
||||||
): Promise<Sudoku> {
|
) => Promise<number[]>;
|
||||||
if (activeWorkers >= WORKERS) {
|
solve: (
|
||||||
throw new Error("No workers available. Please try again in a moment.");
|
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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
export function initializeWorkers() {
|
||||||
activeWorkers++;
|
console.log(`Starting ${WORKERS} worker threads`);
|
||||||
const puzzle = await pool.exec(
|
for (let n = 0; n < WORKERS; n++) {
|
||||||
{
|
spawnWorker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param promise the promise to await
|
||||||
|
* @param ms the timeout in milliseconds
|
||||||
|
* @param cb a callback to call when the timeout is reached. The promise is
|
||||||
|
* rejected with whatever gets returned here.
|
||||||
|
*/
|
||||||
|
function withTimeout<T>(promise: Promise<T>, ms: number, cb: () => any) {
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
reject(cb());
|
||||||
|
}, ms);
|
||||||
|
promise.then(resolve).catch(reject);
|
||||||
|
}).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,
|
regionWidth,
|
||||||
regionHeight,
|
regionHeight,
|
||||||
clues,
|
clues,
|
||||||
},
|
}: GenerateArguments): Promise<Sudoku> {
|
||||||
|
const puzzle = await workerTaskWithTimeout(
|
||||||
|
(worker) => worker.generate(regionWidth, regionHeight, clues),
|
||||||
TIMEOUT
|
TIMEOUT
|
||||||
);
|
);
|
||||||
|
|
||||||
prettyPrint(regionWidth, regionHeight, puzzle);
|
prettyPrint(regionWidth, regionHeight, puzzle);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
regionWidth,
|
regionWidth,
|
||||||
regionHeight,
|
regionHeight,
|
||||||
size: (regionWidth * regionHeight) ** 2,
|
size: (regionWidth * regionHeight) ** 2,
|
||||||
cells: puzzle,
|
cells: puzzle,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
|
||||||
if (isTimeoutError(err)) {
|
|
||||||
throw new Error("Timed out. Try increasing the number of clues.");
|
|
||||||
}
|
}
|
||||||
throw err;
|
|
||||||
} finally {
|
export function generateSync({
|
||||||
activeWorkers--;
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,9 @@ import {
|
|||||||
addNodeToColumn,
|
addNodeToColumn,
|
||||||
maskRow,
|
maskRow,
|
||||||
unmaskRow,
|
unmaskRow,
|
||||||
} from "./dlx";
|
} from "./dlx.js";
|
||||||
import { shuffle, range } from "./util";
|
import { shuffle, range } from "./util.js";
|
||||||
import { Cell } from "./index";
|
import { Cell } from "./index.js";
|
||||||
|
|
||||||
type NodeMeta = {
|
type NodeMeta = {
|
||||||
index: number;
|
index: number;
|
||||||
@ -141,27 +141,28 @@ export class SudokuMath {
|
|||||||
return [...firstRow, ...Array(this.values2 - this.values).fill(0)];
|
return [...firstRow, ...Array(this.values2 - this.values).fill(0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
generateComplete() {
|
generateComplete(): [number[], number] {
|
||||||
const result = this._baseBoard();
|
const result = this._baseBoard();
|
||||||
const [header] = this.getDLXHeader(result, true);
|
const [header] = this.getDLXHeader(result, true);
|
||||||
|
const dlx = new DLX(header, (solution: DNode[]) => {
|
||||||
const callback = (solution: DNode[]) => {
|
|
||||||
solution.forEach((node) => {
|
solution.forEach((node) => {
|
||||||
const meta: NodeMeta = node.meta;
|
const meta: NodeMeta = node.meta;
|
||||||
result[meta.index] = meta.value;
|
result[meta.index] = meta.value;
|
||||||
});
|
});
|
||||||
// return the first solution
|
// stop after the first solution
|
||||||
return true;
|
return true;
|
||||||
};
|
});
|
||||||
|
|
||||||
const dlx = new DLX(header, callback);
|
|
||||||
|
|
||||||
dlx.search();
|
dlx.search();
|
||||||
return result;
|
return [result, dlx.updates];
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(clues: number, attempts = Infinity, totalTime = Infinity) {
|
generate(clues: number, attempts = Infinity, totalTime = Infinity) {
|
||||||
const completed = this.generateComplete();
|
if (clues === 0) {
|
||||||
|
return Array(this.values2).fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let [completed, updates] = this.generateComplete();
|
||||||
|
|
||||||
const [header, dlxRows] = this.getDLXHeader(); // complete header - no candidates removed
|
const [header, dlxRows] = this.getDLXHeader(); // complete header - no candidates removed
|
||||||
|
|
||||||
@ -176,61 +177,93 @@ export class SudokuMath {
|
|||||||
candidates[meta.index][meta.value - 1] = node;
|
candidates[meta.index][meta.value - 1] = node;
|
||||||
});
|
});
|
||||||
|
|
||||||
// board positions which have been removed
|
|
||||||
const removed = new Set<number>();
|
|
||||||
const masked: DNode[] = [];
|
|
||||||
|
|
||||||
const hasOneSolution = () => {
|
const hasOneSolution = () => {
|
||||||
solutions = 0;
|
solutions = 0;
|
||||||
dlx.search();
|
dlx.search();
|
||||||
|
updates += dlx.updates;
|
||||||
return solutions === 1;
|
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 start = Date.now();
|
||||||
const elapsed = () => Date.now() - start;
|
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 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 = () => {
|
const attempt = () => {
|
||||||
// attempt remove cells until 'clues' cells remain
|
// attempt to remove cells until 'clues' cells remain
|
||||||
|
attempts--;
|
||||||
|
removed.clear();
|
||||||
shuffle(removeable);
|
shuffle(removeable);
|
||||||
for (let n = 0; n < this.values2; n++) {
|
maskUpto(0);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.values2; i++) {
|
||||||
if (elapsed() > totalTime || this.values2 - removed.size == clues) {
|
if (elapsed() > totalTime || this.values2 - removed.size == clues) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
unmaskNextCell();
|
||||||
let toRemove = removeable[n];
|
|
||||||
removed.add(toRemove);
|
|
||||||
mask();
|
|
||||||
|
|
||||||
if (!hasOneSolution()) {
|
if (!hasOneSolution()) {
|
||||||
removed.delete(toRemove);
|
// failed attempt. prepare for next
|
||||||
|
unmaskAll();
|
||||||
|
maskUpto(i + 1);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
unmask();
|
removed.add(removeable[i]);
|
||||||
}
|
}
|
||||||
|
if (removed.size > best) {
|
||||||
|
best = removed.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmaskAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
while (
|
while (
|
||||||
@ -240,8 +273,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,16 +282,28 @@ export class SudokuMath {
|
|||||||
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,4 +1,4 @@
|
|||||||
import { Cell } from "./index";
|
import { Cell } from "./index.js";
|
||||||
|
|
||||||
export function randInt(lower: number, upper: number) {
|
export function randInt(lower: number, upper: number) {
|
||||||
return Math.floor(Math.random() * (upper - lower)) + lower;
|
return Math.floor(Math.random() * (upper - lower)) + lower;
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
const { SudokuMath } = require("./math");
|
import { SudokuMath } from "./math.js";
|
||||||
const { parentPort } = require("worker_threads");
|
import { expose } from "threads/worker";
|
||||||
|
|
||||||
const maths = {};
|
expose({
|
||||||
|
generate(regionWidth, regionHeight, clues) {
|
||||||
parentPort.on("message", ({ regionWidth, regionHeight, clues }) => {
|
return SudokuMath.get(regionWidth, regionHeight).generate(clues);
|
||||||
const math =
|
},
|
||||||
maths[`${regionWidth}:${regionHeight}`] ||
|
solve(regionWidth, regionHeight, cells) {
|
||||||
(maths[`${regionWidth}:${regionHeight}`] = new SudokuMath(
|
const result = SudokuMath.get(regionWidth, regionHeight).solve(cells);
|
||||||
regionWidth,
|
return [result, cells];
|
||||||
regionHeight
|
},
|
||||||
));
|
|
||||||
const puzzle = math.generate(clues);
|
|
||||||
parentPort.postMessage(puzzle);
|
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user