@wsabol/sudoku-solver 0.1.7 → 0.1.9
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 +13 -1
- package/dist/sudokuSolver.d.ts.map +1 -1
- package/dist/sudokuSolver.js +74 -0
- 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" | "Hidden Pair" | "Hidden Triple" | "Hidden Quad";
|
|
4
|
+
export type Algorithm = "Last Digit" | "Full House" | "Naked Single" | "Hidden Single" | "Pointing Pair" | "Pointing Triple" | "X-Wing" | "Swordfish" | "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,6 +43,18 @@ export default class SudokuSolver {
|
|
|
43
43
|
private findHiddenSingleInCol;
|
|
44
44
|
private findHiddenSingleInBox;
|
|
45
45
|
private findPointingPairTriple;
|
|
46
|
+
/**
|
|
47
|
+
* Cover indices for a base line: column indices when scanning by row, row indices when scanning by column.
|
|
48
|
+
* Returns only empty cells where `digit` is still a candidate.
|
|
49
|
+
*/
|
|
50
|
+
private coverIndicesForLine;
|
|
51
|
+
/**
|
|
52
|
+
* Generic N-fish detector (X-Wing = 2, Swordfish = 3).
|
|
53
|
+
* Finds `fishSize` base lines whose candidates for a digit span exactly `fishSize` cover lines,
|
|
54
|
+
* then eliminates that digit from those cover lines outside the base lines.
|
|
55
|
+
* Checks row-based orientation first, then column-based.
|
|
56
|
+
*/
|
|
57
|
+
private findFishOfSize;
|
|
46
58
|
private eachHouseInOrder;
|
|
47
59
|
private findNakedSubsetElimination;
|
|
48
60
|
/** `houseCells` is one full row, column, or box (9 cells). */
|
|
@@ -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,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;
|
|
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,WAAW,GACX,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;AAuD7B,MAAM,CAAC,OAAO,OAAO,YAAY;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CASnC;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;IAuBxB,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;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAW3B;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAqDtB,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
|
@@ -41,6 +41,8 @@ export default class SudokuSolver {
|
|
|
41
41
|
"Pointing",
|
|
42
42
|
"NakedSubset",
|
|
43
43
|
"HiddenSubset",
|
|
44
|
+
"Fish",
|
|
45
|
+
"Swordfish",
|
|
44
46
|
"NakedHiddenQuads",
|
|
45
47
|
];
|
|
46
48
|
board;
|
|
@@ -322,6 +324,10 @@ export default class SudokuSolver {
|
|
|
322
324
|
return this.findHiddenSingle();
|
|
323
325
|
case "Pointing":
|
|
324
326
|
return this.findPointingPairTriple();
|
|
327
|
+
case "Fish":
|
|
328
|
+
return this.findFishOfSize(2);
|
|
329
|
+
case "Swordfish":
|
|
330
|
+
return this.findFishOfSize(3);
|
|
325
331
|
case "NakedSubset":
|
|
326
332
|
return this.findNakedSubsetElimination();
|
|
327
333
|
case "HiddenSubset":
|
|
@@ -498,6 +504,74 @@ export default class SudokuSolver {
|
|
|
498
504
|
}
|
|
499
505
|
return null;
|
|
500
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Cover indices for a base line: column indices when scanning by row, row indices when scanning by column.
|
|
509
|
+
* Returns only empty cells where `digit` is still a candidate.
|
|
510
|
+
*/
|
|
511
|
+
coverIndicesForLine(lineIdx, digit, byRow) {
|
|
512
|
+
const result = [];
|
|
513
|
+
for (let crossIdx = 0; crossIdx < 9; crossIdx++) {
|
|
514
|
+
const [row, col] = byRow ? [lineIdx, crossIdx] : [crossIdx, lineIdx];
|
|
515
|
+
if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(digit)) {
|
|
516
|
+
result.push(crossIdx);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Generic N-fish detector (X-Wing = 2, Swordfish = 3).
|
|
523
|
+
* Finds `fishSize` base lines whose candidates for a digit span exactly `fishSize` cover lines,
|
|
524
|
+
* then eliminates that digit from those cover lines outside the base lines.
|
|
525
|
+
* Checks row-based orientation first, then column-based.
|
|
526
|
+
*/
|
|
527
|
+
findFishOfSize(fishSize) {
|
|
528
|
+
const algorithm = fishSize === 2 ? "X-Wing" : "Swordfish";
|
|
529
|
+
for (const byRow of [true, false]) {
|
|
530
|
+
const baseLabel = byRow ? "rows" : "columns";
|
|
531
|
+
const coverLabel = byRow ? "columns" : "rows";
|
|
532
|
+
for (let digit = 1; digit <= 9; digit++) {
|
|
533
|
+
const eligible = [];
|
|
534
|
+
for (let lineIdx = 0; lineIdx < 9; lineIdx++) {
|
|
535
|
+
const cover = this.coverIndicesForLine(lineIdx, digit, byRow);
|
|
536
|
+
if (cover.length >= 2 && cover.length <= fishSize) {
|
|
537
|
+
eligible.push({ lineIdx, cover });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Iterate all C(eligible, fishSize) combinations.
|
|
541
|
+
for (let i = 0; i < eligible.length - (fishSize - 1); i++) {
|
|
542
|
+
for (let j = i + 1; j < eligible.length - (fishSize - 2); j++) {
|
|
543
|
+
const pairs = fishSize === 2
|
|
544
|
+
? [[eligible[i], eligible[j]]]
|
|
545
|
+
: Array.from({ length: eligible.length - j - 1 }, (_, d) => [eligible[i], eligible[j], eligible[j + 1 + d]]);
|
|
546
|
+
for (const combo of pairs) {
|
|
547
|
+
const unionCover = [...new Set(combo.flatMap((e) => e.cover))].sort((a, b) => a - b);
|
|
548
|
+
if (unionCover.length !== fishSize)
|
|
549
|
+
continue;
|
|
550
|
+
const baseIndices = combo.map((e) => e.lineIdx);
|
|
551
|
+
const eliminations = [];
|
|
552
|
+
for (const coverIdx of unionCover) {
|
|
553
|
+
for (let otherLineIdx = 0; otherLineIdx < 9; otherLineIdx++) {
|
|
554
|
+
if (baseIndices.includes(otherLineIdx))
|
|
555
|
+
continue;
|
|
556
|
+
const [row, col] = byRow ? [otherLineIdx, coverIdx] : [coverIdx, otherLineIdx];
|
|
557
|
+
if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(digit)) {
|
|
558
|
+
eliminations.push({ row, col, value: digit });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (eliminations.length > 0) {
|
|
563
|
+
const baseList = baseIndices.map((i) => i + 1).join(", ").replace(/,([^,]*)$/, " and$1");
|
|
564
|
+
const coverList = unionCover.map((i) => i + 1).join(", ").replace(/,([^,]*)$/, " and$1");
|
|
565
|
+
const reasoning = `${algorithm} on ${digit}: ${baseLabel} ${baseList} have ${digit} only in ${coverLabel} ${coverList}, so ${digit} cannot appear elsewhere in those ${coverLabel}.`;
|
|
566
|
+
return this.finalizeElimination(eliminations, algorithm, reasoning);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
501
575
|
eachHouseInOrder() {
|
|
502
576
|
const houses = [];
|
|
503
577
|
for (let r = 0; r < 9; r++) {
|
package/package.json
CHANGED