@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.
@@ -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;AA+C7B,MAAM,CAAC,OAAO,OAAO,YAAY;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAOnC;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;IAmBxB,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,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"}
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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsabol/sudoku-solver",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "TypeScript Sudoku solver module with solve, next move, describe, and validate APIs.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",