@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.
- package/dist/boardGeo.d.ts +2 -0
- package/dist/boardGeo.d.ts.map +1 -0
- package/dist/boardGeo.js +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -11
- package/dist/move.d.ts +24 -0
- package/dist/move.d.ts.map +1 -0
- package/dist/move.js +1 -0
- package/dist/sudokuSolver.d.ts +11 -18
- package/dist/sudokuSolver.d.ts.map +1 -1
- package/dist/sudokuSolver.js +103 -18
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -0
- package/package.json +1 -1
|
@@ -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"}
|
package/dist/boardGeo.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import SudokuSolver, { type Algorithm, type
|
|
2
|
-
import {
|
|
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:
|
|
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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
32
|
-
|
|
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 {};
|
package/dist/sudokuSolver.d.ts
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import { ValidationResult } from "./validate.js";
|
|
2
|
-
|
|
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;
|
|
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"}
|
package/dist/sudokuSolver.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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;
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,
|
|
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
package/package.json
CHANGED