@johnmorrisdotca/kazu 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +69 -27
- package/dist/akari-entry.d.ts +1 -0
- package/dist/akari-entry.js +1 -0
- package/dist/akari.constants.d.ts +12 -0
- package/dist/akari.constants.js +12 -0
- package/dist/akari.types.d.ts +19 -0
- package/dist/akariGenerate.d.ts +10 -5
- package/dist/akariGenerate.js +101 -65
- package/dist/akariLogic.d.ts +10 -0
- package/dist/akariLogic.js +84 -0
- package/dist/akariRate.d.ts +6 -0
- package/dist/akariRate.js +34 -0
- package/dist/akariSolve.d.ts +1 -1
- package/dist/akariSolve.js +8 -96
- package/dist/akariTemplate.d.ts +7 -0
- package/dist/akariTemplate.js +78 -0
- package/dist/cells.d.ts +4 -4
- package/dist/cells.js +6 -6
- package/dist/csp.d.ts +63 -0
- package/dist/csp.js +162 -0
- package/dist/fillomino-entry.d.ts +1 -0
- package/dist/fillomino-entry.js +1 -0
- package/dist/fillomino.constants.d.ts +6 -2
- package/dist/fillomino.constants.js +6 -2
- package/dist/fillomino.types.d.ts +18 -1
- package/dist/fillominoBuild.d.ts +10 -0
- package/dist/fillominoBuild.js +78 -0
- package/dist/fillominoGenerate.d.ts +7 -1
- package/dist/fillominoGenerate.js +75 -107
- package/dist/fillominoLogic.d.ts +14 -0
- package/dist/fillominoLogic.js +177 -0
- package/dist/fillominoMount.js +4 -1
- package/dist/fillominoRate.d.ts +3 -0
- package/dist/fillominoRate.js +57 -0
- package/dist/fillominoSolve.js +15 -88
- package/dist/groupSolve.d.ts +9 -0
- package/dist/groupSolve.js +66 -0
- package/dist/hitori-entry.d.ts +1 -0
- package/dist/hitori-entry.js +1 -0
- package/dist/hitori.constants.d.ts +7 -1
- package/dist/hitori.constants.js +7 -1
- package/dist/hitori.types.d.ts +18 -1
- package/dist/hitoriBoard.js +2 -1
- package/dist/hitoriBuild.d.ts +12 -0
- package/dist/hitoriBuild.js +117 -0
- package/dist/hitoriGenerate.d.ts +10 -3
- package/dist/hitoriGenerate.js +59 -157
- package/dist/hitoriLogic.d.ts +12 -0
- package/dist/hitoriLogic.js +189 -0
- package/dist/hitoriRate.d.ts +3 -0
- package/dist/hitoriRate.js +32 -0
- package/dist/hitoriSolve.d.ts +4 -1
- package/dist/hitoriSolve.js +11 -62
- package/dist/kakuro-entry.d.ts +1 -0
- package/dist/kakuro-entry.js +1 -0
- package/dist/kakuro.constants.d.ts +6 -1
- package/dist/kakuro.constants.js +6 -1
- package/dist/kakuro.types.d.ts +19 -0
- package/dist/kakuroBuild.d.ts +30 -0
- package/dist/kakuroBuild.js +337 -0
- package/dist/kakuroGenerate.d.ts +10 -3
- package/dist/kakuroGenerate.js +54 -122
- package/dist/kakuroLogic.d.ts +13 -0
- package/dist/kakuroLogic.js +153 -0
- package/dist/kakuroRate.d.ts +3 -0
- package/dist/kakuroRate.js +40 -0
- package/dist/kakuroSolve.js +17 -69
- package/dist/kakuroTemplate.d.ts +3 -0
- package/dist/kakuroTemplate.js +130 -0
- package/dist/kinds.js +1 -1
- package/dist/layout.js +1 -0
- package/dist/mount.d.ts +0 -18
- package/dist/mount.js +24 -3
- package/dist/names.js +3 -3
- package/dist/numberPlace.d.ts +1 -1
- package/dist/numberPlace.js +21 -8
- package/dist/shikaku-entry.d.ts +1 -0
- package/dist/shikaku-entry.js +1 -0
- package/dist/shikaku.constants.d.ts +5 -2
- package/dist/shikaku.constants.js +5 -2
- package/dist/shikaku.types.d.ts +18 -1
- package/dist/shikakuBuild.d.ts +21 -0
- package/dist/shikakuBuild.js +135 -0
- package/dist/shikakuGenerate.d.ts +6 -1
- package/dist/shikakuGenerate.js +50 -35
- package/dist/shikakuLogic.d.ts +13 -0
- package/dist/shikakuLogic.js +84 -0
- package/dist/shikakuRate.d.ts +3 -0
- package/dist/shikakuRate.js +27 -0
- package/dist/shikakuSolve.d.ts +1 -1
- package/dist/shikakuSolve.js +23 -44
- package/dist/shikakuTemplate.d.ts +3 -0
- package/dist/shikakuTemplate.js +43 -0
- package/dist/slitherlink-entry.d.ts +1 -0
- package/dist/slitherlink-entry.js +1 -0
- package/dist/slitherlink.constants.d.ts +5 -0
- package/dist/slitherlink.constants.js +5 -0
- package/dist/slitherlink.types.d.ts +18 -0
- package/dist/slitherlinkGenerate.d.ts +10 -3
- package/dist/slitherlinkGenerate.js +161 -80
- package/dist/slitherlinkLogic.d.ts +10 -0
- package/dist/slitherlinkLogic.js +267 -0
- package/dist/slitherlinkRate.d.ts +3 -0
- package/dist/slitherlinkRate.js +26 -0
- package/dist/slitherlinkSolve.d.ts +1 -1
- package/dist/slitherlinkSolve.js +9 -117
- package/dist/slitherlinkTemplate.d.ts +3 -0
- package/dist/slitherlinkTemplate.js +101 -0
- package/dist/strings.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { edgeVertices, slitherlinkCellEdges, slitherlinkEdgeCount, slitherlinkVertexEdges } from "./slitherlinkBoard.js";
|
|
2
|
+
/**
|
|
3
|
+
* Slitherlink as variables: one per edge, slot 0 for off and slot 1 for on. The pruning rules are the ones
|
|
4
|
+
* a person uses first: a number counts the edges round its square, every corner has none or two edges, and
|
|
5
|
+
* a loop may not close while any other part of it is still to be drawn or while a number is not yet met.
|
|
6
|
+
*/
|
|
7
|
+
export function slitherlinkModel(board) {
|
|
8
|
+
const edges = slitherlinkEdgeCount(board), vertexCount = (board.width + 1) * (board.height + 1);
|
|
9
|
+
const at = Array.from({ length: vertexCount }, (_, vertex) => Int32Array.from(slitherlinkVertexEdges(board, vertex)));
|
|
10
|
+
const endA = new Int32Array(edges), endB = new Int32Array(edges);
|
|
11
|
+
for (let edge = 0; edge < edges; edge += 1) {
|
|
12
|
+
const [a, b] = edgeVertices(board, edge);
|
|
13
|
+
endA[edge] = a;
|
|
14
|
+
endB[edge] = b;
|
|
15
|
+
}
|
|
16
|
+
const clues = board.clues.flatMap((clue, cell) => clue === null ? [] : [{ need: clue, edges: Int32Array.from(slitherlinkCellEdges(board, cell)) }]);
|
|
17
|
+
const cluesOf = Array.from({ length: edges }, () => []);
|
|
18
|
+
clues.forEach((clue, index) => clue.edges.forEach(edge => cluesOf[edge].push(index)));
|
|
19
|
+
const starts = Int32Array.from({ length: edges + 1 }, (_, i) => i * 2);
|
|
20
|
+
const endList = new Int32Array(vertexCount), parent = new Int32Array(vertexCount), degree = new Uint8Array(vertexCount), componentEdges = new Int32Array(vertexCount), openEnds = new Int32Array(vertexCount);
|
|
21
|
+
const vertexQueue = new Int32Array(vertexCount * 8), clueQueue = new Int32Array(clues.length * 8 + 8);
|
|
22
|
+
const vertexQueued = new Uint8Array(vertexCount), clueQueued = new Uint8Array(clues.length);
|
|
23
|
+
const reachable = new Int32Array(vertexCount);
|
|
24
|
+
const findIn = (table, v) => { while (table[v] !== v) {
|
|
25
|
+
table[v] = table[table[v]];
|
|
26
|
+
v = table[v];
|
|
27
|
+
} return v; };
|
|
28
|
+
const find = (v) => { while (parent[v] !== v) {
|
|
29
|
+
parent[v] = parent[parent[v]];
|
|
30
|
+
v = parent[v];
|
|
31
|
+
} return v; };
|
|
32
|
+
const propagate = (alive, decided) => {
|
|
33
|
+
let vertexHead = 0, vertexTail = 0, clueHead = 0, clueTail = 0;
|
|
34
|
+
const touch = (e) => {
|
|
35
|
+
const a = endA[e], b = endB[e];
|
|
36
|
+
if (!vertexQueued[a]) {
|
|
37
|
+
vertexQueued[a] = 1;
|
|
38
|
+
vertexQueue[vertexTail++ % vertexQueue.length] = a;
|
|
39
|
+
}
|
|
40
|
+
if (!vertexQueued[b]) {
|
|
41
|
+
vertexQueued[b] = 1;
|
|
42
|
+
vertexQueue[vertexTail++ % vertexQueue.length] = b;
|
|
43
|
+
}
|
|
44
|
+
for (const index of cluesOf[e])
|
|
45
|
+
if (!clueQueued[index]) {
|
|
46
|
+
clueQueued[index] = 1;
|
|
47
|
+
clueQueue[clueTail++ % clueQueue.length] = index;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const clear = () => { vertexQueued.fill(0); clueQueued.fill(0); return false; };
|
|
51
|
+
if (decided === undefined)
|
|
52
|
+
for (let e = 0; e < edges; e += 1)
|
|
53
|
+
touch(e);
|
|
54
|
+
else
|
|
55
|
+
touch(decided);
|
|
56
|
+
for (;;) {
|
|
57
|
+
while (vertexHead < vertexTail || clueHead < clueTail) {
|
|
58
|
+
while (vertexHead < vertexTail) {
|
|
59
|
+
const v = vertexQueue[vertexHead++ % vertexQueue.length];
|
|
60
|
+
vertexQueued[v] = 0;
|
|
61
|
+
const around = at[v];
|
|
62
|
+
let on = 0, open = 0, lastOpen = -1;
|
|
63
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
64
|
+
const e = around[k], off = alive[2 * e], onSlot = alive[2 * e + 1];
|
|
65
|
+
if (!off && !onSlot)
|
|
66
|
+
return clear();
|
|
67
|
+
if (onSlot && !off)
|
|
68
|
+
on += 1;
|
|
69
|
+
else if (off && onSlot) {
|
|
70
|
+
open += 1;
|
|
71
|
+
lastOpen = e;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (on > 2 || on === 1 && !open)
|
|
75
|
+
return clear();
|
|
76
|
+
if (on === 2 && open) {
|
|
77
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
78
|
+
const e = around[k];
|
|
79
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
80
|
+
alive[2 * e + 1] = 0;
|
|
81
|
+
touch(e);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (open === 1 && on < 2) {
|
|
86
|
+
if (on === 1)
|
|
87
|
+
alive[2 * lastOpen] = 0;
|
|
88
|
+
else
|
|
89
|
+
alive[2 * lastOpen + 1] = 0;
|
|
90
|
+
touch(lastOpen);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
while (clueHead < clueTail) {
|
|
94
|
+
const index = clueQueue[clueHead++ % clueQueue.length], clue = clues[index];
|
|
95
|
+
clueQueued[index] = 0;
|
|
96
|
+
let on = 0, open = 0;
|
|
97
|
+
for (let k = 0; k < 4; k += 1) {
|
|
98
|
+
const e = clue.edges[k];
|
|
99
|
+
if (!alive[2 * e] && !alive[2 * e + 1])
|
|
100
|
+
return clear();
|
|
101
|
+
if (alive[2 * e + 1] && !alive[2 * e])
|
|
102
|
+
on += 1;
|
|
103
|
+
else if (alive[2 * e] && alive[2 * e + 1])
|
|
104
|
+
open += 1;
|
|
105
|
+
}
|
|
106
|
+
if (on > clue.need || on + open < clue.need)
|
|
107
|
+
return clear();
|
|
108
|
+
if (open && on === clue.need) {
|
|
109
|
+
for (let k = 0; k < 4; k += 1) {
|
|
110
|
+
const e = clue.edges[k];
|
|
111
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
112
|
+
alive[2 * e + 1] = 0;
|
|
113
|
+
touch(e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (open && on + open === clue.need) {
|
|
118
|
+
for (let k = 0; k < 4; k += 1) {
|
|
119
|
+
const e = clue.edges[k];
|
|
120
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
121
|
+
alive[2 * e] = 0;
|
|
122
|
+
touch(e);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Loops: join the drawn edges into pieces, then forbid an edge that would close a piece too early.
|
|
129
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
130
|
+
parent[v] = v;
|
|
131
|
+
degree[v] = 0;
|
|
132
|
+
componentEdges[v] = 0;
|
|
133
|
+
openEnds[v] = 0;
|
|
134
|
+
}
|
|
135
|
+
let drawn = 0;
|
|
136
|
+
for (let e = 0; e < edges; e += 1) {
|
|
137
|
+
if (!(alive[2 * e + 1] && !alive[2 * e]))
|
|
138
|
+
continue;
|
|
139
|
+
drawn += 1;
|
|
140
|
+
const a = endA[e], b = endB[e];
|
|
141
|
+
degree[a] += 1;
|
|
142
|
+
degree[b] += 1;
|
|
143
|
+
parent[find(a)] = find(b);
|
|
144
|
+
}
|
|
145
|
+
if (!drawn) {
|
|
146
|
+
// No loop yet: a fully decided empty drawing is not an answer.
|
|
147
|
+
for (let e = 0; e < edges; e += 1)
|
|
148
|
+
if (alive[2 * e] && alive[2 * e + 1])
|
|
149
|
+
return true;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
for (let e = 0; e < edges; e += 1)
|
|
153
|
+
if (alive[2 * e + 1] && !alive[2 * e])
|
|
154
|
+
componentEdges[find(endA[e])] += 1;
|
|
155
|
+
// Everything drawn must still be able to join up: the edges not ruled out have to connect all of it.
|
|
156
|
+
for (let v = 0; v < vertexCount; v += 1)
|
|
157
|
+
reachable[v] = v;
|
|
158
|
+
for (let e = 0; e < edges; e += 1)
|
|
159
|
+
if (alive[2 * e + 1])
|
|
160
|
+
reachable[findIn(reachable, endA[e])] = findIn(reachable, endB[e]);
|
|
161
|
+
let anchor = -1;
|
|
162
|
+
for (let e = 0; e < edges; e += 1) {
|
|
163
|
+
if (!(alive[2 * e + 1] && !alive[2 * e]))
|
|
164
|
+
continue;
|
|
165
|
+
const root = findIn(reachable, endA[e]);
|
|
166
|
+
if (anchor < 0)
|
|
167
|
+
anchor = root;
|
|
168
|
+
else if (root !== anchor)
|
|
169
|
+
return clear();
|
|
170
|
+
}
|
|
171
|
+
let ends = 0;
|
|
172
|
+
for (let v = 0; v < vertexCount; v += 1)
|
|
173
|
+
if (degree[v] === 1) {
|
|
174
|
+
openEnds[find(v)] += 1;
|
|
175
|
+
endList[ends++] = v;
|
|
176
|
+
}
|
|
177
|
+
let moved = false;
|
|
178
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
179
|
+
if (degree[v] === 0 || find(v) !== v || openEnds[v])
|
|
180
|
+
continue;
|
|
181
|
+
if (componentEdges[v] !== drawn)
|
|
182
|
+
return clear();
|
|
183
|
+
// A finished loop is the whole answer: nothing else may be drawn.
|
|
184
|
+
for (let e = 0; e < edges; e += 1)
|
|
185
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
186
|
+
alive[2 * e + 1] = 0;
|
|
187
|
+
touch(e);
|
|
188
|
+
moved = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// An edge joining the two ends of one piece would close it.
|
|
192
|
+
for (let k = 0; k < ends; k += 1) {
|
|
193
|
+
const a = endList[k], around = at[a];
|
|
194
|
+
for (let j = 0; j < around.length; j += 1) {
|
|
195
|
+
const e = around[j];
|
|
196
|
+
if (!(alive[2 * e] && alive[2 * e + 1]))
|
|
197
|
+
continue;
|
|
198
|
+
const b = endA[e] === a ? endB[e] : endA[e];
|
|
199
|
+
if (degree[b] !== 1 || find(a) !== find(b))
|
|
200
|
+
continue;
|
|
201
|
+
// Closing this edge finishes the loop: allowed only when it is the whole drawing and every number is met.
|
|
202
|
+
let whole = componentEdges[find(a)] === drawn;
|
|
203
|
+
if (whole) {
|
|
204
|
+
for (const clue of clues) {
|
|
205
|
+
let on = 0;
|
|
206
|
+
for (let k2 = 0; k2 < 4; k2 += 1) {
|
|
207
|
+
const x = clue.edges[k2];
|
|
208
|
+
if (x === e || alive[2 * x + 1] && !alive[2 * x])
|
|
209
|
+
on += 1;
|
|
210
|
+
}
|
|
211
|
+
if (on !== clue.need) {
|
|
212
|
+
whole = false;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!whole) {
|
|
218
|
+
alive[2 * e + 1] = 0;
|
|
219
|
+
touch(e);
|
|
220
|
+
moved = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!moved)
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const choose = (alive) => {
|
|
229
|
+
// Branch where the board is most pinned down: next to a loose end of the drawing, else on the numbered square with the fewest edges left.
|
|
230
|
+
let best = -1, bestScore = 99;
|
|
231
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
232
|
+
const around = at[v];
|
|
233
|
+
let on = 0, first = -1;
|
|
234
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
235
|
+
const e = around[k];
|
|
236
|
+
if (alive[2 * e + 1] && !alive[2 * e])
|
|
237
|
+
on += 1;
|
|
238
|
+
else if (alive[2 * e] && alive[2 * e + 1] && first < 0)
|
|
239
|
+
first = e;
|
|
240
|
+
}
|
|
241
|
+
if (first >= 0 && on === 1)
|
|
242
|
+
return first;
|
|
243
|
+
}
|
|
244
|
+
for (const clue of clues) {
|
|
245
|
+
let open = 0, first = -1;
|
|
246
|
+
for (let k = 0; k < 4; k += 1) {
|
|
247
|
+
const e = clue.edges[k];
|
|
248
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
249
|
+
open += 1;
|
|
250
|
+
if (first < 0)
|
|
251
|
+
first = e;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (open && open < bestScore) {
|
|
255
|
+
bestScore = open;
|
|
256
|
+
best = first;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (best >= 0)
|
|
260
|
+
return best;
|
|
261
|
+
for (let e = 0; e < edges; e += 1)
|
|
262
|
+
if (alive[2 * e] && alive[2 * e + 1])
|
|
263
|
+
return e;
|
|
264
|
+
return -1;
|
|
265
|
+
};
|
|
266
|
+
return { csp: { starts, propagate, choose } };
|
|
267
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SlitherlinkBoard, SlitherlinkRating } from "./slitherlink.types.ts";
|
|
2
|
+
/** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one edge at a time. */
|
|
3
|
+
export declare function rateSlitherlink(board: SlitherlinkBoard): SlitherlinkRating;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isSlitherlinkBoard } from "./slitherlinkBoard.js";
|
|
2
|
+
import { slitherlinkModel } from "./slitherlinkLogic.js";
|
|
3
|
+
import { logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
/** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one edge at a time. */
|
|
5
|
+
export function rateSlitherlink(board) {
|
|
6
|
+
if (!isSlitherlinkBoard(board))
|
|
7
|
+
throw new RangeError("Invalid Slitherlink board");
|
|
8
|
+
const { csp } = slitherlinkModel(board);
|
|
9
|
+
const proof = solveCsp(csp, openSlots(csp), 2, 300000);
|
|
10
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
11
|
+
throw new RangeError("Slitherlink rating needs a board with one answer");
|
|
12
|
+
const plain = logicCsp(csp, openSlots(csp), 0);
|
|
13
|
+
const probing = plain.solved ? plain : logicCsp(csp, openSlots(csp), 1);
|
|
14
|
+
const numbered = board.clues.filter(clue => clue !== null);
|
|
15
|
+
let loop = 0;
|
|
16
|
+
for (let at = 1; at < proof.solution.length; at += 2)
|
|
17
|
+
loop += proof.solution[at];
|
|
18
|
+
return {
|
|
19
|
+
depth: plain.solved ? 0 : probing.solved ? 1 : 2,
|
|
20
|
+
probes: plain.solved ? 0 : probing.probes,
|
|
21
|
+
clues: numbered.length,
|
|
22
|
+
clueShare: numbered.length / board.clues.length,
|
|
23
|
+
zeroShare: numbered.length ? numbered.filter(clue => clue === 0).length / numbered.length : 0,
|
|
24
|
+
loop,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SlitherlinkBoard, SlitherlinkSolve } from "./slitherlink.types.ts";
|
|
2
|
-
/** Counts loop solutions with
|
|
2
|
+
/** Counts loop solutions with the pruning in `slitherlinkLogic.ts` under explicit work bounds. */
|
|
3
3
|
export declare function solveSlitherlink(board: SlitherlinkBoard, options?: {
|
|
4
4
|
limit?: number;
|
|
5
5
|
nodes?: number;
|
package/dist/slitherlinkSolve.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { SLITHERLINK_MOST_NODES } from "./slitherlink.constants.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { isSlitherlinkBoard, slitherlinkEdgeCount } from "./slitherlinkBoard.js";
|
|
3
|
+
import { slitherlinkModel } from "./slitherlinkLogic.js";
|
|
4
|
+
import { openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Counts loop solutions with the pruning in `slitherlinkLogic.ts` under explicit work bounds. */
|
|
4
6
|
export function solveSlitherlink(board, options = {}) {
|
|
5
7
|
if (!isSlitherlinkBoard(board))
|
|
6
8
|
throw new RangeError("Invalid Slitherlink board");
|
|
@@ -9,119 +11,9 @@ export function solveSlitherlink(board, options = {}) {
|
|
|
9
11
|
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1) {
|
|
10
12
|
throw new RangeError("Invalid search bounds");
|
|
11
13
|
}
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let complete = true;
|
|
18
|
-
let solution = null;
|
|
19
|
-
const search = (start) => {
|
|
20
|
-
if (!complete)
|
|
21
|
-
return;
|
|
22
|
-
nodes += 1;
|
|
23
|
-
if (nodes > budget) {
|
|
24
|
-
complete = false;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const state = start.slice();
|
|
28
|
-
let changed = true;
|
|
29
|
-
while (changed) {
|
|
30
|
-
changed = false;
|
|
31
|
-
for (const group of vertices) {
|
|
32
|
-
const on = group.filter(edge => state[edge] === 1).length;
|
|
33
|
-
const unknown = group.filter(edge => state[edge] < 0);
|
|
34
|
-
if (on > 2 || on === 1 && !unknown.length)
|
|
35
|
-
return;
|
|
36
|
-
if (on === 2) {
|
|
37
|
-
for (const edge of unknown) {
|
|
38
|
-
state[edge] = 0;
|
|
39
|
-
changed = true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else if (on === 1 && unknown.length === 1) {
|
|
43
|
-
state[unknown[0]] = 1;
|
|
44
|
-
changed = true;
|
|
45
|
-
}
|
|
46
|
-
else if (on === 0 && unknown.length === 1) {
|
|
47
|
-
state[unknown[0]] = 0;
|
|
48
|
-
changed = true;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
let clueIndex = 0;
|
|
52
|
-
for (const clue of board.clues) {
|
|
53
|
-
if (clue === null)
|
|
54
|
-
continue;
|
|
55
|
-
const group = cells[clueIndex++];
|
|
56
|
-
const on = group.filter(edge => state[edge] === 1).length;
|
|
57
|
-
const unknown = group.filter(edge => state[edge] < 0);
|
|
58
|
-
if (on > clue || on + unknown.length < clue)
|
|
59
|
-
return;
|
|
60
|
-
if (on === clue) {
|
|
61
|
-
for (const edge of unknown) {
|
|
62
|
-
state[edge] = 0;
|
|
63
|
-
changed = true;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (on + unknown.length === clue) {
|
|
67
|
-
for (const edge of unknown) {
|
|
68
|
-
state[edge] = 1;
|
|
69
|
-
changed = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const selected = Array.from(state).flatMap((value, edge) => value === 1 ? [edge] : []);
|
|
75
|
-
const closed = closedComponents(board, selected);
|
|
76
|
-
if (closed.length > 1)
|
|
77
|
-
return;
|
|
78
|
-
if (closed.length === 1 && closed[0].length !== selected.length)
|
|
79
|
-
return;
|
|
80
|
-
const branch = state.findIndex(value => value < 0);
|
|
81
|
-
if (branch < 0) {
|
|
82
|
-
if (!checkSlitherlink(board, selected).ok)
|
|
83
|
-
return;
|
|
84
|
-
count += 1;
|
|
85
|
-
solution ?? (solution = selected);
|
|
86
|
-
if (count >= limit)
|
|
87
|
-
complete = false;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
for (const value of [1, 0]) {
|
|
91
|
-
const next = state.slice();
|
|
92
|
-
next[branch] = value;
|
|
93
|
-
search(next);
|
|
94
|
-
if (!complete)
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
search(new Int8Array(edgeCount).fill(-1));
|
|
99
|
-
return { count, solution, complete, nodes };
|
|
100
|
-
}
|
|
101
|
-
function closedComponents(board, selected) {
|
|
102
|
-
const unseen = new Set(selected);
|
|
103
|
-
const components = [];
|
|
104
|
-
while (unseen.size) {
|
|
105
|
-
const first = unseen.values().next().value;
|
|
106
|
-
const component = [first];
|
|
107
|
-
const queue = [first];
|
|
108
|
-
unseen.delete(first);
|
|
109
|
-
while (queue.length) {
|
|
110
|
-
const edge = queue.pop();
|
|
111
|
-
for (const vertex of edgeVertices(board, edge)) {
|
|
112
|
-
for (const adjacent of slitherlinkVertexEdges(board, vertex)) {
|
|
113
|
-
if (!unseen.has(adjacent))
|
|
114
|
-
continue;
|
|
115
|
-
unseen.delete(adjacent);
|
|
116
|
-
component.push(adjacent);
|
|
117
|
-
queue.push(adjacent);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const vertices = new Set(component.flatMap(edge => edgeVertices(board, edge)));
|
|
122
|
-
if ([...vertices].every(vertex => slitherlinkVertexEdges(board, vertex)
|
|
123
|
-
.filter(edge => component.includes(edge)).length === 2))
|
|
124
|
-
components.push(component);
|
|
125
|
-
}
|
|
126
|
-
return components;
|
|
14
|
+
const { csp } = slitherlinkModel(board);
|
|
15
|
+
const found = solveCsp(csp, openSlots(csp), limit, budget);
|
|
16
|
+
const edges = slitherlinkEdgeCount(board);
|
|
17
|
+
const solution = found.solution ? Array.from({ length: edges }, (_, edge) => edge).filter(edge => found.solution[2 * edge + 1] === 1) : null;
|
|
18
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
127
19
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SlitherlinkPuzzle } from "./slitherlink.types.ts";
|
|
2
|
+
/** The first generator, kept as the fallback that cannot fail: a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
|
|
3
|
+
export declare function templateSlitherlink(width?: number, height?: number, seed?: number): Omit<SlitherlinkPuzzle, "level">;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { SLITHERLINK_MOST_SIDE } from "./slitherlink.constants.js";
|
|
2
|
+
import { checkSlitherlink, isSlitherlinkBoard, slitherlinkCellEdges, } from "./slitherlinkBoard.js";
|
|
3
|
+
import { solveSlitherlink } from "./slitherlinkSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/** The first generator, kept as the fallback that cannot fail: a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
|
|
6
|
+
export function templateSlitherlink(width = 5, height = width, seed = 1) {
|
|
7
|
+
if (![width, height].every(side => Number.isInteger(side) && side >= 2 && side <= SLITHERLINK_MOST_SIDE)
|
|
8
|
+
|| !isKazuSeed(seed))
|
|
9
|
+
throw new RangeError("Invalid Slitherlink settings");
|
|
10
|
+
const random = seededRandom(seed);
|
|
11
|
+
const useL = width >= 3 && height >= 3 && random() < .5;
|
|
12
|
+
const region = [];
|
|
13
|
+
let x0;
|
|
14
|
+
let y0;
|
|
15
|
+
let blockWidth;
|
|
16
|
+
let blockHeight;
|
|
17
|
+
if (useL) {
|
|
18
|
+
blockWidth = 2 + Math.floor(random() * (width - 1));
|
|
19
|
+
blockHeight = 2 + Math.floor(random() * (height - 1));
|
|
20
|
+
x0 = Math.floor(random() * (width - blockWidth + 1));
|
|
21
|
+
y0 = Math.floor(random() * (height - blockHeight + 1));
|
|
22
|
+
for (let y = 0; y < blockHeight; y += 1) {
|
|
23
|
+
for (let x = 0; x < blockWidth; x += 1) {
|
|
24
|
+
if (x === 0 || y === 0)
|
|
25
|
+
region.push((y0 + y) * width + x0 + x);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
blockWidth = 1 + Math.floor(random() * width);
|
|
31
|
+
blockHeight = 1 + Math.floor(random() * height);
|
|
32
|
+
if (blockWidth === 1 && blockHeight === 1)
|
|
33
|
+
blockWidth = 2;
|
|
34
|
+
x0 = Math.floor(random() * (width - blockWidth + 1));
|
|
35
|
+
y0 = Math.floor(random() * (height - blockHeight + 1));
|
|
36
|
+
for (let y = 0; y < blockHeight; y += 1) {
|
|
37
|
+
for (let x = 0; x < blockWidth; x += 1)
|
|
38
|
+
region.push((y0 + y) * width + x0 + x);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const blank = { width, height, clues: Array(width * height).fill(null) };
|
|
42
|
+
const regionSet = new Set(region);
|
|
43
|
+
const edges = new Set();
|
|
44
|
+
for (const cell of region) {
|
|
45
|
+
const [top, right, bottom, left] = slitherlinkCellEdges(blank, cell);
|
|
46
|
+
const x = cell % width;
|
|
47
|
+
if (!regionSet.has(cell - width))
|
|
48
|
+
edges.add(top);
|
|
49
|
+
if (x === width - 1 || !regionSet.has(cell + 1))
|
|
50
|
+
edges.add(right);
|
|
51
|
+
if (!regionSet.has(cell + width))
|
|
52
|
+
edges.add(bottom);
|
|
53
|
+
if (x === 0 || !regionSet.has(cell - 1))
|
|
54
|
+
edges.add(left);
|
|
55
|
+
}
|
|
56
|
+
let board = {
|
|
57
|
+
width,
|
|
58
|
+
height,
|
|
59
|
+
clues: Array.from({ length: width * height }, (_, cell) => slitherlinkCellEdges(blank, cell).filter(edge => edges.has(edge)).length),
|
|
60
|
+
};
|
|
61
|
+
let answer = [...edges];
|
|
62
|
+
// Seeded axis reflections give each selected shape more than one orientation.
|
|
63
|
+
const reflectX = random() < .5;
|
|
64
|
+
const reflectY = random() < .5;
|
|
65
|
+
if (reflectX || reflectY) {
|
|
66
|
+
const horizontalCount = width * (height + 1);
|
|
67
|
+
const mapEdge = (edge) => {
|
|
68
|
+
if (edge < horizontalCount) {
|
|
69
|
+
const x = edge % width;
|
|
70
|
+
const y = Math.floor(edge / width);
|
|
71
|
+
const nextX = reflectX ? width - 1 - x : x;
|
|
72
|
+
const nextY = reflectY ? height - y : y;
|
|
73
|
+
return nextY * width + nextX;
|
|
74
|
+
}
|
|
75
|
+
const local = edge - horizontalCount;
|
|
76
|
+
const x = local % (width + 1);
|
|
77
|
+
const y = Math.floor(local / (width + 1));
|
|
78
|
+
const nextX = reflectX ? width - x : x;
|
|
79
|
+
const nextY = reflectY ? height - 1 - y : y;
|
|
80
|
+
return horizontalCount + nextY * (width + 1) + nextX;
|
|
81
|
+
};
|
|
82
|
+
const reflected = Array(width * height).fill(null);
|
|
83
|
+
board.clues.forEach((clue, cell) => {
|
|
84
|
+
const x = cell % width;
|
|
85
|
+
const y = Math.floor(cell / width);
|
|
86
|
+
const nextX = reflectX ? width - 1 - x : x;
|
|
87
|
+
const nextY = reflectY ? height - 1 - y : y;
|
|
88
|
+
reflected[nextY * width + nextX] = clue;
|
|
89
|
+
});
|
|
90
|
+
board = { width, height, clues: reflected };
|
|
91
|
+
answer = answer.map(mapEdge);
|
|
92
|
+
}
|
|
93
|
+
if (!isSlitherlinkBoard(board) || !checkSlitherlink(board, answer).ok) {
|
|
94
|
+
throw new Error("Unable to construct Slitherlink loop");
|
|
95
|
+
}
|
|
96
|
+
const proof = solveSlitherlink(board, { limit: 2 });
|
|
97
|
+
if (!proof.complete || proof.count !== 1 || !proof.solution) {
|
|
98
|
+
throw new Error("No uniquely solvable Slitherlink found within the generation budget");
|
|
99
|
+
}
|
|
100
|
+
return { ...board, seed, solution: proof.solution };
|
|
101
|
+
}
|
package/dist/strings.js
CHANGED
|
@@ -19,6 +19,7 @@ export const KAZU_STRINGS = {
|
|
|
19
19
|
tap: "Tap a cell, then a number.",
|
|
20
20
|
tapPencil: "Pencil marks are on: tap a number to note it in the cell.",
|
|
21
21
|
keys: "Arrow keys move, a number fills the cell, Backspace empties it, N turns pencil marks on or off.",
|
|
22
|
+
keysColossus: "Arrow keys move, a number or a letter from A to P fills the cell, Backspace empties it, the slash key turns pencil marks on or off (N is a number here).",
|
|
22
23
|
undo: "Undo",
|
|
23
24
|
pencil: "Pencil",
|
|
24
25
|
pencilTitle: "Write small notes in a cell instead of a number",
|
|
@@ -83,6 +84,7 @@ export const KAZU_STRINGS = {
|
|
|
83
84
|
tap: "マスをタップして、数字を選びます。",
|
|
84
85
|
tapPencil: "メモがオンです。数字をタップすると、マスにメモします。",
|
|
85
86
|
keys: "矢印キーで移動、数字キーで入力、Backspaceで消去、Nでメモのオンとオフ。",
|
|
87
|
+
keysColossus: "矢印キーで移動、数字またはAからPのキーで入力、Backspaceで消去、スラッシュキーでメモのオンとオフ(ここではNは数字です)。",
|
|
86
88
|
undo: "元に戻す",
|
|
87
89
|
pencil: "メモ",
|
|
88
90
|
pencilTitle: "数字のかわりに、マスに小さくメモを書きます",
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export declare const VERSION = "1.
|
|
2
|
+
export declare const VERSION = "1.4.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export const VERSION = "1.
|
|
2
|
+
export const VERSION = "1.4.0";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kazu",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Grid number and logic puzzles for JavaScript and TypeScript: Sudoku and its variants, Futoshiki, Skyscrapers, Shikaku, Hitori, Nurikabe, Akari, Juosan, Slitherlink, Masyu, Yajilin, Ripple Effect, Kakuro, Fillomino and Heyawake. Dedicated typed engines, independently checked puzzles, SVG boards and bilingual touch and keyboard players. Zero runtime dependencies.",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Grid number and logic puzzles for JavaScript and TypeScript: Sudoku (up to 25×25) and its variants, Futoshiki, Skyscrapers, Shikaku, Hitori, Nurikabe, Akari, Juosan, Slitherlink, Masyu, Yajilin, Ripple Effect, Kakuro, Fillomino and Heyawake. Dedicated typed engines, independently checked puzzles, SVG boards and bilingual touch and keyboard players. Zero runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"puzzle",
|
|
7
7
|
"sudoku",
|