human-sudoku-solver 0.1.1 → 1.0.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/dist/index.cjs +2313 -0
- package/dist/index.d.cts +92 -0
- package/package.json +10 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2313 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/humanSolverNotation.ts
|
|
3
|
+
const cellRow$1 = (cell) => Math.floor(cell / 9);
|
|
4
|
+
const cellCol$1 = (cell) => cell % 9;
|
|
5
|
+
const cellBox$1 = (cell) => Math.floor(cellRow$1(cell) / 3) * 3 + Math.floor(cellCol$1(cell) / 3);
|
|
6
|
+
const eurekaCell = (cell) => `r${cellRow$1(cell) + 1}c${cellCol$1(cell) + 1}`;
|
|
7
|
+
const conclusionStr = (hint) => {
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const { cell, digit } of hint.placements) parts.push(`${eurekaCell(cell)}=${digit}`);
|
|
10
|
+
for (const { cell, digit } of hint.eliminations) parts.push(`${eurekaCell(cell)}<>${digit}`);
|
|
11
|
+
return parts.join(", ");
|
|
12
|
+
};
|
|
13
|
+
const renderChainPath = (path) => {
|
|
14
|
+
if (path.length === 0) return "";
|
|
15
|
+
const parts = [];
|
|
16
|
+
for (let i = 0; i < path.length; i++) {
|
|
17
|
+
const node = path[i];
|
|
18
|
+
const next = path[i + 1];
|
|
19
|
+
const cells = node.cells ?? [node.cell];
|
|
20
|
+
let nodeStr;
|
|
21
|
+
if (cells.length === 1) nodeStr = `(${node.digit})${eurekaCell(cells[0])}`;
|
|
22
|
+
else {
|
|
23
|
+
const rows = [...new Set(cells.map(cellRow$1))];
|
|
24
|
+
const cols = [...new Set(cells.map(cellCol$1))];
|
|
25
|
+
if (rows.length === 1) {
|
|
26
|
+
const colStr = cols.map((c) => c + 1).join("");
|
|
27
|
+
nodeStr = `(${node.digit})r${rows[0] + 1}c[${colStr}]`;
|
|
28
|
+
} else if (cols.length === 1) {
|
|
29
|
+
const rowStr = rows.map((r) => r + 1).join("");
|
|
30
|
+
nodeStr = `(${node.digit})r[${rowStr}]c${cols[0] + 1}`;
|
|
31
|
+
} else nodeStr = `(${node.digit})${cells.map(eurekaCell).join("|")}`;
|
|
32
|
+
}
|
|
33
|
+
parts.push(nodeStr);
|
|
34
|
+
if (next !== void 0) {
|
|
35
|
+
const link = node.linkToNext ?? (node.isOn ? "weak" : "strong");
|
|
36
|
+
parts.push(link === "strong" ? "=" : "-");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return parts.join("");
|
|
40
|
+
};
|
|
41
|
+
const buildEureka = (hint) => {
|
|
42
|
+
const conc = conclusionStr(hint);
|
|
43
|
+
if ([
|
|
44
|
+
"uniqueRectangleType5",
|
|
45
|
+
"nishio",
|
|
46
|
+
"nishioNet",
|
|
47
|
+
"cellRegionForcingChain",
|
|
48
|
+
"cellRegionForcingNet",
|
|
49
|
+
"forcingChain"
|
|
50
|
+
].includes(hint.technique)) return "";
|
|
51
|
+
switch (hint.technique) {
|
|
52
|
+
case "nakedSingle": {
|
|
53
|
+
const { cell, digit } = hint.placements[0];
|
|
54
|
+
return `(${digit})${eurekaCell(cell)} => ${eurekaCell(cell)}=${digit}`;
|
|
55
|
+
}
|
|
56
|
+
case "hiddenSingleBox":
|
|
57
|
+
case "hiddenSingleRow":
|
|
58
|
+
case "hiddenSingleCol": {
|
|
59
|
+
const { cell, digit } = hint.placements[0];
|
|
60
|
+
return `(${digit})${eurekaCell(cell)} => ${eurekaCell(cell)}=${digit}`;
|
|
61
|
+
}
|
|
62
|
+
case "lockedCandidatePointing": {
|
|
63
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
64
|
+
const cells = hint.patternCells;
|
|
65
|
+
return `(${digit})${`box${cellBox$1(cells[0]) + 1}`} => ${conc}`;
|
|
66
|
+
}
|
|
67
|
+
case "lockedCandidateClaiming": {
|
|
68
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
69
|
+
const cells = hint.patternCells;
|
|
70
|
+
const rows = [...new Set(cells.map(cellRow$1))];
|
|
71
|
+
const cols = [...new Set(cells.map(cellCol$1))];
|
|
72
|
+
let lineStr;
|
|
73
|
+
if (rows.length === 1) lineStr = `r${rows[0] + 1}`;
|
|
74
|
+
else lineStr = `c${cols[0] + 1}`;
|
|
75
|
+
return `(${digit})${lineStr} => ${conc}`;
|
|
76
|
+
}
|
|
77
|
+
case "nakedPair":
|
|
78
|
+
case "nakedTriple":
|
|
79
|
+
case "nakedQuad": {
|
|
80
|
+
const digits = [...new Set(hint.eliminations.map((e) => e.digit))].sort();
|
|
81
|
+
const cellsStr = hint.patternCells.map(eurekaCell).join(",");
|
|
82
|
+
return `(${digits.join(",")})${cellsStr} => ${conc}`;
|
|
83
|
+
}
|
|
84
|
+
case "hiddenPair":
|
|
85
|
+
case "hiddenTriple":
|
|
86
|
+
case "hiddenQuad": {
|
|
87
|
+
const digits = hint.hiddenDigits ?? [];
|
|
88
|
+
const cellsStr = hint.patternCells.map(eurekaCell).join(",");
|
|
89
|
+
return `(${digits.join(",")})${cellsStr} => ${conc}`;
|
|
90
|
+
}
|
|
91
|
+
case "xWing":
|
|
92
|
+
case "swordfish":
|
|
93
|
+
case "jellyfish": {
|
|
94
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
95
|
+
const cells = hint.patternCells;
|
|
96
|
+
const rows = [...new Set(cells.map(cellRow$1))].sort((a, b) => a - b);
|
|
97
|
+
const cols = [...new Set(cells.map(cellCol$1))].sort((a, b) => a - b);
|
|
98
|
+
if (rows.length <= cols.length) {
|
|
99
|
+
const rowStr = rows.map((r) => r + 1).join("");
|
|
100
|
+
return `${cols.map((c) => `(${digit})r[${rowStr}]c${c + 1}`).join("-")} => ${conc}`;
|
|
101
|
+
} else {
|
|
102
|
+
const colStr = cols.map((c) => c + 1).join("");
|
|
103
|
+
return `${rows.map((r) => `(${digit})r${r + 1}c[${colStr}]`).join("-")} => ${conc}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
case "finnedXWing":
|
|
107
|
+
case "finnedSwordfish":
|
|
108
|
+
case "finnedJellyfish": {
|
|
109
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
110
|
+
return `${hint.patternCells.map((c) => `(${digit})${eurekaCell(c)}`).join("|")} => ${conc}`;
|
|
111
|
+
}
|
|
112
|
+
case "skyscraper": {
|
|
113
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
114
|
+
const [c0, c1, c2, c3] = hint.patternCells;
|
|
115
|
+
return `${`(${digit})${eurekaCell(c0)}=(${digit})${eurekaCell(c1)}-(${digit})${eurekaCell(c2)}=(${digit})${eurekaCell(c3)}`} => ${conc}`;
|
|
116
|
+
}
|
|
117
|
+
case "twoStringKite": {
|
|
118
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
119
|
+
const [colCell, rowCell, colRoof, rowRoof] = hint.patternCells;
|
|
120
|
+
return `${`(${digit})${eurekaCell(colCell)}=(${digit})${eurekaCell(rowCell)}-(${digit})${eurekaCell(colRoof)}=(${digit})${eurekaCell(rowRoof)}`} => ${conc}`;
|
|
121
|
+
}
|
|
122
|
+
case "emptyRectangle": {
|
|
123
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
124
|
+
const pivot = hint.patternCells[hint.patternCells.length - 2];
|
|
125
|
+
const strongEnd = hint.patternCells[hint.patternCells.length - 1];
|
|
126
|
+
return `(${digit})box${cellBox$1(hint.patternCells.slice(0, hint.patternCells.length - 2)[0]) + 1}-(${digit})${eurekaCell(pivot)}=(${digit})${eurekaCell(strongEnd)} => ${conc}`;
|
|
127
|
+
}
|
|
128
|
+
case "wWing": {
|
|
129
|
+
const [c1, c2, lc1, lc2] = hint.patternCells;
|
|
130
|
+
const elimDigit = hint.eliminations[0]?.digit ?? 0;
|
|
131
|
+
const linkDigit = hint.patternDigits?.[0] ?? elimDigit;
|
|
132
|
+
return `${`(${elimDigit})${eurekaCell(c1)}-(${linkDigit})${eurekaCell(lc1)}=(${linkDigit})${eurekaCell(lc2)}-(${elimDigit})${eurekaCell(c2)}`} => ${conc}`;
|
|
133
|
+
}
|
|
134
|
+
case "yWing": {
|
|
135
|
+
const [pivot, p1, p2] = hint.patternCells;
|
|
136
|
+
const elimDigit = hint.eliminations[0]?.digit ?? 0;
|
|
137
|
+
if (hint.chainPath && hint.chainPath.length >= 3) return `${renderChainPath(hint.chainPath)} => ${conc}`;
|
|
138
|
+
return `(?)${eurekaCell(pivot)}-(?)${eurekaCell(p1)}-(?)${eurekaCell(p2)} => ${eurekaCell(hint.eliminations[0]?.cell ?? 0)}<>${elimDigit}`;
|
|
139
|
+
}
|
|
140
|
+
case "xyzWing": {
|
|
141
|
+
const [pivot, p1, p2] = hint.patternCells;
|
|
142
|
+
const elimDigit = hint.eliminations[0]?.digit ?? 0;
|
|
143
|
+
if (hint.chainPath && hint.chainPath.length >= 3) return `${renderChainPath(hint.chainPath)} => ${conc}`;
|
|
144
|
+
return `(?)${eurekaCell(pivot)}-(?)${eurekaCell(p1)}-(?)${eurekaCell(p2)} => ${eurekaCell(hint.eliminations[0]?.cell ?? 0)}<>${elimDigit}`;
|
|
145
|
+
}
|
|
146
|
+
case "xyChain": {
|
|
147
|
+
if (hint.chainPath && hint.chainPath.length >= 2) return `${renderChainPath(hint.chainPath)} => ${conc}`;
|
|
148
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
149
|
+
return `${hint.patternCells.map((c) => `(?)${eurekaCell(c)}`).join("-")} => ${eurekaCell(hint.eliminations[0]?.cell ?? 0)}<>${digit}`;
|
|
150
|
+
}
|
|
151
|
+
case "aic":
|
|
152
|
+
case "aicRing":
|
|
153
|
+
case "groupedAIC": {
|
|
154
|
+
if (hint.chainPath && hint.chainPath.length >= 2) {
|
|
155
|
+
const chainStr = renderChainPath(hint.chainPath);
|
|
156
|
+
if (hint.technique === "aicRing") {
|
|
157
|
+
const firstNode = hint.chainPath[0];
|
|
158
|
+
const lastNode = hint.chainPath[hint.chainPath.length - 1];
|
|
159
|
+
const lastLink = lastNode.linkToNext === "strong" ? "=" : lastNode.linkToNext === "weak" ? "-" : lastNode.isOn ? "-" : "=";
|
|
160
|
+
const firstCells = firstNode.cells ?? [firstNode.cell];
|
|
161
|
+
let firstNodeStr;
|
|
162
|
+
if (firstCells.length === 1) firstNodeStr = `(${firstNode.digit})${eurekaCell(firstCells[0])}`;
|
|
163
|
+
else firstNodeStr = `(${firstNode.digit})${firstCells.map(eurekaCell).join("|")}`;
|
|
164
|
+
return `${chainStr}${lastLink}${firstNodeStr} => ${conc}`;
|
|
165
|
+
}
|
|
166
|
+
return `${chainStr} => ${conc}`;
|
|
167
|
+
}
|
|
168
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
169
|
+
return `${hint.patternCells.map((c) => `(${digit})${eurekaCell(c)}`).join("-")} => ${conc}`;
|
|
170
|
+
}
|
|
171
|
+
case "uniqueRectangleType1":
|
|
172
|
+
case "uniqueRectangleType2":
|
|
173
|
+
case "uniqueRectangleType3":
|
|
174
|
+
case "uniqueRectangleType4": {
|
|
175
|
+
const rows = [...new Set(hint.patternCells.map(cellRow$1))].sort().map((r) => r + 1).join("");
|
|
176
|
+
const cols = [...new Set(hint.patternCells.map(cellCol$1))].sort().map((c) => c + 1).join("");
|
|
177
|
+
return `UR(${(hint.patternDigits ?? []).join(",")})r${rows}c${cols} => ${conc}`;
|
|
178
|
+
}
|
|
179
|
+
case "bug": {
|
|
180
|
+
const { cell, digit } = hint.placements[0];
|
|
181
|
+
return `BUG+1 => ${eurekaCell(cell)}=${digit}`;
|
|
182
|
+
}
|
|
183
|
+
case "alsXZ": {
|
|
184
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
185
|
+
const elimCells = hint.eliminations.map((e) => eurekaCell(e.cell)).join(",");
|
|
186
|
+
return `A(${hint.als1Cells ? hint.als1Cells.map(eurekaCell).join(",") : ""}) - B(${hint.als2Cells ? hint.als2Cells.map(eurekaCell).join(",") : ""}) => ${elimCells}<>${digit}`;
|
|
187
|
+
}
|
|
188
|
+
case "sueDeCoq": return `SDC(${hint.patternCells.map(eurekaCell).join(",")}) => ${conc}`;
|
|
189
|
+
case "deathBlossom": return `DB: stem=${hint.stemCell !== void 0 ? eurekaCell(hint.stemCell) : ""} | ${hint.petalCells ? hint.petalCells.map((cells, i) => `Petal${String.fromCharCode(65 + i)}(${cells.map(eurekaCell).join(",")})`).join(" | ") : ""} => ${conc}`;
|
|
190
|
+
default: return "";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const buildExplanation = (hint) => {
|
|
194
|
+
switch (hint.technique) {
|
|
195
|
+
case "nakedSingle": {
|
|
196
|
+
const { cell, digit } = hint.placements[0];
|
|
197
|
+
return `${digit} is the only remaining candidate in the cell ${eurekaCell(cell)}. This is a Naked Single.`;
|
|
198
|
+
}
|
|
199
|
+
case "hiddenSingleBox": {
|
|
200
|
+
const { cell, digit } = hint.placements[0];
|
|
201
|
+
const box = cellBox$1(cell);
|
|
202
|
+
return `${digit} can only go in ${eurekaCell(cell)} within box ${box + 1}. This is a Hidden Single.`;
|
|
203
|
+
}
|
|
204
|
+
case "hiddenSingleRow": {
|
|
205
|
+
const { cell, digit } = hint.placements[0];
|
|
206
|
+
const row = cellRow$1(cell);
|
|
207
|
+
return `${digit} can only go in ${eurekaCell(cell)} within row ${row + 1}. This is a Hidden Single.`;
|
|
208
|
+
}
|
|
209
|
+
case "hiddenSingleCol": {
|
|
210
|
+
const { cell, digit } = hint.placements[0];
|
|
211
|
+
const col = cellCol$1(cell);
|
|
212
|
+
return `${digit} can only go in ${eurekaCell(cell)} within column ${col + 1}. This is a Hidden Single.`;
|
|
213
|
+
}
|
|
214
|
+
case "lockedCandidatePointing": {
|
|
215
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
216
|
+
const cells = hint.patternCells;
|
|
217
|
+
const box = cellBox$1(cells[0]);
|
|
218
|
+
const rows = [...new Set(cells.map(cellRow$1))];
|
|
219
|
+
const cols = [...new Set(cells.map(cellCol$1))];
|
|
220
|
+
const line = rows.length === 1 ? `row ${rows[0] + 1}` : `column ${cols[0] + 1}`;
|
|
221
|
+
const elimCells = hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ");
|
|
222
|
+
return `${digit} in box ${box + 1} is confined to ${line}, eliminating ${digit} from ${elimCells}. This is the Locked Candidates Pointing technique.`;
|
|
223
|
+
}
|
|
224
|
+
case "lockedCandidateClaiming": {
|
|
225
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
226
|
+
const cells = hint.patternCells;
|
|
227
|
+
const rows = [...new Set(cells.map(cellRow$1))];
|
|
228
|
+
const cols = [...new Set(cells.map(cellCol$1))];
|
|
229
|
+
const line = rows.length === 1 ? `row ${rows[0] + 1}` : `column ${cols[0] + 1}`;
|
|
230
|
+
const box = cellBox$1(hint.eliminations[0]?.cell ?? 0);
|
|
231
|
+
const elimCells = hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ");
|
|
232
|
+
return `${digit} in ${line} is confined to box ${box + 1}, eliminating ${digit} from ${elimCells}. This is the Locked Candidates Claiming technique.`;
|
|
233
|
+
}
|
|
234
|
+
case "nakedPair": {
|
|
235
|
+
const digits = [...new Set(hint.eliminations.map((e) => e.digit))].sort();
|
|
236
|
+
return `${hint.patternCells.map(eurekaCell).join(" and ")} share candidates {${digits.join(",")}}, eliminating those from the rest of their shared house. This is a Naked Pair.`;
|
|
237
|
+
}
|
|
238
|
+
case "nakedTriple": {
|
|
239
|
+
const digits = [...new Set(hint.eliminations.map((e) => e.digit))].sort();
|
|
240
|
+
return `${hint.patternCells.map(eurekaCell).join(", ")} share candidates {${digits.join(",")}}, eliminating those from the rest of their shared house. This is a Naked Triple.`;
|
|
241
|
+
}
|
|
242
|
+
case "nakedQuad": {
|
|
243
|
+
const digits = [...new Set(hint.eliminations.map((e) => e.digit))].sort();
|
|
244
|
+
return `${hint.patternCells.map(eurekaCell).join(", ")} share candidates {${digits.join(",")}}, eliminating those from the rest of their shared house. This is a Naked Quad.`;
|
|
245
|
+
}
|
|
246
|
+
case "hiddenPair": {
|
|
247
|
+
const digits = hint.hiddenDigits ?? [];
|
|
248
|
+
return `${hint.patternCells.map(eurekaCell).join(" and ")} are the only cells for digits {${digits.join(",")}}, so other candidates can be eliminated from those cells. This is a Hidden Pair.`;
|
|
249
|
+
}
|
|
250
|
+
case "hiddenTriple": {
|
|
251
|
+
const digits = hint.hiddenDigits ?? [];
|
|
252
|
+
return `${hint.patternCells.map(eurekaCell).join(", ")} are the only cells for digits {${digits.join(",")}}, so other candidates can be eliminated from those cells. This is a Hidden Triple.`;
|
|
253
|
+
}
|
|
254
|
+
case "hiddenQuad": {
|
|
255
|
+
const digits = hint.hiddenDigits ?? [];
|
|
256
|
+
return `${hint.patternCells.map(eurekaCell).join(", ")} are the only cells for digits {${digits.join(",")}}, so other candidates can be eliminated from those cells. This is a Hidden Quad.`;
|
|
257
|
+
}
|
|
258
|
+
case "xWing": {
|
|
259
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
260
|
+
const cells = hint.patternCells;
|
|
261
|
+
const rows = [...new Set(cells.map(cellRow$1))].sort((a, b) => a - b);
|
|
262
|
+
const cols = [...new Set(cells.map(cellCol$1))].sort((a, b) => a - b);
|
|
263
|
+
if (rows.length === 2 && cols.length === 2) return `${digit} forms an X-Wing pattern, in rows ${rows.map((r) => r + 1).join(" and ")} is confined to columns ${cols.map((c) => c + 1).join(" and ")}, eliminating ${digit} from the rest of those columns.`;
|
|
264
|
+
return `${digit} forms an X-Wing pattern, eliminating ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
265
|
+
}
|
|
266
|
+
case "swordfish": {
|
|
267
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
268
|
+
return `${digit} forms a Swordfish pattern, eliminating ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
269
|
+
}
|
|
270
|
+
case "jellyfish": {
|
|
271
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
272
|
+
return `${digit} forms a Jellyfish pattern, eliminating ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
273
|
+
}
|
|
274
|
+
case "finnedXWing": {
|
|
275
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
276
|
+
return `${digit} forms a Finned X-Wing pattern. Eliminates ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
277
|
+
}
|
|
278
|
+
case "finnedSwordfish": {
|
|
279
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
280
|
+
return `${digit} forms a Finned Swordfish pattern. Eliminates ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
281
|
+
}
|
|
282
|
+
case "finnedJellyfish": {
|
|
283
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
284
|
+
return `${digit} forms a Finned Jellyfish pattern. Eliminates ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
285
|
+
}
|
|
286
|
+
case "skyscraper": {
|
|
287
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
288
|
+
const cells = hint.patternCells;
|
|
289
|
+
const rows = [...new Set(cells.map(cellRow$1))].sort((a, b) => a - b);
|
|
290
|
+
const cols = [...new Set(cells.map(cellCol$1))].sort((a, b) => a - b);
|
|
291
|
+
if (rows.length === 2) return `${digit} forms a skyscraper in rows ${rows.map((r) => r + 1).join(" and ")} with a shared column. Eliminates ${digit} from cells seeing both roof cells.`;
|
|
292
|
+
return `${digit} forms a skyscraper in columns ${cols.map((c) => c + 1).join(" and ")} with a shared row. Eliminates ${digit} from cells seeing both roof cells.`;
|
|
293
|
+
}
|
|
294
|
+
case "twoStringKite": {
|
|
295
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
296
|
+
return `${digit} forms a kite with a conjugate row and column sharing a box. Eliminates ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
297
|
+
}
|
|
298
|
+
case "emptyRectangle": {
|
|
299
|
+
const digit = hint.eliminations[0]?.digit ?? 0;
|
|
300
|
+
return `${digit} in box ${cellBox$1(hint.patternCells.slice(0, hint.patternCells.length - 2)[0]) + 1} forms an empty rectangle. Eliminates ${digit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
301
|
+
}
|
|
302
|
+
case "wWing": {
|
|
303
|
+
const [c1, c2] = hint.patternCells;
|
|
304
|
+
const elimDigit = hint.eliminations[0]?.digit ?? 0;
|
|
305
|
+
return `W-Wing cells ${eurekaCell(c1)} and ${eurekaCell(c2)} share candidates connected via a strong link. Eliminates ${elimDigit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
306
|
+
}
|
|
307
|
+
case "yWing": {
|
|
308
|
+
const [pivot, p1, p2] = hint.patternCells;
|
|
309
|
+
const elimDigit = hint.eliminations[0]?.digit ?? 0;
|
|
310
|
+
return `Y-Wing pivot ${eurekaCell(pivot)} links pincers ${eurekaCell(p1)} and ${eurekaCell(p2)}. Eliminates ${elimDigit} from cells seeing both pincers.`;
|
|
311
|
+
}
|
|
312
|
+
case "xyzWing": {
|
|
313
|
+
const [pivot, p1, p2] = hint.patternCells;
|
|
314
|
+
const elimDigit = hint.eliminations[0]?.digit ?? 0;
|
|
315
|
+
return `XYZ-Wing pivot ${eurekaCell(pivot)} with wings ${eurekaCell(p1)} and ${eurekaCell(p2)}. Eliminates ${elimDigit} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
316
|
+
}
|
|
317
|
+
case "uniqueRectangleType1": {
|
|
318
|
+
const rows = [...new Set(hint.patternCells.map(cellRow$1))].sort().map((r) => r + 1);
|
|
319
|
+
const cols = [...new Set(hint.patternCells.map(cellCol$1))].sort().map((c) => c + 1);
|
|
320
|
+
return `The UR(${(hint.patternDigits ?? []).join(",")}) pattern in rows ${rows.join(",")} cols ${cols.join(",")} has floor digits that cannot all be the solution. Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
321
|
+
}
|
|
322
|
+
case "uniqueRectangleType2": return `The UR(${(hint.patternDigits ?? []).join(",")}) Type 2 pattern forces an extra digit. Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
323
|
+
case "uniqueRectangleType3": return `The UR(${(hint.patternDigits ?? []).join(",")}) Type 3 pattern forms a naked group. Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
324
|
+
case "uniqueRectangleType4": return `A floor digit is locked within the UR(${(hint.patternDigits ?? []).join(",")}) Type 4, eliminating the other floor digit from extra cells. Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
325
|
+
case "uniqueRectangleType5": return `Unique Rectangle Type 5: Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
326
|
+
case "bug": {
|
|
327
|
+
const { cell, digit } = hint.placements[0];
|
|
328
|
+
return `BUG+1: All unsolved cells are bivalue except ${eurekaCell(cell)}. Placing ${digit} in ${eurekaCell(cell)} resolves the BUG.`;
|
|
329
|
+
}
|
|
330
|
+
case "xyChain": return `XY-Chain: The alternating bivalue chain eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
331
|
+
case "aic": return `AIC: The alternating inference chain eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
332
|
+
case "aicRing": return `AIC Ring: The chain forms a closed loop. Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
333
|
+
case "groupedAIC": return `Grouped AIC: The grouped alternating inference chain eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
334
|
+
case "alsXZ": return `ALS-XZ: Two Almost Locked Sets share a restricted common, eliminating ${hint.eliminations[0]?.digit ?? 0} from ${hint.eliminations.map((e) => eurekaCell(e.cell)).join(", ")}.`;
|
|
335
|
+
case "sueDeCoq": return `Sue de Coq: The two-sector disjoint subset pattern eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
336
|
+
case "deathBlossom": return `Death Blossom: The stem cell's ALS petals lock digits, eliminating ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
337
|
+
case "nishio": return `Nishio: Assuming ${hint.eliminations[0]?.digit} in ${eurekaCell(hint.eliminations[0]?.cell ?? 0)} leads to a contradiction.`;
|
|
338
|
+
case "nishioNet": return `Nishio Net: Assuming ${hint.eliminations[0]?.digit} in ${eurekaCell(hint.eliminations[0]?.cell ?? 0)} leads to a contradiction via full propagation.`;
|
|
339
|
+
case "cellRegionForcingChain":
|
|
340
|
+
if (hint.placements.length > 0) {
|
|
341
|
+
const { cell, digit } = hint.placements[0];
|
|
342
|
+
return `Cell/Region Forcing Chain: All branches force ${digit} into ${eurekaCell(cell)}.`;
|
|
343
|
+
}
|
|
344
|
+
return `Cell/Region Forcing Chain: Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
345
|
+
case "cellRegionForcingNet":
|
|
346
|
+
if (hint.placements.length > 0) {
|
|
347
|
+
const { cell, digit } = hint.placements[0];
|
|
348
|
+
return `Cell/Region Forcing Net: All branches force ${digit} into ${eurekaCell(cell)}.`;
|
|
349
|
+
}
|
|
350
|
+
return `Cell/Region Forcing Net: Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
351
|
+
case "forcingChain":
|
|
352
|
+
if (hint.placements.length > 0) {
|
|
353
|
+
const { cell, digit } = hint.placements[0];
|
|
354
|
+
return `Forcing Chain: Both branches from the bivalue cell force ${digit} into ${eurekaCell(cell)}.`;
|
|
355
|
+
}
|
|
356
|
+
return `Forcing Chain: Eliminates ${hint.eliminations.map((e) => `${e.digit} from ${eurekaCell(e.cell)}`).join(", ")}.`;
|
|
357
|
+
default: return "";
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/humanSolver.ts
|
|
362
|
+
const withNotation = (hint) => {
|
|
363
|
+
if (hint === null) return null;
|
|
364
|
+
return {
|
|
365
|
+
...hint,
|
|
366
|
+
eureka: buildEureka(hint),
|
|
367
|
+
explanation: buildExplanation(hint)
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
const cellRow = (cell) => Math.floor(cell / 9);
|
|
371
|
+
const cellCol = (cell) => cell % 9;
|
|
372
|
+
const cellBox = (cell) => Math.floor(cellRow(cell) / 3) * 3 + Math.floor(cellCol(cell) / 3);
|
|
373
|
+
const buildHouses = () => {
|
|
374
|
+
const houses = [];
|
|
375
|
+
for (let r = 0; r < 9; r++) {
|
|
376
|
+
const row = [];
|
|
377
|
+
for (let c = 0; c < 9; c++) row.push(r * 9 + c);
|
|
378
|
+
houses.push(row);
|
|
379
|
+
}
|
|
380
|
+
for (let c = 0; c < 9; c++) {
|
|
381
|
+
const col = [];
|
|
382
|
+
for (let r = 0; r < 9; r++) col.push(r * 9 + c);
|
|
383
|
+
houses.push(col);
|
|
384
|
+
}
|
|
385
|
+
for (let br = 0; br < 3; br++) for (let bc = 0; bc < 3; bc++) {
|
|
386
|
+
const box = [];
|
|
387
|
+
for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) box.push((br * 3 + r) * 9 + (bc * 3 + c));
|
|
388
|
+
houses.push(box);
|
|
389
|
+
}
|
|
390
|
+
return houses;
|
|
391
|
+
};
|
|
392
|
+
const HOUSES = buildHouses();
|
|
393
|
+
const ROW_HOUSES = HOUSES.slice(0, 9);
|
|
394
|
+
const COL_HOUSES = HOUSES.slice(9, 18);
|
|
395
|
+
const BOX_HOUSES = HOUSES.slice(18, 27);
|
|
396
|
+
const buildPeers = () => {
|
|
397
|
+
const peers = [];
|
|
398
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
399
|
+
const p = /* @__PURE__ */ new Set();
|
|
400
|
+
const r = cellRow(cell), c = cellCol(cell), b = cellBox(cell);
|
|
401
|
+
for (const c2 of [
|
|
402
|
+
...ROW_HOUSES[r],
|
|
403
|
+
...COL_HOUSES[c],
|
|
404
|
+
...BOX_HOUSES[b]
|
|
405
|
+
]) if (c2 !== cell) p.add(c2);
|
|
406
|
+
peers.push(p);
|
|
407
|
+
}
|
|
408
|
+
return peers;
|
|
409
|
+
};
|
|
410
|
+
const PEERS = buildPeers();
|
|
411
|
+
const puzzleToGrid = (puzzle) => {
|
|
412
|
+
const grid = new Array(81).fill(0);
|
|
413
|
+
for (let row = 0; row < 9; row++) for (let col = 0; col < 9; col++) {
|
|
414
|
+
const boxX = Math.floor(col / 3);
|
|
415
|
+
const boxY = Math.floor(row / 3);
|
|
416
|
+
const cellX = col % 3;
|
|
417
|
+
const cellY = row % 3;
|
|
418
|
+
const val = puzzle[boxX][boxY][cellX][cellY];
|
|
419
|
+
grid[row * 9 + col] = typeof val === "number" ? val : 0;
|
|
420
|
+
}
|
|
421
|
+
return grid;
|
|
422
|
+
};
|
|
423
|
+
const gridToPuzzle = (grid) => {
|
|
424
|
+
const puzzle = {
|
|
425
|
+
0: {
|
|
426
|
+
0: {
|
|
427
|
+
0: [],
|
|
428
|
+
1: [],
|
|
429
|
+
2: []
|
|
430
|
+
},
|
|
431
|
+
1: {
|
|
432
|
+
0: [],
|
|
433
|
+
1: [],
|
|
434
|
+
2: []
|
|
435
|
+
},
|
|
436
|
+
2: {
|
|
437
|
+
0: [],
|
|
438
|
+
1: [],
|
|
439
|
+
2: []
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
1: {
|
|
443
|
+
0: {
|
|
444
|
+
0: [],
|
|
445
|
+
1: [],
|
|
446
|
+
2: []
|
|
447
|
+
},
|
|
448
|
+
1: {
|
|
449
|
+
0: [],
|
|
450
|
+
1: [],
|
|
451
|
+
2: []
|
|
452
|
+
},
|
|
453
|
+
2: {
|
|
454
|
+
0: [],
|
|
455
|
+
1: [],
|
|
456
|
+
2: []
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
2: {
|
|
460
|
+
0: {
|
|
461
|
+
0: [],
|
|
462
|
+
1: [],
|
|
463
|
+
2: []
|
|
464
|
+
},
|
|
465
|
+
1: {
|
|
466
|
+
0: [],
|
|
467
|
+
1: [],
|
|
468
|
+
2: []
|
|
469
|
+
},
|
|
470
|
+
2: {
|
|
471
|
+
0: [],
|
|
472
|
+
1: [],
|
|
473
|
+
2: []
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
for (let row = 0; row < 9; row++) for (let col = 0; col < 9; col++) {
|
|
478
|
+
const boxX = Math.floor(col / 3);
|
|
479
|
+
const boxY = Math.floor(row / 3);
|
|
480
|
+
const cellX = col % 3;
|
|
481
|
+
const cellY = row % 3;
|
|
482
|
+
if (!puzzle[boxX][boxY][cellX]) puzzle[boxX][boxY][cellX] = [];
|
|
483
|
+
puzzle[boxX][boxY][cellX][cellY] = grid[row * 9 + col];
|
|
484
|
+
}
|
|
485
|
+
return puzzle;
|
|
486
|
+
};
|
|
487
|
+
const buildCandidates = (grid) => {
|
|
488
|
+
const candidates = [];
|
|
489
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
490
|
+
if (grid[cell] !== 0) {
|
|
491
|
+
candidates.push(/* @__PURE__ */ new Set());
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const possible = new Set([
|
|
495
|
+
1,
|
|
496
|
+
2,
|
|
497
|
+
3,
|
|
498
|
+
4,
|
|
499
|
+
5,
|
|
500
|
+
6,
|
|
501
|
+
7,
|
|
502
|
+
8,
|
|
503
|
+
9
|
|
504
|
+
]);
|
|
505
|
+
for (const peer of PEERS[cell]) if (grid[peer] !== 0) possible.delete(grid[peer]);
|
|
506
|
+
candidates.push(possible);
|
|
507
|
+
}
|
|
508
|
+
return candidates;
|
|
509
|
+
};
|
|
510
|
+
const isGridInvalid = (grid) => {
|
|
511
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
512
|
+
const val = grid[cell];
|
|
513
|
+
if (val === 0) continue;
|
|
514
|
+
for (const peer of PEERS[cell]) if (grid[peer] === val) return true;
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
};
|
|
518
|
+
const combinations = (arr, size) => {
|
|
519
|
+
if (size === 0) return [[]];
|
|
520
|
+
if (arr.length < size) return [];
|
|
521
|
+
const [first, ...rest] = arr;
|
|
522
|
+
return [...combinations(rest, size - 1).map((c) => [first, ...c]), ...combinations(rest, size)];
|
|
523
|
+
};
|
|
524
|
+
const findNakedSingle = (candidates) => {
|
|
525
|
+
for (let cell = 0; cell < 81; cell++) if (candidates[cell].size === 1) {
|
|
526
|
+
const digit = [...candidates[cell]][0];
|
|
527
|
+
return {
|
|
528
|
+
technique: "nakedSingle",
|
|
529
|
+
placements: [{
|
|
530
|
+
cell,
|
|
531
|
+
digit
|
|
532
|
+
}],
|
|
533
|
+
eliminations: [],
|
|
534
|
+
patternCells: [cell]
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
};
|
|
539
|
+
const findHiddenSingle = (candidates, houses, technique) => {
|
|
540
|
+
for (const house of houses) for (let digit = 1; digit <= 9; digit++) {
|
|
541
|
+
const cells = house.filter((c) => candidates[c].has(digit));
|
|
542
|
+
if (cells.length === 1) return {
|
|
543
|
+
technique,
|
|
544
|
+
placements: [{
|
|
545
|
+
cell: cells[0],
|
|
546
|
+
digit
|
|
547
|
+
}],
|
|
548
|
+
eliminations: [],
|
|
549
|
+
patternCells: house
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
};
|
|
554
|
+
const findNakedGroup = (candidates, size) => {
|
|
555
|
+
const techMap = {
|
|
556
|
+
2: "nakedPair",
|
|
557
|
+
3: "nakedTriple",
|
|
558
|
+
4: "nakedQuad"
|
|
559
|
+
};
|
|
560
|
+
for (const house of HOUSES) {
|
|
561
|
+
const emptyCells = house.filter((c) => candidates[c].size > 0);
|
|
562
|
+
if (emptyCells.length <= size) continue;
|
|
563
|
+
for (const group of combinations(emptyCells, size)) {
|
|
564
|
+
const union = /* @__PURE__ */ new Set();
|
|
565
|
+
for (const c of group) for (const d of candidates[c]) union.add(d);
|
|
566
|
+
if (union.size !== size) continue;
|
|
567
|
+
if (group.some((c) => candidates[c].size === 0)) continue;
|
|
568
|
+
const eliminations = [];
|
|
569
|
+
for (const c of house) {
|
|
570
|
+
if (group.includes(c)) continue;
|
|
571
|
+
for (const d of union) if (candidates[c].has(d)) eliminations.push({
|
|
572
|
+
cell: c,
|
|
573
|
+
digit: d
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (eliminations.length > 0) return {
|
|
577
|
+
technique: techMap[size],
|
|
578
|
+
placements: [],
|
|
579
|
+
eliminations,
|
|
580
|
+
patternCells: group
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
};
|
|
586
|
+
const findHiddenGroup = (candidates, size) => {
|
|
587
|
+
const techMap = {
|
|
588
|
+
2: "hiddenPair",
|
|
589
|
+
3: "hiddenTriple",
|
|
590
|
+
4: "hiddenQuad"
|
|
591
|
+
};
|
|
592
|
+
for (const house of HOUSES) {
|
|
593
|
+
const digitCells = /* @__PURE__ */ new Map();
|
|
594
|
+
for (let d = 1; d <= 9; d++) {
|
|
595
|
+
const cells = house.filter((c) => candidates[c].has(d));
|
|
596
|
+
if (cells.length >= 2 && cells.length <= size) digitCells.set(d, cells);
|
|
597
|
+
}
|
|
598
|
+
if (digitCells.size < size) continue;
|
|
599
|
+
for (const digits of combinations([...digitCells.keys()], size)) {
|
|
600
|
+
const cellSet = /* @__PURE__ */ new Set();
|
|
601
|
+
for (const d of digits) for (const c of digitCells.get(d)) cellSet.add(c);
|
|
602
|
+
if (cellSet.size !== size) continue;
|
|
603
|
+
const digitSet = new Set(digits);
|
|
604
|
+
const eliminations = [];
|
|
605
|
+
for (const c of cellSet) for (const d of candidates[c]) if (!digitSet.has(d)) eliminations.push({
|
|
606
|
+
cell: c,
|
|
607
|
+
digit: d
|
|
608
|
+
});
|
|
609
|
+
if (eliminations.length > 0) return {
|
|
610
|
+
technique: techMap[size],
|
|
611
|
+
placements: [],
|
|
612
|
+
eliminations,
|
|
613
|
+
patternCells: [...cellSet],
|
|
614
|
+
hiddenDigits: [...digitSet].sort((a, b) => a - b)
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
};
|
|
620
|
+
const findLockedCandidates = (candidates) => {
|
|
621
|
+
for (const box of BOX_HOUSES) for (let digit = 1; digit <= 9; digit++) {
|
|
622
|
+
const cells = box.filter((c) => candidates[c].has(digit));
|
|
623
|
+
if (cells.length < 2) continue;
|
|
624
|
+
const rows = new Set(cells.map(cellRow));
|
|
625
|
+
const cols = new Set(cells.map(cellCol));
|
|
626
|
+
if (rows.size === 1) {
|
|
627
|
+
const eliminations = ROW_HOUSES[[...rows][0]].filter((c) => !box.includes(c) && candidates[c].has(digit)).map((c) => ({
|
|
628
|
+
cell: c,
|
|
629
|
+
digit
|
|
630
|
+
}));
|
|
631
|
+
if (eliminations.length > 0) return {
|
|
632
|
+
technique: "lockedCandidatePointing",
|
|
633
|
+
placements: [],
|
|
634
|
+
eliminations,
|
|
635
|
+
patternCells: cells
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (cols.size === 1) {
|
|
639
|
+
const eliminations = COL_HOUSES[[...cols][0]].filter((c) => !box.includes(c) && candidates[c].has(digit)).map((c) => ({
|
|
640
|
+
cell: c,
|
|
641
|
+
digit
|
|
642
|
+
}));
|
|
643
|
+
if (eliminations.length > 0) return {
|
|
644
|
+
technique: "lockedCandidatePointing",
|
|
645
|
+
placements: [],
|
|
646
|
+
eliminations,
|
|
647
|
+
patternCells: cells
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
for (const line of [...ROW_HOUSES, ...COL_HOUSES]) for (let digit = 1; digit <= 9; digit++) {
|
|
652
|
+
const cells = line.filter((c) => candidates[c].has(digit));
|
|
653
|
+
if (cells.length < 2) continue;
|
|
654
|
+
const boxes = new Set(cells.map(cellBox));
|
|
655
|
+
if (boxes.size === 1) {
|
|
656
|
+
const eliminations = BOX_HOUSES[[...boxes][0]].filter((c) => !line.includes(c) && candidates[c].has(digit)).map((c) => ({
|
|
657
|
+
cell: c,
|
|
658
|
+
digit
|
|
659
|
+
}));
|
|
660
|
+
if (eliminations.length > 0) return {
|
|
661
|
+
technique: "lockedCandidateClaiming",
|
|
662
|
+
placements: [],
|
|
663
|
+
eliminations,
|
|
664
|
+
patternCells: cells
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
};
|
|
670
|
+
const findFish = (candidates, size) => {
|
|
671
|
+
const techMap = {
|
|
672
|
+
2: "xWing",
|
|
673
|
+
3: "swordfish",
|
|
674
|
+
4: "jellyfish"
|
|
675
|
+
};
|
|
676
|
+
for (let digit = 1; digit <= 9; digit++) {
|
|
677
|
+
const linePairs = [[ROW_HOUSES, COL_HOUSES], [COL_HOUSES, ROW_HOUSES]];
|
|
678
|
+
for (const [baseLines, coverLines] of linePairs) {
|
|
679
|
+
const qualifying = baseLines.map((line, idx) => ({
|
|
680
|
+
idx,
|
|
681
|
+
cells: line.filter((c) => candidates[c].has(digit))
|
|
682
|
+
})).filter((l) => l.cells.length >= 2 && l.cells.length <= size);
|
|
683
|
+
if (qualifying.length < size) continue;
|
|
684
|
+
for (const baseCombo of combinations(qualifying, size)) {
|
|
685
|
+
const baseCells = baseCombo.flatMap((l) => l.cells);
|
|
686
|
+
const coverIdxSet = new Set(baseLines === ROW_HOUSES ? baseCells.map(cellCol) : baseCells.map(cellRow));
|
|
687
|
+
if (coverIdxSet.size !== size) continue;
|
|
688
|
+
const eliminations = [];
|
|
689
|
+
for (const coverIdx of coverIdxSet) for (const c of coverLines[coverIdx]) if (!baseCells.includes(c) && candidates[c].has(digit)) eliminations.push({
|
|
690
|
+
cell: c,
|
|
691
|
+
digit
|
|
692
|
+
});
|
|
693
|
+
if (eliminations.length > 0) return {
|
|
694
|
+
technique: techMap[size],
|
|
695
|
+
placements: [],
|
|
696
|
+
eliminations,
|
|
697
|
+
patternCells: baseCells
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
};
|
|
704
|
+
const findFinnedFish = (candidates, size) => {
|
|
705
|
+
const techMap = {
|
|
706
|
+
2: "finnedXWing",
|
|
707
|
+
3: "finnedSwordfish",
|
|
708
|
+
4: "finnedJellyfish"
|
|
709
|
+
};
|
|
710
|
+
for (let digit = 1; digit <= 9; digit++) for (const [baseLines, coverLines] of [[ROW_HOUSES, COL_HOUSES], [COL_HOUSES, ROW_HOUSES]]) {
|
|
711
|
+
const coverPerp = baseLines === ROW_HOUSES ? (c) => cellCol(c) : (c) => cellRow(c);
|
|
712
|
+
const qualifying = baseLines.map((line, idx) => ({
|
|
713
|
+
idx,
|
|
714
|
+
cells: line.filter((c) => candidates[c].has(digit))
|
|
715
|
+
})).filter((l) => l.cells.length >= 2);
|
|
716
|
+
if (qualifying.length < size) continue;
|
|
717
|
+
for (const baseCombo of combinations(qualifying, size)) {
|
|
718
|
+
const allBaseCells = baseCombo.flatMap((l) => l.cells);
|
|
719
|
+
const allCoverArr = [...new Set(allBaseCells.map(coverPerp))];
|
|
720
|
+
for (const coreCoverCombo of combinations(allCoverArr.map((idx) => ({ idx })), size)) {
|
|
721
|
+
const coreCoverIdxs = new Set(coreCoverCombo.map((x) => x.idx));
|
|
722
|
+
if (baseCombo.some((l) => !l.cells.some((c) => coreCoverIdxs.has(coverPerp(c))))) continue;
|
|
723
|
+
const finCells = allBaseCells.filter((c) => !coreCoverIdxs.has(coverPerp(c)));
|
|
724
|
+
if (finCells.length === 0) continue;
|
|
725
|
+
const finBoxes = new Set(finCells.map(cellBox));
|
|
726
|
+
if (finBoxes.size !== 1) continue;
|
|
727
|
+
const finBox = [...finBoxes][0];
|
|
728
|
+
const baseIdxs = baseCombo.map((l) => l.idx);
|
|
729
|
+
let anchorFound = false;
|
|
730
|
+
anchorLoop: for (const baseIdx of baseIdxs) for (const coverIdx of coreCoverIdxs) if (cellBox(baseLines === ROW_HOUSES ? baseIdx * 9 + coverIdx : coverIdx * 9 + baseIdx) === finBox) {
|
|
731
|
+
anchorFound = true;
|
|
732
|
+
break anchorLoop;
|
|
733
|
+
}
|
|
734
|
+
if (!anchorFound) continue;
|
|
735
|
+
const eliminations = [];
|
|
736
|
+
for (const coverIdx of coreCoverIdxs) for (const c of coverLines[coverIdx]) {
|
|
737
|
+
if (allBaseCells.includes(c)) continue;
|
|
738
|
+
if (!candidates[c].has(digit)) continue;
|
|
739
|
+
if (cellBox(c) === finBox) eliminations.push({
|
|
740
|
+
cell: c,
|
|
741
|
+
digit
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
if (eliminations.length > 0) return {
|
|
745
|
+
technique: techMap[size],
|
|
746
|
+
placements: [],
|
|
747
|
+
eliminations,
|
|
748
|
+
patternCells: [...allBaseCells]
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return null;
|
|
754
|
+
};
|
|
755
|
+
const findSkyscraper = (candidates) => {
|
|
756
|
+
for (let digit = 1; digit <= 9; digit++) for (const [baseLines, perpFn] of [[ROW_HOUSES, cellCol], [COL_HOUSES, cellRow]]) {
|
|
757
|
+
const qualifying = baseLines.map((line, idx) => ({
|
|
758
|
+
idx,
|
|
759
|
+
cells: line.filter((c) => candidates[c].has(digit))
|
|
760
|
+
})).filter((l) => l.cells.length === 2);
|
|
761
|
+
for (let i = 0; i < qualifying.length - 1; i++) for (let j = i + 1; j < qualifying.length; j++) {
|
|
762
|
+
const lineA = qualifying[i];
|
|
763
|
+
const lineB = qualifying[j];
|
|
764
|
+
const aPerp = lineA.cells.map(perpFn);
|
|
765
|
+
const bPerp = lineB.cells.map(perpFn);
|
|
766
|
+
let sharedPerp = -1;
|
|
767
|
+
let aRoof = -1;
|
|
768
|
+
let bRoof = -1;
|
|
769
|
+
for (let ai = 0; ai < 2; ai++) for (let bi = 0; bi < 2; bi++) if (aPerp[ai] === bPerp[bi]) {
|
|
770
|
+
sharedPerp = aPerp[ai];
|
|
771
|
+
aRoof = lineA.cells[1 - ai];
|
|
772
|
+
bRoof = lineB.cells[1 - bi];
|
|
773
|
+
}
|
|
774
|
+
if (sharedPerp === -1) continue;
|
|
775
|
+
const eliminations = [];
|
|
776
|
+
for (let c = 0; c < 81; c++) {
|
|
777
|
+
if (c === aRoof || c === bRoof) continue;
|
|
778
|
+
if (candidates[c].has(digit) && PEERS[aRoof].has(c) && PEERS[bRoof].has(c)) eliminations.push({
|
|
779
|
+
cell: c,
|
|
780
|
+
digit
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (eliminations.length > 0) return {
|
|
784
|
+
technique: "skyscraper",
|
|
785
|
+
placements: [],
|
|
786
|
+
eliminations,
|
|
787
|
+
patternCells: [...lineA.cells, ...lineB.cells]
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
};
|
|
793
|
+
const findTwoStringKite = (candidates) => {
|
|
794
|
+
for (let digit = 1; digit <= 9; digit++) for (const col of COL_HOUSES) {
|
|
795
|
+
const colCells = col.filter((c) => candidates[c].has(digit));
|
|
796
|
+
if (colCells.length !== 2) continue;
|
|
797
|
+
for (const row of ROW_HOUSES) {
|
|
798
|
+
const rowCells = row.filter((c) => candidates[c].has(digit));
|
|
799
|
+
if (rowCells.length !== 2) continue;
|
|
800
|
+
for (const colCell of colCells) for (const rowCell of rowCells) {
|
|
801
|
+
if (colCell === rowCell) continue;
|
|
802
|
+
if (cellBox(colCell) !== cellBox(rowCell)) continue;
|
|
803
|
+
const colRoof = colCells.find((c) => c !== colCell);
|
|
804
|
+
const rowRoof = rowCells.find((c) => c !== rowCell);
|
|
805
|
+
if (colRoof === rowRoof) continue;
|
|
806
|
+
const eliminations = [];
|
|
807
|
+
for (let c = 0; c < 81; c++) {
|
|
808
|
+
if (c === colRoof || c === rowRoof) continue;
|
|
809
|
+
if (candidates[c].has(digit) && PEERS[colRoof].has(c) && PEERS[rowRoof].has(c)) eliminations.push({
|
|
810
|
+
cell: c,
|
|
811
|
+
digit
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (eliminations.length > 0) return {
|
|
815
|
+
technique: "twoStringKite",
|
|
816
|
+
placements: [],
|
|
817
|
+
eliminations,
|
|
818
|
+
patternCells: [
|
|
819
|
+
colCell,
|
|
820
|
+
rowCell,
|
|
821
|
+
colRoof,
|
|
822
|
+
rowRoof
|
|
823
|
+
]
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return null;
|
|
829
|
+
};
|
|
830
|
+
const findEmptyRectangle = (candidates) => {
|
|
831
|
+
for (let digit = 1; digit <= 9; digit++) for (let boxIdx = 0; boxIdx < 9; boxIdx++) {
|
|
832
|
+
const box = BOX_HOUSES[boxIdx];
|
|
833
|
+
const boxCells = box.filter((c) => candidates[c].has(digit));
|
|
834
|
+
if (boxCells.length < 2 || boxCells.length > 4) continue;
|
|
835
|
+
const rows = new Set(boxCells.map(cellRow));
|
|
836
|
+
const cols = new Set(boxCells.map(cellCol));
|
|
837
|
+
const erCandidates = [];
|
|
838
|
+
for (const erRow of rows) for (const erCol of cols) if (boxCells.every((c) => cellRow(c) === erRow || cellCol(c) === erCol)) erCandidates.push({
|
|
839
|
+
erRow,
|
|
840
|
+
erCol
|
|
841
|
+
});
|
|
842
|
+
for (const { erRow, erCol } of erCandidates) {
|
|
843
|
+
for (let col = 0; col < 9; col++) {
|
|
844
|
+
const colCells = COL_HOUSES[col].filter((c) => candidates[c].has(digit));
|
|
845
|
+
if (colCells.length !== 2) continue;
|
|
846
|
+
const [ca, cb] = colCells;
|
|
847
|
+
for (const [pivot, strongEnd] of [[ca, cb], [cb, ca]]) {
|
|
848
|
+
if (cellRow(pivot) !== erRow) continue;
|
|
849
|
+
if (box.includes(pivot)) continue;
|
|
850
|
+
if (box.includes(strongEnd)) continue;
|
|
851
|
+
const target = cellRow(strongEnd) * 9 + erCol;
|
|
852
|
+
if (box.includes(target)) continue;
|
|
853
|
+
if (target === pivot || target === strongEnd) continue;
|
|
854
|
+
if (!candidates[target].has(digit)) continue;
|
|
855
|
+
return {
|
|
856
|
+
technique: "emptyRectangle",
|
|
857
|
+
placements: [],
|
|
858
|
+
eliminations: [{
|
|
859
|
+
cell: target,
|
|
860
|
+
digit
|
|
861
|
+
}],
|
|
862
|
+
patternCells: [
|
|
863
|
+
...boxCells,
|
|
864
|
+
pivot,
|
|
865
|
+
strongEnd
|
|
866
|
+
]
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
for (let row = 0; row < 9; row++) {
|
|
871
|
+
const rowCells = ROW_HOUSES[row].filter((c) => candidates[c].has(digit));
|
|
872
|
+
if (rowCells.length !== 2) continue;
|
|
873
|
+
const [ca, cb] = rowCells;
|
|
874
|
+
for (const [pivot, strongEnd] of [[ca, cb], [cb, ca]]) {
|
|
875
|
+
if (cellCol(pivot) !== erCol) continue;
|
|
876
|
+
if (box.includes(pivot)) continue;
|
|
877
|
+
if (box.includes(strongEnd)) continue;
|
|
878
|
+
const targetCol = cellCol(strongEnd);
|
|
879
|
+
const target = erRow * 9 + targetCol;
|
|
880
|
+
if (box.includes(target)) continue;
|
|
881
|
+
if (target === pivot || target === strongEnd) continue;
|
|
882
|
+
if (!candidates[target].has(digit)) continue;
|
|
883
|
+
return {
|
|
884
|
+
technique: "emptyRectangle",
|
|
885
|
+
placements: [],
|
|
886
|
+
eliminations: [{
|
|
887
|
+
cell: target,
|
|
888
|
+
digit
|
|
889
|
+
}],
|
|
890
|
+
patternCells: [
|
|
891
|
+
...boxCells,
|
|
892
|
+
pivot,
|
|
893
|
+
strongEnd
|
|
894
|
+
]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
};
|
|
902
|
+
const findWWing = (candidates) => {
|
|
903
|
+
const bivalue = Array.from({ length: 81 }, (_, c) => c).filter((c) => candidates[c].size === 2);
|
|
904
|
+
for (let i = 0; i < bivalue.length - 1; i++) for (let j = i + 1; j < bivalue.length; j++) {
|
|
905
|
+
const c1 = bivalue[i];
|
|
906
|
+
const c2 = bivalue[j];
|
|
907
|
+
const cands1 = [...candidates[c1]];
|
|
908
|
+
const cands2 = [...candidates[c2]];
|
|
909
|
+
if (cands1[0] !== cands2[0] || cands1[1] !== cands2[1]) continue;
|
|
910
|
+
const [dA, dB] = cands1;
|
|
911
|
+
for (const linkDigit of [dA, dB]) {
|
|
912
|
+
const elimDigit = linkDigit === dA ? dB : dA;
|
|
913
|
+
for (const house of HOUSES) {
|
|
914
|
+
const linkCells = house.filter((c) => candidates[c].has(linkDigit));
|
|
915
|
+
if (linkCells.length !== 2) continue;
|
|
916
|
+
const [lc1, lc2] = linkCells;
|
|
917
|
+
if (lc1 === c1 || lc1 === c2 || lc2 === c1 || lc2 === c2) continue;
|
|
918
|
+
if (!(PEERS[lc1].has(c1) && PEERS[lc2].has(c2) || PEERS[lc1].has(c2) && PEERS[lc2].has(c1))) continue;
|
|
919
|
+
const eliminations = [];
|
|
920
|
+
for (let c = 0; c < 81; c++) {
|
|
921
|
+
if (c === c1 || c === c2) continue;
|
|
922
|
+
if (candidates[c].has(elimDigit) && PEERS[c1].has(c) && PEERS[c2].has(c)) eliminations.push({
|
|
923
|
+
cell: c,
|
|
924
|
+
digit: elimDigit
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (eliminations.length > 0) return {
|
|
928
|
+
technique: "wWing",
|
|
929
|
+
placements: [],
|
|
930
|
+
eliminations,
|
|
931
|
+
patternCells: [
|
|
932
|
+
c1,
|
|
933
|
+
c2,
|
|
934
|
+
lc1,
|
|
935
|
+
lc2
|
|
936
|
+
],
|
|
937
|
+
patternDigits: [linkDigit]
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return null;
|
|
943
|
+
};
|
|
944
|
+
const findYWing = (candidates) => {
|
|
945
|
+
const bivalue = Array.from({ length: 81 }, (_, c) => c).filter((c) => candidates[c].size === 2);
|
|
946
|
+
for (const pivot of bivalue) {
|
|
947
|
+
const [a, b] = [...candidates[pivot]];
|
|
948
|
+
const pincers = bivalue.filter((c) => c !== pivot && PEERS[pivot].has(c) && (candidates[c].has(a) || candidates[c].has(b)));
|
|
949
|
+
for (const p1 of pincers) for (const p2 of pincers) {
|
|
950
|
+
if (p2 <= p1) continue;
|
|
951
|
+
const p1other = [...candidates[p1]].find((d) => d !== a && d !== b);
|
|
952
|
+
const p2other = [...candidates[p2]].find((d) => d !== a && d !== b);
|
|
953
|
+
if (p1other === void 0 || p2other === void 0 || p1other !== p2other) continue;
|
|
954
|
+
if (candidates[p1].has(a) && candidates[p1].has(b) || candidates[p2].has(a) && candidates[p2].has(b)) continue;
|
|
955
|
+
if (candidates[p1].has(a) === candidates[p2].has(a)) continue;
|
|
956
|
+
const elimDigit = p1other;
|
|
957
|
+
const eliminations = [];
|
|
958
|
+
for (let c = 0; c < 81; c++) {
|
|
959
|
+
if (c === pivot || c === p1 || c === p2) continue;
|
|
960
|
+
if (PEERS[p1].has(c) && PEERS[p2].has(c) && candidates[c].has(elimDigit)) eliminations.push({
|
|
961
|
+
cell: c,
|
|
962
|
+
digit: elimDigit
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
if (eliminations.length > 0) {
|
|
966
|
+
const p1shared = candidates[p1].has(a) ? a : b;
|
|
967
|
+
const p2shared = p1shared === a ? b : a;
|
|
968
|
+
return {
|
|
969
|
+
technique: "yWing",
|
|
970
|
+
placements: [],
|
|
971
|
+
eliminations,
|
|
972
|
+
patternCells: [
|
|
973
|
+
pivot,
|
|
974
|
+
p1,
|
|
975
|
+
p2
|
|
976
|
+
],
|
|
977
|
+
chainPath: [
|
|
978
|
+
{
|
|
979
|
+
cell: p1,
|
|
980
|
+
digit: elimDigit,
|
|
981
|
+
isOn: false,
|
|
982
|
+
linkToNext: "strong"
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
cell: p1,
|
|
986
|
+
digit: p1shared,
|
|
987
|
+
isOn: true,
|
|
988
|
+
linkToNext: "weak"
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
cell: pivot,
|
|
992
|
+
digit: p1shared,
|
|
993
|
+
isOn: false,
|
|
994
|
+
linkToNext: "strong"
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
cell: pivot,
|
|
998
|
+
digit: p2shared,
|
|
999
|
+
isOn: true,
|
|
1000
|
+
linkToNext: "weak"
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
cell: p2,
|
|
1004
|
+
digit: p2shared,
|
|
1005
|
+
isOn: false,
|
|
1006
|
+
linkToNext: "strong"
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
cell: p2,
|
|
1010
|
+
digit: elimDigit,
|
|
1011
|
+
isOn: true
|
|
1012
|
+
}
|
|
1013
|
+
]
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return null;
|
|
1019
|
+
};
|
|
1020
|
+
const findXYZWing = (candidates) => {
|
|
1021
|
+
for (let pivot = 0; pivot < 81; pivot++) {
|
|
1022
|
+
if (candidates[pivot].size !== 3) continue;
|
|
1023
|
+
const [a, b, c] = [...candidates[pivot]];
|
|
1024
|
+
const eligiblePincers = [];
|
|
1025
|
+
for (const p of PEERS[pivot]) if (candidates[p].size === 2 && [...candidates[p]].every((d) => d === a || d === b || d === c)) eligiblePincers.push(p);
|
|
1026
|
+
if (eligiblePincers.length < 2) continue;
|
|
1027
|
+
for (const p1 of eligiblePincers) for (const p2 of eligiblePincers) {
|
|
1028
|
+
if (p2 <= p1) continue;
|
|
1029
|
+
if ([
|
|
1030
|
+
a,
|
|
1031
|
+
b,
|
|
1032
|
+
c
|
|
1033
|
+
].find((d) => !candidates[p1].has(d)) === [
|
|
1034
|
+
a,
|
|
1035
|
+
b,
|
|
1036
|
+
c
|
|
1037
|
+
].find((d) => !candidates[p2].has(d))) continue;
|
|
1038
|
+
let elimDigit = null;
|
|
1039
|
+
for (const d of [
|
|
1040
|
+
a,
|
|
1041
|
+
b,
|
|
1042
|
+
c
|
|
1043
|
+
]) if (candidates[pivot].has(d) && candidates[p1].has(d) && candidates[p2].has(d)) {
|
|
1044
|
+
elimDigit = d;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
if (elimDigit === null) continue;
|
|
1048
|
+
const elim = elimDigit;
|
|
1049
|
+
const eliminations = [];
|
|
1050
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1051
|
+
if (cell === pivot || cell === p1 || cell === p2) continue;
|
|
1052
|
+
if (PEERS[pivot].has(cell) && PEERS[p1].has(cell) && PEERS[p2].has(cell) && candidates[cell].has(elim)) eliminations.push({
|
|
1053
|
+
cell,
|
|
1054
|
+
digit: elim
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
if (eliminations.length > 0) {
|
|
1058
|
+
const notZ1 = [...candidates[p1]].find((d) => d !== elim);
|
|
1059
|
+
const notZ2 = [...candidates[p2]].find((d) => d !== elim);
|
|
1060
|
+
return {
|
|
1061
|
+
technique: "xyzWing",
|
|
1062
|
+
placements: [],
|
|
1063
|
+
eliminations,
|
|
1064
|
+
patternCells: [
|
|
1065
|
+
pivot,
|
|
1066
|
+
p1,
|
|
1067
|
+
p2
|
|
1068
|
+
],
|
|
1069
|
+
chainPath: [
|
|
1070
|
+
{
|
|
1071
|
+
cell: p1,
|
|
1072
|
+
digit: elim,
|
|
1073
|
+
isOn: false,
|
|
1074
|
+
linkToNext: "strong"
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
cell: p1,
|
|
1078
|
+
digit: notZ1,
|
|
1079
|
+
isOn: true,
|
|
1080
|
+
linkToNext: "weak"
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
cell: pivot,
|
|
1084
|
+
digit: notZ1,
|
|
1085
|
+
isOn: false,
|
|
1086
|
+
linkToNext: "strong"
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
cell: pivot,
|
|
1090
|
+
digit: notZ2,
|
|
1091
|
+
isOn: true,
|
|
1092
|
+
linkToNext: "weak"
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
cell: p2,
|
|
1096
|
+
digit: notZ2,
|
|
1097
|
+
isOn: false,
|
|
1098
|
+
linkToNext: "strong"
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
cell: p2,
|
|
1102
|
+
digit: elim,
|
|
1103
|
+
isOn: true
|
|
1104
|
+
}
|
|
1105
|
+
]
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return null;
|
|
1111
|
+
};
|
|
1112
|
+
const buildURCells = () => {
|
|
1113
|
+
const rects = [];
|
|
1114
|
+
for (let r1 = 0; r1 < 9; r1++) for (let r2 = r1 + 1; r2 < 9; r2++) for (let c1 = 0; c1 < 9; c1++) for (let c2 = c1 + 1; c2 < 9; c2++) {
|
|
1115
|
+
const cells = [
|
|
1116
|
+
r1 * 9 + c1,
|
|
1117
|
+
r1 * 9 + c2,
|
|
1118
|
+
r2 * 9 + c1,
|
|
1119
|
+
r2 * 9 + c2
|
|
1120
|
+
];
|
|
1121
|
+
if (new Set(cells.map(cellBox)).size === 2) rects.push(cells);
|
|
1122
|
+
}
|
|
1123
|
+
return rects;
|
|
1124
|
+
};
|
|
1125
|
+
const UR_RECTS = buildURCells();
|
|
1126
|
+
const findUniqueRectangle = (grid, candidates) => {
|
|
1127
|
+
for (const [c0, c1, c2, c3] of UR_RECTS) {
|
|
1128
|
+
if (grid[c0] !== 0 || grid[c1] !== 0 || grid[c2] !== 0 || grid[c3] !== 0) continue;
|
|
1129
|
+
const cands = [
|
|
1130
|
+
candidates[c0],
|
|
1131
|
+
candidates[c1],
|
|
1132
|
+
candidates[c2],
|
|
1133
|
+
candidates[c3]
|
|
1134
|
+
];
|
|
1135
|
+
const floor = new Set([
|
|
1136
|
+
1,
|
|
1137
|
+
2,
|
|
1138
|
+
3,
|
|
1139
|
+
4,
|
|
1140
|
+
5,
|
|
1141
|
+
6,
|
|
1142
|
+
7,
|
|
1143
|
+
8,
|
|
1144
|
+
9
|
|
1145
|
+
]);
|
|
1146
|
+
for (const s of cands) for (const d of floor) if (!s.has(d)) floor.delete(d);
|
|
1147
|
+
if (floor.size !== 2) continue;
|
|
1148
|
+
const extras = cands.map((s) => [...s].filter((d) => !floor.has(d)));
|
|
1149
|
+
const cells = [
|
|
1150
|
+
c0,
|
|
1151
|
+
c1,
|
|
1152
|
+
c2,
|
|
1153
|
+
c3
|
|
1154
|
+
];
|
|
1155
|
+
for (let i = 0; i < 4; i++) if (extras[i].length > 0 && extras.filter((_, j) => j !== i && extras[j].length === 0).length === 3) {
|
|
1156
|
+
const eliminations = [...floor].map((d) => ({
|
|
1157
|
+
cell: cells[i],
|
|
1158
|
+
digit: d
|
|
1159
|
+
}));
|
|
1160
|
+
if (eliminations.length > 0) return {
|
|
1161
|
+
technique: "uniqueRectangleType1",
|
|
1162
|
+
placements: [],
|
|
1163
|
+
eliminations,
|
|
1164
|
+
patternCells: cells,
|
|
1165
|
+
patternDigits: [...floor].sort((a, b) => a - b)
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
const extraCells = cells.filter((_, i) => extras[i].length === 1);
|
|
1169
|
+
const floorOnlyCount = cells.filter((_, i) => extras[i].length === 0).length;
|
|
1170
|
+
if (extraCells.length === 2 && floorOnlyCount === 2) {
|
|
1171
|
+
const [ec0, ec1] = extraCells;
|
|
1172
|
+
const i0 = cells.indexOf(ec0), i1 = cells.indexOf(ec1);
|
|
1173
|
+
if (extras[i0][0] === extras[i1][0]) {
|
|
1174
|
+
const extraDigit = extras[i0][0];
|
|
1175
|
+
const eliminations = [];
|
|
1176
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1177
|
+
if (cells.includes(cell)) continue;
|
|
1178
|
+
if (PEERS[ec0].has(cell) && PEERS[ec1].has(cell) && candidates[cell].has(extraDigit)) eliminations.push({
|
|
1179
|
+
cell,
|
|
1180
|
+
digit: extraDigit
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
if (eliminations.length > 0) return {
|
|
1184
|
+
technique: "uniqueRectangleType2",
|
|
1185
|
+
placements: [],
|
|
1186
|
+
eliminations,
|
|
1187
|
+
patternCells: cells,
|
|
1188
|
+
patternDigits: [...floor].sort((a, b) => a - b)
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (extraCells.length === 2 && floorOnlyCount === 2) {
|
|
1193
|
+
const [ec0, ec1] = extraCells;
|
|
1194
|
+
const i0 = cells.indexOf(ec0), i1 = cells.indexOf(ec1);
|
|
1195
|
+
const combinedExtras = new Set([...extras[i0], ...extras[i1]]);
|
|
1196
|
+
const sharedHouses = HOUSES.filter((h) => h.includes(ec0) && h.includes(ec1));
|
|
1197
|
+
for (const house of sharedHouses) {
|
|
1198
|
+
const groupSize = combinedExtras.size;
|
|
1199
|
+
if (groupSize < 1 || groupSize > 4) continue;
|
|
1200
|
+
const otherCells = house.filter((c) => !cells.includes(c) && candidates[c].size > 0);
|
|
1201
|
+
for (const extra of combinations(otherCells, groupSize - 1)) {
|
|
1202
|
+
const groupUnion = new Set(combinedExtras);
|
|
1203
|
+
for (const c of extra) for (const d of candidates[c]) groupUnion.add(d);
|
|
1204
|
+
if (groupUnion.size !== groupSize) continue;
|
|
1205
|
+
const groupCells = [
|
|
1206
|
+
ec0,
|
|
1207
|
+
ec1,
|
|
1208
|
+
...extra
|
|
1209
|
+
];
|
|
1210
|
+
const eliminations = [];
|
|
1211
|
+
for (const c of house) {
|
|
1212
|
+
if (groupCells.includes(c)) continue;
|
|
1213
|
+
for (const d of groupUnion) if (candidates[c].has(d)) eliminations.push({
|
|
1214
|
+
cell: c,
|
|
1215
|
+
digit: d
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (eliminations.length > 0) return {
|
|
1219
|
+
technique: "uniqueRectangleType3",
|
|
1220
|
+
placements: [],
|
|
1221
|
+
eliminations,
|
|
1222
|
+
patternCells: cells,
|
|
1223
|
+
patternDigits: [...floor].sort((a, b) => a - b)
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (extraCells.length === 2 && floorOnlyCount === 2) {
|
|
1229
|
+
const [ec0, ec1] = extraCells;
|
|
1230
|
+
const sharedHouses = HOUSES.filter((h) => h.includes(ec0) && h.includes(ec1));
|
|
1231
|
+
for (const house of sharedHouses) for (const floorDigit of floor) {
|
|
1232
|
+
const otherFloorDigit = [...floor].find((d) => d !== floorDigit);
|
|
1233
|
+
if (house.filter((c) => !cells.includes(c)).every((c) => !candidates[c].has(floorDigit))) {
|
|
1234
|
+
const eliminations = [ec0, ec1].filter((c) => candidates[c].has(otherFloorDigit)).map((c) => ({
|
|
1235
|
+
cell: c,
|
|
1236
|
+
digit: otherFloorDigit
|
|
1237
|
+
}));
|
|
1238
|
+
if (eliminations.length > 0) return {
|
|
1239
|
+
technique: "uniqueRectangleType4",
|
|
1240
|
+
placements: [],
|
|
1241
|
+
eliminations,
|
|
1242
|
+
patternCells: cells,
|
|
1243
|
+
patternDigits: [...floor].sort((a, b) => a - b)
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (cells.filter((_, i) => extras[i].length === 0).length === 3) {
|
|
1249
|
+
const extraCell = cells.find((_, i) => extras[i].length > 0);
|
|
1250
|
+
const extraIdx = cells.indexOf(extraCell);
|
|
1251
|
+
const diagCell = cells[extraIdx < 2 ? extraIdx + 2 : extraIdx - 2];
|
|
1252
|
+
for (const d of extras[extraIdx]) {
|
|
1253
|
+
const eliminations = [];
|
|
1254
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1255
|
+
if (cells.includes(cell)) continue;
|
|
1256
|
+
if (PEERS[extraCell].has(cell) && PEERS[diagCell].has(cell) && candidates[cell].has(d)) eliminations.push({
|
|
1257
|
+
cell,
|
|
1258
|
+
digit: d
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (eliminations.length > 0) return {
|
|
1262
|
+
technique: "uniqueRectangleType5",
|
|
1263
|
+
placements: [],
|
|
1264
|
+
eliminations,
|
|
1265
|
+
patternCells: cells
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
};
|
|
1272
|
+
const findBUG = (candidates) => {
|
|
1273
|
+
let trivialCell = -1;
|
|
1274
|
+
let trivialDigit = -1;
|
|
1275
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1276
|
+
if (candidates[cell].size === 0) continue;
|
|
1277
|
+
if (candidates[cell].size > 3) return null;
|
|
1278
|
+
if (candidates[cell].size === 3) {
|
|
1279
|
+
if (trivialCell !== -1) return null;
|
|
1280
|
+
trivialCell = cell;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (trivialCell === -1) return null;
|
|
1284
|
+
for (const d of candidates[trivialCell]) {
|
|
1285
|
+
let valid = true;
|
|
1286
|
+
for (const house of HOUSES) {
|
|
1287
|
+
if (!house.includes(trivialCell)) continue;
|
|
1288
|
+
if (house.filter((c) => candidates[c].has(d)).length !== 3) {
|
|
1289
|
+
valid = false;
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (valid) {
|
|
1294
|
+
if (Array.from({ length: 81 }, (_, c) => c).filter((c) => c !== trivialCell && candidates[c].size > 0).every((c) => candidates[c].size === 2)) {
|
|
1295
|
+
trivialDigit = d;
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (trivialDigit === -1) return null;
|
|
1301
|
+
return {
|
|
1302
|
+
technique: "bug",
|
|
1303
|
+
placements: [{
|
|
1304
|
+
cell: trivialCell,
|
|
1305
|
+
digit: trivialDigit
|
|
1306
|
+
}],
|
|
1307
|
+
eliminations: [],
|
|
1308
|
+
patternCells: [trivialCell]
|
|
1309
|
+
};
|
|
1310
|
+
};
|
|
1311
|
+
const findXYChain = (candidates) => {
|
|
1312
|
+
const bivalue = Array.from({ length: 81 }, (_, c) => c).filter((c) => candidates[c].size === 2);
|
|
1313
|
+
for (const start of bivalue) {
|
|
1314
|
+
const [dA, dB] = [...candidates[start]];
|
|
1315
|
+
for (const exitDigit of [dA, dB]) {
|
|
1316
|
+
const enterDigit = exitDigit === dA ? dB : dA;
|
|
1317
|
+
const stack = [{
|
|
1318
|
+
cell: start,
|
|
1319
|
+
exit: exitDigit,
|
|
1320
|
+
path: [start],
|
|
1321
|
+
exits: [exitDigit]
|
|
1322
|
+
}];
|
|
1323
|
+
while (stack.length > 0) {
|
|
1324
|
+
const { cell, exit, path, exits } = stack.pop();
|
|
1325
|
+
for (const next of bivalue) {
|
|
1326
|
+
if (path.includes(next)) continue;
|
|
1327
|
+
if (!PEERS[cell].has(next)) continue;
|
|
1328
|
+
if (!candidates[next].has(exit)) continue;
|
|
1329
|
+
const nextExit = [...candidates[next]].find((d) => d !== exit);
|
|
1330
|
+
const newPath = [...path, next];
|
|
1331
|
+
const newExits = [...exits, nextExit];
|
|
1332
|
+
if (newPath.length >= 4 && enterDigit === nextExit) {
|
|
1333
|
+
const elimDigit = enterDigit;
|
|
1334
|
+
const eliminations = [];
|
|
1335
|
+
for (let c = 0; c < 81; c++) {
|
|
1336
|
+
if (newPath.includes(c) || !candidates[c].has(elimDigit)) continue;
|
|
1337
|
+
if (PEERS[start].has(c) && PEERS[next].has(c)) eliminations.push({
|
|
1338
|
+
cell: c,
|
|
1339
|
+
digit: elimDigit
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (eliminations.length > 0) {
|
|
1343
|
+
const chainPath = [];
|
|
1344
|
+
for (let ci = 0; ci < newPath.length; ci++) {
|
|
1345
|
+
const c = newPath[ci];
|
|
1346
|
+
const cellEnter = ci === 0 ? enterDigit : newExits[ci - 1];
|
|
1347
|
+
const cellExit = newExits[ci];
|
|
1348
|
+
chainPath.push({
|
|
1349
|
+
cell: c,
|
|
1350
|
+
digit: cellEnter,
|
|
1351
|
+
isOn: false,
|
|
1352
|
+
linkToNext: "strong"
|
|
1353
|
+
});
|
|
1354
|
+
const isLast = ci === newPath.length - 1;
|
|
1355
|
+
chainPath.push({
|
|
1356
|
+
cell: c,
|
|
1357
|
+
digit: cellExit,
|
|
1358
|
+
isOn: true,
|
|
1359
|
+
linkToNext: isLast ? void 0 : "weak"
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
technique: "xyChain",
|
|
1364
|
+
placements: [],
|
|
1365
|
+
eliminations,
|
|
1366
|
+
patternCells: newPath,
|
|
1367
|
+
chainPath
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
stack.push({
|
|
1372
|
+
cell: next,
|
|
1373
|
+
exit: nextExit,
|
|
1374
|
+
path: newPath,
|
|
1375
|
+
exits: newExits
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return null;
|
|
1382
|
+
};
|
|
1383
|
+
const findAIC = (candidates, grid) => {
|
|
1384
|
+
const encode = (cell, digit) => cell * 9 + (digit - 1);
|
|
1385
|
+
const decodeCell = (node) => Math.floor(node / 9);
|
|
1386
|
+
const decodeDigit = (node) => node % 9 + 1;
|
|
1387
|
+
const strongLinks = /* @__PURE__ */ new Map();
|
|
1388
|
+
const addStrong = (a, b) => {
|
|
1389
|
+
if (!strongLinks.has(a)) strongLinks.set(a, /* @__PURE__ */ new Set());
|
|
1390
|
+
if (!strongLinks.has(b)) strongLinks.set(b, /* @__PURE__ */ new Set());
|
|
1391
|
+
strongLinks.get(a).add(b);
|
|
1392
|
+
strongLinks.get(b).add(a);
|
|
1393
|
+
};
|
|
1394
|
+
for (const house of HOUSES) for (let digit = 1; digit <= 9; digit++) {
|
|
1395
|
+
const cells = house.filter((c) => candidates[c].has(digit));
|
|
1396
|
+
if (cells.length === 2) addStrong(encode(cells[0], digit), encode(cells[1], digit));
|
|
1397
|
+
}
|
|
1398
|
+
for (let cell = 0; cell < 81; cell++) if (candidates[cell].size === 2) {
|
|
1399
|
+
const [d1, d2] = [...candidates[cell]];
|
|
1400
|
+
addStrong(encode(cell, d1), encode(cell, d2));
|
|
1401
|
+
}
|
|
1402
|
+
const weakNeighbors = (node) => {
|
|
1403
|
+
const cell = decodeCell(node);
|
|
1404
|
+
const digit = decodeDigit(node);
|
|
1405
|
+
const result = [];
|
|
1406
|
+
for (const peer of PEERS[cell]) if (candidates[peer].has(digit)) result.push(encode(peer, digit));
|
|
1407
|
+
for (const d of candidates[cell]) if (d !== digit) result.push(encode(cell, d));
|
|
1408
|
+
return result;
|
|
1409
|
+
};
|
|
1410
|
+
const allNodes = [];
|
|
1411
|
+
for (let cell = 0; cell < 81; cell++) for (const digit of candidates[cell]) allNodes.push(encode(cell, digit));
|
|
1412
|
+
const MAX_CHAIN = 12;
|
|
1413
|
+
for (const startNode of allNodes) {
|
|
1414
|
+
if (!strongLinks.has(startNode)) continue;
|
|
1415
|
+
const queue = [{
|
|
1416
|
+
node: startNode,
|
|
1417
|
+
isOn: true,
|
|
1418
|
+
path: [startNode]
|
|
1419
|
+
}];
|
|
1420
|
+
while (queue.length > 0) {
|
|
1421
|
+
const { node, isOn, path } = queue.shift();
|
|
1422
|
+
if (path.length > MAX_CHAIN) continue;
|
|
1423
|
+
const next_nodes = isOn ? weakNeighbors(node) : [...strongLinks.get(node) ?? []];
|
|
1424
|
+
for (const next of next_nodes) {
|
|
1425
|
+
if (path.includes(next)) continue;
|
|
1426
|
+
const newPath = [...path, next];
|
|
1427
|
+
if (!isOn && newPath.length >= 4) {
|
|
1428
|
+
if (weakNeighbors(next).includes(startNode)) {
|
|
1429
|
+
const onNodes = newPath.filter((_, i) => i % 2 === 0);
|
|
1430
|
+
const offNodes = newPath.filter((_, i) => i % 2 === 1);
|
|
1431
|
+
const hasWeakConflict = (nodes, allowedPair) => nodes.some((a, i) => nodes.slice(i + 1).some((b) => {
|
|
1432
|
+
if (allowedPair === `${Math.min(a, b)},${Math.max(a, b)}`) return false;
|
|
1433
|
+
return weakNeighbors(a).includes(b);
|
|
1434
|
+
}));
|
|
1435
|
+
if (!!hasWeakConflict(onNodes, `${Math.min(startNode, next)},${Math.max(startNode, next)}`)) continue;
|
|
1436
|
+
const weakLinks = [];
|
|
1437
|
+
for (let i = 0; i < newPath.length - 1; i += 2) weakLinks.push([newPath[i], newPath[i + 1]]);
|
|
1438
|
+
const eliminations = [];
|
|
1439
|
+
const pathCells = new Set(newPath.map(decodeCell));
|
|
1440
|
+
const offDigitConflict = (digit) => hasWeakConflict(offNodes.filter((node) => decodeDigit(node) === digit), null);
|
|
1441
|
+
for (const [onNode, offNode] of weakLinks) {
|
|
1442
|
+
const onCell = decodeCell(onNode);
|
|
1443
|
+
const onDigit = decodeDigit(onNode);
|
|
1444
|
+
const offCell = decodeCell(offNode);
|
|
1445
|
+
if (onDigit === decodeDigit(offNode)) {
|
|
1446
|
+
const d = onDigit;
|
|
1447
|
+
if (offDigitConflict(d)) continue;
|
|
1448
|
+
for (let c = 0; c < 81; c++) {
|
|
1449
|
+
if (pathCells.has(c)) continue;
|
|
1450
|
+
if (candidates[c].has(d) && PEERS[onCell].has(c) && PEERS[offCell].has(c)) eliminations.push({
|
|
1451
|
+
cell: c,
|
|
1452
|
+
digit: d
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1458
|
+
const unique = eliminations.filter((e) => {
|
|
1459
|
+
const key = `${e.cell},${e.digit}`;
|
|
1460
|
+
if (seen.has(key)) return false;
|
|
1461
|
+
seen.add(key);
|
|
1462
|
+
return true;
|
|
1463
|
+
});
|
|
1464
|
+
const validated = !(grid !== void 0) ? unique : unique.filter(({ cell, digit }) => propagateDeep(cell, digit, grid, candidates) === null);
|
|
1465
|
+
if (validated.length > 0) {
|
|
1466
|
+
const chainPath = newPath.map((n, i) => {
|
|
1467
|
+
const isOn = i % 2 === 0;
|
|
1468
|
+
return {
|
|
1469
|
+
cell: decodeCell(n),
|
|
1470
|
+
digit: decodeDigit(n),
|
|
1471
|
+
isOn,
|
|
1472
|
+
linkToNext: i < newPath.length - 1 ? isOn ? "weak" : "strong" : "weak"
|
|
1473
|
+
};
|
|
1474
|
+
});
|
|
1475
|
+
return {
|
|
1476
|
+
technique: "aicRing",
|
|
1477
|
+
placements: [],
|
|
1478
|
+
eliminations: validated,
|
|
1479
|
+
patternCells: [...new Set([...newPath, startNode].map(decodeCell))],
|
|
1480
|
+
chainPath
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const startCell = decodeCell(startNode);
|
|
1485
|
+
const startDigit = decodeDigit(startNode);
|
|
1486
|
+
const endCell = decodeCell(next);
|
|
1487
|
+
const eliminations = [];
|
|
1488
|
+
if (startCell === endCell) {
|
|
1489
|
+
if (candidates[startCell].has(startDigit)) eliminations.push({
|
|
1490
|
+
cell: startCell,
|
|
1491
|
+
digit: startDigit
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (eliminations.length > 0) {
|
|
1495
|
+
const chainPath = newPath.map((n, i) => {
|
|
1496
|
+
const isOn = i % 2 === 0;
|
|
1497
|
+
return {
|
|
1498
|
+
cell: decodeCell(n),
|
|
1499
|
+
digit: decodeDigit(n),
|
|
1500
|
+
isOn,
|
|
1501
|
+
linkToNext: i < newPath.length - 1 ? isOn ? "weak" : "strong" : void 0
|
|
1502
|
+
};
|
|
1503
|
+
});
|
|
1504
|
+
return {
|
|
1505
|
+
technique: "aic",
|
|
1506
|
+
placements: [],
|
|
1507
|
+
eliminations,
|
|
1508
|
+
patternCells: [...new Set(newPath.map(decodeCell))],
|
|
1509
|
+
chainPath
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
queue.push({
|
|
1514
|
+
node: next,
|
|
1515
|
+
isOn: !isOn,
|
|
1516
|
+
path: newPath
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
for (const startNode of allNodes) {
|
|
1522
|
+
if (!strongLinks.has(startNode)) continue;
|
|
1523
|
+
const queue = [{
|
|
1524
|
+
node: startNode,
|
|
1525
|
+
isOn: false,
|
|
1526
|
+
path: [startNode]
|
|
1527
|
+
}];
|
|
1528
|
+
while (queue.length > 0) {
|
|
1529
|
+
const { node, isOn, path } = queue.shift();
|
|
1530
|
+
if (path.length > MAX_CHAIN) continue;
|
|
1531
|
+
const next_nodes = isOn ? weakNeighbors(node) : [...strongLinks.get(node) ?? []];
|
|
1532
|
+
for (const next of next_nodes) {
|
|
1533
|
+
if (path.includes(next)) continue;
|
|
1534
|
+
const newPath = [...path, next];
|
|
1535
|
+
if (!isOn && newPath.length >= 4) {
|
|
1536
|
+
const startCell = decodeCell(startNode);
|
|
1537
|
+
const startDigit = decodeDigit(startNode);
|
|
1538
|
+
const endCell = decodeCell(next);
|
|
1539
|
+
const endDigit = decodeDigit(next);
|
|
1540
|
+
const eliminations = [];
|
|
1541
|
+
if (startDigit === endDigit) {
|
|
1542
|
+
const d = startDigit;
|
|
1543
|
+
for (let c = 0; c < 81; c++) {
|
|
1544
|
+
if (newPath.some((n) => decodeCell(n) === c)) continue;
|
|
1545
|
+
if (candidates[c].has(d) && PEERS[startCell].has(c) && PEERS[endCell].has(c)) eliminations.push({
|
|
1546
|
+
cell: c,
|
|
1547
|
+
digit: d
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (eliminations.length > 0) {
|
|
1552
|
+
const chainPath = newPath.map((n, i) => {
|
|
1553
|
+
const isOn = i % 2 !== 0;
|
|
1554
|
+
return {
|
|
1555
|
+
cell: decodeCell(n),
|
|
1556
|
+
digit: decodeDigit(n),
|
|
1557
|
+
isOn,
|
|
1558
|
+
linkToNext: i < newPath.length - 1 ? isOn ? "weak" : "strong" : void 0
|
|
1559
|
+
};
|
|
1560
|
+
});
|
|
1561
|
+
return {
|
|
1562
|
+
technique: "aic",
|
|
1563
|
+
placements: [],
|
|
1564
|
+
eliminations,
|
|
1565
|
+
patternCells: [...new Set(newPath.map(decodeCell))],
|
|
1566
|
+
chainPath
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
queue.push({
|
|
1571
|
+
node: next,
|
|
1572
|
+
isOn: !isOn,
|
|
1573
|
+
path: newPath
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return null;
|
|
1579
|
+
};
|
|
1580
|
+
const findGroupedAIC = (candidates, grid) => {
|
|
1581
|
+
const nodes = [];
|
|
1582
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
1583
|
+
let nextId = 0;
|
|
1584
|
+
for (let cell = 0; cell < 81; cell++) for (const digit of candidates[cell]) {
|
|
1585
|
+
const id = nextId++;
|
|
1586
|
+
const nd = {
|
|
1587
|
+
type: "cell",
|
|
1588
|
+
cell,
|
|
1589
|
+
digit,
|
|
1590
|
+
id
|
|
1591
|
+
};
|
|
1592
|
+
nodes.push(nd);
|
|
1593
|
+
nodeById.set(id, nd);
|
|
1594
|
+
}
|
|
1595
|
+
const groupNodes = [];
|
|
1596
|
+
for (const box of BOX_HOUSES) for (let digit = 1; digit <= 9; digit++) {
|
|
1597
|
+
const boxCells = box.filter((c) => candidates[c].has(digit));
|
|
1598
|
+
if (boxCells.length < 2) continue;
|
|
1599
|
+
const byRow = /* @__PURE__ */ new Map();
|
|
1600
|
+
const byCol = /* @__PURE__ */ new Map();
|
|
1601
|
+
for (const c of boxCells) {
|
|
1602
|
+
const r = cellRow(c);
|
|
1603
|
+
const col = cellCol(c);
|
|
1604
|
+
if (!byRow.has(r)) byRow.set(r, []);
|
|
1605
|
+
if (!byCol.has(col)) byCol.set(col, []);
|
|
1606
|
+
byRow.get(r).push(c);
|
|
1607
|
+
byCol.get(col).push(c);
|
|
1608
|
+
}
|
|
1609
|
+
for (const [, cells] of [...byRow, ...byCol]) {
|
|
1610
|
+
if (cells.length < 2) continue;
|
|
1611
|
+
const id = nextId++;
|
|
1612
|
+
const nd = {
|
|
1613
|
+
type: "group",
|
|
1614
|
+
cells,
|
|
1615
|
+
digit,
|
|
1616
|
+
id
|
|
1617
|
+
};
|
|
1618
|
+
groupNodes.push(nd);
|
|
1619
|
+
nodeById.set(id, nd);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const allNodes = [...nodes, ...groupNodes];
|
|
1623
|
+
const strongLinks = /* @__PURE__ */ new Map();
|
|
1624
|
+
const addStrong = (a, b) => {
|
|
1625
|
+
if (!strongLinks.has(a)) strongLinks.set(a, /* @__PURE__ */ new Set());
|
|
1626
|
+
if (!strongLinks.has(b)) strongLinks.set(b, /* @__PURE__ */ new Set());
|
|
1627
|
+
strongLinks.get(a).add(b);
|
|
1628
|
+
strongLinks.get(b).add(a);
|
|
1629
|
+
};
|
|
1630
|
+
for (const house of HOUSES) for (let digit = 1; digit <= 9; digit++) {
|
|
1631
|
+
const houseCells = new Set(house.filter((c) => candidates[c].has(digit)));
|
|
1632
|
+
if (houseCells.size < 2) continue;
|
|
1633
|
+
const houseNodes = allNodes.filter((nd) => {
|
|
1634
|
+
if (nd.digit !== digit) return false;
|
|
1635
|
+
return (nd.type === "cell" ? [nd.cell] : nd.cells).every((c) => houseCells.has(c));
|
|
1636
|
+
});
|
|
1637
|
+
for (let i = 0; i < houseNodes.length - 1; i++) for (let j = i + 1; j < houseNodes.length; j++) {
|
|
1638
|
+
const ni = houseNodes[i];
|
|
1639
|
+
const nj = houseNodes[j];
|
|
1640
|
+
const niCells = ni.type === "cell" ? [ni.cell] : ni.cells;
|
|
1641
|
+
const njCells = nj.type === "cell" ? [nj.cell] : nj.cells;
|
|
1642
|
+
if (niCells.some((c) => njCells.includes(c))) continue;
|
|
1643
|
+
const covered = new Set([...niCells, ...njCells]);
|
|
1644
|
+
if ([...houseCells].every((c) => covered.has(c))) addStrong(ni.id, nj.id);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1648
|
+
if (candidates[cell].size !== 2) continue;
|
|
1649
|
+
const [d1, d2] = [...candidates[cell]];
|
|
1650
|
+
const n1 = nodes.find((n) => n.type === "cell" && n.cell === cell && n.digit === d1);
|
|
1651
|
+
const n2 = nodes.find((n) => n.type === "cell" && n.cell === cell && n.digit === d2);
|
|
1652
|
+
if (n1 && n2) addStrong(n1.id, n2.id);
|
|
1653
|
+
}
|
|
1654
|
+
const weakNeighbors = (nodeId) => {
|
|
1655
|
+
const nd = nodeById.get(nodeId);
|
|
1656
|
+
const ndCells = nd.type === "cell" ? [nd.cell] : nd.cells;
|
|
1657
|
+
const result = [];
|
|
1658
|
+
for (const other of allNodes) {
|
|
1659
|
+
if (other.id === nodeId || other.digit !== nd.digit) continue;
|
|
1660
|
+
const otherCells = other.type === "cell" ? [other.cell] : other.cells;
|
|
1661
|
+
if (ndCells.every((c1) => otherCells.every((c2) => c1 !== c2 && PEERS[c1].has(c2)))) result.push(other.id);
|
|
1662
|
+
}
|
|
1663
|
+
if (nd.type === "cell") for (const other of nodes) {
|
|
1664
|
+
if (other.type !== "cell") continue;
|
|
1665
|
+
if (other.cell === nd.cell && other.digit !== nd.digit) result.push(other.id);
|
|
1666
|
+
}
|
|
1667
|
+
return result;
|
|
1668
|
+
};
|
|
1669
|
+
const nodeCells = (id) => {
|
|
1670
|
+
const nd = nodeById.get(id);
|
|
1671
|
+
return nd.type === "cell" ? [nd.cell] : nd.cells;
|
|
1672
|
+
};
|
|
1673
|
+
const MAX_CHAIN = 12;
|
|
1674
|
+
const buildGroupedChainPath = (path, startsOn) => path.map((id, i) => {
|
|
1675
|
+
const nd = nodeById.get(id);
|
|
1676
|
+
const isOn = startsOn ? i % 2 === 0 : i % 2 !== 0;
|
|
1677
|
+
return {
|
|
1678
|
+
cell: nd.type === "cell" ? nd.cell : nd.cells[0],
|
|
1679
|
+
digit: nd.digit,
|
|
1680
|
+
isOn,
|
|
1681
|
+
cells: nd.type === "group" ? nd.cells : void 0,
|
|
1682
|
+
linkToNext: i < path.length - 1 ? isOn ? "weak" : "strong" : void 0
|
|
1683
|
+
};
|
|
1684
|
+
});
|
|
1685
|
+
const cellsInPathByDigit = (path) => {
|
|
1686
|
+
const m = /* @__PURE__ */ new Map();
|
|
1687
|
+
for (const id of path) {
|
|
1688
|
+
const nd = nodeById.get(id);
|
|
1689
|
+
if (!m.has(nd.digit)) m.set(nd.digit, /* @__PURE__ */ new Set());
|
|
1690
|
+
for (const c of nodeCells(id)) m.get(nd.digit).add(c);
|
|
1691
|
+
}
|
|
1692
|
+
return m;
|
|
1693
|
+
};
|
|
1694
|
+
const wouldOverlapSameDigit = (path, next) => {
|
|
1695
|
+
const nd = nodeById.get(next);
|
|
1696
|
+
const usedCells = cellsInPathByDigit(path).get(nd.digit);
|
|
1697
|
+
if (!usedCells) return false;
|
|
1698
|
+
return nodeCells(next).some((c) => usedCells.has(c));
|
|
1699
|
+
};
|
|
1700
|
+
for (const startNode of allNodes) {
|
|
1701
|
+
if (!strongLinks.has(startNode.id)) continue;
|
|
1702
|
+
const queue = [{
|
|
1703
|
+
id: startNode.id,
|
|
1704
|
+
isOn: true,
|
|
1705
|
+
path: [startNode.id]
|
|
1706
|
+
}];
|
|
1707
|
+
while (queue.length > 0) {
|
|
1708
|
+
const { id, isOn, path } = queue.shift();
|
|
1709
|
+
if (path.length > MAX_CHAIN) continue;
|
|
1710
|
+
const nextIds = isOn ? weakNeighbors(id) : [...strongLinks.get(id) ?? []];
|
|
1711
|
+
for (const next of nextIds) {
|
|
1712
|
+
if (path.includes(next)) continue;
|
|
1713
|
+
if (wouldOverlapSameDigit(path, next)) continue;
|
|
1714
|
+
const newPath = [...path, next];
|
|
1715
|
+
if (!isOn && newPath.length >= 4) {
|
|
1716
|
+
if (weakNeighbors(next).includes(startNode.id)) {
|
|
1717
|
+
const startNd = nodeById.get(startNode.id);
|
|
1718
|
+
const endNd = nodeById.get(next);
|
|
1719
|
+
const closingIsSameDigitPeer = startNd.digit === endNd.digit;
|
|
1720
|
+
const onNodes = newPath.filter((_, i) => i % 2 === 0);
|
|
1721
|
+
const allowedOnPair = `${Math.min(startNode.id, next)},${Math.max(startNode.id, next)}`;
|
|
1722
|
+
const hasWeakConflict = (nodes) => nodes.some((a, i) => nodes.slice(i + 1).some((b) => {
|
|
1723
|
+
if (allowedOnPair === `${Math.min(a, b)},${Math.max(a, b)}`) return false;
|
|
1724
|
+
return weakNeighbors(a).includes(b);
|
|
1725
|
+
}));
|
|
1726
|
+
if (hasWeakConflict(onNodes)) continue;
|
|
1727
|
+
const weakLinks = [];
|
|
1728
|
+
for (let i = 0; i < newPath.length - 1; i += 2) weakLinks.push([newPath[i], newPath[i + 1]]);
|
|
1729
|
+
const eliminations = [];
|
|
1730
|
+
const pathCells = new Set(newPath.flatMap(nodeCells));
|
|
1731
|
+
for (const [onId, offId] of weakLinks) {
|
|
1732
|
+
const onNd = nodeById.get(onId);
|
|
1733
|
+
const offNd = nodeById.get(offId);
|
|
1734
|
+
if (onNd.digit !== offNd.digit) continue;
|
|
1735
|
+
const d = onNd.digit;
|
|
1736
|
+
const onCs = nodeCells(onId);
|
|
1737
|
+
const offCs = nodeCells(offId);
|
|
1738
|
+
for (let c = 0; c < 81; c++) {
|
|
1739
|
+
if (pathCells.has(c)) continue;
|
|
1740
|
+
if (candidates[c].has(d) && onCs.every((oc) => PEERS[oc].has(c)) && offCs.every((oc) => PEERS[oc].has(c))) eliminations.push({
|
|
1741
|
+
cell: c,
|
|
1742
|
+
digit: d
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1747
|
+
const unique = eliminations.filter((e) => {
|
|
1748
|
+
const key = `${e.cell},${e.digit}`;
|
|
1749
|
+
if (seen.has(key)) return false;
|
|
1750
|
+
seen.add(key);
|
|
1751
|
+
return true;
|
|
1752
|
+
});
|
|
1753
|
+
const hasInCellWeakLink = startNd.type === "cell" && endNd.type === "cell" && startNd.cell === endNd.cell || weakLinks.some(([onId, offId]) => {
|
|
1754
|
+
const onNd = nodeById.get(onId);
|
|
1755
|
+
const offNd = nodeById.get(offId);
|
|
1756
|
+
return onNd.type === "cell" && offNd.type === "cell" && onNd.cell === offNd.cell;
|
|
1757
|
+
});
|
|
1758
|
+
const shouldValidate = (hasInCellWeakLink || closingIsSameDigitPeer) && grid !== void 0;
|
|
1759
|
+
if ((hasInCellWeakLink || closingIsSameDigitPeer) && !grid) continue;
|
|
1760
|
+
const validated = !shouldValidate ? unique : unique.filter(({ cell, digit }) => propagateDeep(cell, digit, grid, candidates) === null);
|
|
1761
|
+
if (validated.length > 0) {
|
|
1762
|
+
const chainPath = buildGroupedChainPath(newPath, true);
|
|
1763
|
+
if (chainPath.length > 0) chainPath[chainPath.length - 1].linkToNext = "weak";
|
|
1764
|
+
return {
|
|
1765
|
+
technique: "groupedAIC",
|
|
1766
|
+
placements: [],
|
|
1767
|
+
eliminations: validated,
|
|
1768
|
+
patternCells: [...new Set([...newPath, startNode.id].flatMap(nodeCells))],
|
|
1769
|
+
chainPath
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
const startNd = nodeById.get(startNode.id);
|
|
1774
|
+
const endNd = nodeById.get(next);
|
|
1775
|
+
if (startNd.type === "cell" && endNd.type === "cell" && startNd.cell === endNd.cell && startNd.digit !== endNd.digit) {
|
|
1776
|
+
if (candidates[startNd.cell].has(startNd.digit)) return {
|
|
1777
|
+
technique: "groupedAIC",
|
|
1778
|
+
placements: [],
|
|
1779
|
+
eliminations: [{
|
|
1780
|
+
cell: startNd.cell,
|
|
1781
|
+
digit: startNd.digit
|
|
1782
|
+
}],
|
|
1783
|
+
patternCells: [...new Set(newPath.flatMap(nodeCells))],
|
|
1784
|
+
chainPath: buildGroupedChainPath(newPath, true)
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
queue.push({
|
|
1789
|
+
id: next,
|
|
1790
|
+
isOn: !isOn,
|
|
1791
|
+
path: newPath
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
for (const startNode of allNodes) {
|
|
1797
|
+
if (!strongLinks.has(startNode.id)) continue;
|
|
1798
|
+
const queue = [{
|
|
1799
|
+
id: startNode.id,
|
|
1800
|
+
isOn: false,
|
|
1801
|
+
path: [startNode.id]
|
|
1802
|
+
}];
|
|
1803
|
+
while (queue.length > 0) {
|
|
1804
|
+
const { id, isOn, path } = queue.shift();
|
|
1805
|
+
if (path.length > MAX_CHAIN) continue;
|
|
1806
|
+
const nextIds = isOn ? weakNeighbors(id) : [...strongLinks.get(id) ?? []];
|
|
1807
|
+
for (const next of nextIds) {
|
|
1808
|
+
if (path.includes(next)) continue;
|
|
1809
|
+
if (wouldOverlapSameDigit(path, next)) continue;
|
|
1810
|
+
const newPath = [...path, next];
|
|
1811
|
+
if (!isOn && newPath.length >= 4) {
|
|
1812
|
+
const startNd = nodeById.get(startNode.id);
|
|
1813
|
+
const endNd = nodeById.get(next);
|
|
1814
|
+
if (startNd.digit === endNd.digit) {
|
|
1815
|
+
const d = startNd.digit;
|
|
1816
|
+
const startCs = nodeCells(startNode.id);
|
|
1817
|
+
const endCs = nodeCells(next);
|
|
1818
|
+
const eliminations = [];
|
|
1819
|
+
const pathCells = new Set(newPath.flatMap(nodeCells));
|
|
1820
|
+
for (let c = 0; c < 81; c++) {
|
|
1821
|
+
if (pathCells.has(c)) continue;
|
|
1822
|
+
if (candidates[c].has(d) && startCs.every((sc) => PEERS[sc].has(c)) && endCs.every((ec) => PEERS[ec].has(c))) eliminations.push({
|
|
1823
|
+
cell: c,
|
|
1824
|
+
digit: d
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
if (eliminations.length > 0) return {
|
|
1828
|
+
technique: "groupedAIC",
|
|
1829
|
+
placements: [],
|
|
1830
|
+
eliminations,
|
|
1831
|
+
patternCells: [...new Set(newPath.flatMap(nodeCells))],
|
|
1832
|
+
chainPath: buildGroupedChainPath(newPath, false)
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
queue.push({
|
|
1837
|
+
id: next,
|
|
1838
|
+
isOn: !isOn,
|
|
1839
|
+
path: newPath
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return null;
|
|
1845
|
+
};
|
|
1846
|
+
const propagate = (cell, digit, gridIn, candsIn) => {
|
|
1847
|
+
const grid = [...gridIn];
|
|
1848
|
+
const cands = candsIn.map((s) => new Set(s));
|
|
1849
|
+
const queue = [[cell, digit]];
|
|
1850
|
+
while (queue.length > 0) {
|
|
1851
|
+
const [c, d] = queue.shift();
|
|
1852
|
+
if (grid[c] !== 0) {
|
|
1853
|
+
if (grid[c] !== d) return null;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (!cands[c].has(d)) return null;
|
|
1857
|
+
grid[c] = d;
|
|
1858
|
+
cands[c] = /* @__PURE__ */ new Set();
|
|
1859
|
+
for (const peer of PEERS[c]) {
|
|
1860
|
+
if (grid[peer] !== 0) continue;
|
|
1861
|
+
cands[peer].delete(d);
|
|
1862
|
+
if (cands[peer].size === 0) return null;
|
|
1863
|
+
if (cands[peer].size === 1) queue.push([peer, [...cands[peer]][0]]);
|
|
1864
|
+
}
|
|
1865
|
+
for (const house of HOUSES) {
|
|
1866
|
+
if (!house.includes(c)) continue;
|
|
1867
|
+
for (let hd = 1; hd <= 9; hd++) {
|
|
1868
|
+
const cells = house.filter((hc) => grid[hc] === 0 && cands[hc].has(hd));
|
|
1869
|
+
if (cells.length === 0 && !house.some((hc) => grid[hc] === hd)) return null;
|
|
1870
|
+
if (cells.length === 1 && grid[cells[0]] === 0) queue.push([cells[0], hd]);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return {
|
|
1875
|
+
grid,
|
|
1876
|
+
candidates: cands
|
|
1877
|
+
};
|
|
1878
|
+
};
|
|
1879
|
+
const propagateDeep = (cell, digit, gridIn, candsIn) => {
|
|
1880
|
+
const base = propagate(cell, digit, gridIn, candsIn);
|
|
1881
|
+
if (base === null) return null;
|
|
1882
|
+
let { grid, candidates } = base;
|
|
1883
|
+
for (;;) {
|
|
1884
|
+
let changed = false;
|
|
1885
|
+
const lockedHint = findLockedCandidates(candidates) ?? findNakedGroup(candidates, 2) ?? findHiddenGroup(candidates, 2);
|
|
1886
|
+
if (lockedHint !== null) {
|
|
1887
|
+
const newCands = candidates.map((s) => new Set(s));
|
|
1888
|
+
for (const { cell: c, digit: d } of lockedHint.eliminations) {
|
|
1889
|
+
newCands[c].delete(d);
|
|
1890
|
+
if (newCands[c].size === 0 && grid[c] === 0) return null;
|
|
1891
|
+
}
|
|
1892
|
+
const forced = [];
|
|
1893
|
+
for (let c = 0; c < 81; c++) if (grid[c] === 0 && newCands[c].size === 1) forced.push([c, [...newCands[c]][0]]);
|
|
1894
|
+
if (forced.length > 0 || lockedHint.eliminations.length > 0) {
|
|
1895
|
+
changed = true;
|
|
1896
|
+
candidates = newCands;
|
|
1897
|
+
for (const [fc, fd] of forced) {
|
|
1898
|
+
if (grid[fc] !== 0) continue;
|
|
1899
|
+
const r = propagate(fc, fd, grid, candidates);
|
|
1900
|
+
if (r === null) return null;
|
|
1901
|
+
grid = r.grid;
|
|
1902
|
+
candidates = r.candidates;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (!changed) break;
|
|
1907
|
+
}
|
|
1908
|
+
return {
|
|
1909
|
+
grid,
|
|
1910
|
+
candidates
|
|
1911
|
+
};
|
|
1912
|
+
};
|
|
1913
|
+
const findNishio = (grid, candidates) => {
|
|
1914
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1915
|
+
if (candidates[cell].size === 0) continue;
|
|
1916
|
+
for (const digit of candidates[cell]) if (propagateDeep(cell, digit, grid, candidates) === null) return {
|
|
1917
|
+
technique: "nishio",
|
|
1918
|
+
placements: [],
|
|
1919
|
+
eliminations: [{
|
|
1920
|
+
cell,
|
|
1921
|
+
digit
|
|
1922
|
+
}],
|
|
1923
|
+
patternCells: [cell]
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
return null;
|
|
1927
|
+
};
|
|
1928
|
+
const findNishioNet = (grid, candidates) => {
|
|
1929
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1930
|
+
if (candidates[cell].size < 2) continue;
|
|
1931
|
+
for (const digit of candidates[cell]) if (propagateDeep(cell, digit, grid, candidates) === null) return {
|
|
1932
|
+
technique: "nishioNet",
|
|
1933
|
+
placements: [],
|
|
1934
|
+
eliminations: [{
|
|
1935
|
+
cell,
|
|
1936
|
+
digit
|
|
1937
|
+
}],
|
|
1938
|
+
patternCells: [cell]
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
return null;
|
|
1942
|
+
};
|
|
1943
|
+
const findCellRegionForcingChain = (grid, candidates) => {
|
|
1944
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
1945
|
+
if (candidates[cell].size < 2) continue;
|
|
1946
|
+
const branches = [];
|
|
1947
|
+
for (const digit of candidates[cell]) branches.push(propagateDeep(cell, digit, grid, candidates));
|
|
1948
|
+
if (branches.some((b) => b === null)) continue;
|
|
1949
|
+
const validBranches = branches;
|
|
1950
|
+
for (let c = 0; c < 81; c++) {
|
|
1951
|
+
if (grid[c] !== 0) continue;
|
|
1952
|
+
if (validBranches.every((b) => b.grid[c] !== 0 && b.grid[c] === validBranches[0].grid[c]) && validBranches[0].grid[c] !== 0) return {
|
|
1953
|
+
technique: "cellRegionForcingChain",
|
|
1954
|
+
placements: [{
|
|
1955
|
+
cell: c,
|
|
1956
|
+
digit: validBranches[0].grid[c]
|
|
1957
|
+
}],
|
|
1958
|
+
eliminations: [],
|
|
1959
|
+
patternCells: [cell, c]
|
|
1960
|
+
};
|
|
1961
|
+
for (const digit of candidates[c]) if (validBranches.every((b) => !b.candidates[c].has(digit))) return {
|
|
1962
|
+
technique: "cellRegionForcingChain",
|
|
1963
|
+
placements: [],
|
|
1964
|
+
eliminations: [{
|
|
1965
|
+
cell: c,
|
|
1966
|
+
digit
|
|
1967
|
+
}],
|
|
1968
|
+
patternCells: [cell, c]
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
for (const house of HOUSES) for (let digit = 1; digit <= 9; digit++) {
|
|
1973
|
+
const cells = house.filter((c) => candidates[c].has(digit));
|
|
1974
|
+
if (cells.length < 2) continue;
|
|
1975
|
+
const branches = cells.map((c) => propagateDeep(c, digit, grid, candidates));
|
|
1976
|
+
if (branches.some((b) => b === null)) continue;
|
|
1977
|
+
const validBranches = branches;
|
|
1978
|
+
for (let c = 0; c < 81; c++) {
|
|
1979
|
+
if (grid[c] !== 0) continue;
|
|
1980
|
+
if (validBranches.every((b) => b.grid[c] !== 0 && b.grid[c] === validBranches[0].grid[c]) && validBranches[0].grid[c] !== 0) return {
|
|
1981
|
+
technique: "cellRegionForcingChain",
|
|
1982
|
+
placements: [{
|
|
1983
|
+
cell: c,
|
|
1984
|
+
digit: validBranches[0].grid[c]
|
|
1985
|
+
}],
|
|
1986
|
+
eliminations: [],
|
|
1987
|
+
patternCells: [...cells, c]
|
|
1988
|
+
};
|
|
1989
|
+
for (const d of candidates[c]) if (validBranches.every((b) => !b.candidates[c].has(d))) return {
|
|
1990
|
+
technique: "cellRegionForcingChain",
|
|
1991
|
+
placements: [],
|
|
1992
|
+
eliminations: [{
|
|
1993
|
+
cell: c,
|
|
1994
|
+
digit: d
|
|
1995
|
+
}],
|
|
1996
|
+
patternCells: [...cells, c]
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return null;
|
|
2001
|
+
};
|
|
2002
|
+
const findCellRegionForcingNet = (grid, candidates) => {
|
|
2003
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
2004
|
+
if (candidates[cell].size < 3) continue;
|
|
2005
|
+
const branches = [];
|
|
2006
|
+
for (const digit of candidates[cell]) branches.push(propagateDeep(cell, digit, grid, candidates));
|
|
2007
|
+
if (branches.some((b) => b === null)) continue;
|
|
2008
|
+
const validBranches = branches;
|
|
2009
|
+
for (let c = 0; c < 81; c++) {
|
|
2010
|
+
if (grid[c] !== 0) continue;
|
|
2011
|
+
if (validBranches.every((b) => b.grid[c] !== 0 && b.grid[c] === validBranches[0].grid[c]) && validBranches[0].grid[c] !== 0) return {
|
|
2012
|
+
technique: "cellRegionForcingNet",
|
|
2013
|
+
placements: [{
|
|
2014
|
+
cell: c,
|
|
2015
|
+
digit: validBranches[0].grid[c]
|
|
2016
|
+
}],
|
|
2017
|
+
eliminations: [],
|
|
2018
|
+
patternCells: [cell, c]
|
|
2019
|
+
};
|
|
2020
|
+
for (const digit of candidates[c]) if (validBranches.every((b) => !b.candidates[c].has(digit))) return {
|
|
2021
|
+
technique: "cellRegionForcingNet",
|
|
2022
|
+
placements: [],
|
|
2023
|
+
eliminations: [{
|
|
2024
|
+
cell: c,
|
|
2025
|
+
digit
|
|
2026
|
+
}],
|
|
2027
|
+
patternCells: [cell, c]
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return null;
|
|
2032
|
+
};
|
|
2033
|
+
const findForcingChain = (grid, candidates) => {
|
|
2034
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
2035
|
+
if (candidates[cell].size !== 2) continue;
|
|
2036
|
+
const [d1, d2] = [...candidates[cell]];
|
|
2037
|
+
const r1 = propagateDeep(cell, d1, grid, candidates);
|
|
2038
|
+
const r2 = propagateDeep(cell, d2, grid, candidates);
|
|
2039
|
+
if (!r1 || !r2) continue;
|
|
2040
|
+
for (let c = 0; c < 81; c++) {
|
|
2041
|
+
if (grid[c] !== 0) continue;
|
|
2042
|
+
if (r1.grid[c] !== 0 && r1.grid[c] === r2.grid[c]) return {
|
|
2043
|
+
technique: "forcingChain",
|
|
2044
|
+
placements: [{
|
|
2045
|
+
cell: c,
|
|
2046
|
+
digit: r1.grid[c]
|
|
2047
|
+
}],
|
|
2048
|
+
eliminations: [],
|
|
2049
|
+
patternCells: [cell, c]
|
|
2050
|
+
};
|
|
2051
|
+
for (const digit of candidates[c]) if (!r1.candidates[c].has(digit) && !r2.candidates[c].has(digit)) return {
|
|
2052
|
+
technique: "forcingChain",
|
|
2053
|
+
placements: [],
|
|
2054
|
+
eliminations: [{
|
|
2055
|
+
cell: c,
|
|
2056
|
+
digit
|
|
2057
|
+
}],
|
|
2058
|
+
patternCells: [cell, c]
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return null;
|
|
2063
|
+
};
|
|
2064
|
+
const findAlmostLockedSets = (candidates) => {
|
|
2065
|
+
const als = [];
|
|
2066
|
+
for (let sector = 0; sector < 27; sector++) {
|
|
2067
|
+
const emptyCells = (sector < 9 ? ROW_HOUSES[sector] : sector < 18 ? COL_HOUSES[sector - 9] : BOX_HOUSES[sector - 18]).filter((c) => candidates[c].size > 0);
|
|
2068
|
+
if (emptyCells.length === 0) continue;
|
|
2069
|
+
for (let size = 1; size <= Math.min(8, emptyCells.length); size++) for (const cellCombo of combinations(emptyCells, size)) {
|
|
2070
|
+
const allDigits = /* @__PURE__ */ new Set();
|
|
2071
|
+
for (const cell of cellCombo) for (const d of candidates[cell]) allDigits.add(d);
|
|
2072
|
+
if (allDigits.size === size + 1) als.push({
|
|
2073
|
+
cells: new Set(cellCombo),
|
|
2074
|
+
digits: allDigits,
|
|
2075
|
+
sector
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return als;
|
|
2080
|
+
};
|
|
2081
|
+
const cellSectors = (cell) => {
|
|
2082
|
+
const row = cellRow(cell);
|
|
2083
|
+
const col = cellCol(cell);
|
|
2084
|
+
const box = cellBox(cell);
|
|
2085
|
+
return new Set([
|
|
2086
|
+
row,
|
|
2087
|
+
col + 9,
|
|
2088
|
+
box + 18
|
|
2089
|
+
]);
|
|
2090
|
+
};
|
|
2091
|
+
const getRestrictedCommon = (als1, als2, candidates) => {
|
|
2092
|
+
const rc = /* @__PURE__ */ new Set();
|
|
2093
|
+
for (const digit of als1.digits) {
|
|
2094
|
+
if (!als2.digits.has(digit)) continue;
|
|
2095
|
+
const als1Cells = Array.from(als1.cells).filter((c) => candidates[c].has(digit));
|
|
2096
|
+
const als2Cells = Array.from(als2.cells).filter((c) => candidates[c].has(digit));
|
|
2097
|
+
if (als1Cells.some((c) => als2.cells.has(c))) continue;
|
|
2098
|
+
if (als1Cells.length === 0 || als2Cells.length === 0) continue;
|
|
2099
|
+
const allCells = [...als1Cells, ...als2Cells];
|
|
2100
|
+
let commonSectors = new Set(Array.from({ length: 27 }, (_, i) => i));
|
|
2101
|
+
for (const c of allCells) {
|
|
2102
|
+
const cs = cellSectors(c);
|
|
2103
|
+
commonSectors = new Set([...commonSectors].filter((s) => cs.has(s)));
|
|
2104
|
+
}
|
|
2105
|
+
if (commonSectors.size > 0 && commonSectors.size < 3) rc.add(digit);
|
|
2106
|
+
}
|
|
2107
|
+
return rc;
|
|
2108
|
+
};
|
|
2109
|
+
const findALSXZ = (candidates) => {
|
|
2110
|
+
const alsList = findAlmostLockedSets(candidates);
|
|
2111
|
+
for (let i = 0; i < alsList.length; i++) for (let j = i + 1; j < alsList.length; j++) {
|
|
2112
|
+
const als1 = alsList[i];
|
|
2113
|
+
const als2 = alsList[j];
|
|
2114
|
+
if (Array.from(als1.cells).filter((c) => als2.cells.has(c)).length > 0) continue;
|
|
2115
|
+
const commonDigits = Array.from(als1.digits).filter((d) => als2.digits.has(d));
|
|
2116
|
+
if (commonDigits.length < 2) continue;
|
|
2117
|
+
const rc = getRestrictedCommon(als1, als2, candidates);
|
|
2118
|
+
if (rc.size !== 1) continue;
|
|
2119
|
+
const restrictedDigit = Array.from(rc)[0];
|
|
2120
|
+
const xDigits = commonDigits.filter((d) => d !== restrictedDigit);
|
|
2121
|
+
for (const xDigit of xDigits) {
|
|
2122
|
+
const eliminations = [];
|
|
2123
|
+
const als1XCells = Array.from(als1.cells).filter((c) => candidates[c].has(xDigit));
|
|
2124
|
+
const als2XCells = Array.from(als2.cells).filter((c) => candidates[c].has(xDigit));
|
|
2125
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
2126
|
+
if (als1.cells.has(cell) || als2.cells.has(cell)) continue;
|
|
2127
|
+
if (!candidates[cell].has(xDigit)) continue;
|
|
2128
|
+
const seesAll1 = als1XCells.every((c) => PEERS[cell].has(c));
|
|
2129
|
+
const seesAll2 = als2XCells.every((c) => PEERS[cell].has(c));
|
|
2130
|
+
if (seesAll1 && seesAll2) eliminations.push({
|
|
2131
|
+
cell,
|
|
2132
|
+
digit: xDigit
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
if (eliminations.length > 0) return {
|
|
2136
|
+
technique: "alsXZ",
|
|
2137
|
+
placements: [],
|
|
2138
|
+
eliminations,
|
|
2139
|
+
patternCells: [...als1.cells, ...als2.cells],
|
|
2140
|
+
als1Cells: Array.from(als1.cells),
|
|
2141
|
+
als2Cells: Array.from(als2.cells)
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return null;
|
|
2146
|
+
};
|
|
2147
|
+
const findSueDeCoq = (candidates) => {
|
|
2148
|
+
const alsList = findAlmostLockedSets(candidates);
|
|
2149
|
+
for (let box = 0; box < 9; box++) {
|
|
2150
|
+
const boxCells = BOX_HOUSES[box];
|
|
2151
|
+
for (let lineType = 0; lineType < 2; lineType++) {
|
|
2152
|
+
const lineHouses = lineType === 0 ? ROW_HOUSES : COL_HOUSES;
|
|
2153
|
+
for (let lineIdx = 0; lineIdx < 9; lineIdx++) {
|
|
2154
|
+
const lineCells = lineHouses[lineIdx];
|
|
2155
|
+
const intersection = boxCells.filter((c) => lineCells.includes(c) && candidates[c].size > 0);
|
|
2156
|
+
if (intersection.length < 2 || intersection.length > 3) continue;
|
|
2157
|
+
for (const stemCells of intersection.length === 2 ? [intersection] : [...combinations(intersection, 2), intersection]) {
|
|
2158
|
+
const stemDigits = /* @__PURE__ */ new Set();
|
|
2159
|
+
for (const c of stemCells) for (const d of candidates[c]) stemDigits.add(d);
|
|
2160
|
+
if (stemDigits.size <= stemCells.length) continue;
|
|
2161
|
+
const lineRest = lineCells.filter((c) => !boxCells.includes(c) && candidates[c].size > 0);
|
|
2162
|
+
const boxRest = boxCells.filter((c) => !lineCells.includes(c) && candidates[c].size > 0);
|
|
2163
|
+
const lineALSCandidates = alsList.filter((als) => {
|
|
2164
|
+
if (!Array.from(als.cells).every((c) => lineRest.includes(c))) return false;
|
|
2165
|
+
return Array.from(als.digits).filter((d) => stemDigits.has(d)).length >= 1;
|
|
2166
|
+
});
|
|
2167
|
+
const boxALSCandidates = alsList.filter((als) => {
|
|
2168
|
+
if (!Array.from(als.cells).every((c) => boxRest.includes(c))) return false;
|
|
2169
|
+
return Array.from(als.digits).filter((d) => stemDigits.has(d)).length >= 1;
|
|
2170
|
+
});
|
|
2171
|
+
for (const lineALS of lineALSCandidates) for (const boxALS of boxALSCandidates) {
|
|
2172
|
+
const allCells = new Set([
|
|
2173
|
+
...stemCells,
|
|
2174
|
+
...lineALS.cells,
|
|
2175
|
+
...boxALS.cells
|
|
2176
|
+
]);
|
|
2177
|
+
const allDigits = /* @__PURE__ */ new Set();
|
|
2178
|
+
for (const c of allCells) for (const d of candidates[c]) allDigits.add(d);
|
|
2179
|
+
if (allCells.size !== allDigits.size) continue;
|
|
2180
|
+
if (!Array.from(allDigits).every((digit) => {
|
|
2181
|
+
const cellsWithDigit = Array.from(allCells).filter((c) => candidates[c].has(digit));
|
|
2182
|
+
return HOUSES.some((house) => cellsWithDigit.every((c) => house.includes(c)));
|
|
2183
|
+
})) continue;
|
|
2184
|
+
const eliminations = [];
|
|
2185
|
+
for (const digit of allDigits) {
|
|
2186
|
+
const cellsWithDigit = Array.from(allCells).filter((c) => candidates[c].has(digit));
|
|
2187
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
2188
|
+
if (allCells.has(cell)) continue;
|
|
2189
|
+
if (!candidates[cell].has(digit)) continue;
|
|
2190
|
+
if (cellsWithDigit.every((c) => PEERS[cell].has(c))) eliminations.push({
|
|
2191
|
+
cell,
|
|
2192
|
+
digit
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
if (eliminations.length > 0) return {
|
|
2197
|
+
technique: "sueDeCoq",
|
|
2198
|
+
placements: [],
|
|
2199
|
+
eliminations,
|
|
2200
|
+
patternCells: Array.from(allCells)
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return null;
|
|
2208
|
+
};
|
|
2209
|
+
const findDeathBlossom = (candidates, grid) => {
|
|
2210
|
+
const alsList = findAlmostLockedSets(candidates);
|
|
2211
|
+
for (let stemCell = 0; stemCell < 81; stemCell++) {
|
|
2212
|
+
const stemCands = candidates[stemCell];
|
|
2213
|
+
if (stemCands.size < 2 || stemCands.size > 4) continue;
|
|
2214
|
+
const petalMap = /* @__PURE__ */ new Map();
|
|
2215
|
+
for (const digit of stemCands) {
|
|
2216
|
+
const petals = alsList.filter((als) => {
|
|
2217
|
+
if (als.cells.has(stemCell)) return false;
|
|
2218
|
+
if (!als.digits.has(digit)) return false;
|
|
2219
|
+
return Array.from(als.cells).filter((c) => candidates[c].has(digit)).every((c) => PEERS[stemCell].has(c));
|
|
2220
|
+
});
|
|
2221
|
+
if (petals.length === 0) break;
|
|
2222
|
+
petalMap.set(digit, petals);
|
|
2223
|
+
}
|
|
2224
|
+
if (petalMap.size !== stemCands.size) continue;
|
|
2225
|
+
const stemDigits = Array.from(stemCands);
|
|
2226
|
+
const petalArrays = stemDigits.map((d) => petalMap.get(d));
|
|
2227
|
+
const search = (idx, usedCells, chosen) => {
|
|
2228
|
+
if (idx === stemDigits.length) {
|
|
2229
|
+
const allCells = new Set([stemCell, ...usedCells]);
|
|
2230
|
+
const nonStemDigits = /* @__PURE__ */ new Set();
|
|
2231
|
+
for (let pi = 0; pi < chosen.length; pi++) for (const d of chosen[pi].digits) if (!stemCands.has(d)) nonStemDigits.add(d);
|
|
2232
|
+
const eliminations = [];
|
|
2233
|
+
for (const digit of nonStemDigits) {
|
|
2234
|
+
const allCellsWithDigit = [];
|
|
2235
|
+
for (const als of chosen) {
|
|
2236
|
+
if (!als.digits.has(digit)) continue;
|
|
2237
|
+
for (const c of als.cells) if (candidates[c].has(digit)) allCellsWithDigit.push(c);
|
|
2238
|
+
}
|
|
2239
|
+
if (allCellsWithDigit.length === 0) continue;
|
|
2240
|
+
for (let cell = 0; cell < 81; cell++) {
|
|
2241
|
+
if (allCells.has(cell)) continue;
|
|
2242
|
+
if (!candidates[cell].has(digit)) continue;
|
|
2243
|
+
if (allCellsWithDigit.every((c) => PEERS[cell].has(c))) eliminations.push({
|
|
2244
|
+
cell,
|
|
2245
|
+
digit
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
const validated = grid === void 0 ? eliminations : eliminations.filter(({ cell, digit }) => propagateDeep(cell, digit, grid, candidates) === null);
|
|
2250
|
+
if (validated.length > 0) return {
|
|
2251
|
+
technique: "deathBlossom",
|
|
2252
|
+
placements: [],
|
|
2253
|
+
eliminations: validated,
|
|
2254
|
+
patternCells: Array.from(allCells),
|
|
2255
|
+
stemCell,
|
|
2256
|
+
petalCells: chosen.map((als) => Array.from(als.cells))
|
|
2257
|
+
};
|
|
2258
|
+
return null;
|
|
2259
|
+
}
|
|
2260
|
+
for (const als of petalArrays[idx]) {
|
|
2261
|
+
if (Array.from(als.cells).some((c) => usedCells.has(c))) continue;
|
|
2262
|
+
const nextUsed = new Set([...usedCells, ...als.cells]);
|
|
2263
|
+
const result = search(idx + 1, nextUsed, [...chosen, als]);
|
|
2264
|
+
if (result) return result;
|
|
2265
|
+
}
|
|
2266
|
+
return null;
|
|
2267
|
+
};
|
|
2268
|
+
const result = search(0, /* @__PURE__ */ new Set(), []);
|
|
2269
|
+
if (result) return result;
|
|
2270
|
+
}
|
|
2271
|
+
return null;
|
|
2272
|
+
};
|
|
2273
|
+
const applyHint = (grid, candidates, hint) => {
|
|
2274
|
+
const newGrid = [...grid];
|
|
2275
|
+
const newCandidates = candidates.map((s) => new Set(s));
|
|
2276
|
+
for (const { cell, digit } of hint.placements) {
|
|
2277
|
+
newGrid[cell] = digit;
|
|
2278
|
+
newCandidates[cell] = /* @__PURE__ */ new Set();
|
|
2279
|
+
for (const peer of PEERS[cell]) newCandidates[peer].delete(digit);
|
|
2280
|
+
}
|
|
2281
|
+
for (const { cell, digit } of hint.eliminations) newCandidates[cell].delete(digit);
|
|
2282
|
+
return {
|
|
2283
|
+
grid: newGrid,
|
|
2284
|
+
candidates: newCandidates
|
|
2285
|
+
};
|
|
2286
|
+
};
|
|
2287
|
+
const findHint = (grid, candidates) => withNotation(findNakedSingle(candidates) ?? findHiddenSingle(candidates, BOX_HOUSES, "hiddenSingleBox") ?? findHiddenSingle(candidates, ROW_HOUSES, "hiddenSingleRow") ?? findHiddenSingle(candidates, COL_HOUSES, "hiddenSingleCol") ?? findHiddenGroup(candidates, 2) ?? findLockedCandidates(candidates) ?? findHiddenGroup(candidates, 3) ?? findHiddenGroup(candidates, 4) ?? findNakedGroup(candidates, 2) ?? findNakedGroup(candidates, 3) ?? findNakedGroup(candidates, 4) ?? findFish(candidates, 2) ?? findFish(candidates, 3) ?? findSkyscraper(candidates) ?? findTwoStringKite(candidates) ?? findYWing(candidates) ?? findXYZWing(candidates) ?? findFinnedFish(candidates, 2) ?? findWWing(candidates) ?? findEmptyRectangle(candidates) ?? findUniqueRectangle(grid, candidates) ?? findFinnedFish(candidates, 3) ?? findFish(candidates, 4) ?? findBUG(candidates) ?? findFinnedFish(candidates, 4) ?? findXYChain(candidates) ?? findAIC(candidates, grid) ?? findGroupedAIC(candidates, grid) ?? findALSXZ(candidates) ?? findSueDeCoq(candidates) ?? findDeathBlossom(candidates, grid) ?? findNishio(grid, candidates) ?? findNishioNet(grid, candidates) ?? findCellRegionForcingChain(grid, candidates) ?? findCellRegionForcingNet(grid, candidates) ?? findForcingChain(grid, candidates));
|
|
2288
|
+
const humanSolve = (initialGrid) => {
|
|
2289
|
+
let grid = [...initialGrid];
|
|
2290
|
+
let candidates = buildCandidates(grid);
|
|
2291
|
+
const steps = [];
|
|
2292
|
+
while (true) {
|
|
2293
|
+
const hint = findHint(grid, candidates);
|
|
2294
|
+
if (!hint) break;
|
|
2295
|
+
const next = applyHint(grid, candidates, hint);
|
|
2296
|
+
grid = next.grid;
|
|
2297
|
+
candidates = next.candidates;
|
|
2298
|
+
steps.push(hint);
|
|
2299
|
+
}
|
|
2300
|
+
return {
|
|
2301
|
+
grid,
|
|
2302
|
+
candidates,
|
|
2303
|
+
steps
|
|
2304
|
+
};
|
|
2305
|
+
};
|
|
2306
|
+
//#endregion
|
|
2307
|
+
exports.applyHint = applyHint;
|
|
2308
|
+
exports.buildCandidates = buildCandidates;
|
|
2309
|
+
exports.findHint = findHint;
|
|
2310
|
+
exports.gridToPuzzle = gridToPuzzle;
|
|
2311
|
+
exports.humanSolve = humanSolve;
|
|
2312
|
+
exports.isGridInvalid = isGridInvalid;
|
|
2313
|
+
exports.puzzleToGrid = puzzleToGrid;
|