@wsabol/sudoku-solver 0.1.6 → 0.1.8
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/sudokuSolver.d.ts +10 -1
- package/dist/sudokuSolver.d.ts.map +1 -1
- package/dist/sudokuSolver.js +180 -2
- package/package.json +1 -1
package/dist/sudokuSolver.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ValidationResult } from "./validate.js";
|
|
2
2
|
import type { Board } from "./boardGeo.js";
|
|
3
3
|
import type { Move, EliminationMove } from "./move.js";
|
|
4
|
-
export type Algorithm = "Last Digit" | "Full House" | "Naked Single" | "Hidden Single" | "Pointing Pair" | "Pointing Triple" | "Naked Pair" | "Naked Triple" | "Naked Quad";
|
|
4
|
+
export type Algorithm = "Last Digit" | "Full House" | "Naked Single" | "Hidden Single" | "Pointing Pair" | "Pointing Triple" | "X-Wing" | "Naked Pair" | "Naked Triple" | "Naked Quad" | "Hidden Pair" | "Hidden Triple" | "Hidden Quad";
|
|
5
5
|
export type DifficultyLevel = "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible";
|
|
6
6
|
export type ValidationReasonType = "duplicate_in_row" | "duplicate_in_column" | "duplicate_in_box" | "invalid_value" | "invalid_board_length" | "invalid_board_characters" | "empty_cell_no_candidates" | "too_many_empty_cells";
|
|
7
7
|
export default class SudokuSolver {
|
|
@@ -43,11 +43,20 @@ export default class SudokuSolver {
|
|
|
43
43
|
private findHiddenSingleInCol;
|
|
44
44
|
private findHiddenSingleInBox;
|
|
45
45
|
private findPointingPairTriple;
|
|
46
|
+
/** Empty cells in `row` where `digit` is still a candidate (column indices). */
|
|
47
|
+
private colsWithDigitCandidates;
|
|
48
|
+
/** Empty cells in `col` where `digit` is still a candidate (row indices). */
|
|
49
|
+
private rowsWithDigitCandidates;
|
|
50
|
+
/** Classic row/column X-Wing only (box forms duplicate pointing / line–box). */
|
|
51
|
+
private findXWing;
|
|
46
52
|
private eachHouseInOrder;
|
|
47
53
|
private findNakedSubsetElimination;
|
|
48
54
|
/** `houseCells` is one full row, column, or box (9 cells). */
|
|
49
55
|
private getHouseContext;
|
|
50
56
|
private tryNakedSubsetInHouse;
|
|
57
|
+
private findHiddenSubsetElimination;
|
|
58
|
+
private findNakedHiddenQuadsElimination;
|
|
59
|
+
private tryHiddenSubsetInHouse;
|
|
51
60
|
private findHiddenSingle;
|
|
52
61
|
}
|
|
53
62
|
//# 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;AACrF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAiB,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtE,MAAM,MAAM,SAAS,GACf,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,eAAe,GACf,eAAe,GACf,iBAAiB,GACjB,YAAY,GACZ,cAAc,GACd,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"sudokuSolver.d.ts","sourceRoot":"","sources":["../src/sudokuSolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,gBAAgB,EAAoB,MAAM,eAAe,CAAC;AACrF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAiB,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtE,MAAM,MAAM,SAAS,GACf,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,eAAe,GACf,eAAe,GACf,iBAAiB,GACjB,QAAQ,GACR,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,aAAa,GACb,eAAe,GACf,aAAa,CAAC;AAEpB,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAEvF,MAAM,MAAM,oBAAoB,GAC1B,kBAAkB,GAClB,qBAAqB,GACrB,kBAAkB,GAClB,eAAe,GACf,sBAAsB,GACtB,0BAA0B,GAC1B,0BAA0B,GAC1B,sBAAsB,CAAC;AAsD7B,MAAM,CAAC,OAAO,OAAO,YAAY;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAQnC;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,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAQ3B,UAAU,IAAI,OAAO;IAIrB,eAAe,IAAI,MAAM;IAIzB,WAAW,CAAC,KAAK,GAAE,MAAU,GAAG,MAAM;IAOtC,QAAQ,IAAI,gBAAgB;IAiG5B,OAAO,IAAI,OAAO;IAKlB,UAAU,IAAI,eAAe;IAiB7B,WAAW,IAAI,IAAI,GAAG,IAAI;IAO1B,KAAK,IAAI,OAAO;IAchB,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,iBAAiB;IAkBzB,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,eAAe;IA+CvB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,qBAAqB;IAsB7B,OAAO,CAAC,qBAAqB;IAsB7B,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,sBAAsB;IAqD9B,gFAAgF;IAChF,OAAO,CAAC,uBAAuB;IAU/B,6EAA6E;IAC7E,OAAO,CAAC,uBAAuB;IAU/B,gFAAgF;IAChF,OAAO,CAAC,SAAS;IA8DjB,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,0BAA0B;IAUlC,8DAA8D;IAC9D,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,qBAAqB;IAwD7B,OAAO,CAAC,2BAA2B;IAUnC,OAAO,CAAC,+BAA+B;IAWvC,OAAO,CAAC,sBAAsB;IAgE9B,OAAO,CAAC,gBAAgB;CAe3B"}
|
package/dist/sudokuSolver.js
CHANGED
|
@@ -40,6 +40,9 @@ export default class SudokuSolver {
|
|
|
40
40
|
"HiddenSingle",
|
|
41
41
|
"Pointing",
|
|
42
42
|
"NakedSubset",
|
|
43
|
+
"HiddenSubset",
|
|
44
|
+
"Fish",
|
|
45
|
+
"NakedHiddenQuads",
|
|
43
46
|
];
|
|
44
47
|
board;
|
|
45
48
|
possiblesGrid;
|
|
@@ -290,7 +293,7 @@ export default class SudokuSolver {
|
|
|
290
293
|
}
|
|
291
294
|
finalizeElimination(eliminations, algorithm, reasoning) {
|
|
292
295
|
const digits = [...new Set(eliminations.map((e) => e.value))].sort((a, b) => a - b);
|
|
293
|
-
const digitPart = digits.length === 1 ? String(digits[0]) :
|
|
296
|
+
const digitPart = digits.length === 1 ? String(digits[0]) : `${digits.join("/")}`;
|
|
294
297
|
const uniqueCells = uniqueCellCoordinates(eliminations);
|
|
295
298
|
const uniqueCellCount = uniqueCells.length;
|
|
296
299
|
const cellWord = uniqueCellCount === 1 ? "cell" : "cells";
|
|
@@ -320,8 +323,16 @@ export default class SudokuSolver {
|
|
|
320
323
|
return this.findHiddenSingle();
|
|
321
324
|
case "Pointing":
|
|
322
325
|
return this.findPointingPairTriple();
|
|
326
|
+
case "Fish":
|
|
327
|
+
return this.findXWing();
|
|
323
328
|
case "NakedSubset":
|
|
324
329
|
return this.findNakedSubsetElimination();
|
|
330
|
+
case "HiddenSubset":
|
|
331
|
+
return this.findHiddenSubsetElimination();
|
|
332
|
+
case "NakedHiddenQuads":
|
|
333
|
+
return this.findNakedHiddenQuadsElimination();
|
|
334
|
+
default:
|
|
335
|
+
return null;
|
|
325
336
|
}
|
|
326
337
|
}
|
|
327
338
|
findNakedSingle() {
|
|
@@ -490,6 +501,90 @@ export default class SudokuSolver {
|
|
|
490
501
|
}
|
|
491
502
|
return null;
|
|
492
503
|
}
|
|
504
|
+
/** Empty cells in `row` where `digit` is still a candidate (column indices). */
|
|
505
|
+
colsWithDigitCandidates(row, digit) {
|
|
506
|
+
const cols = [];
|
|
507
|
+
for (let col = 0; col < 9; col++) {
|
|
508
|
+
if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(digit)) {
|
|
509
|
+
cols.push(col);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return cols;
|
|
513
|
+
}
|
|
514
|
+
/** Empty cells in `col` where `digit` is still a candidate (row indices). */
|
|
515
|
+
rowsWithDigitCandidates(col, digit) {
|
|
516
|
+
const rows = [];
|
|
517
|
+
for (let row = 0; row < 9; row++) {
|
|
518
|
+
if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(digit)) {
|
|
519
|
+
rows.push(row);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return rows;
|
|
523
|
+
}
|
|
524
|
+
/** Classic row/column X-Wing only (box forms duplicate pointing / line–box). */
|
|
525
|
+
findXWing() {
|
|
526
|
+
for (let digit = 1; digit <= 9; digit++) {
|
|
527
|
+
for (let r1 = 0; r1 < 9; r1++) {
|
|
528
|
+
const cols1 = this.colsWithDigitCandidates(r1, digit);
|
|
529
|
+
if (cols1.length !== 2)
|
|
530
|
+
continue;
|
|
531
|
+
const baseCols = [...cols1].sort((a, b) => a - b);
|
|
532
|
+
for (let r2 = r1 + 1; r2 < 9; r2++) {
|
|
533
|
+
const cols2 = this.colsWithDigitCandidates(r2, digit);
|
|
534
|
+
if (cols2.length !== 2)
|
|
535
|
+
continue;
|
|
536
|
+
const pair = [...cols2].sort((a, b) => a - b);
|
|
537
|
+
if (pair[0] !== baseCols[0] || pair[1] !== baseCols[1])
|
|
538
|
+
continue;
|
|
539
|
+
const eliminations = [];
|
|
540
|
+
for (const col of baseCols) {
|
|
541
|
+
for (let row = 0; row < 9; row++) {
|
|
542
|
+
if (row === r1 || row === r2)
|
|
543
|
+
continue;
|
|
544
|
+
if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(digit)) {
|
|
545
|
+
eliminations.push({ row, col, value: digit });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (eliminations.length > 0) {
|
|
550
|
+
const reasoning = `X-Wing on ${digit}: rows ${r1 + 1} and ${r2 + 1} each have ${digit} only in columns ${baseCols[0] + 1} and ${baseCols[1] + 1}, so ${digit} cannot appear elsewhere in those columns.`;
|
|
551
|
+
return this.finalizeElimination(eliminations, "X-Wing", reasoning);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (let digit = 1; digit <= 9; digit++) {
|
|
557
|
+
for (let c1 = 0; c1 < 9; c1++) {
|
|
558
|
+
const rows1 = this.rowsWithDigitCandidates(c1, digit);
|
|
559
|
+
if (rows1.length !== 2)
|
|
560
|
+
continue;
|
|
561
|
+
const baseRows = [...rows1].sort((a, b) => a - b);
|
|
562
|
+
for (let c2 = c1 + 1; c2 < 9; c2++) {
|
|
563
|
+
const rows2 = this.rowsWithDigitCandidates(c2, digit);
|
|
564
|
+
if (rows2.length !== 2)
|
|
565
|
+
continue;
|
|
566
|
+
const pair = [...rows2].sort((a, b) => a - b);
|
|
567
|
+
if (pair[0] !== baseRows[0] || pair[1] !== baseRows[1])
|
|
568
|
+
continue;
|
|
569
|
+
const eliminations = [];
|
|
570
|
+
for (const row of baseRows) {
|
|
571
|
+
for (let col = 0; col < 9; col++) {
|
|
572
|
+
if (col === c1 || col === c2)
|
|
573
|
+
continue;
|
|
574
|
+
if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(digit)) {
|
|
575
|
+
eliminations.push({ row, col, value: digit });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (eliminations.length > 0) {
|
|
580
|
+
const reasoning = `X-Wing on ${digit}: columns ${c1 + 1} and ${c2 + 1} each have ${digit} only in rows ${baseRows[0] + 1} and ${baseRows[1] + 1}, so ${digit} cannot appear elsewhere in those rows.`;
|
|
581
|
+
return this.finalizeElimination(eliminations, "X-Wing", reasoning);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
493
588
|
eachHouseInOrder() {
|
|
494
589
|
const houses = [];
|
|
495
590
|
for (let r = 0; r < 9; r++) {
|
|
@@ -509,7 +604,7 @@ export default class SudokuSolver {
|
|
|
509
604
|
}
|
|
510
605
|
findNakedSubsetElimination() {
|
|
511
606
|
for (const houseCells of this.eachHouseInOrder()) {
|
|
512
|
-
for (let k = 2; k <=
|
|
607
|
+
for (let k = 2; k <= 3; k++) {
|
|
513
608
|
const move = this.tryNakedSubsetInHouse(houseCells, k);
|
|
514
609
|
if (move)
|
|
515
610
|
return move;
|
|
@@ -578,6 +673,89 @@ export default class SudokuSolver {
|
|
|
578
673
|
};
|
|
579
674
|
return dfs(0);
|
|
580
675
|
}
|
|
676
|
+
findHiddenSubsetElimination() {
|
|
677
|
+
for (const houseCells of this.eachHouseInOrder()) {
|
|
678
|
+
for (let k = 2; k <= 3; k++) {
|
|
679
|
+
const move = this.tryHiddenSubsetInHouse(houseCells, k);
|
|
680
|
+
if (move)
|
|
681
|
+
return move;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
findNakedHiddenQuadsElimination() {
|
|
687
|
+
for (const houseCells of this.eachHouseInOrder()) {
|
|
688
|
+
const moveNaked = this.tryNakedSubsetInHouse(houseCells, 4);
|
|
689
|
+
if (moveNaked)
|
|
690
|
+
return moveNaked;
|
|
691
|
+
const moveHidden = this.tryHiddenSubsetInHouse(houseCells, 4);
|
|
692
|
+
if (moveHidden)
|
|
693
|
+
return moveHidden;
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
tryHiddenSubsetInHouse(houseCells, k) {
|
|
698
|
+
const algorithm = k === 2 ? "Hidden Pair" : k === 3 ? "Hidden Triple" : "Hidden Quad";
|
|
699
|
+
const { wherePhrase, sameKindWord } = this.getHouseContext(houseCells);
|
|
700
|
+
const digitIndices = [];
|
|
701
|
+
const dfsDigits = (start) => {
|
|
702
|
+
if (digitIndices.length === k) {
|
|
703
|
+
const digits = digitIndices.map((i) => COMPLETE[i]);
|
|
704
|
+
const digitSet = new Set(digits);
|
|
705
|
+
for (const d of digits) {
|
|
706
|
+
let seen = false;
|
|
707
|
+
for (const { row, col } of houseCells) {
|
|
708
|
+
if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(d)) {
|
|
709
|
+
seen = true;
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (!seen) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const reserved = [];
|
|
718
|
+
for (const { row, col } of houseCells) {
|
|
719
|
+
if (this.board[row][col] !== 0) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
for (const d of digits) {
|
|
723
|
+
if (this.possiblesGrid[row][col].includes(d)) {
|
|
724
|
+
reserved.push({ row, col });
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (reserved.length !== k) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const eliminations = [];
|
|
733
|
+
for (const { row, col } of reserved) {
|
|
734
|
+
for (const v of this.possiblesGrid[row][col]) {
|
|
735
|
+
if (!digitSet.has(v)) {
|
|
736
|
+
eliminations.push({ row, col, value: v });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (eliminations.length === 0) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
const digitStr = [...digits].sort((a, b) => a - b).join("/");
|
|
744
|
+
const cellPhrase = reserved.map(({ row, col }) => `r${row + 1}c${col + 1}`).join(", ");
|
|
745
|
+
const reasoning = `${algorithm} ${digitStr} in ${wherePhrase}: in that ${sameKindWord}, those digits appear only in ${cellPhrase}, so candidates other than ${digitStr} can be removed from those cells.`;
|
|
746
|
+
return this.finalizeElimination(eliminations, algorithm, reasoning);
|
|
747
|
+
}
|
|
748
|
+
for (let i = start; i < 9; i++) {
|
|
749
|
+
digitIndices.push(i);
|
|
750
|
+
const res = dfsDigits(i + 1);
|
|
751
|
+
digitIndices.pop();
|
|
752
|
+
if (res)
|
|
753
|
+
return res;
|
|
754
|
+
}
|
|
755
|
+
return null;
|
|
756
|
+
};
|
|
757
|
+
return dfsDigits(0);
|
|
758
|
+
}
|
|
581
759
|
findHiddenSingle() {
|
|
582
760
|
for (let row = 0; row < 9; row++) {
|
|
583
761
|
const move = this.findHiddenSingleInRow(row);
|
package/package.json
CHANGED