@wsabol/sudoku-solver 0.1.4 → 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;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,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,13 +346,14 @@ export default class SudokuSolver {
299
346
  }
300
347
  }
301
348
  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";
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);
307
355
  }
308
- return move;
356
+ return this.finalizePlacement(row, col, value, algorithm, reasoning);
309
357
  }
310
358
  }
311
359
  return null;
@@ -319,13 +367,14 @@ export default class SudokuSolver {
319
367
  }
320
368
  }
321
369
  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";
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);
327
376
  }
328
- return move;
377
+ return this.finalizePlacement(row, col, value, algorithm, reasoning);
329
378
  }
330
379
  }
331
380
  return null;
@@ -340,13 +389,14 @@ export default class SudokuSolver {
340
389
  }
341
390
  }
342
391
  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";
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);
348
398
  }
349
- return move;
399
+ return this.finalizePlacement(r, c, value, algorithm, reasoning);
350
400
  }
351
401
  }
352
402
  return null;
@@ -355,6 +405,7 @@ export default class SudokuSolver {
355
405
  for (let ibox = 0; ibox < 9; ibox++) {
356
406
  const boxStartRow = Math.floor(ibox / 3) * 3;
357
407
  const boxStartCol = (ibox % 3) * 3;
408
+ const boxNum = ibox + 1;
358
409
  for (let digit = 1; digit <= 9; digit++) {
359
410
  const cells = [];
360
411
  for (let idx = 0; idx < 9; idx++) {
@@ -376,7 +427,8 @@ export default class SudokuSolver {
376
427
  }
377
428
  }
378
429
  if (eliminations.length > 0) {
379
- 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);
380
432
  }
381
433
  }
382
434
  if (cells.every((c) => c.col === cells[0].col)) {
@@ -390,7 +442,8 @@ export default class SudokuSolver {
390
442
  }
391
443
  }
392
444
  if (eliminations.length > 0) {
393
- 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);
394
447
  }
395
448
  }
396
449
  }
@@ -424,12 +477,24 @@ export default class SudokuSolver {
424
477
  }
425
478
  return null;
426
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
+ }
427
491
  tryNakedSubsetInHouse(houseCells, k) {
428
492
  const empties = houseCells.filter(({ row, col }) => this.board[row][col] === 0 && this.possiblesGrid[row][col].length > 0);
429
493
  if (empties.length < k) {
430
494
  return null;
431
495
  }
432
496
  const algorithm = k === 2 ? "Naked Pair" : k === 3 ? "Naked Triple" : "Naked Quad";
497
+ const { wherePhrase, sameKindWord } = this.getHouseContext(houseCells);
433
498
  const n = empties.length;
434
499
  const indices = [];
435
500
  const dfs = (start) => {
@@ -460,7 +525,9 @@ export default class SudokuSolver {
460
525
  if (eliminations.length === 0) {
461
526
  return null;
462
527
  }
463
- 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);
464
531
  }
465
532
  for (let i = start; i < n; i++) {
466
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.4",
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",