@wsabol/sudoku-solver 0.1.3 → 0.1.5

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.
@@ -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[][];
2
+ import type { Board } from "./boardGeo.js";
3
+ import type { Move, EliminationMove } from "./move.js";
3
4
  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;
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;IAe7B,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,qBAAqB;IAgB7B,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,sBAAsB,GACtB,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;AAO7B,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;IAgB3B,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;IAmD9B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,0BAA0B;IAUlC,8DAA8D;IAC9D,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,qBAAqB;IAwD7B,OAAO,CAAC,gBAAgB;CAe3B"}
@@ -1,4 +1,4 @@
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
4
  export default class SudokuSolver {
@@ -54,12 +54,26 @@ export default class SudokuSolver {
54
54
  this.possiblesGrid[row][col] = this.possiblesGrid[row][col].filter((v) => v !== value);
55
55
  }
56
56
  }
57
+ applyMove(move) {
58
+ if (move.type === "placement") {
59
+ this.setSquareValue(move.row, move.col, move.value);
60
+ }
61
+ else {
62
+ this.applyElimination(move);
63
+ }
64
+ }
57
65
  isComplete() {
58
66
  return this.board.every((row) => row.every((v) => v !== 0));
59
67
  }
60
68
  countEmptyCells() {
61
69
  return this.board.flat().filter((v) => v === 0).length;
62
70
  }
71
+ countPlaced(value = 0) {
72
+ if (value === 0) {
73
+ return 81 - this.countEmptyCells(); // total cells - empty cells
74
+ }
75
+ return this.board.flat().filter((v) => v === value).length;
76
+ }
63
77
  validate() {
64
78
  const reasons = [];
65
79
  if (this.board.length !== 9 || this.board.some((row) => row.length !== 9)) {
@@ -180,12 +194,7 @@ export default class SudokuSolver {
180
194
  }
181
195
  let move = this.findBestMove();
182
196
  while (move) {
183
- if (move.type === "placement") {
184
- this.setSquareValue(move.row, move.col, move.value);
185
- }
186
- else {
187
- this.applyElimination(move);
188
- }
197
+ this.applyMove(move);
189
198
  move = this.findBestMove();
190
199
  }
191
200
  return this.isComplete();
@@ -235,6 +244,28 @@ export default class SudokuSolver {
235
244
  }
236
245
  }
237
246
  }
247
+ finalizePlacement(row, col, value, algorithm, reasoning) {
248
+ return {
249
+ type: "placement",
250
+ row,
251
+ col,
252
+ value,
253
+ algorithm,
254
+ message: `Place ${value} in r${row + 1}c${col + 1} (${algorithm})`,
255
+ reasoning,
256
+ };
257
+ }
258
+ finalizeElimination(eliminations, algorithm, reasoning) {
259
+ const digits = [...new Set(eliminations.map((e) => e.value))].sort((a, b) => a - b);
260
+ const digitPart = digits.length === 1 ? String(digits[0]) : `{${digits.join("/")}}`;
261
+ return {
262
+ type: "elimination",
263
+ eliminations,
264
+ algorithm,
265
+ message: `Eliminate ${digitPart} from ${eliminations.length} cell(s) (${algorithm})`,
266
+ reasoning,
267
+ };
268
+ }
238
269
  findBestMove() {
239
270
  for (const phase of SudokuSolver.SEARCH_PHASES) {
240
271
  const move = this.findMoveForPhase(phase);
@@ -263,33 +294,49 @@ export default class SudokuSolver {
263
294
  if (p.length === 1) {
264
295
  const placeValue = p[0];
265
296
  let algo = "Naked Single";
297
+ let house = '';
266
298
  // check if full house
267
299
  const rowValues = this.getRow(row);
268
300
  if (rowValues.filter((v) => v === 0).length === 1) {
269
301
  algo = "Full House";
302
+ house = `row ${row + 1}`;
270
303
  }
271
304
  const colValues = this.getColumn(col);
272
305
  if (colValues.filter((v) => v === 0).length === 1) {
273
306
  algo = "Full House";
307
+ house = `column ${col + 1}`;
274
308
  }
275
309
  const boxValues = this.getBox(this.boxIndex(row, col));
276
310
  if (boxValues.filter((v) => v === 0).length === 1) {
277
311
  algo = "Full House";
312
+ house = `box ${boxNumber(row, col)}`;
313
+ }
314
+ let reasoning;
315
+ if (algo === "Full House") {
316
+ reasoning = `This is the last empty cell in ${house} and must be ${placeValue}.`;
278
317
  }
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) {
318
+ else {
319
+ if (this.countPlaced(placeValue) === 8) {
283
320
  algo = "Last Digit";
321
+ reasoning = this.lastDigitReasoning(placeValue);
322
+ }
323
+ else {
324
+ reasoning = `${placeValue} is the only remaining candidate for this cell.`;
284
325
  }
285
326
  }
286
- return { type: "placement", row, col, value: placeValue, algorithm: algo };
327
+ return this.finalizePlacement(row, col, placeValue, algo, reasoning);
287
328
  }
288
329
  }
289
330
  }
290
331
  }
291
332
  return null;
292
333
  }
