Compare commits
	
		
			3 Commits
		
	
	
		
			master
			...
			3c6d69c343
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3c6d69c343 | |||
| 1ff71d099e | |||
| ae9016fbf6 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { gql } from "../mods.js";
 | 
					import { gql } from "../mods.js";
 | 
				
			||||||
import { Sudoku, GenerateArguments, SolveArguments } from "../sudoku/index.js";
 | 
					import { generate, GenerateArguments } from "../sudoku/index.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const typeDefs = gql`
 | 
					export const typeDefs = gql`
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
@ -22,22 +22,16 @@ 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: (obj: any, args: GenerateArguments, ctx: SudokuFuncs) =>
 | 
					    generate: (
 | 
				
			||||||
      ctx.generate(args),
 | 
					      obj: any,
 | 
				
			||||||
    solve: (obj: any, args: SolveArguments, ctx: SudokuFuncs) =>
 | 
					      { regionWidth, regionHeight, clues }: GenerateArguments
 | 
				
			||||||
      ctx.solve(args),
 | 
					    ) => {
 | 
				
			||||||
 | 
					      return generate(regionWidth, regionHeight, clues);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/main.ts
									
									
									
									
									
								
							@ -1,13 +1,6 @@
 | 
				
			|||||||
import { Application, bodyParser } from "./mods.js";
 | 
					import { Application, bodyParser } from "./mods.js";
 | 
				
			||||||
import { applyGraphQL } from "./graphql.js";
 | 
					import { applyGraphQL } from "./graphql.js";
 | 
				
			||||||
import { typeDefs, resolvers } from "./graphql/index.js";
 | 
					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";
 | 
				
			||||||
@ -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({
 | 
					  applyGraphQL({
 | 
				
			||||||
    app,
 | 
					    app,
 | 
				
			||||||
    typeDefs: typeDefs,
 | 
					    typeDefs: typeDefs,
 | 
				
			||||||
    resolvers: resolvers,
 | 
					    resolvers: resolvers,
 | 
				
			||||||
    context: () => sudokuFuncs,
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  runtime.server = stoppable(
 | 
					  runtime.server = stoppable(
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
					      // traverse DOWN the rows this column contained
 | 
				
			||||||
      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,9 +1,7 @@
 | 
				
			|||||||
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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,12 +13,6 @@ 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;
 | 
				
			||||||
@ -28,41 +20,19 @@ export type Sudoku = {
 | 
				
			|||||||
  cells: Cell[];
 | 
					  cells: Cell[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SudokuWorker = {
 | 
					function getWorker() {
 | 
				
			||||||
  generate: (
 | 
					  return spawn(new Worker("./worker"));
 | 
				
			||||||
    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)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initializeWorkers() {
 | 
					const available: any = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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++) {
 | 
				
			||||||
    spawnWorker();
 | 
					    getWorker().then((worker) => available.push(worker));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					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.
 | 
				
			||||||
@ -82,33 +52,27 @@ function withTimeout<T>(promise: Promise<T>, ms: number, cb: () => any) {
 | 
				
			|||||||
  }).finally(() => clearTimeout(timeout!));
 | 
					  }).finally(() => clearTimeout(timeout!));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function workerTaskWithTimeout(
 | 
					export async function generate(
 | 
				
			||||||
  task: (worker: ModuleThread) => Promise<any>,
 | 
					  regionWidth: number,
 | 
				
			||||||
  timeout: number
 | 
					  regionHeight: number,
 | 
				
			||||||
) {
 | 
					  clues: number
 | 
				
			||||||
  const worker = pickWorker();
 | 
					): Promise<Sudoku> {
 | 
				
			||||||
  let timedOut = false;
 | 
					  const proxy = available.pop();
 | 
				
			||||||
  return withTimeout(task(worker), timeout, () => {
 | 
					  if (!proxy) {
 | 
				
			||||||
    timedOut = true;
 | 
					    throw new Error("No workers available right now. Please try again.");
 | 
				
			||||||
    Thread.terminate(worker);
 | 
					 | 
				
			||||||
    spawnWorker();
 | 
					 | 
				
			||||||
    return new Error("Timed out.");
 | 
					 | 
				
			||||||
  }).finally(() => {
 | 
					 | 
				
			||||||
    if (!timedOut) {
 | 
					 | 
				
			||||||
      available.push(worker);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function generate({
 | 
					  const puzzle = await withTimeout<number[]>(
 | 
				
			||||||
  regionWidth,
 | 
					    proxy.generate(regionWidth, regionHeight, clues),
 | 
				
			||||||
  regionHeight,
 | 
					    TIMEOUT,
 | 
				
			||||||
  clues,
 | 
					    () => {
 | 
				
			||||||
}: GenerateArguments): Promise<Sudoku> {
 | 
					      Thread.terminate(proxy);
 | 
				
			||||||
  const puzzle = await workerTaskWithTimeout(
 | 
					      getWorker().then((worker) => available.push(worker));
 | 
				
			||||||
    (worker) => worker.generate(regionWidth, regionHeight, clues),
 | 
					      return new Error("Timed out. Try reducing the number of clues.");
 | 
				
			||||||
    TIMEOUT
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  available.push(proxy);
 | 
				
			||||||
  prettyPrint(regionWidth, regionHeight, puzzle);
 | 
					  prettyPrint(regionWidth, regionHeight, puzzle);
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    regionWidth,
 | 
					    regionWidth,
 | 
				
			||||||
@ -117,67 +81,3 @@ 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): 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)];
 | 
					    return [...firstRow, ...Array(this.values2 - this.values).fill(0)];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  generateComplete(): [number[], number] {
 | 
					  generateComplete() {
 | 
				
			||||||
    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;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      // stop after the first solution
 | 
					      // return the first solution
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    });
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const dlx = new DLX(header, callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dlx.search();
 | 
					    dlx.search();
 | 
				
			||||||
    return [result, dlx.updates];
 | 
					    return result;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  generate(clues: number, attempts = Infinity, totalTime = Infinity) {
 | 
					  generate(clues: number, attempts = Infinity, totalTime = Infinity) {
 | 
				
			||||||
    if (clues === 0) {
 | 
					    const completed = this.generateComplete();
 | 
				
			||||||
      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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -177,93 +176,61 @@ 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 to remove cells until 'clues' cells remain
 | 
					      // attempt remove cells until 'clues' cells remain
 | 
				
			||||||
      attempts--;
 | 
					 | 
				
			||||||
      removed.clear();
 | 
					 | 
				
			||||||
      shuffle(removeable);
 | 
					      shuffle(removeable);
 | 
				
			||||||
      maskUpto(0);
 | 
					      for (let n = 0; n < this.values2; n++) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
      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()) {
 | 
				
			||||||
          // failed attempt. prepare for next
 | 
					          removed.delete(toRemove);
 | 
				
			||||||
          unmaskAll();
 | 
					 | 
				
			||||||
          maskUpto(i + 1);
 | 
					 | 
				
			||||||
          continue;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        removed.add(removeable[i]);
 | 
					        unmask();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (removed.size > best) {
 | 
					 | 
				
			||||||
        best = removed.size;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      unmaskAll();
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    while (
 | 
					    while (
 | 
				
			||||||
@ -273,6 +240,8 @@ 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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -282,28 +251,16 @@ export class SudokuMath {
 | 
				
			|||||||
    return completed;
 | 
					    return completed;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  solve(puzzle: Cell[]) {
 | 
					  solve(existing: Cell[]): void {
 | 
				
			||||||
    const [header] = this.getDLXHeader(puzzle);
 | 
					    const [header] = this.getDLXHeader(existing);
 | 
				
			||||||
    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;
 | 
				
			||||||
        puzzle[meta.index] = meta.value;
 | 
					        existing[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,12 +1,16 @@
 | 
				
			|||||||
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) {
 | 
				
			||||||
    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
 | 
				
			||||||
 | 
					      ));
 | 
				
			||||||
 | 
					    return math.generate(clues);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user