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:
		@ -1,5 +1,10 @@
 | 
			
		||||
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`
 | 
			
		||||
  """
 | 
			
		||||
@ -22,16 +27,15 @@ 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!
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const resolvers = {
 | 
			
		||||
  Query: {
 | 
			
		||||
    generate: (
 | 
			
		||||
      obj: any,
 | 
			
		||||
      { regionWidth, regionHeight, clues }: GenerateArguments
 | 
			
		||||
    ) => {
 | 
			
		||||
      return generate(regionWidth, regionHeight, clues);
 | 
			
		||||
    },
 | 
			
		||||
    generate: (obj: any, args: GenerateArguments) => generate(args),
 | 
			
		||||
    solve: (obj: any, args: SolveArguments) => solve(args),
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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++;
 | 
			
		||||
 | 
			
		||||
@ -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,20 +28,43 @@ 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: any = [];
 | 
			
		||||
const available: ModuleThread<SudokuWorker>[] = [];
 | 
			
		||||
 | 
			
		||||
function spawnWorker() {
 | 
			
		||||
  spawn<SudokuWorker>(new Worker("./worker")).then((worker) =>
 | 
			
		||||
    available.push(worker)
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initialize() {
 | 
			
		||||
  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 +83,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 +118,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) {
 | 
			
		||||
  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 = () => {
 | 
			
		||||
      // 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))
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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];
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user