@wsabol/sudoku-solver 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -30,11 +30,12 @@ console.log(solved.board); // number[][]
30
30
 
31
31
  const next = Sudoku.nextMove(board);
32
32
  console.log(next.status); // "In progress"
33
- console.log(next.message); // e.g. "Place 4 in r1c3" or "Eliminate 5 from 2 cell(s) (Pointing Pair/Triple)"
33
+ console.log(next.message); // same as next.move?.message when a move exists, e.g. "Place 4 in r1c3 (Naked Single)"
34
34
  if (next.move?.type === "placement") {
35
- console.log(next.move.row, next.move.col, next.move.value);
35
+ console.log(next.move.row, next.move.col, next.move.value, next.move.reasoning);
36
36
  } else if (next.move?.type === "elimination") {
37
37
  console.log(next.move.eliminations); // [{ row, col, value }, ...]
38
+ // multi-value eliminations: message uses sorted set like "2/5/7" in the Eliminate … part
38
39
  }
39
40
 
40
41
  const check = Sudoku.validate(board);
@@ -81,16 +82,32 @@ Returns:
81
82
 
82
83
  ```ts
83
84
  // digit placement
84
- { type: "placement"; row: number; col: number; value: number; algorithm: Algorithm }
85
+ {
86
+ type: "placement";
87
+ row: number;
88
+ col: number;
89
+ value: number;
90
+ algorithm: Algorithm;
91
+ message: string;
92
+ reasoning: string;
93
+ }
85
94
 
86
- // candidate elimination (e.g. Pointing Pair/Triple)
87
- { type: "elimination"; eliminations: Array<{ row: number; col: number; value: number }>; algorithm: Algorithm }
95
+ // candidate elimination (e.g. Pointing Pair or Pointing Triple)
96
+ {
97
+ type: "elimination";
98
+ eliminations: Array<{ row: number; col: number; value: number }>;
99
+ algorithm: Algorithm;
100
+ message: string;
101
+ reasoning: string;
102
+ }
88
103
  ```
89
104
 
90
105
  Notes:
91
106
  - `move` is `null` when the board is complete or invalid.
92
107
  - `move.type` must be checked before accessing placement-specific fields (`row`, `col`, `value`).
93
- - `message` is human-readable: `"Place 4 in r1c3"` for placements, `"Eliminate 5 from 2 cell(s) (Pointing Pair/Triple)"` for eliminations.
108
+ - `message`: placement uses 1-based coords in text, e.g. `Place 4 in r1c3 (Naked Single)`. Elimination counts distinct cells and names a common house when all targets share one row, column, or 3×3 box, e.g. `Eliminate 5 from 2 cells in row 4 (Pointing Pair)` or `Eliminate 2/7/9 from 3 cells in box 7 (Naked Triple)`; several distinct values use `{2/5/7}` in the digit part.
109
+ - `reasoning`: one sentence describing why the move is sound (technique-specific).
110
+ - Top-level `message` matches `move.message` when `move` is non-null.
94
111
 
95
112
  ### `Sudoku.validate(boardInput)`
96
113
 
@@ -117,12 +134,12 @@ Returns:
117
134
  isValid: boolean;
118
135
  isComplete: boolean;
119
136
  message: string;
120
- difficulty: "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible";
137
+ difficulty: "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible" | null;
121
138
  solutions: number;
122
139
  }
