human-sudoku-solver 0.1.1
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/LICENSE +674 -0
- package/README.md +203 -0
- package/dist/index.d.mts +92 -0
- package/dist/index.mjs +2306 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# human-sudoku-solver
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/human-sudoku-solver)
|
|
4
|
+
|
|
5
|
+
A TypeScript library that solves Sudoku puzzles using human techniques. Rather than brute-force backtracking, it applies logical solving methods in the same order a human would — making it suitable for hint systems, puzzle analysis, and step-by-step walkthroughs.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install human-sudoku-solver
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Solve a puzzle
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { humanSolve } from 'human-sudoku-solver';
|
|
19
|
+
|
|
20
|
+
// 81-cell flat array, row-major order. 0 = empty, 1–9 = digit.
|
|
21
|
+
const grid = [
|
|
22
|
+
2,0,1,0,8,0,0,0,0,
|
|
23
|
+
9,0,0,1,7,0,0,4,0,
|
|
24
|
+
0,7,0,0,0,1,5,2,8,
|
|
25
|
+
// ...
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const { grid: solved, steps } = humanSolve(grid);
|
|
29
|
+
|
|
30
|
+
console.log(`Solved in ${steps.length} steps`);
|
|
31
|
+
steps.forEach(step => console.log(step.technique, step.explanation));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Step-by-step solving
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { buildCandidates, findHint, applyHint } from 'human-sudoku-solver';
|
|
38
|
+
|
|
39
|
+
let grid = [/* initial puzzle */];
|
|
40
|
+
let candidates = buildCandidates(grid);
|
|
41
|
+
|
|
42
|
+
while (true) {
|
|
43
|
+
const hint = findHint(grid, candidates);
|
|
44
|
+
if (!hint) break;
|
|
45
|
+
|
|
46
|
+
console.log(hint.technique); // e.g. 'nakedSingle'
|
|
47
|
+
console.log(hint.eureka); // e.g. 'r1c3=5'
|
|
48
|
+
console.log(hint.explanation); // human-readable description
|
|
49
|
+
|
|
50
|
+
({ grid, candidates } = applyHint(grid, candidates, hint));
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Parse puzzle text format
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { puzzleTextToPuzzle, puzzleToGrid, humanSolve } from 'human-sudoku-solver';
|
|
58
|
+
|
|
59
|
+
// 81-character string: '.' or '0' = empty, '1'–'9' = digit
|
|
60
|
+
const puzzle = puzzleTextToPuzzle('2.1.8....9..17..4..7....528...');
|
|
61
|
+
const grid = puzzleToGrid(puzzle);
|
|
62
|
+
const { grid: solved } = humanSolve(grid);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### Core functions
|
|
68
|
+
|
|
69
|
+
#### `humanSolve(grid: Grid)`
|
|
70
|
+
|
|
71
|
+
Solves a puzzle to completion using human techniques.
|
|
72
|
+
|
|
73
|
+
Returns `{ grid: Grid; candidates: Candidates; steps: HintResult[] }`.
|
|
74
|
+
|
|
75
|
+
#### `findHint(grid: Grid, candidates: Candidates)`
|
|
76
|
+
|
|
77
|
+
Finds the next logical step without applying it.
|
|
78
|
+
|
|
79
|
+
Returns `HintResult | null`.
|
|
80
|
+
|
|
81
|
+
#### `applyHint(grid: Grid, candidates: Candidates, hint: HintResult)`
|
|
82
|
+
|
|
83
|
+
Applies a hint to the current state.
|
|
84
|
+
|
|
85
|
+
Returns `{ grid: Grid; candidates: Candidates }`.
|
|
86
|
+
|
|
87
|
+
#### `buildCandidates(grid: Grid)`
|
|
88
|
+
|
|
89
|
+
Builds the initial candidate sets for all empty cells.
|
|
90
|
+
|
|
91
|
+
Returns `Candidates` — an array of 81 `Set<number>` values (one per cell).
|
|
92
|
+
|
|
93
|
+
#### `isGridInvalid(grid: Grid)`
|
|
94
|
+
|
|
95
|
+
Returns `true` if the grid contains duplicate digits in any row, column, or box.
|
|
96
|
+
|
|
97
|
+
### Format conversion
|
|
98
|
+
|
|
99
|
+
#### `puzzleTextToPuzzle(text: string)`
|
|
100
|
+
|
|
101
|
+
Converts an 81-character puzzle string to a `Puzzle<number>`.
|
|
102
|
+
|
|
103
|
+
#### `puzzleToPuzzleText(puzzle: Puzzle<number | Notes>)`
|
|
104
|
+
|
|
105
|
+
Converts a `Puzzle` back to an 81-character string.
|
|
106
|
+
|
|
107
|
+
#### `puzzleToGrid(puzzle: Puzzle<number | Notes>)`
|
|
108
|
+
|
|
109
|
+
Converts a `Puzzle` to a flat 81-cell `Grid`.
|
|
110
|
+
|
|
111
|
+
#### `gridToPuzzle(grid: Grid)`
|
|
112
|
+
|
|
113
|
+
Converts a flat `Grid` to a `Puzzle<number>`.
|
|
114
|
+
|
|
115
|
+
## Types
|
|
116
|
+
|
|
117
|
+
### `Grid`
|
|
118
|
+
|
|
119
|
+
Flat array of 81 numbers in row-major order. `0` = empty, `1`–`9` = digit.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
cell index = row * 9 + col
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `HintResult`
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
interface HintResult {
|
|
129
|
+
technique: Technique;
|
|
130
|
+
placements: Placement[]; // cells to fill: { cell, digit }
|
|
131
|
+
eliminations: Elimination[]; // candidates to remove: { cell, digit }
|
|
132
|
+
patternCells: number[]; // cells involved in the pattern (for highlighting)
|
|
133
|
+
eureka?: string; // standard Sudoku notation
|
|
134
|
+
explanation?: string; // human-readable description
|
|
135
|
+
chainPath?: ChainNode[]; // populated for chain techniques
|
|
136
|
+
hiddenDigits?: number[]; // populated for hidden group techniques
|
|
137
|
+
patternDigits?: number[]; // populated for some techniques (e.g. UR floor digits)
|
|
138
|
+
als1Cells?: number[]; // populated for ALS XZ
|
|
139
|
+
als2Cells?: number[]; // populated for ALS XZ
|
|
140
|
+
stemCell?: number; // populated for Death Blossom
|
|
141
|
+
petalCells?: number[][]; // populated for Death Blossom
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `ChainNode`
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
interface ChainNode {
|
|
149
|
+
cell: number;
|
|
150
|
+
digit: number;
|
|
151
|
+
isOn: boolean; // true = candidate is ON in this node
|
|
152
|
+
cells?: number[]; // grouped node: multiple cells in same house
|
|
153
|
+
linkToNext?: 'strong' | 'weak';
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `Technique`
|
|
158
|
+
|
|
159
|
+
The full set of supported techniques, in the order they are attempted:
|
|
160
|
+
|
|
161
|
+
| Technique | Description |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `nakedSingle` | Only one candidate remains in a cell |
|
|
164
|
+
| `hiddenSingleBox` / `hiddenSingleRow` / `hiddenSingleCol` | Digit can only go in one cell within a house |
|
|
165
|
+
| `hiddenPair` / `hiddenTriple` / `hiddenQuad` | N digits confined to N cells in a house |
|
|
166
|
+
| `lockedCandidatePointing` | Box candidates for a digit align in one row/col, eliminating from that row/col outside the box |
|
|
167
|
+
| `lockedCandidateClaiming` | Row/col candidates for a digit align in one box, eliminating from that box |
|
|
168
|
+
| `nakedPair` / `nakedTriple` / `nakedQuad` | N cells in a house share exactly N candidates |
|
|
169
|
+
| `xWing` / `swordfish` / `jellyfish` | Fish patterns of size 2, 3, 4 |
|
|
170
|
+
| `skyscraper` | Two rows/cols share a digit in exactly two columns/rows |
|
|
171
|
+
| `twoStringKite` | Intersecting conjugate pairs in a box |
|
|
172
|
+
| `yWing` / `xyzWing` | Pivot cell with two/three bivalue wings |
|
|
173
|
+
| `finnedXWing` / `finnedSwordfish` / `finnedJellyfish` | Fish with extra fin candidates |
|
|
174
|
+
| `wWing` | Two bivalue cells connected by a strong link on a shared digit |
|
|
175
|
+
| `emptyRectangle` | Eliminates via an empty rectangle in a box |
|
|
176
|
+
| `uniqueRectangleType1`–`Type4` | Avoids deadly pattern in a rectangle of four cells |
|
|
177
|
+
| `bug` | Bivalue Universal Grave — all candidates bivalue except one |
|
|
178
|
+
| `xyChain` | Chain of bivalue cells |
|
|
179
|
+
| `aic` / `aicRing` | Alternating Inference Chain |
|
|
180
|
+
| `groupedAIC` | AIC with grouped nodes (multiple cells as one node) |
|
|
181
|
+
| `alsXZ` | Almost Locked Sets linked by a restricted common candidate |
|
|
182
|
+
| `sueDeCoq` | Sue de Coq pattern in a box/line intersection |
|
|
183
|
+
| `deathBlossom` | ALS petals hanging off a stem cell |
|
|
184
|
+
| `nishio` | Contradiction-based elimination |
|
|
185
|
+
| `nishioNet` | Nishio extended to a network |
|
|
186
|
+
| `cellRegionForcingChain` / `cellRegionForcingNet` | All candidates in a cell/region force the same conclusion |
|
|
187
|
+
| `forcingChain` | Multiple chains from the same candidate converge |
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npm install # install dependencies
|
|
193
|
+
npm test # run tests
|
|
194
|
+
npm run build # compile to dist/
|
|
195
|
+
npm run lint # lint
|
|
196
|
+
npm run typecheck # type-check without emitting
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Output is an ES module at `dist/index.mjs` with TypeScript declarations at `dist/index.d.mts`.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
[GPL-3.0](LICENSE)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/types/Notes.d.ts
|
|
2
|
+
interface Notes {
|
|
3
|
+
[key: number]: boolean;
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/types/Value.d.ts
|
|
7
|
+
type Value = number | boolean | Notes | undefined;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/types/PuzzleBox.d.ts
|
|
10
|
+
interface PuzzleBox<T extends Value = number | Notes> {
|
|
11
|
+
0: T[];
|
|
12
|
+
1: T[];
|
|
13
|
+
2: T[];
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/types/PuzzleRow.d.ts
|
|
17
|
+
interface PuzzleRow<T extends Value = number | Notes> {
|
|
18
|
+
0: PuzzleBox<T>;
|
|
19
|
+
1: PuzzleBox<T>;
|
|
20
|
+
2: PuzzleBox<T>;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/types/Puzzle.d.ts
|
|
24
|
+
interface Puzzle<T extends Value = number | Notes> {
|
|
25
|
+
0: PuzzleRow<T>;
|
|
26
|
+
1: PuzzleRow<T>;
|
|
27
|
+
2: PuzzleRow<T>;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/types/Placement.d.ts
|
|
31
|
+
interface Placement {
|
|
32
|
+
cell: number;
|
|
33
|
+
digit: number;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/types/Elimination.d.ts
|
|
37
|
+
interface Elimination {
|
|
38
|
+
cell: number;
|
|
39
|
+
digit: number;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/types/ChainNode.d.ts
|
|
43
|
+
interface ChainNode {
|
|
44
|
+
cell: number;
|
|
45
|
+
digit: number;
|
|
46
|
+
isOn: boolean;
|
|
47
|
+
cells?: number[];
|
|
48
|
+
linkToNext?: 'strong' | 'weak';
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/types/Technique.d.ts
|
|
52
|
+
type Technique = 'nakedSingle' | 'hiddenSingleBox' | 'hiddenSingleRow' | 'hiddenSingleCol' | 'nakedPair' | 'nakedTriple' | 'nakedQuad' | 'hiddenPair' | 'hiddenTriple' | 'hiddenQuad' | 'lockedCandidatePointing' | 'lockedCandidateClaiming' | 'xWing' | 'swordfish' | 'jellyfish' | 'skyscraper' | 'twoStringKite' | 'finnedXWing' | 'finnedSwordfish' | 'finnedJellyfish' | 'emptyRectangle' | 'wWing' | 'yWing' | 'xyzWing' | 'uniqueRectangleType1' | 'uniqueRectangleType2' | 'uniqueRectangleType3' | 'uniqueRectangleType4' | 'uniqueRectangleType5' | 'bug' | 'xyChain' | 'aic' | 'aicRing' | 'groupedAIC' | 'alsXZ' | 'sueDeCoq' | 'deathBlossom' | 'nishio' | 'nishioNet' | 'cellRegionForcingChain' | 'cellRegionForcingNet' | 'forcingChain';
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/types/HintResult.d.ts
|
|
55
|
+
interface HintResult {
|
|
56
|
+
technique: Technique;
|
|
57
|
+
placements: Placement[];
|
|
58
|
+
eliminations: Elimination[];
|
|
59
|
+
patternCells: number[];
|
|
60
|
+
hiddenDigits?: number[];
|
|
61
|
+
patternDigits?: number[];
|
|
62
|
+
chainPath?: ChainNode[];
|
|
63
|
+
stemCell?: number;
|
|
64
|
+
petalCells?: number[][];
|
|
65
|
+
als1Cells?: number[];
|
|
66
|
+
als2Cells?: number[];
|
|
67
|
+
eureka?: string;
|
|
68
|
+
explanation?: string;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/humanSolver.d.ts
|
|
72
|
+
type Grid = number[];
|
|
73
|
+
type Candidates = Set<number>[];
|
|
74
|
+
declare const puzzleToGrid: (puzzle: Puzzle<number | Notes>) => Grid;
|
|
75
|
+
declare const gridToPuzzle: (grid: Grid) => Puzzle<number>;
|
|
76
|
+
declare const buildCandidates: (grid: Grid) => Candidates;
|
|
77
|
+
declare const isGridInvalid: (grid: Grid) => boolean;
|
|
78
|
+
declare const applyHint: (grid: Grid, candidates: Candidates, hint: Pick<HintResult, 'placements' | 'eliminations'>) => {
|
|
79
|
+
grid: Grid;
|
|
80
|
+
candidates: Candidates;
|
|
81
|
+
};
|
|
82
|
+
declare const findHint: (grid: Grid, candidates: Candidates) => HintResult | null;
|
|
83
|
+
declare const humanSolve: (initialGrid: Grid) => {
|
|
84
|
+
grid: Grid;
|
|
85
|
+
candidates: Candidates;
|
|
86
|
+
steps: HintResult[];
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/types/PuzzleRowOrColumn.d.ts
|
|
90
|
+
type PuzzleRowOrColumn = 0 | 1 | 2;
|
|
91
|
+
//#endregion
|
|
92
|
+
export { type ChainNode, type Elimination, type HintResult, type Notes, type Placement, type Puzzle, type PuzzleBox, type PuzzleRow, type PuzzleRowOrColumn, type Technique, type Value, applyHint, buildCandidates, findHint, gridToPuzzle, humanSolve, isGridInvalid, puzzleToGrid };
|