@wsabol/sudoku-solver 0.1.0

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 ADDED
@@ -0,0 +1,132 @@
1
+ # Sudoku Solver (Node Module)
2
+
3
+ TypeScript module for Sudoku solve, next move, describe, and validate operations.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install
9
+ ```
10
+
11
+ ## Build
12
+
13
+ ```bash
14
+ npm run build
15
+ ```
16
+
17
+ Build output is written to `dist/`.
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import Sudoku from "sudoku-solver";
23
+
24
+ const board =
25
+ "000010080302607000070000003080070500004000600003050010200000050000705108060040000";
26
+
27
+ const solved = Sudoku.solve(board);
28
+ console.log(solved.isValid); // true
29
+ console.log(solved.board); // number[][]
30
+
31
+ const next = Sudoku.nextMove(board);
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)"
34
+ if (next.move?.type === "placement") {
35
+ console.log(next.move.row, next.move.col, next.move.value);
36
+ } else if (next.move?.type === "elimination") {
37
+ console.log(next.move.eliminations); // [{ row, col, value }, ...]
38
+ }
39
+
40
+ const check = Sudoku.validate(board);
41
+ console.log(check.isValid); // true/false
42
+ console.log(check.reasons); // ValidationReason[]
43
+
44
+ const info = Sudoku.describe(board);
45
+ console.log(info.difficulty); // "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible"
46
+ console.log(info.solutions); // 0 | 1
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### `Sudoku.solve(boardInput)`
52
+
53
+ Accepts `string | number[][]`.
54
+
55
+ Returns:
56
+
57
+ ```ts
58
+ {
59
+ isValid: boolean;
60
+ board: number[][];
61
+ }
62
+ ```
63
+
64
+ `isValid` is `false` when the board is structurally invalid or has no solution.
65
+
66
+ ### `Sudoku.nextMove(boardInput)`
67
+
68
+ Accepts `string | number[][]`.
69
+
70
+ Returns:
71
+
72
+ ```ts
73
+ {
74
+ status: "Complete" | "In progress" | "Invalid";
75
+ move: PlacementMove | EliminationMove | null;
76
+ message: string;
77
+ }
78
+ ```
79
+
80
+ `move` is a discriminated union:
81
+
82
+ ```ts
83
+ // digit placement
84
+ { type: "placement"; row: number; col: number; value: number; algorithm: Algorithm }
85
+
86
+ // candidate elimination (e.g. Pointing Pair/Triple)
87
+ { type: "elimination"; eliminations: Array<{ row: number; col: number; value: number }>; algorithm: Algorithm }
88
+ ```
89
+
90
+ Notes:
91
+ - `move` is `null` when the board is complete or invalid.
92
+ - `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.
94
+
95
+ ### `Sudoku.validate(boardInput)`
96
+
97
+ Accepts `string | number[][]`.
98
+
99
+ Returns:
100
+
101
+ ```ts
102
+ {
103
+ isValid: boolean;
104
+ message: string;
105
+ reasons: ValidationReason[];
106
+ }
107
+ ```
108
+
109
+ ### `Sudoku.describe(boardInput)`
110
+
111
+ Accepts `string | number[][]`.
112
+
113
+ Returns:
114
+
115
+ ```ts
116
+ {
117
+ isValid: boolean;
118
+ isComplete: boolean;
119
+ message: string;
120
+ difficulty: "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible";
121
+ solutions: number;
122
+ }
123
+ ```
124
+
125
+ `difficulty` is derived from empty cell count. `solutions` is `0` when the board is invalid/unsolvable, `1` when a unique solution exists.
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ npm test
131
+ npm run typecheck
132
+ ```
@@ -0,0 +1,33 @@
1
+ import SudokuSolver, { type Algorithm, type Board, type Move, type PlacementMove, type EliminationMove } from "./sudokuSolver";
2
+ import { type ValidationReason, type ValidationResult } from "./validate";
3
+ interface SolveResult {
4
+ isValid: boolean;
5
+ board: Board;
6
+ }
7
+ interface DescribeResult {
8
+ isValid: boolean;
9
+ isComplete: boolean;
10
+ message: string;
11
+ difficulty: string;
12
+ solutions: number;
13
+ }
14
+ type MoveStatus = "Complete" | "In progress" | "Invalid";
15
+ interface MoveResult {
16
+ status: MoveStatus;
17
+ move: Move | null;
18
+ message: string;
19
+ }
20
+ declare function solve(boardInput: string | Board): SolveResult;
21
+ declare function nextMove(boardInput: string | Board): MoveResult;
22
+ declare function validate(boardInput: string | Board): ValidationResult;
23
+ declare function describeBoard(boardInput: string | Board): DescribeResult;
24
+ declare const Sudoku: {
25
+ solve: typeof solve;
26
+ nextMove: typeof nextMove;
27
+ validate: typeof validate;
28
+ describe: typeof describeBoard;
29
+ };
30
+ export { SudokuSolver };
31
+ export type { Algorithm, Board, Move, PlacementMove, EliminationMove, ValidationReason, ValidationResult, SolveResult, MoveResult, MoveStatus, DescribeResult };
32
+ export default Sudoku;
33
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,gBAAgB,CAAC;AAC/H,OAAO,EAA8C,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtH,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,CA8BxD;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 ADDED
@@ -0,0 +1,95 @@
1
+ import SudokuSolver from "./sudokuSolver";
2
+ import { invalidBoardLength, invalidBoardCharacters } from "./validate";
3
+ function solve(boardInput) {
4
+ const sudoku = new SudokuSolver(boardInput);
5
+ const isValid = sudoku.solve();
6
+ return {
7
+ isValid: isValid,
8
+ board: sudoku.toArray(),
9
+ };
10
+ }
11
+ function nextMove(boardInput) {
12
+ const sudoku = new SudokuSolver(boardInput);
13
+ const validation = sudoku.validate();
14
+ if (!validation.isValid) {
15
+ return {
16
+ status: "Invalid",
17
+ move: null,
18
+ message: `Invalid Puzzle: ${validation.message}`,
19
+ };
20
+ }
21
+ const move = sudoku.getNextMove();
22
+ let message;
23
+ if (!move) {
24
+ message = "No more moves";
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
+ else {
31
+ const digit = move.eliminations[0].value;
32
+ message = `Eliminate ${digit} from ${move.eliminations.length} cell(s) (${move.algorithm})`;
33
+ sudoku.applyElimination(move);
34
+ }
35
+ return {
36
+ status: sudoku.isComplete() ? "Complete" : "In progress",
37
+ move,
38
+ message,
39
+ };
40
+ }
41
+ function validate(boardInput) {
42
+ if (typeof boardInput === "string") {
43
+ if (boardInput.length !== 81) {
44
+ return invalidBoardLength(boardInput.length);
45
+ }
46
+ if (!/^[0-9.]{81}$/.test(boardInput)) {
47
+ return invalidBoardCharacters();
48
+ }
49
+ }
50
+ const test = new SudokuSolver(boardInput);
51
+ return test.validate();
52
+ }
53
+ function describeBoard(boardInput) {
54
+ const sudoku = new SudokuSolver(boardInput);
55
+ const initValidation = sudoku.validate();
56
+ if (!initValidation.isValid) {
57
+ return {
58
+ isValid: false,
59
+ isComplete: false,
60
+ message: initValidation.message,
61
+ difficulty: '',
62
+ solutions: 0,
63
+ };
64
+ }
65
+ let result = {
66
+ isValid: true,
67
+ isComplete: sudoku.isComplete(),
68
+ message: '',
69
+ difficulty: sudoku.difficulty(),
70
+ solutions: 0,
71
+ };
72
+ if (result.isComplete) {
73
+ result.solutions = 1;
74
+ result.message = 'Solvable with a single solution';
75
+ }
76
+ else {
77
+ sudoku.solve();
78
+ if (sudoku.isComplete()) {
79
+ result.solutions = 1;
80
+ result.message = 'Unique Solution';
81
+ }
82
+ else {
83
+ result.message = 'Invalid Puzzle (\"no unique solution\")';
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ const Sudoku = {
89
+ solve: solve,
90
+ nextMove: nextMove,
91
+ validate: validate,
92
+ describe: describeBoard,
93
+ };
94
+ export { SudokuSolver };
95
+ export default Sudoku;
@@ -0,0 +1,35 @@
1
+ export type Board = number[][];
2
+ export interface Move {
3
+ row: number;
4
+ col: number;
5
+ value: number;
6
+ }
7
+ export declare function parseBoardString(board: string): Board;
8
+ export declare class Sudoku {
9
+ private board;
10
+ private possiblesGrid;
11
+ private numGivens;
12
+ constructor(input: string | Board);
13
+ toJSONBoard(): Board;
14
+ private boxIndex;
15
+ private boxToPuzzle;
16
+ private getRow;
17
+ private getColumn;
18
+ private getBox;
19
+ private valuesMissing;
20
+ possibles(row: number, col: number): number[];
21
+ private calcSquarePossibles;
22
+ calcPossibles(): void;
23
+ setSquareValue(row: number, col: number, value: number): void;
24
+ isValid(): boolean;
25
+ isComplete(): boolean;
26
+ private findNakedSingle;
27
+ private findHiddenSingleInRow;
28
+ private findHiddenSingleInCol;
29
+ private findHiddenSingleInBox;
30
+ getNextMove(): Move | null;
31
+ private simpleSolve;
32
+ private uniPossiblesSolve;
33
+ solve(): string;
34
+ }
35
+ //# sourceMappingURL=sudoku.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sudoku.d.ts","sourceRoot":"","sources":["../src/sudoku.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAC;AAE/B,MAAM,WAAW,IAAI;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACjB;AAcD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAmBrD;AAED,qBAAa,MAAM;IACf,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,SAAS,CAAS;gBAEd,KAAK,EAAE,MAAM,GAAG,KAAK;IAmBjC,WAAW,IAAI,KAAK;IAIpB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,aAAa;IAIrB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IAI7C,OAAO,CAAC,mBAAmB;IAU3B,aAAa,IAAI,IAAI;IAQrB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAa7D,OAAO,IAAI,OAAO;IAiFlB,UAAU,IAAI,OAAO;IAIrB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,qBAAqB;IAgB7B,WAAW,IAAI,IAAI,GAAG,IAAI;IAgC1B,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,iBAAiB;IA2BzB,KAAK,IAAI,MAAM;CAqClB"}
package/dist/sudoku.js ADDED
@@ -0,0 +1,332 @@
1
+ const COMPLETE = [1, 2, 3, 4, 5, 6, 7, 8, 9];
2
+ function cloneBoard(board) {
3
+ return board.map((row) => [...row]);
4
+ }
5
+ function assertBoardShape(board) {
6
+ if (board.length !== 9 || board.some((row) => row.length !== 9)) {
7
+ throw new Error("Board must be a 9x9 matrix");
8
+ }
9
+ }
10
+ export function parseBoardString(board) {
11
+ if (board.length !== 81) {
12
+ throw new Error(`Board must be 81 characters, got ${board.length}`);
13
+ }
14
+ const rows = Array.from({ length: 9 }, () => Array(9).fill(0));
15
+ for (let i = 0; i < 81; i += 1) {
16
+ const ch = board[i];
17
+ const row = Math.floor(i / 9);
18
+ const col = i % 9;
19
+ if (ch >= "1" && ch <= "9") {
20
+ rows[row][col] = Number(ch);
21
+ }
22
+ else if (ch === "0" || ch === ".") {
23
+ rows[row][col] = 0;
24
+ }
25
+ else {
26
+ throw new Error(`Invalid character '${ch}' at position ${i}`);
27
+ }
28
+ }
29
+ return rows;
30
+ }
31
+ export class Sudoku {
32
+ board;
33
+ possiblesGrid;
34
+ numGivens;
35
+ constructor(input) {
36
+ this.board = typeof input === "string" ? parseBoardString(input) : cloneBoard(input);
37
+ assertBoardShape(this.board);
38
+ this.numGivens = 0;
39
+ for (let r = 0; r < 9; r += 1) {
40
+ for (let c = 0; c < 9; c += 1) {
41
+ if (COMPLETE.includes(this.board[r][c])) {
42
+ this.numGivens += 1;
43
+ }
44
+ }
45
+ }
46
+ this.possiblesGrid = Array.from({ length: 9 }, () => Array.from({ length: 9 }, () => []));
47
+ this.calcPossibles();
48
+ }
49
+ toJSONBoard() {
50
+ return cloneBoard(this.board);
51
+ }
52
+ boxIndex(row, col) {
53
+ return Math.floor(row / 3) * 3 + Math.floor(col / 3);
54
+ }
55
+ boxToPuzzle(ibox, idx) {
56
+ return {
57
+ row: Math.floor(ibox / 3) * 3 + Math.floor(idx / 3),
58
+ col: (ibox % 3) * 3 + (idx % 3),
59
+ };
60
+ }
61
+ getRow(row) {
62
+ return [...this.board[row]];
63
+ }
64
+ getColumn(col) {
65
+ return this.board.map((row) => row[col]);
66
+ }
67
+ getBox(ibox) {
68
+ const startRow = Math.floor(ibox / 3) * 3;
69
+ const startCol = (ibox % 3) * 3;
70
+ const out = [];
71
+ for (let r = startRow; r < startRow + 3; r += 1) {
72
+ for (let c = startCol; c < startCol + 3; c += 1) {
73
+ out.push(this.board[r][c]);
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+ valuesMissing(values) {
79
+ return COMPLETE.filter((n) => !values.includes(n));
80
+ }
81
+ possibles(row, col) {
82
+ return [...this.possiblesGrid[row][col]];
83
+ }
84
+ calcSquarePossibles(row, col) {
85
+ if (this.board[row][col] > 0) {
86
+ return [];
87
+ }
88
+ const rowPossible = this.valuesMissing(this.getRow(row));
89
+ const colPossible = this.valuesMissing(this.getColumn(col));
90
+ const boxPossible = this.valuesMissing(this.getBox(this.boxIndex(row, col)));
91
+ return rowPossible.filter((n) => colPossible.includes(n) && boxPossible.includes(n));
92
+ }
93
+ calcPossibles() {
94
+ for (let r = 0; r < 9; r += 1) {
95
+ for (let c = 0; c < 9; c += 1) {
96
+ this.possiblesGrid[r][c] = this.calcSquarePossibles(r, c);
97
+ }
98
+ }
99
+ }
100
+ setSquareValue(row, col, value) {
101
+ const original = this.board[row][col];
102
+ if (!this.possibles(row, col).includes(value)) {
103
+ return;
104
+ }
105
+ this.board[row][col] = value;
106
+ this.calcPossibles();
107
+ if (!this.isValid()) {
108
+ this.board[row][col] = original;
109
+ this.calcPossibles();
110
+ }
111
+ }
112
+ isValid() {
113
+ for (let i = 0; i < 9; i += 1) {
114
+ const row = this.getRow(i).filter((v) => v > 0);
115
+ if (row.some((v) => v < 1 || v > 9)) {
116
+ return false;
117
+ }
118
+ if (new Set(row).size !== row.length) {
119
+ return false;
120
+ }
121
+ }
122
+ for (let i = 0; i < 9; i += 1) {
123
+ const col = this.getColumn(i).filter((v) => v > 0);
124
+ if (col.some((v) => v < 1 || v > 9)) {
125
+ return false;
126
+ }
127
+ if (new Set(col).size !== col.length) {
128
+ return false;
129
+ }
130
+ }
131
+ for (let i = 0; i < 9; i += 1) {
132
+ const box = this.getBox(i).filter((v) => v > 0);
133
+ if (box.some((v) => v < 1 || v > 9)) {
134
+ return false;
135
+ }
136
+ if (new Set(box).size !== box.length) {
137
+ return false;
138
+ }
139
+ }
140
+ for (let row = 0; row < 9; row += 1) {
141
+ for (let col = 0; col < 9; col += 1) {
142
+ if (this.board[row][col] === 0 && this.possibles(row, col).length === 0) {
143
+ return false;
144
+ }
145
+ }
146
+ }
147
+ for (let row = 0; row < 9; row += 1) {
148
+ const rowValues = this.getRow(row);
149
+ for (let n = 1; n <= 9; n += 1) {
150
+ const hasCandidate = rowValues.some((v, col) => v === n || (v === 0 && this.possibles(row, col).includes(n)));
151
+ if (!hasCandidate) {
152
+ return false;
153
+ }
154
+ }
155
+ }
156
+ for (let col = 0; col < 9; col += 1) {
157
+ const colValues = this.getColumn(col);
158
+ for (let n = 1; n <= 9; n += 1) {
159
+ const hasCandidate = colValues.some((v, row) => v === n || (v === 0 && this.possibles(row, col).includes(n)));
160
+ if (!hasCandidate) {
161
+ return false;
162
+ }
163
+ }
164
+ }
165
+ for (let ibox = 0; ibox < 9; ibox += 1) {
166
+ const boxValues = this.getBox(ibox);
167
+ for (let n = 1; n <= 9; n += 1) {
168
+ let hasCandidate = false;
169
+ for (let idx = 0; idx < 9 && !hasCandidate; idx += 1) {
170
+ const { row, col } = this.boxToPuzzle(ibox, idx);
171
+ const value = boxValues[idx];
172
+ hasCandidate = value === n || (value === 0 && this.possibles(row, col).includes(n));
173
+ }
174
+ if (!hasCandidate) {
175
+ return false;
176
+ }
177
+ }
178
+ }
179
+ return true;
180
+ }
181
+ isComplete() {
182
+ return this.isValid() && this.board.every((row) => row.every((v) => v !== 0));
183
+ }
184
+ findNakedSingle() {
185
+ for (let row = 0; row < 9; row += 1) {
186
+ for (let col = 0; col < 9; col += 1) {
187
+ if (this.board[row][col] === 0) {
188
+ const p = this.possibles(row, col);
189
+ if (p.length === 1) {
190
+ return { row, col, value: p[0] };
191
+ }
192
+ }
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+ findHiddenSingleInRow(row) {
198
+ for (let value = 1; value <= 9; value += 1) {
199
+ const candidates = [];
200
+ for (let col = 0; col < 9; col += 1) {
201
+ if (this.board[row][col] === 0 && this.possibles(row, col).includes(value)) {
202
+ candidates.push(col);
203
+ }
204
+ }
205
+ if (candidates.length === 1) {
206
+ return { row, col: candidates[0], value };
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ findHiddenSingleInCol(col) {
212
+ for (let value = 1; value <= 9; value += 1) {
213
+ const candidates = [];
214
+ for (let row = 0; row < 9; row += 1) {
215
+ if (this.board[row][col] === 0 && this.possibles(row, col).includes(value)) {
216
+ candidates.push(row);
217
+ }
218
+ }
219
+ if (candidates.length === 1) {
220
+ return { row: candidates[0], col, value };
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ findHiddenSingleInBox(ibox) {
226
+ for (let value = 1; value <= 9; value += 1) {
227
+ const candidates = [];
228
+ for (let idx = 0; idx < 9; idx += 1) {
229
+ const { row, col } = this.boxToPuzzle(ibox, idx);
230
+ if (this.board[row][col] === 0 && this.possibles(row, col).includes(value)) {
231
+ candidates.push({ row, col });
232
+ }
233
+ }
234
+ if (candidates.length === 1) {
235
+ return { row: candidates[0].row, col: candidates[0].col, value };
236
+ }
237
+ }
238
+ return null;
239
+ }
240
+ getNextMove() {
241
+ if (this.isComplete() || !this.isValid()) {
242
+ return null;
243
+ }
244
+ const naked = this.findNakedSingle();
245
+ if (naked) {
246
+ return naked;
247
+ }
248
+ for (let row = 0; row < 9; row += 1) {
249
+ const move = this.findHiddenSingleInRow(row);
250
+ if (move) {
251
+ return move;
252
+ }
253
+ }
254
+ for (let col = 0; col < 9; col += 1) {
255
+ const move = this.findHiddenSingleInCol(col);
256
+ if (move) {
257
+ return move;
258
+ }
259
+ }
260
+ for (let box = 0; box < 9; box += 1) {
261
+ const move = this.findHiddenSingleInBox(box);
262
+ if (move) {
263
+ return move;
264
+ }
265
+ }
266
+ return null;
267
+ }
268
+ simpleSolve() {
269
+ let move = this.findNakedSingle();
270
+ while (move) {
271
+ this.setSquareValue(move.row, move.col, move.value);
272
+ move = this.findNakedSingle();
273
+ }
274
+ }
275
+ uniPossiblesSolve() {
276
+ for (let row = 0; row < 9; row += 1) {
277
+ let move = this.findHiddenSingleInRow(row);
278
+ while (move) {
279
+ this.setSquareValue(move.row, move.col, move.value);
280
+ this.simpleSolve();
281
+ move = this.findHiddenSingleInRow(row);
282
+ }
283
+ }
284
+ for (let col = 0; col < 9; col += 1) {
285
+ let move = this.findHiddenSingleInCol(col);
286
+ while (move) {
287
+ this.setSquareValue(move.row, move.col, move.value);
288
+ this.simpleSolve();
289
+ move = this.findHiddenSingleInCol(col);
290
+ }
291
+ }
292
+ for (let box = 0; box < 9; box += 1) {
293
+ let move = this.findHiddenSingleInBox(box);
294
+ while (move) {
295
+ this.setSquareValue(move.row, move.col, move.value);
296
+ this.simpleSolve();
297
+ move = this.findHiddenSingleInBox(box);
298
+ }
299
+ }
300
+ }
301
+ solve() {
302
+ if (!this.isValid()) {
303
+ return 'Invalid Puzzle ("no solution")';
304
+ }
305
+ this.simpleSolve();
306
+ if (this.isComplete()) {
307
+ return "Unique Solution";
308
+ }
309
+ let boardChanged = true;
310
+ while (!this.isComplete() && boardChanged) {
311
+ const before = JSON.stringify(this.board);
312
+ this.uniPossiblesSolve();
313
+ const after = JSON.stringify(this.board);
314
+ boardChanged = before !== after;
315
+ }
316
+ if (this.isComplete()) {
317
+ return "Unique Solution";
318
+ }
319
+ const valid = this.isValid();
320
+ const complete = this.isComplete();
321
+ if (!valid) {
322
+ return 'Invalid Puzzle ("no solution")';
323
+ }
324
+ if (this.numGivens < 17 && valid && !complete) {
325
+ return 'Invalid Puzzle ("not enough givens" / "multiple solutions")';
326
+ }
327
+ if (valid && !complete) {
328
+ return 'Invalid Puzzle ("no unique solution")';
329
+ }
330
+ return 'Invalid Puzzle ("unknown")';
331
+ }
332
+ }
@@ -0,0 +1,56 @@
1
+ import { ValidationResult } from "./validate";
2
+ export type Board = number[][];
3
+ export type Algorithm = "Full House" | "Naked Single" | "Hidden Single" | "Pointing Pair/Triple";
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
+ export type DifficultyLevel = "Easy" | "Medium" | "Hard" | "Diabolical" | "Impossible";
22
+ export default class SudokuSolver {
23
+ static readonly ALGORITHMS: Algorithm[];
24
+ private board;
25
+ private possiblesGrid;
26
+ constructor(input: string | Board);
27
+ setBoard(board: Board): void;
28
+ toArray(): Board;
29
+ getPossibles(row: number, col: number): number[];
30
+ setSquareValue(row: number, col: number, value: number): void;
31
+ applyElimination(move: EliminationMove): void;
32
+ isComplete(): boolean;
33
+ countEmptyCells(): number;
34
+ validate(): ValidationResult;
35
+ isValid(): boolean;
36
+ difficulty(): DifficultyLevel;
37
+ getNextMove(): Move | null;
38
+ solve(): boolean;
39
+ private boxIndex;
40
+ private boxToPuzzle;
41
+ private getRow;
42
+ private getColumn;
43
+ private getBox;
44
+ private valuesMissing;
45
+ private calcSquarePossibles;
46
+ private calcPossibles;
47
+ private findBestMove;
48
+ private findNextPlacement;
49
+ private findNakedSingle;
50
+ private findHiddenSingleInRow;
51
+ private findHiddenSingleInCol;
52
+ private findHiddenSingleInBox;
53
+ private findPointingPairTriple;
54
+ private findHiddenSingle;
55
+ }
56
+ //# sourceMappingURL=sudokuSolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sudokuSolver.d.ts","sourceRoot":"","sources":["../src/sudokuSolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,gBAAgB,EAAoB,MAAM,YAAY,CAAC;AAElF,MAAM,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAC;AAE/B,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,eAAe,GAAG,sBAAsB,CAAC;AAEjG,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,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,EAAE,CAA2E;IAElH,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,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,iBAAiB;IAiBzB,OAAO,CAAC,eAAe;IA+BvB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,qBAAqB;IAgB7B,OAAO,CAAC,sBAAsB;IAgD9B,OAAO,CAAC,gBAAgB;CAe3B"}
@@ -0,0 +1,389 @@
1
+ import { cloneBoard, assertBoardShape, parseBoardString, duplicateValues } from "./utils";
2
+ import { pushUniqueReason } from "./validate";
3
+ const COMPLETE = [1, 2, 3, 4, 5, 6, 7, 8, 9];
4
+ export default class SudokuSolver {
5
+ static ALGORITHMS = ["Full House", "Naked Single", "Hidden Single", "Pointing Pair/Triple"];
6
+ board;
7
+ possiblesGrid;
8
+ constructor(input) {
9
+ this.board = [];
10
+ this.possiblesGrid = [];
11
+ let inputBoard = typeof input === "string" ? parseBoardString(input) : cloneBoard(input);
12
+ this.setBoard(inputBoard);
13
+ }
14
+ setBoard(board) {
15
+ this.board = cloneBoard(board);
16
+ assertBoardShape(this.board);
17
+ this.possiblesGrid = Array.from({ length: 9 }, () => Array.from({ length: 9 }, () => []));
18
+ this.calcPossibles();
19
+ }
20
+ toArray() {
21
+ return cloneBoard(this.board);
22
+ }
23
+ getPossibles(row, col) {
24
+ return [...this.possiblesGrid[row][col]];
25
+ }
26
+ setSquareValue(row, col, value) {
27
+ this.board[row][col] = value;
28
+ this.possiblesGrid[row][col] = [];
29
+ const ibox = this.boxIndex(row, col);
30
+ const startRow = Math.floor(ibox / 3) * 3;
31
+ const startCol = (ibox % 3) * 3;
32
+ for (let c = 0; c < 9; c++) {
33
+ this.possiblesGrid[row][c] = this.possiblesGrid[row][c].filter((v) => v !== value);
34
+ }
35
+ for (let r = 0; r < 9; r++) {
36
+ this.possiblesGrid[r][col] = this.possiblesGrid[r][col].filter((v) => v !== value);
37
+ }
38
+ for (let r = startRow; r < startRow + 3; r++) {
39
+ for (let c = startCol; c < startCol + 3; c++) {
40
+ this.possiblesGrid[r][c] = this.possiblesGrid[r][c].filter((v) => v !== value);
41
+ }
42
+ }
43
+ }
44
+ applyElimination(move) {
45
+ for (const { row, col, value } of move.eliminations) {
46
+ this.possiblesGrid[row][col] = this.possiblesGrid[row][col].filter((v) => v !== value);
47
+ }
48
+ }
49
+ isComplete() {
50
+ return this.board.every((row) => row.every((v) => v !== 0));
51
+ }
52
+ countEmptyCells() {
53
+ return this.board.flat().filter((v) => v === 0).length;
54
+ }
55
+ validate() {
56
+ const reasons = [];
57
+ if (this.board.length !== 9 || this.board.some((row) => row.length !== 9)) {
58
+ reasons.push({
59
+ type: "invalid_board_length",
60
+ detail: "Board must be a 9x9 matrix",
61
+ });
62
+ return { isValid: false, message: reasons[0].detail, reasons };
63
+ }
64
+ if (this.countEmptyCells() > 64) {
65
+ reasons.push({
66
+ type: "too_many_empty_cells",
67
+ detail: "Board has too many empty cells, normal Sudoku must have at least 17 given values",
68
+ });
69
+ }
70
+ for (let row = 0; row < 9; row++) {
71
+ for (let col = 0; col < 9; col++) {
72
+ const value = this.board[row][col];
73
+ if (!Number.isInteger(value) || value < 0 || value > 9) {
74
+ pushUniqueReason(reasons, {
75
+ type: "invalid_value",
76
+ detail: `Invalid value ${value} at row ${row}, col ${col}`,
77
+ row,
78
+ col,
79
+ value,
80
+ });
81
+ }
82
+ }
83
+ }
84
+ for (let row = 0; row < 9; row++) {
85
+ for (const value of duplicateValues(this.board[row])) {
86
+ pushUniqueReason(reasons, {
87
+ type: "duplicate_in_row",
88
+ detail: `Duplicate value ${value} in row ${row}`,
89
+ row,
90
+ value,
91
+ });
92
+ }
93
+ }
94
+ for (let col = 0; col < 9; col++) {
95
+ const colVals = this.board.map((row) => row[col]);
96
+ for (const value of duplicateValues(colVals)) {
97
+ pushUniqueReason(reasons, {
98
+ type: "duplicate_in_column",
99
+ detail: `Duplicate value ${value} in column ${col}`,
100
+ col,
101
+ value,
102
+ });
103
+ }
104
+ }
105
+ for (let box = 0; box < 9; box++) {
106
+ const startRow = Math.floor(box / 3) * 3;
107
+ const startCol = (box % 3) * 3;
108
+ const values = [];
109
+ for (let row = startRow; row < startRow + 3; row++) {
110
+ for (let col = startCol; col < startCol + 3; col++) {
111
+ values.push(this.board[row][col]);
112
+ }
113
+ }
114
+ for (const value of duplicateValues(values)) {
115
+ pushUniqueReason(reasons, {
116
+ type: "duplicate_in_box",
117
+ detail: `Duplicate value ${value} in box ${box}`,
118
+ box,
119
+ value,
120
+ });
121
+ }
122
+ }
123
+ if (reasons.length === 0) {
124
+ for (let row = 0; row < 9; row++) {
125
+ for (let col = 0; col < 9; col++) {
126
+ if (this.board[row][col] === 0 && this.possiblesGrid[row][col].length === 0) {
127
+ pushUniqueReason(reasons, {
128
+ type: "empty_cell_no_candidates",
129
+ detail: `Empty cell at row ${row}, col ${col} has no valid candidates`,
130
+ row,
131
+ col,
132
+ });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return {
138
+ isValid: reasons.length === 0,
139
+ message: reasons[0]?.detail ?? "Valid",
140
+ reasons,
141
+ };
142
+ }
143
+ isValid() {
144
+ const result = this.validate();
145
+ return result.isValid;
146
+ }
147
+ difficulty() {
148
+ const emptyCells = this.countEmptyCells();
149
+ if (emptyCells <= 17) {
150
+ return "Easy";
151
+ }
152
+ if (emptyCells <= 30) {
153
+ return "Medium";
154
+ }
155
+ if (emptyCells <= 40) {
156
+ return "Hard";
157
+ }
158
+ if (emptyCells <= 55) {
159
+ return "Diabolical";
160
+ }
161
+ return "Impossible";
162
+ }
163
+ getNextMove() {
164
+ if (this.isComplete() || !this.isValid()) {
165
+ return null;
166
+ }
167
+ return this.findBestMove();
168
+ }
169
+ solve() {
170
+ if (!this.isValid()) {
171
+ return false;
172
+ }
173
+ let move = this.findBestMove();
174
+ while (move) {
175
+ if (move.type === "placement") {
176
+ this.setSquareValue(move.row, move.col, move.value);
177
+ }
178
+ else {
179
+ this.applyElimination(move);
180
+ }
181
+ move = this.findBestMove();
182
+ }
183
+ return this.isComplete();
184
+ }
185
+ boxIndex(row, col) {
186
+ return Math.floor(row / 3) * 3 + Math.floor(col / 3);
187
+ }
188
+ boxToPuzzle(ibox, idx) {
189
+ return {
190
+ row: Math.floor(ibox / 3) * 3 + Math.floor(idx / 3),
191
+ col: (ibox % 3) * 3 + (idx % 3),
192
+ };
193
+ }
194
+ getRow(row) {
195
+ return [...this.board[row]];
196
+ }
197
+ getColumn(col) {
198
+ return this.board.map((row) => row[col]);
199
+ }
200
+ getBox(ibox) {
201
+ const startRow = Math.floor(ibox / 3) * 3;
202
+ const startCol = (ibox % 3) * 3;
203
+ const out = [];
204
+ for (let r = startRow; r < startRow + 3; r++) {
205
+ for (let c = startCol; c < startCol + 3; c++) {
206
+ out.push(this.board[r][c]);
207
+ }
208
+ }
209
+ return out;
210
+ }
211
+ valuesMissing(values) {
212
+ return COMPLETE.filter((n) => !values.includes(n));
213
+ }
214
+ calcSquarePossibles(row, col) {
215
+ if (this.board[row][col] > 0) {
216
+ return [];
217
+ }
218
+ const rowPossible = this.valuesMissing(this.getRow(row));
219
+ const colPossible = this.valuesMissing(this.getColumn(col));
220
+ const boxPossible = this.valuesMissing(this.getBox(this.boxIndex(row, col)));
221
+ return rowPossible.filter((n) => colPossible.includes(n) && boxPossible.includes(n));
222
+ }
223
+ calcPossibles() {
224
+ for (let r = 0; r < 9; r++) {
225
+ for (let c = 0; c < 9; c++) {
226
+ this.possiblesGrid[r][c] = this.calcSquarePossibles(r, c);
227
+ }
228
+ }
229
+ }
230
+ findBestMove() {
231
+ for (const algorithm of SudokuSolver.ALGORITHMS) {
232
+ const move = this.findNextPlacement(algorithm);
233
+ if (move)
234
+ return move;
235
+ }
236
+ return null;
237
+ }
238
+ findNextPlacement(algorithm) {
239
+ switch (algorithm) {
240
+ case "Full House":
241
+ case "Naked Single": {
242
+ const move = this.findNakedSingle();
243
+ if (algorithm === "Full House") {
244
+ return move?.algorithm === "Full House" ? move : null;
245
+ }
246
+ return move;
247
+ }
248
+ case "Hidden Single":
249
+ return this.findHiddenSingle();
250
+ case "Pointing Pair/Triple":
251
+ return this.findPointingPairTriple();
252
+ }
253
+ }
254
+ findNakedSingle() {
255
+ for (let row = 0; row < 9; row++) {
256
+ for (let col = 0; col < 9; col++) {
257
+ if (this.board[row][col] === 0) {
258
+ const p = this.possiblesGrid[row][col];
259
+ if (p.length === 1) {
260
+ const placeValue = p[0];
261
+ let algo = "Naked Single";
262
+ // check if full house
263
+ const rowValues = this.getRow(row);
264
+ if (rowValues.filter((v) => v === 0).length === 1) {
265
+ algo = "Full House";
266
+ }
267
+ const colValues = this.getColumn(col);
268
+ if (colValues.filter((v) => v === 0).length === 1) {
269
+ algo = "Full House";
270
+ }
271
+ const boxValues = this.getBox(this.boxIndex(row, col));
272
+ if (boxValues.filter((v) => v === 0).length === 1) {
273
+ algo = "Full House";
274
+ }
275
+ return { type: "placement", row, col, value: placeValue, algorithm: algo };
276
+ }
277
+ }
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+ findHiddenSingleInRow(row) {
283
+ for (let value = 1; value <= 9; value++) {
284
+ const candidates = [];
285
+ for (let col = 0; col < 9; col++) {
286
+ if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(value)) {
287
+ candidates.push(col);
288
+ }
289
+ }
290
+ if (candidates.length === 1) {
291
+ return { type: "placement", row, col: candidates[0], value, algorithm: "Hidden Single" };
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+ findHiddenSingleInCol(col) {
297
+ for (let value = 1; value <= 9; value++) {
298
+ const candidates = [];
299
+ for (let row = 0; row < 9; row++) {
300
+ if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(value)) {
301
+ candidates.push(row);
302
+ }
303
+ }
304
+ if (candidates.length === 1) {
305
+ return { type: "placement", row: candidates[0], col, value, algorithm: "Hidden Single" };
306
+ }
307
+ }
308
+ return null;
309
+ }
310
+ findHiddenSingleInBox(ibox) {
311
+ for (let value = 1; value <= 9; value++) {
312
+ const candidates = [];
313
+ for (let idx = 0; idx < 9; idx++) {
314
+ const { row, col } = this.boxToPuzzle(ibox, idx);
315
+ if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(value)) {
316
+ candidates.push({ row, col });
317
+ }
318
+ }
319
+ if (candidates.length === 1) {
320
+ return { type: "placement", row: candidates[0].row, col: candidates[0].col, value, algorithm: "Hidden Single" };
321
+ }
322
+ }
323
+ return null;
324
+ }
325
+ findPointingPairTriple() {
326
+ for (let ibox = 0; ibox < 9; ibox++) {
327
+ const boxStartRow = Math.floor(ibox / 3) * 3;
328
+ const boxStartCol = (ibox % 3) * 3;
329
+ for (let digit = 1; digit <= 9; digit++) {
330
+ const cells = [];
331
+ for (let idx = 0; idx < 9; idx++) {
332
+ const { row, col } = this.boxToPuzzle(ibox, idx);
333
+ if (this.board[row][col] === 0 && this.possiblesGrid[row][col].includes(digit)) {
334
+ cells.push({ row, col });
335
+ }
336
+ }
337
+ if (cells.length < 2)
338
+ continue;
339
+ if (cells.every((c) => c.row === cells[0].row)) {
340
+ const sharedRow = cells[0].row;
341
+ const eliminations = [];
342
+ for (let c = 0; c < 9; c++) {
343
+ if (c < boxStartCol || c >= boxStartCol + 3) {
344
+ if (this.board[sharedRow][c] === 0 && this.possiblesGrid[sharedRow][c].includes(digit)) {
345
+ eliminations.push({ row: sharedRow, col: c, value: digit });
346
+ }
347
+ }
348
+ }
349
+ if (eliminations.length > 0) {
350
+ return { type: "elimination", eliminations, algorithm: "Pointing Pair/Triple" };
351
+ }
352
+ }
353
+ if (cells.every((c) => c.col === cells[0].col)) {
354
+ const sharedCol = cells[0].col;
355
+ const eliminations = [];
356
+ for (let r = 0; r < 9; r++) {
357
+ if (r < boxStartRow || r >= boxStartRow + 3) {
358
+ if (this.board[r][sharedCol] === 0 && this.possiblesGrid[r][sharedCol].includes(digit)) {
359
+ eliminations.push({ row: r, col: sharedCol, value: digit });
360
+ }
361
+ }
362
+ }
363
+ if (eliminations.length > 0) {
364
+ return { type: "elimination", eliminations, algorithm: "Pointing Pair/Triple" };
365
+ }
366
+ }
367
+ }
368
+ }
369
+ return null;
370
+ }
371
+ findHiddenSingle() {
372
+ for (let row = 0; row < 9; row++) {
373
+ const move = this.findHiddenSingleInRow(row);
374
+ if (move)
375
+ return move;
376
+ }
377
+ for (let col = 0; col < 9; col++) {
378
+ const move = this.findHiddenSingleInCol(col);
379
+ if (move)
380
+ return move;
381
+ }
382
+ for (let box = 0; box < 9; box++) {
383
+ const move = this.findHiddenSingleInBox(box);
384
+ if (move)
385
+ return move;
386
+ }
387
+ return null;
388
+ }
389
+ }
@@ -0,0 +1,8 @@
1
+ import { type Board } from "./sudokuSolver";
2
+ export declare function cloneBoard(board: Board): Board;
3
+ export declare function assertBoardShape(board: Board): void;
4
+ export declare function assertBoardValues(board: Board): void;
5
+ export declare function parseBoardString(board: string): Board;
6
+ export declare function duplicateValues(values: number[]): number[];
7
+ export declare function rowLetter(row: number): string;
8
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,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 ADDED
@@ -0,0 +1,53 @@
1
+ export function cloneBoard(board) {
2
+ return board.map((row) => [...row]);
3
+ }
4
+ export function assertBoardShape(board) {
5
+ if (board.length !== 9 || board.some((row) => row.length !== 9)) {
6
+ throw new Error("Board must be a 9x9 matrix");
7
+ }
8
+ }
9
+ export function assertBoardValues(board) {
10
+ for (let row = 0; row < 9; row += 1) {
11
+ for (let col = 0; col < 9; col += 1) {
12
+ const value = board[row][col];
13
+ if (!Number.isInteger(value) || value < 0 || value > 9) {
14
+ throw new Error(`Invalid value ${value} at row ${row}, col ${col}`);
15
+ }
16
+ }
17
+ }
18
+ }
19
+ export function parseBoardString(board) {
20
+ if (board.length !== 81) {
21
+ throw new Error(`Board must be 81 characters, got ${board.length}`);
22
+ }
23
+ const rows = Array.from({ length: 9 }, () => Array(9).fill(0));
24
+ for (let i = 0; i < 81; i += 1) {
25
+ const ch = board[i];
26
+ const row = Math.floor(i / 9);
27
+ const col = i % 9;
28
+ if (ch >= "1" && ch <= "9") {
29
+ rows[row][col] = Number(ch);
30
+ }
31
+ else if (ch === "0" || ch === ".") {
32
+ rows[row][col] = 0;
33
+ }
34
+ else {
35
+ throw new Error(`Invalid character '${ch}' at position ${i}`);
36
+ }
37
+ }
38
+ return rows;
39
+ }
40
+ export function duplicateValues(values) {
41
+ const counts = new Map();
42
+ for (const value of values) {
43
+ if (value === 0) {
44
+ continue;
45
+ }
46
+ counts.set(value, (counts.get(value) ?? 0) + 1);
47
+ }
48
+ return [...counts.entries()].filter(([, count]) => count > 1).map(([value]) => value);
49
+ }
50
+ const LABELS = 'ABCDEFGHJ';
51
+ export function rowLetter(row) {
52
+ return LABELS[row];
53
+ }
@@ -0,0 +1,18 @@
1
+ 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";
2
+ export interface ValidationReason {
3
+ type: ValidationReasonType;
4
+ detail: string;
5
+ row?: number;
6
+ col?: number;
7
+ box?: number;
8
+ value?: number;
9
+ }
10
+ export interface ValidationResult {
11
+ isValid: boolean;
12
+ message: string;
13
+ reasons: ValidationReason[];
14
+ }
15
+ export declare function pushUniqueReason(reasons: ValidationReason[], reason: ValidationReason): void;
16
+ export declare function invalidBoardLength(length: number): ValidationResult;
17
+ export declare function invalidBoardCharacters(): ValidationResult;
18
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC1B,kBAAkB,GAClB,qBAAqB,GACrB,kBAAkB,GAClB,eAAe,GACf,sBAAsB,GACtB,0BAA0B,GAC1B,0BAA0B,GAC1B,sBAAsB,CAAC;AAE7B,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,oBAAoB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAY5F;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAWnE;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CAWzD"}
@@ -0,0 +1,34 @@
1
+ export function pushUniqueReason(reasons, reason) {
2
+ const found = reasons.some((r) => r.type === reason.type &&
3
+ r.row === reason.row &&
4
+ r.col === reason.col &&
5
+ r.box === reason.box &&
6
+ r.value === reason.value);
7
+ if (!found) {
8
+ reasons.push(reason);
9
+ }
10
+ }
11
+ export function invalidBoardLength(length) {
12
+ return {
13
+ isValid: false,
14
+ message: "Board must be 81 characters",
15
+ reasons: [
16
+ {
17
+ type: "invalid_board_length",
18
+ detail: `Board must be 81 characters, got ${length}`,
19
+ },
20
+ ],
21
+ };
22
+ }
23
+ export function invalidBoardCharacters() {
24
+ return {
25
+ isValid: false,
26
+ message: "Board contains invalid characters",
27
+ reasons: [
28
+ {
29
+ type: "invalid_board_characters",
30
+ detail: "Only digits 0-9 and '.' are allowed",
31
+ },
32
+ ],
33
+ };
34
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@wsabol/sudoku-solver",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript Sudoku solver module with solve, next move, describe, and validate APIs.",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.build.json",
18
+ "prepublishOnly": "npm run build",
19
+ "test": "vitest run",
20
+ "test:coverage": "vitest run --coverage",
21
+ "test:watch": "vitest",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "keywords": [
25
+ "sudoku",
26
+ "solver",
27
+ "puzzle",
28
+ "sudoku engine",
29
+ "sudoku solver"
30
+ ],
31
+ "author": "Will Sabol",
32
+ "license": "MIT",
33
+ "type": "module",
34
+ "devDependencies": {
35
+ "@types/node": "^25.5.0",
36
+ "@vitest/coverage-v8": "^4.1.0",
37
+ "tsx": "^4.21.0",
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^4.1.0"
40
+ }
41
+ }