123
140
  ```
124
141
 
125
- `difficulty` is derived from empty cell count. `solutions` is `0` when the board is invalid/unsolvable, `1` when a unique solution exists.
142
+ `difficulty` is derived from empty cell count when the board is valid; `null` when `isValid === false`. `solutions` is `0` when the board is invalid/unsolvable, `1` when a unique solution exists.
126
143
 
127
144
  ## Development
128
145
 
@@ -0,0 +1,2 @@
1
+ export type Board = number[][];
2
+ //# sourceMappingURL=boardGeo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boardGeo.d.ts","sourceRoot":"","sources":["../src/boardGeo.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import SudokuSolver, { type Algorithm, type Board, type Move, type PlacementMove, type EliminationMove } from "./sudokuSolver.js";
2
- import { type ValidationReason, type ValidationResult } from "./validate.js";
1
+ import SudokuSolver, { type Algorithm, type DifficultyLevel } from "./sudokuSolver.js";
2
+ import { ValidationReason, ValidationResult } from "./validate.js";
3
+ import type { Board } from "./boardGeo.js";
4
+ import type { Move, MoveStatus, EliminationMove, PlacementMove } from "./move.js";
3
5
  interface SolveResult {
4
6
  isValid: boolean;
5
7
  board: Board;
@@ -8,10 +10,9 @@ interface DescribeResult {
8
10
  isValid: boolean;
9
11
  isComplete: boolean;
10
12
  message: string;
11
- difficulty: string;
13
+ difficulty: DifficultyLevel | null;
12
14
  solutions: number;
13
15
  }
14
- type MoveStatus = "Complete" | "In progress" | "Invalid";
15
16
  interface MoveResult {
16
17
  status: MoveStatus;
17
18
  move: Move | null;
@@ -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,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,EAAE,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAA8C,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC/G,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAElF,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,eAAe,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,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,CA0BxD;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
@@ -23,17 +23,9 @@ function nextMove(boardInput) {
23
23
  if (!move) {
24
24
  message = "No more moves";
25
25
  }
26
- else if (move.type === "placement") {
27
- message = `Place ${move.value} in r${move.row + 1}c${move.col + 1} (${move.algorithm})`;
28
- sudoku.setSquareValue(move.row, move.col, move.value);
29
- }
30
26
  else {
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})`;
36
- sudoku.applyElimination(move);
27
+ message = move.message;
28
+ sudoku.applyMove(move);
37
29
  }
38
30
  return {
39
31
  status: sudoku.isComplete() ? "Complete" : "In progress",
@@ -61,7 +53,7 @@ function describeBoard(boardInput) {
61
53
  isValid: false,
62
54
  isComplete: false,
63
55
  message: initValidation.message,
64
- difficulty: '',
56
+ difficulty: null,
65
57
  solutions: 0,
66
58
  };
67
59
  }
package/dist/move.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { Algorithm } from "./sudokuSolver.js";
2
+ export type MoveStatus = "Complete" | "In progress" | "Invalid";
3
+ export interface PlacementMove {
4
+ type: "placement";
5
+ row: number;
6
+ col: number;
7
+ value: number;
8
+ algorithm: Algorithm;
9
+ message: string;
10
+ reasoning: string;
11
+ }
12
+ export interface EliminationMove {
13
+ type: "elimination";
14
+ eliminations: Array<{
15
+ row: number;
16
+ col: number;
17
+ value: number;
18
+ }>;
19
+ algorithm: Algorithm;
20
+ message: string;
21
+ reasoning: string;
22
+ }
23
+ export type Move = PlacementMove | EliminationMove;
24
+ //# sourceMappingURL=move.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"move.d.ts","sourceRoot":"","sources":["../src/move.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;AAEhE,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;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACrB;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;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,IAAI,GAAG,aAAa,GAAG,eAAe,CAAC"}
package/dist/move.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -1,24 +1,9 @@
1
1
  import { ValidationResult } from "./validate.js";