334
+ hiddenSingleReasoning(house, value) {
335
+ return `This is the only empty cell in ${house} that can be a ${value}.`;
336
+ }
337
+ lastDigitReasoning(value) {
338
+ return `Eight cells already contain ${value}, so the 9th occurrence must be placed here.`;
339
+ }
293
340
  findHiddenSingleInRow(row) {
294
341
  for (let value = 1; value <= 9; value++) {
295
342
  const candidates = [];
@@ -299,7 +346,14 @@ export default class SudokuSolver {
299
346
  }
300
347
  }
301
348
  if (candidates.length === 1) {
302
- return { type: "placement", row, col: candidates[0], value, algorithm: "Hidden Single" };
349
+ const col = candidates[0];
350
+ let algorithm = "Hidden Single";
351
+ let reasoning = this.hiddenSingleReasoning(`row ${row + 1}`, value);
352
+ if (this.countPlaced(value) === 8) {
353
+ algorithm = "Last Digit";
354
+ reasoning = this.lastDigitReasoning(value);
355
+ }
356
+ return this.finalizePlacement(row, col, value, algorithm, reasoning);
303
357
  }
304
358
  }
305
359
  return null;
@@ -313,7 +367,14 @@ export default class SudokuSolver {
313
367
  }
314
368
  }
315
369
  if (candidates.length === 1) {
316
- return { type: "placement", row: candidates[0], col, value, algorithm: "Hidden Single" };
370
+ const row = candidates[0];
371
+ let algorithm = "Hidden Single";
372
+ let reasoning = this.hiddenSingleReasoning(`column ${col + 1}`, value);
373
+ if (this.countPlaced(value) === 8) {
374
+ algorithm = "Last Digit";
375
+ reasoning = this.lastDigitReasoning(value);
376
+ }
377
+ return this.finalizePlacement(row, col, value, algorithm, reasoning);
317
378
  }
318
379
  }
319
380
  return null;
@@ -328,7 +389,14 @@ export default class SudokuSolver {
328
389
  }
329
390
  }
330
391
  if (candidates.length === 1) {
331
- return { type: "placement", row: candidates[0].row, col: candidates[0].col, value, algorithm: "Hidden Single" };
392
+ const { row: r, col: c } = candidates[0];
393
+ let algorithm = "Hidden Single";
394
+ let reasoning = this.hiddenSingleReasoning(`box ${boxNumber(r, c)}`, value);
395
+ if (this.countPlaced(value) === 8) {
396
+ algorithm = "Last Digit";
397
+ reasoning = this.lastDigitReasoning(value);
398
+ }
399
+ return this.finalizePlacement(r, c, value, algorithm, reasoning);
332
400
  }
333
401
  }
334
402
  return null;
@@ -337,6 +405,7 @@ export default class SudokuSolver {
337
405
  for (let ibox = 0; ibox < 9; ibox++) {
338
406
  const boxStartRow = Math.floor(ibox / 3) * 3;
339
407
  const boxStartCol = (ibox % 3) * 3;
408
+ const boxNum = ibox + 1;
340
409
  for (let digit = 1; digit <= 9; digit++) {
341
410
  const cells = [];
342
411
  for (let idx = 0; idx < 9; idx++) {
@@ -358,7 +427,8 @@ export default class SudokuSolver {
358
427
  }
359
428
  }
360
429
  if (eliminations.length > 0) {
361
- return { type: "elimination", eliminations, algorithm: "Pointing Pair/Triple" };
430
+ 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.`;
431
+ return this.finalizeElimination(eliminations, "Pointing Pair/Triple", reasoning);
362
432
  }
363
433
  }
364
434
  if (cells.every((c) => c.col === cells[0].col)) {
@@ -372,7 +442,8 @@ export default class SudokuSolver {
372
442
  }
373
443
  }
374
444
  if (eliminations.length > 0) {
375
- return { type: "elimination", eliminations, algorithm: "Pointing Pair/Triple" };
445
+ 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.`;
446
+ return this.finalizeElimination(eliminations, "Pointing Pair/Triple", reasoning);
376
447
  }
377
448
  }
378
449
  }
@@ -406,12 +477,24 @@ export default class SudokuSolver {
406
477
  }
407
478
  return null;
408
479
  }
480
+ /** `houseCells` is one full row, column, or box (9 cells). */
481
+ getHouseContext(houseCells) {
482
+ const h = houseCells[0];
483
+ if (houseCells.every((c) => c.row === h.row)) {
484
+ return { wherePhrase: `row ${h.row + 1}`, sameKindWord: "row" };
485
+ }
486
+ if (houseCells.every((c) => c.col === h.col)) {
487
+ return { wherePhrase: `column ${h.col + 1}`, sameKindWord: "column" };
488
+ }
489
+ return { wherePhrase: `box ${boxNumber(h.row, h.col)}`, sameKindWord: "box" };
490
+ }
409
491
  tryNakedSubsetInHouse(houseCells, k) {
410
492
  const empties = houseCells.filter(({ row, col }) => this.board[row][col] === 0 && this.possiblesGrid[row][col].length > 0);
411
493
  if (empties.length < k) {
412
494
  return null;
413
495
  }
414
496
  const algorithm = k === 2 ? "Naked Pair" : k === 3 ? "Naked Triple" : "Naked Quad";
497
+ const { wherePhrase, sameKindWord } = this.getHouseContext(houseCells);
415
498
  const n = empties.length;
416
499
  const indices = [];
417
500
  const dfs = (start) => {
@@ -442,7 +525,9 @@ export default class SudokuSolver {
442
525
  if (eliminations.length === 0) {
443
526
  return null;
444
527
  }
445
- return { type: "elimination", eliminations, algorithm };
528
+ const digitStr = [...union].sort((a, b) => a - b).join("/");
529
+ const reasoning = `${algorithm} ${digitStr} in ${wherePhrase} means those digits can be eliminated from the other cells in that same ${sameKindWord}.`;
530
+ return this.finalizeElimination(eliminations, algorithm, reasoning);
446
531
  }
447
532
  for (let i = start; i < n; i++) {
448
533
  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.3",
3
+ "version": "0.1.5",
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",