@wsabol/sudoku-solver 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/sudokuSolver.d.ts +6 -3
- package/dist/sudokuSolver.d.ts.map +1 -1
- package/dist/sudokuSolver.js +120 -18
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,EAAE,EAAE,KAAK,SAAS,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAClI,OAAO,EAA8C,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEzH,UAAU,WAAW;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CAChB;AAED,UAAU,cAAc;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,KAAK,UAAU,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;AAEzD,UAAU,UAAU;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,iBAAS,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,WAAW,CAOtD;AAED,iBAAS,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,EAAE,EAAE,KAAK,SAAS,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAClI,OAAO,EAA8C,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEzH,UAAU,WAAW;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CAChB;AAED,UAAU,cAAc;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,KAAK,UAAU,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;AAEzD,UAAU,UAAU;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,iBAAS,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,WAAW,CAOtD;AAED,iBAAS,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,UAAU,CAkCxD;AAED,iBAAS,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,gBAAgB,CAY9D;AAED,iBAAS,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,cAAc,CAoCjE;AAED,QAAA,MAAM,MAAM;;;;;CAKX,CAAC;AAEF,OAAO,EACH,YAAY,EACf,CAAA;AAGD,YAAY,EACR,SAAS,EACT,KAAK,EACL,IAAI,EACJ,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,UAAU,EACV,cAAc,EACjB,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -28,8 +28,11 @@ function nextMove(boardInput) {
|
|
|
28
28
|
sudoku.setSquareValue(move.row, move.col, move.value);
|
|
29
29
|
}
|
|
30
30
|
else {
|
|
31
|
-
const
|
|
32
|
-
|
|
31
|
+
const digits = [...new Set(move.eliminations.map((e) => e.value))].sort((a, b) => a - b);
|
|
32
|
+
const digitPart = digits.length === 1
|
|
33
|
+
? String(digits[0])
|
|
34
|
+
: `{${digits.join(",")}}`;
|
|
35
|
+
message = `Eliminate ${digitPart} from ${move.eliminations.length} cell(s) (${move.algorithm})`;
|
|
33
36
|
sudoku.applyElimination(move);
|
|
34
37
|
}
|
|
35
38
|
return {
|
package/dist/sudokuSolver.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ValidationResult } from "./validate.js";
|
|
2
2
|
export type Board = number[][];
|
|
3
|
-
export type Algorithm = "Full House" | "Naked Single" | "Hidden Single" | "Pointing Pair/Triple";
|
|
3
|
+
export type Algorithm = "Last Digit" | "Full House" | "Naked Single" | "Hidden Single" | "Pointing Pair/Triple" | "Naked Pair" | "Naked Triple" | "Naked Quad";
|
|
4
4
|
export interface PlacementMove {
|
|
5
5
|
type: "placement";
|
|
6
6
|
row: number;
|
|
@@ -20,7 +20,7 @@ export interface EliminationMove {
|
|
|
20
20
|
export type Move = PlacementMove | EliminationMove;
|
|
21
21
|
export type DifficultyLevel = "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible";
|
|
22
22
|
export default class SudokuSolver {
|
|
23
|
-
static readonly
|
|
23
|
+
private static readonly SEARCH_PHASES;
|
|
24
24
|
private board;
|
|
25
25
|
private possiblesGrid;
|
|
26
26
|
constructor(input: string | Board);
|
|
@@ -46,12 +46,15 @@ export default class SudokuSolver {
|
|
|
46
46
|
private calcSquarePossibles;
|
|
47
47
|
private calcPossibles;
|
|
48
48
|
private findBestMove;
|
|
49
|
-
private
|
|
49
|
+
private findMoveForPhase;
|
|
50
50
|
private findNakedSingle;
|
|
51
51
|
private findHiddenSingleInRow;
|
|
52
52
|
private findHiddenSingleInCol;
|
|
53
53
|
private findHiddenSingleInBox;
|
|
54
54
|
private findPointingPairTriple;
|
|
55
|
+
private eachHouseInOrder;
|
|
56
|
+
private findNakedSubsetElimination;
|
|
57
|
+
private tryNakedSubsetInHouse;
|
|
55
58
|
private findHiddenSingle;
|
|
56
59
|
}
|
|
57
60
|
//# sourceMappingURL=sudokuSolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sudokuSolver.d.ts","sourceRoot":"","sources":["../src/sudokuSolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,gBAAgB,EAAoB,MAAM,eAAe,CAAC;AAErF,MAAM,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAC;AAE/B,MAAM,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"sudokuSolver.d.ts","sourceRoot":"","sources":["../src/sudokuSolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,gBAAgB,EAAoB,MAAM,eAAe,CAAC;AAErF,MAAM,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAC;AAE/B,MAAM,MAAM,SAAS,GACf,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,eAAe,GACf,sBAAsB,GACtB,YAAY,GACZ,cAAc,GACd,YAAY,CAAC;AAKnB,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,SAAS,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,aAAa,CAAC;IACpB,YAAY,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,SAAS,EAAE,SAAS,CAAC;CACxB;AAED,MAAM,MAAM,IAAI,GAAG,aAAa,GAAG,eAAe,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAIvF,MAAM,CAAC,OAAO,OAAO,YAAY;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAKnC;IAEF,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,aAAa,CAAe;gBAExB,KAAK,EAAE,MAAM,GAAG,KAAK;IAOjC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAS5B,OAAO,IAAI,KAAK;IAIhB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IAIhD,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI;IAIjE,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAmB7D,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAM7C,UAAU,IAAI,OAAO;IAIrB,eAAe,IAAI,MAAM;IAIzB,QAAQ,IAAI,gBAAgB;IAiG5B,OAAO,IAAI,OAAO;IAKlB,UAAU,IAAI,eAAe;IAiB7B,WAAW,IAAI,IAAI,GAAG,IAAI;IAO1B,KAAK,IAAI,OAAO;IAkBhB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,sBAAsB;IAgD9B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,0BAA0B;IAUlC,OAAO,CAAC,qBAAqB;IAqD7B,OAAO,CAAC,gBAAgB;CAe3B"}
|
package/dist/sudokuSolver.js
CHANGED
|
@@ -2,7 +2,12 @@ import { cloneBoard, assertBoardShape, parseBoardString, duplicateValues } from
|
|
|
2
2
|
import { pushUniqueReason } from "./validate.js";
|
|
3
3
|
const COMPLETE = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
4
4
|
export default class SudokuSolver {
|
|
5
|
-
static
|
|
5
|
+
static SEARCH_PHASES = [
|
|
6
|
+
"NakedSingle",
|
|
7
|
+
"HiddenSingle",
|
|
8
|
+
"Pointing",
|
|
9
|
+
"NakedSubset",
|
|
10
|
+
];
|
|
6
11
|
board;
|
|
7
12
|
possiblesGrid;
|
|
8
13
|
constructor(input) {
|
|
@@ -231,27 +236,23 @@ export default class SudokuSolver {
|
|
|
231
236
|
}
|
|
232
237
|
}
|
|
233
238
|
findBestMove() {
|
|
234
|
-
for (const
|
|
235
|
-
const move = this.
|
|
239
|
+
for (const phase of SudokuSolver.SEARCH_PHASES) {
|
|
240
|
+
const move = this.findMoveForPhase(phase);
|
|
236
241
|
if (move)
|
|
237
242
|
return move;
|
|
238
243
|
}
|
|
239
244
|
return null;
|
|
240
245
|
}
|
|
241
|
-
|
|
242
|
-
switch (
|
|
243
|
-
case "
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (algorithm === "Full House") {
|
|
247
|
-
return move?.algorithm === "Full House" ? move : null;
|
|
248
|
-
}
|
|
249
|
-
return move;
|
|
250
|
-
}
|
|
251
|
-
case "Hidden Single":
|
|
246
|
+
findMoveForPhase(phase) {
|
|
247
|
+
switch (phase) {
|
|
248
|
+
case "NakedSingle":
|
|
249
|
+
return this.findNakedSingle();
|
|
250
|
+
case "HiddenSingle":
|
|
252
251
|
return this.findHiddenSingle();
|
|
253
|
-
case "Pointing
|
|
252
|
+
case "Pointing":
|
|
254
253
|
return this.findPointingPairTriple();
|
|
254
|
+
case "NakedSubset":
|
|
255
|
+
return this.findNakedSubsetElimination();
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
findNakedSingle() {
|
|
@@ -275,6 +276,13 @@ export default class SudokuSolver {
|
|
|
275
276
|
if (boxValues.filter((v) => v === 0).length === 1) {
|
|
276
277
|
algo = "Full House";
|
|
277
278
|
}
|
|
279
|
+
if (algo === "Naked Single") {
|
|
280
|
+
// check if last digit
|
|
281
|
+
const placementsOfDigit = this.board.flat().filter((v) => v === placeValue).length;
|
|
282
|
+
if (placementsOfDigit === 8) {
|
|
283
|
+
algo = "Last Digit";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
278
286
|
return { type: "placement", row, col, value: placeValue, algorithm: algo };
|
|
279
287
|
}
|
|
280
288
|
}
|
|
@@ -291,7 +299,13 @@ export default class SudokuSolver {
|
|
|
291
299
|
}
|
|
292
300
|
}
|
|
293
301
|
if (candidates.length === 1) {
|
|
294
|
-
|
|
302
|
+
let move = { type: "placement", row, col: candidates[0], value, algorithm: "Hidden Single" };
|
|
303
|
+
// check if last digit
|
|
304
|
+
const placementsOfDigit = this.board.flat().filter((v) => v === value).length;
|
|
305
|
+
if (placementsOfDigit === 8) {
|
|
306
|
+
move.algorithm = "Last Digit";
|
|
307
|
+
}
|
|
308
|
+
return move;
|
|
295
309
|
}
|
|
296
310
|
}
|
|
297
311
|
return null;
|
|
@@ -305,7 +319,13 @@ export default class SudokuSolver {
|
|
|
305
319
|
}
|
|
306
320
|
}
|
|
307
321
|
if (candidates.length === 1) {
|
|
308
|
-
|
|
322
|
+
let move = { type: "placement", row: candidates[0], col, value, algorithm: "Hidden Single" };
|
|
323
|
+
// check if last digit
|
|
324
|
+
const placementsOfDigit = this.board.flat().filter((v) => v === value).length;
|
|
325
|
+
if (placementsOfDigit === 8) {
|
|
326
|
+
move.algorithm = "Last Digit";
|
|
327
|
+
}
|
|
328
|
+
return move;
|
|
309
329
|
}
|
|
310
330
|
}
|
|
311
331
|
return null;
|
|
@@ -320,7 +340,13 @@ export default class SudokuSolver {
|
|
|
320
340
|
}
|
|
321
341
|
}
|
|
322
342
|
if (candidates.length === 1) {
|
|
323
|
-
|
|
343
|
+
let move = { type: "placement", row: candidates[0].row, col: candidates[0].col, value, algorithm: "Hidden Single" };
|
|
344
|
+
// check if last digit
|
|
345
|
+
const placementsOfDigit = this.board.flat().filter((v) => v === value).length;
|
|
346
|
+
if (placementsOfDigit === 8) {
|
|
347
|
+
move.algorithm = "Last Digit";
|
|
348
|
+
}
|
|
349
|
+
return move;
|
|
324
350
|
}
|
|
325
351
|
}
|
|
326
352
|
return null;
|
|
@@ -371,6 +397,82 @@ export default class SudokuSolver {
|
|
|
371
397
|
}
|
|
372
398
|
return null;
|
|
373
399
|
}
|
|
400
|
+
eachHouseInOrder() {
|
|
401
|
+
const houses = [];
|
|
402
|
+
for (let r = 0; r < 9; r++) {
|
|
403
|
+
houses.push(Array.from({ length: 9 }, (_, c) => ({ row: r, col: c })));
|
|
404
|
+
}
|
|
405
|
+
for (let c = 0; c < 9; c++) {
|
|
406
|
+
houses.push(Array.from({ length: 9 }, (_, row) => ({ row, col: c })));
|
|
407
|
+
}
|
|
408
|
+
for (let b = 0; b < 9; b++) {
|
|
409
|
+
const cells = [];
|
|
410
|
+
for (let idx = 0; idx < 9; idx++) {
|
|
411
|
+
cells.push(this.boxToPuzzle(b, idx));
|
|
412
|
+
}
|
|
413
|
+
houses.push(cells);
|
|
414
|
+
}
|
|
415
|
+
return houses;
|
|
416
|
+
}
|
|
417
|
+
findNakedSubsetElimination() {
|
|
418
|
+
for (const houseCells of this.eachHouseInOrder()) {
|
|
419
|
+
for (let k = 2; k <= 4; k++) {
|
|
420
|
+
const move = this.tryNakedSubsetInHouse(houseCells, k);
|
|
421
|
+
if (move)
|
|
422
|
+
return move;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
tryNakedSubsetInHouse(houseCells, k) {
|
|
428
|
+
const empties = houseCells.filter(({ row, col }) => this.board[row][col] === 0 && this.possiblesGrid[row][col].length > 0);
|
|
429
|
+
if (empties.length < k) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
const algorithm = k === 2 ? "Naked Pair" : k === 3 ? "Naked Triple" : "Naked Quad";
|
|
433
|
+
const n = empties.length;
|
|
434
|
+
const indices = [];
|
|
435
|
+
const dfs = (start) => {
|
|
436
|
+
if (indices.length === k) {
|
|
437
|
+
const subset = indices.map((i) => empties[i]);
|
|
438
|
+
const union = new Set();
|
|
439
|
+
for (const { row, col } of subset) {
|
|
440
|
+
for (const v of this.possiblesGrid[row][col]) {
|
|
441
|
+
union.add(v);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (union.size !== k) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const subsetKeys = new Set(subset.map(({ row, col }) => row * 9 + col));
|
|
448
|
+
const eliminations = [];
|
|
449
|
+
for (const { row, col } of houseCells) {
|
|
450
|
+
if (this.board[row][col] !== 0 || subsetKeys.has(row * 9 + col)) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const poss = this.possiblesGrid[row][col];
|
|
454
|
+
for (const d of union) {
|
|
455
|
+
if (poss.includes(d)) {
|
|
456
|
+
eliminations.push({ row, col, value: d });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (eliminations.length === 0) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
return { type: "elimination", eliminations, algorithm };
|
|
464
|
+
}
|
|
465
|
+
for (let i = start; i < n; i++) {
|
|
466
|
+
indices.push(i);
|
|
467
|
+
const res = dfs(i + 1);
|
|
468
|
+
indices.pop();
|
|
469
|
+
if (res)
|
|
470
|
+
return res;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
};
|
|
474
|
+
return dfs(0);
|
|
475
|
+
}
|
|
374
476
|
findHiddenSingle() {
|
|
375
477
|
for (let row = 0; row < 9; row++) {
|
|
376
478
|
const move = this.findHiddenSingleInRow(row);
|
package/package.json
CHANGED