2
- export type Board = number[][];
3
- export type Algorithm = "Last Digit" | "Full House" | "Naked Single" | "Hidden Single" | "Pointing Pair/Triple" | "Naked Pair" | "Naked Triple" | "Naked Quad";
4
- export interface PlacementMove {
5
- type: "placement";
6
- row: number;
7
- col: number;
8
- value: number;
9
- algorithm: Algorithm;
10
- }
11
- export interface EliminationMove {
12
- type: "elimination";
13
- eliminations: Array<{
14
- row: number;
15
- col: number;
16
- value: number;
17
- }>;
18
- algorithm: Algorithm;
19
- }
20
- export type Move = PlacementMove | EliminationMove;
2
+ import type { Board } from "./boardGeo.js";
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";
21
5
  export type DifficultyLevel = "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible";
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";
22
7
  export default class SudokuSolver {
23
8
  private static readonly SEARCH_PHASES;
24
9
  private board;
@@ -30,8 +15,10 @@ export default class SudokuSolver {
30
15
  setPossibles(row: number, col: number, possibles: number[]): void;
31
16
  setSquareValue(row: number, col: number, value: number): void;
32
17
  applyElimination(move: EliminationMove): void;
18
+ applyMove(move: Move): void;
33
19
  isComplete(): boolean;
34
20
  countEmptyCells(): number;
21
+ countPlaced(value?: number): number;
35
22
  validate(): ValidationResult;
36
23
  isValid(): boolean;
37
24
  difficulty(): DifficultyLevel;
@@ -45,15 +32,21 @@ export default class SudokuSolver {
45
32
  private valuesMissing;
46
33
  private calcSquarePossibles;
47
34
  private calcPossibles;
35
+ private finalizePlacement;
36
+ private finalizeElimination;
48
37
  private findBestMove;
49
38
  private findMoveForPhase;
50
39
  private findNakedSingle;
40
+ private hiddenSingleReasoning;
41
+ private lastDigitReasoning;
51
42
  private findHiddenSingleInRow;
52
43
  private findHiddenSingleInCol;
53
44
  private findHiddenSingleInBox;
54
45
  private findPointingPairTriple;
55
46
  private eachHouseInOrder;
56
47
  private findNakedSubsetElimination;
48
+ /** `houseCells` is one full row, column, or box (9 cells). */
49
+ private getHouseContext;
57
50
  private tryNakedSubsetInHouse;
58
51
  private findHiddenSingle;
59
52
  }
@@ -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,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"}
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,6 +1,39 @@
1
- import { cloneBoard, assertBoardShape, parseBoardString, duplicateValues } from "./utils.js";
1
+ import { cloneBoard, assertBoardShape, parseBoardString, duplicateValues, boxNumber } from "./utils.js";
2
2
  import { pushUniqueReason } from "./validate.js";
3
3
  const COMPLETE = [1, 2, 3, 4, 5, 6, 7, 8, 9];
4
+ function uniqueCellCoordinates(items) {
5
+ const seen = new Set();
6
+ const out = [];
7
+ for (const { row, col } of items) {
8
+ const k = `${row},${col}`;
9
+ if (!seen.has(k)) {
10
+ seen.add(k);
11
+ out.push({ row, col });
12
+ }
13
+ }
14
+ return out;
15
+ }
16
+ /**
17
+ * If every cell lies in the same row, same column, or same 3×3 box, return that house.
18
+ * Row wins over column when both apply (e.g. a single cell). Otherwise `null`.
19
+ */
20
+ function sharedHouseContainingCells(cells) {
21
+ if (cells.length === 0) {
22
+ return null;
23
+ }
24
+ const first = cells[0];
25
+ if (cells.every((c) => c.row === first.row)) {
26
+ return { kind: "row", wherePhrase: `row ${first.row + 1}` };
27
+ }
28
+ if (cells.every((c) => c.col === first.col)) {
29
+ return { kind: "column", wherePhrase: `column ${first.col + 1}` };
30
+ }
31
+ const box = boxNumber(first.row, first.col);
32
+ if (cells.every((c) => boxNumber(c.row, c.col) === box)) {
33
+ return { kind: "box", wherePhrase: `box ${box}` };
34
+ }
35
+ return null;
36
+ }
4
37
  export default class SudokuSolver {
5
38
  static SEARCH_PHASES = [
6
39
  "NakedSingle",
@@ -54,12 +87,26 @@ export default class SudokuSolver {
54
87
  this.possiblesGrid[row][col] = this.possiblesGrid[row][col].filter((v) => v !== value);
55
88
  }
56
89
  }
90
+ applyMove(move) {
91
+ if (move.type === "placement") {
92
+ this.setSquareValue(move.row, move.col, move.value);
93
+ }
94
+ else {
95
+ this.applyElimination(move);
96
+ }
97
+ }
57
98
  isComplete() {
58
99
  return this.board.every((row) => row.every((v) => v !== 0));
59
100
  }
60
101
  countEmptyCells() {
61
102
  return this.board.flat().filter((v) => v === 0).length;
62
103
  }
104
+ countPlaced(value = 0) {
105
+ if (value === 0) {
106
+ return 81 - this.countEmptyCells(); // total cells - empty cells
107
+ }
108
+ return this.board.flat().filter((v) => v === value).length;
109
+ }
63
110
  validate() {
64
111
  const reasons = [];
65
112
  if (this.board.length !== 9 || this.board.some((row) => row.length !== 9)) {
@@ -180,12 +227,7 @@ export default class SudokuSolver {
180
227
  }
181
228
  let move = this.findBestMove();
182
229
  while (move) {
183
- if (move.type === "placement") {
184
- this.setSquareValue(move.row, move.col, move.value);
185
- }
186
- else {
187
- this.applyElimination(move);
188
- }
230
+ this.applyMove(move);
189
231
  move = this.findBestMove();
190
232
  }
191
233
  return this.isComplete();
@@ -235,6 +277,33 @@ export default class SudokuSolver {
235
277
  }
236
278
  }
237
279
  }
280
+ finalizePlacement(row, col, value, algorithm, reasoning) {
281
+ return {
282
+ type: "placement",
283
+ row,
284
+ col,
285
+ value,
286
+ algorithm,
287
+ message: `Place ${value} in r${row + 1}c${col + 1} (${algorithm})`,
288
+ reasoning,
289
+ };
290
+ }
291
+ finalizeElimination(eliminations, algorithm, reasoning) {
292
+ 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("/")}}`;
294
+ const uniqueCells = uniqueCellCoordinates(eliminations);
295
+ const uniqueCellCount = uniqueCells.length;
296
+ const cellWord = uniqueCellCount === 1 ? "cell" : "cells";
297
+ const housePhrase = sharedHouseContainingCells(uniqueCells)?.wherePhrase;
298
+ const wherePart = housePhrase ? ` in ${housePhrase}` : "";
299
+ return {
300
+ type: "elimination",
301
+ eliminations,
302
+ algorithm,
303
+ message: `Eliminate ${digitPart} from ${uniqueCellCount} ${cellWord}${wherePart} (${algorithm})`,
304
+ reasoning,
305
+ };
306
+ }
238
307
  findBestMove() {
239
308
  for (const phase of SudokuSolver.SEARCH_PHASES) {
240
309
  const move = this.findMoveForPhase(phase);
@@ -263,33 +332,49 @@ export default class SudokuSolver {
263
332
  if (p.length === 1) {
264
333
  const placeValue = p[0];
265
334
  let algo = "Naked Single";
335
+ let house = '';
266
336
  // check if full house
267
337
  const rowValues = this.getRow(row);
268
338
  if (rowValues.filter((v) => v === 0).length === 1) {
269
339
  algo = "Full House";
340
+ house = `row ${row + 1}`;
270
341
  }
271
342
  const colValues = this.getColumn(col);
272
343
  if (colValues.filter((v) => v === 0).length === 1) {
273
344
  algo = "Full House";
345
+ house = `column ${col + 1}`;
274
346
  }
275
347
  const boxValues = this.getBox(this.boxIndex(row, col));
276
348
  if (boxValues.filter((v) => v === 0).length === 1) {
277
349
  algo = "Full House";
350
+ house = `box ${boxNumber(row, col)}`;
278
351
  }
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) {
352
+ let reasoning;
353
+ if (algo === "Full House") {
354
+ reasoning = `This is the last empty cell in ${house} and must be ${placeValue}.`;
355
+ }
356
+ else {
357
+ if (this.countPlaced(placeValue) === 8) {
283
358
  algo = "Last Digit";
359
+ reasoning = this.lastDigitReasoning(placeValue);
360
+ }
361
+ else {
362
+ reasoning = `${placeValue} is the only remaining candidate for this cell.`;
284
363
  }
285
364
  }
286
- return { type: "placement", row, col, value: placeValue, algorithm: algo };
365
+ return this.finalizePlacement(row, col, placeValue, algo, reasoning);
287
366
  }
288
367
  }
289
368
  }
290
369
  }
291
370
  return null;
292
371
  }
372
+ hiddenSingleReasoning(house, value) {
373
+ return `This is the only empty cell in ${house} that can be a ${value}.`;
374
+ }
375
+ lastDigitReasoning(value) {
376
+ return `Eight cells already contain ${value}, so the 9th occurrence must be placed here.`;
377
+ }
293
378
  findHiddenSingleInRow(row) {
294
379
  for (let value = 1; value <= 9; value++) {
295
380
  const candidates = [];
@@ -299,13 +384,14 @@ export default class SudokuSolver {
299
384
  }
300
385
  }
301
386
  if (candidates.length === 1) {
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";
387
+ const col = candidates[0];
388
+ let algorithm = "Hidden Single";
389
+ let reasoning = this.hiddenSingleReasoning(`row ${row + 1}`, value);
390
+ if (this.countPlaced(value) === 8) {
391
+ algorithm = "Last Digit";
392
+ reasoning = this.lastDigitReasoning(value);
307
393
  }
308
- return move;
394
+ return this.finalizePlacement(row, col, value, algorithm, reasoning);
309
395
  }
310
396
  }
311
397
  return null;
@@ -319,13 +405,14 @@ export default class SudokuSolver {
319
405
  }
320
406
  }
321
407
  if (candidates.length === 1) {
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";
408
+ const row = candidates[0];
409
+ let algorithm = "Hidden Single";
410
+ let reasoning = this.hiddenSingleReasoning(`column ${col + 1}`, value);
411
+ if (this.countPlaced(value) === 8) {
412
+ algorithm = "Last Digit";
413
+ reasoning = this.lastDigitReasoning(value);
327
414
  }
328
- return move;
415
+ return this.finalizePlacement(row, col, value, algorithm, reasoning);
329
416
  }
330
417
  }
331
418
  return null;
@@ -340,13 +427,14 @@ export default class SudokuSolver {
340
427
  }
341
428
  }
342
429
  if (candidates.length === 1) {
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";
430
+ const { row: r, col: c } = candidates[0];
431
+ let algorithm = "Hidden Single";
432
+ let reasoning = this.hiddenSingleReasoning(`box ${boxNumber(r, c)}`, value);
433
+ if (this.countPlaced(value) === 8) {
434
+ algorithm = "Last Digit";
435
+ reasoning = this.lastDigitReasoning(value);
348
436
  }
349
- return move;
437
+ return this.finalizePlacement(r, c, value, algorithm, reasoning);
350
438
  }
351
439
  }
352
440
  return null;
@@ -355,6 +443,7 @@ export default class SudokuSolver {
355
443
  for (let ibox = 0; ibox < 9; ibox++) {
356
444
  const boxStartRow = Math.floor(ibox / 3) * 3;
357
445
  const boxStartCol = (ibox % 3) * 3;
446
+ const boxNum = ibox + 1;
358
447
  for (let digit = 1; digit <= 9; digit++) {
359
448
  const cells = [];
360
449
  for (let idx = 0; idx < 9; idx++) {
@@ -376,7 +465,9 @@ export default class SudokuSolver {
376
465
  }
377
466
  }
378
467
  if (eliminations.length > 0) {
379
- return { type: "elimination", eliminations, algorithm: "Pointing Pair/Triple" };
468
+ const algorithm = cells.length === 2 ? "Pointing Pair" : "Pointing Triple";
469
+ const reasoning = `In box ${boxNum}, every candidate for ${digit} lies in row ${sharedRow + 1}, so ${digit} cannot appear elsewhere in that row outside the box.`;
470
+ return this.finalizeElimination(eliminations, algorithm, reasoning);
380
471
  }
381
472
  }
382
473
  if (cells.every((c) => c.col === cells[0].col)) {
@@ -390,7 +481,9 @@ export default class SudokuSolver {
390
481
  }
391
482
  }
392
483
  if (eliminations.length > 0) {
393
- return { type: "elimination", eliminations, algorithm: "Pointing Pair/Triple" };
484
+ const algorithm = cells.length === 2 ? "Pointing Pair" : "Pointing Triple";
485
+ const reasoning = `In box ${boxNum}, every candidate for ${digit} lies in column ${sharedCol + 1}, so ${digit} cannot appear elsewhere in that column outside the box.`;
486
+ return this.finalizeElimination(eliminations, algorithm, reasoning);
394
487
  }
395
488
  }
396
489
  }
@@ -424,12 +517,22 @@ export default class SudokuSolver {
424
517
  }
425
518
  return null;
426
519
  }
520
+ /** `houseCells` is one full row, column, or box (9 cells). */
521
+ getHouseContext(houseCells) {
522
+ const shared = sharedHouseContainingCells(houseCells);
523
+ if (shared) {
524
+ return { wherePhrase: shared.wherePhrase, sameKindWord: shared.kind };
525
+ }
526
+ const h = houseCells[0];
527
+ return { wherePhrase: `box ${boxNumber(h.row, h.col)}`, sameKindWord: "box" };
528
+ }
427
529
  tryNakedSubsetInHouse(houseCells, k) {
428
530
  const empties = houseCells.filter(({ row, col }) => this.board[row][col] === 0 && this.possiblesGrid[row][col].length > 0);
429
531
  if (empties.length < k) {
430
532
  return null;
431
533
  }
432
534
  const algorithm = k === 2 ? "Naked Pair" : k === 3 ? "Naked Triple" : "Naked Quad";
535
+ const { wherePhrase, sameKindWord } = this.getHouseContext(houseCells);
433
536
  const n = empties.length;
434
537
  const indices = [];
435
538
  const dfs = (start) => {
@@ -460,7 +563,9 @@ export default class SudokuSolver {
460
563
  if (eliminations.length === 0) {
461
564
  return null;
462
565
  }
463
- return { type: "elimination", eliminations, algorithm };
566
+ const digitStr = [...union].sort((a, b) => a - b).join("/");
567
+ const reasoning = `${algorithm} ${digitStr} in ${wherePhrase} means those digits can be eliminated from the other cells in that same ${sameKindWord}.`;
568
+ return this.finalizeElimination(eliminations, algorithm, reasoning);
464
569
  }
465
570
  for (let i = start; i < n; i++) {
466
571
  indices.push(i);
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type Board } from "./sudokuSolver.js";
1
+ import type { Board } from "./boardGeo.js";
2
+ export declare function boxNumber(row: number, col: number): number;
2
3
  export declare function cloneBoard(board: Board): Board;
3
4
  export declare function assertBoardShape(board: Board): void;
4
5
  export declare function assertBoardValues(board: Board): void;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAE9C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAInD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CASpD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAmBrD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAS1D;AAID,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAE9C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAInD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CASpD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAmBrD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAS1D;AAID,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C"}
package/dist/utils.js CHANGED
@@ -1,3 +1,6 @@
1
+ export function boxNumber(row, col) {
2
+ return Math.floor(row / 3) * 3 + Math.floor(col / 3) + 1; // 1-9
3
+ }
1
4
  export function cloneBoard(board) {
2
5
  return board.map((row) => [...row]);
3
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsabol/sudoku-solver",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",