@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.
@@ -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;AAEnB,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,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,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;IAaxB,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,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,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"}
@@ -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]) : `{${digits.join("/")}}`;
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 <= 4; 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsabol/sudoku-solver",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",