@johnmorrisdotca/kazu 1.2.0 → 1.3.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 +23 -0
- package/README.md +58 -16
- package/dist/akari-entry.d.ts +1 -0
- package/dist/akari-entry.js +1 -0
- package/dist/akari.constants.d.ts +5 -0
- package/dist/akari.constants.js +5 -0
- package/dist/akari.types.d.ts +19 -0
- package/dist/akariGenerate.d.ts +8 -5
- package/dist/akariGenerate.js +88 -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/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/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/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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { isKakuroBoard } from "./kakuroBoard.js";
|
|
2
|
+
import { kakuroModel } from "./kakuroLogic.js";
|
|
3
|
+
import { countCsp, logicCsp, openSlots } from "./csp.js";
|
|
4
|
+
import { shuffled } from "./random.js";
|
|
5
|
+
/** How the black squares are scattered, and the longest run a level allows. */
|
|
6
|
+
export const SHAPE = {
|
|
7
|
+
easy: { black: .34, longest: 4 },
|
|
8
|
+
medium: { black: .3, longest: 5 },
|
|
9
|
+
hard: { black: .26, longest: 7 },
|
|
10
|
+
"extra-hard": { black: .22, longest: 9 },
|
|
11
|
+
};
|
|
12
|
+
/** Which squares are white: no run shorter than two, none longer than the level allows, and all of them one connected piece. */
|
|
13
|
+
export function kakuroPattern(side, level, random) {
|
|
14
|
+
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
15
|
+
const white = laidRows(side, level, random);
|
|
16
|
+
if (white && oneStrip(side, white))
|
|
17
|
+
return white;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Lays the squares out row by row, each row left to right, so that a run is never one square or longer than the
|
|
23
|
+
* level allows: a column whose run is one square long must go on, one at the limit must stop, and a row's own
|
|
24
|
+
* runs are kept the same way. A square is white with the level's chance, except where a rule forces it.
|
|
25
|
+
*/
|
|
26
|
+
function laidRows(side, level, random) {
|
|
27
|
+
const { black, longest } = SHAPE[level];
|
|
28
|
+
const white = Array(side * side).fill(false), above = Array(side).fill(0);
|
|
29
|
+
const chance = 1 - black * (.85 + random() * .3);
|
|
30
|
+
for (let row = 1; row < side; row += 1) {
|
|
31
|
+
const last = row === side - 1, chosen = Array(side).fill(false);
|
|
32
|
+
let steps = 0;
|
|
33
|
+
const place = (col, run) => {
|
|
34
|
+
if (++steps > 4000)
|
|
35
|
+
return false;
|
|
36
|
+
if (col === side)
|
|
37
|
+
return run !== 1;
|
|
38
|
+
const mustWhite = above[col] === 1;
|
|
39
|
+
const mustBlack = above[col] === longest || last && above[col] === 0;
|
|
40
|
+
const options = [];
|
|
41
|
+
if (mustWhite && mustBlack)
|
|
42
|
+
return false;
|
|
43
|
+
if (mustWhite)
|
|
44
|
+
options.push(true);
|
|
45
|
+
else if (mustBlack)
|
|
46
|
+
options.push(false);
|
|
47
|
+
else
|
|
48
|
+
options.push(...(random() < chance ? [true, false] : [false, true]));
|
|
49
|
+
for (const isWhite of options) {
|
|
50
|
+
if (isWhite && run + 1 > longest)
|
|
51
|
+
continue;
|
|
52
|
+
if (!isWhite && run === 1)
|
|
53
|
+
continue;
|
|
54
|
+
chosen[col] = isWhite;
|
|
55
|
+
if (place(col + 1, isWhite ? run + 1 : 0))
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
if (!place(1, 0))
|
|
61
|
+
return null;
|
|
62
|
+
for (let col = 1; col < side; col += 1) {
|
|
63
|
+
white[row * side + col] = chosen[col];
|
|
64
|
+
above[col] = chosen[col] ? above[col] + 1 : 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return white;
|
|
68
|
+
}
|
|
69
|
+
/** How many different sets of digits can make `sum` out of `length` squares. */
|
|
70
|
+
export function kakuroWays(length, sum) {
|
|
71
|
+
let ways = 0;
|
|
72
|
+
for (let mask = 1; mask < 512; mask += 1) {
|
|
73
|
+
let n = 0, total = 0;
|
|
74
|
+
for (let d = 1; d <= 9; d += 1)
|
|
75
|
+
if (mask & 1 << d - 1) {
|
|
76
|
+
n += 1;
|
|
77
|
+
total += d;
|
|
78
|
+
}
|
|
79
|
+
if (n === length && total === sum)
|
|
80
|
+
ways += 1;
|
|
81
|
+
}
|
|
82
|
+
return ways;
|
|
83
|
+
}
|
|
84
|
+
const WAYS = Array.from({ length: 10 }, (_, length) => Array.from({ length: 46 }, (_, sum) => length < 2 ? 0 : kakuroWays(length, sum)));
|
|
85
|
+
/** A random digit for every white square with no digit repeated in a run, or null when the search gives up. */
|
|
86
|
+
export function kakuroDigits(side, white, random) {
|
|
87
|
+
const digits = Array(white.length).fill(0);
|
|
88
|
+
let steps = 0;
|
|
89
|
+
const fill = (at) => {
|
|
90
|
+
while (at < white.length && !white[at])
|
|
91
|
+
at += 1;
|
|
92
|
+
if (at === white.length)
|
|
93
|
+
return true;
|
|
94
|
+
if (++steps > 40000)
|
|
95
|
+
return false;
|
|
96
|
+
const used = new Set();
|
|
97
|
+
for (let left = at - 1; left >= 0 && white[left] && (left + 1) % side !== 0; left -= 1)
|
|
98
|
+
used.add(digits[left]);
|
|
99
|
+
for (let up = at - side; up >= 0 && white[up]; up -= side)
|
|
100
|
+
used.add(digits[up]);
|
|
101
|
+
for (const digit of shuffled([1, 2, 3, 4, 5, 6, 7, 8, 9].filter(d => !used.has(d)), random)) {
|
|
102
|
+
digits[at] = digit;
|
|
103
|
+
if (fill(at + 1))
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
digits[at] = 0;
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
return fill(0) ? digits : null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Changes digits one square at a time, keeping every run free of repeats, to make runs whose totals can be made in
|
|
113
|
+
* few ways: a run that says a lot about its digits lets the solver start from the rules alone. A change is kept when
|
|
114
|
+
* it does not raise the count of ways, summed over runs, which is `weight` raised to the number of ways.
|
|
115
|
+
*/
|
|
116
|
+
export function kakuroTighten(side, white, digits, random, rounds) {
|
|
117
|
+
const runsAt = (cell) => {
|
|
118
|
+
const across = [cell], down = [cell];
|
|
119
|
+
for (let at = cell - 1; at >= 0 && white[at] && (at + 1) % side !== 0; at -= 1)
|
|
120
|
+
across.push(at);
|
|
121
|
+
for (let at = cell + 1; at < white.length && white[at] && at % side !== 0; at += 1)
|
|
122
|
+
across.push(at);
|
|
123
|
+
for (let at = cell - side; at >= 0 && white[at]; at -= side)
|
|
124
|
+
down.push(at);
|
|
125
|
+
for (let at = cell + side; at < white.length && white[at]; at += side)
|
|
126
|
+
down.push(at);
|
|
127
|
+
return { across, down };
|
|
128
|
+
};
|
|
129
|
+
const ways = (run, swap) => {
|
|
130
|
+
let sum = 0;
|
|
131
|
+
for (const at of run)
|
|
132
|
+
sum += swap && at === swap.cell ? swap.digit : digits[at];
|
|
133
|
+
return WAYS[run.length][sum];
|
|
134
|
+
};
|
|
135
|
+
const cells = white.flatMap((w, i) => w ? [i] : []);
|
|
136
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
137
|
+
const cell = cells[Math.floor(random() * cells.length)];
|
|
138
|
+
const { across, down } = runsAt(cell);
|
|
139
|
+
const used = new Set();
|
|
140
|
+
for (const at of [...across, ...down])
|
|
141
|
+
if (at !== cell)
|
|
142
|
+
used.add(digits[at]);
|
|
143
|
+
const options = [1, 2, 3, 4, 5, 6, 7, 8, 9].filter(d => !used.has(d) && d !== digits[cell]);
|
|
144
|
+
if (!options.length)
|
|
145
|
+
continue;
|
|
146
|
+
const digit = options[Math.floor(random() * options.length)];
|
|
147
|
+
const before = ways(across) + ways(down), after = ways(across, { cell, digit }) + ways(down, { cell, digit });
|
|
148
|
+
if (after <= before)
|
|
149
|
+
digits[cell] = digit;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** The totals of a pattern and its digits, as a board. */
|
|
153
|
+
export function kakuroBoardOf(side, white, digits) {
|
|
154
|
+
const cells = white.map(w => w ? { kind: "white" } : { kind: "black", across: null, down: null });
|
|
155
|
+
for (let cell = 0; cell < white.length; cell += 1) {
|
|
156
|
+
if (white[cell])
|
|
157
|
+
continue;
|
|
158
|
+
const black = cells[cell];
|
|
159
|
+
if (cell % side < side - 1 && white[cell + 1]) {
|
|
160
|
+
let sum = 0;
|
|
161
|
+
for (let at = cell + 1; white[at]; at += 1) {
|
|
162
|
+
sum += digits[at];
|
|
163
|
+
if ((at + 1) % side === 0)
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
black.across = sum;
|
|
167
|
+
}
|
|
168
|
+
if (cell + side < white.length && white[cell + side]) {
|
|
169
|
+
let sum = 0;
|
|
170
|
+
for (let at = cell + side; at < white.length && white[at]; at += side)
|
|
171
|
+
sum += digits[at];
|
|
172
|
+
black.down = sum;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { width: side, height: side, cells };
|
|
176
|
+
}
|
|
177
|
+
/** Whether the white squares are one connected piece. */
|
|
178
|
+
function oneStrip(side, white) {
|
|
179
|
+
const first = white.indexOf(true);
|
|
180
|
+
if (first < 0)
|
|
181
|
+
return false;
|
|
182
|
+
const seen = new Set([first]), stack = [first];
|
|
183
|
+
while (stack.length) {
|
|
184
|
+
const at = stack.pop();
|
|
185
|
+
for (const next of [at - 1, at + 1, at - side, at + side]) {
|
|
186
|
+
if (next < 0 || next >= white.length || !white[next] || seen.has(next) || (next === at + 1 && next % side === 0))
|
|
187
|
+
continue;
|
|
188
|
+
seen.add(next);
|
|
189
|
+
stack.push(next);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return seen.size === white.filter(Boolean).length;
|
|
193
|
+
}
|
|
194
|
+
/** The squares in the same row or column strip as `cell` that have the given digit, to see what digits it may take. */
|
|
195
|
+
function digitsAround(side, white, digits, cell) {
|
|
196
|
+
const used = new Set();
|
|
197
|
+
for (let at = cell - 1; at >= 0 && white[at] && (at + 1) % side !== 0; at -= 1)
|
|
198
|
+
used.add(digits[at]);
|
|
199
|
+
for (let at = cell + 1; at < white.length && white[at] && at % side !== 0; at += 1)
|
|
200
|
+
used.add(digits[at]);
|
|
201
|
+
for (let at = cell - side; at >= 0 && white[at]; at -= side)
|
|
202
|
+
used.add(digits[at]);
|
|
203
|
+
for (let at = cell + side; at < white.length && white[at]; at += side)
|
|
204
|
+
used.add(digits[at]);
|
|
205
|
+
return used;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* A board with exactly one answer, or null. While the board has other answers, each one is broken: a square the
|
|
209
|
+
* two answers disagree on is turned black (when every run stays two long and the squares stay one piece) or given
|
|
210
|
+
* another digit that still repeats nowhere in its runs, which changes two totals.
|
|
211
|
+
*/
|
|
212
|
+
export function candidateKakuro(side, level, random) {
|
|
213
|
+
// Easy and medium boards are also eased until the rules they promise are enough to solve them.
|
|
214
|
+
const eased = level === "easy" ? 0 : level === "medium" ? 1 : -1;
|
|
215
|
+
const white = kakuroPattern(side, level, random);
|
|
216
|
+
if (!white)
|
|
217
|
+
return null;
|
|
218
|
+
const digits = kakuroDigits(side, white, random);
|
|
219
|
+
if (!digits)
|
|
220
|
+
return null;
|
|
221
|
+
if (level === "easy" || level === "medium")
|
|
222
|
+
kakuroTighten(side, white, digits, random, side * side * (level === "easy" ? 60 : 25));
|
|
223
|
+
const valid = () => {
|
|
224
|
+
for (let cell = 0; cell < white.length; cell += 1) {
|
|
225
|
+
if (!white[cell])
|
|
226
|
+
continue;
|
|
227
|
+
let across = 1, down = 1;
|
|
228
|
+
for (let at = cell - 1; at >= 0 && white[at] && (at + 1) % side !== 0; at -= 1)
|
|
229
|
+
across += 1;
|
|
230
|
+
for (let at = cell + 1; at < white.length && white[at] && at % side !== 0; at += 1)
|
|
231
|
+
across += 1;
|
|
232
|
+
for (let at = cell - side; at >= 0 && white[at]; at -= side)
|
|
233
|
+
down += 1;
|
|
234
|
+
for (let at = cell + side; at < white.length && white[at]; at += side)
|
|
235
|
+
down += 1;
|
|
236
|
+
if (across < 2 || down < 2)
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return oneStrip(side, white);
|
|
240
|
+
};
|
|
241
|
+
for (let repair = 0; repair < 150; repair += 1) {
|
|
242
|
+
const board = kakuroBoardOf(side, white, digits);
|
|
243
|
+
if (!isKakuroBoard(board))
|
|
244
|
+
return null;
|
|
245
|
+
const model = kakuroModel(board, 1, random);
|
|
246
|
+
let found = countCsp(model.csp, openSlots(model.csp), 8, 3000);
|
|
247
|
+
for (let restart = 0; restart < 5 && found.exhausted; restart += 1)
|
|
248
|
+
found = countCsp(model.csp, openSlots(model.csp), 8, 3000);
|
|
249
|
+
if (found.exhausted) {
|
|
250
|
+
// Too many answers to even count: darken any square that can be darkened and look again.
|
|
251
|
+
const any = shuffled(white.flatMap((w, cell) => w ? [cell] : []), random).find(cell => { white[cell] = false; if (valid())
|
|
252
|
+
return true; white[cell] = true; return false; });
|
|
253
|
+
if (any === undefined)
|
|
254
|
+
return null;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (found.count === 1 && eased < 0)
|
|
258
|
+
return { board, digits };
|
|
259
|
+
if (found.count === 1) {
|
|
260
|
+
const rules = kakuroModel(board, eased);
|
|
261
|
+
const run = logicCsp(rules.csp, openSlots(rules.csp), 0);
|
|
262
|
+
if (run.solved)
|
|
263
|
+
return { board, digits };
|
|
264
|
+
if (white.filter(Boolean).length < (side - 1) * (side - 1) * .42)
|
|
265
|
+
return null;
|
|
266
|
+
const open = rules.whites.filter((_, i) => { let n = 0; for (let k = 0; k < 9; k += 1)
|
|
267
|
+
n += run.alive[9 * i + k]; return n > 1; });
|
|
268
|
+
// Squares still open, then the others that share a run with one.
|
|
269
|
+
const near = new Set(open);
|
|
270
|
+
for (const cell of open) {
|
|
271
|
+
for (let at = cell - 1; at >= 0 && white[at] && (at + 1) % side !== 0; at -= 1)
|
|
272
|
+
near.add(at);
|
|
273
|
+
for (let at = cell + 1; at < white.length && white[at] && at % side !== 0; at += 1)
|
|
274
|
+
near.add(at);
|
|
275
|
+
for (let at = cell - side; at >= 0 && white[at]; at -= side)
|
|
276
|
+
near.add(at);
|
|
277
|
+
for (let at = cell + side; at < white.length && white[at]; at += side)
|
|
278
|
+
near.add(at);
|
|
279
|
+
}
|
|
280
|
+
const stuck = [...shuffled(open, random), ...shuffled([...near].filter(cell => !open.includes(cell)), random)];
|
|
281
|
+
let eased_ = false;
|
|
282
|
+
for (const cell of stuck) {
|
|
283
|
+
white[cell] = false;
|
|
284
|
+
if (valid()) {
|
|
285
|
+
eased_ = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
white[cell] = true;
|
|
289
|
+
}
|
|
290
|
+
if (!eased_) {
|
|
291
|
+
const any = shuffled(white.flatMap((w, cell) => w ? [cell] : []), random).find(cell => { white[cell] = false; if (valid())
|
|
292
|
+
return true; white[cell] = true; return false; });
|
|
293
|
+
if (any === undefined)
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const digitOf = (answer, cell) => { const index = model.index[cell]; let d = 0; for (let k = 0; k < 9; k += 1)
|
|
299
|
+
if (answer[9 * index + k])
|
|
300
|
+
d = k + 1; return d; };
|
|
301
|
+
let moved = false;
|
|
302
|
+
for (const answer of found.solutions) {
|
|
303
|
+
const differing = shuffled(white.flatMap((w, cell) => w && digitOf(answer, cell) !== digits[cell] ? [cell] : []), random);
|
|
304
|
+
if (!differing.length)
|
|
305
|
+
continue;
|
|
306
|
+
const blacken = () => {
|
|
307
|
+
for (const cell of differing) {
|
|
308
|
+
if (!white[cell])
|
|
309
|
+
continue;
|
|
310
|
+
white[cell] = false;
|
|
311
|
+
if (valid())
|
|
312
|
+
return true;
|
|
313
|
+
white[cell] = true;
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
};
|
|
317
|
+
const change = () => {
|
|
318
|
+
for (const cell of differing) {
|
|
319
|
+
if (!white[cell])
|
|
320
|
+
continue;
|
|
321
|
+
const used = digitsAround(side, white, digits, cell);
|
|
322
|
+
const options = [1, 2, 3, 4, 5, 6, 7, 8, 9].filter(d => !used.has(d));
|
|
323
|
+
if (!options.length)
|
|
324
|
+
continue;
|
|
325
|
+
digits[cell] = options[Math.floor(random() * options.length)];
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
};
|
|
330
|
+
if (random() < .5 ? blacken() || change() : change() || blacken())
|
|
331
|
+
moved = true;
|
|
332
|
+
}
|
|
333
|
+
if (!moved)
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
package/dist/kakuroGenerate.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import type { KakuroPuzzle } from "./kakuro.types.ts";
|
|
2
|
-
/**
|
|
3
|
-
|
|
1
|
+
import type { KakuroLevel, KakuroPuzzle } from "./kakuro.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Makes a seeded board of `size` squares a side (counting the row and column of totals) and proves it has one
|
|
4
|
+
* answer. A random pattern of black squares sets the runs, random digits fill them, and digits are changed until
|
|
5
|
+
* the answer is single. The board is then rated by solving it: easy by the single-run rules, medium once a digit
|
|
6
|
+
* that must appear has to be traced, hard by supposing, and extra-hard the most-supposing of several boards.
|
|
7
|
+
* If no board of the level is found within the attempts the next level down is tried, and the first generator
|
|
8
|
+
* (a 10×10 only) is the last resort, so a seed never throws.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateKakuro(seed?: number, level?: KakuroLevel, size?: number): KakuroPuzzle;
|
package/dist/kakuroGenerate.js
CHANGED
|
@@ -1,130 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { KAKURO_LEAST_GENERATED_SIDE, KAKURO_LEVELS, KAKURO_MAX_SIDE } from "./kakuro.constants.js";
|
|
2
|
+
import { checkKakuro } from "./kakuroBoard.js";
|
|
3
|
+
import { candidateKakuro } from "./kakuroBuild.js";
|
|
4
|
+
import { kakuroModel } from "./kakuroLogic.js";
|
|
3
5
|
import { solveKakuro } from "./kakuroSolve.js";
|
|
6
|
+
import { templateKakuro } from "./kakuroTemplate.js";
|
|
7
|
+
import { logicCsp, openSlots } from "./csp.js";
|
|
4
8
|
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
const MOST_ATTEMPTS = 400;
|
|
10
|
+
/**
|
|
11
|
+
* Makes a seeded board of `size` squares a side (counting the row and column of totals) and proves it has one
|
|
12
|
+
* answer. A random pattern of black squares sets the runs, random digits fill them, and digits are changed until
|
|
13
|
+
* the answer is single. The board is then rated by solving it: easy by the single-run rules, medium once a digit
|
|
14
|
+
* that must appear has to be traced, hard by supposing, and extra-hard the most-supposing of several boards.
|
|
15
|
+
* If no board of the level is found within the attempts the next level down is tried, and the first generator
|
|
16
|
+
* (a 10×10 only) is the last resort, so a seed never throws.
|
|
17
|
+
*/
|
|
18
|
+
export function generateKakuro(seed = 1, level = "medium", size = 10) {
|
|
19
|
+
if (!isKazuSeed(seed) || !KAKURO_LEVELS.includes(level) || !Number.isInteger(size)
|
|
20
|
+
|| size < KAKURO_LEAST_GENERATED_SIDE || size > KAKURO_MAX_SIDE)
|
|
21
|
+
throw new RangeError("Invalid Kakuro settings");
|
|
9
22
|
const random = seededRandom(seed);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (const [columnIndex, columnStart] of columnStarts.entries()) {
|
|
25
|
-
const shape = { height: rowGroups[rowIndex], width: columnGroups[columnIndex] };
|
|
26
|
-
const local = chooseUniqueTile(shape.width, shape.height, random, budget);
|
|
27
|
-
if (!local) {
|
|
28
|
-
failed = true;
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
placeTile(board, solution, rowStart, columnStart, local);
|
|
32
|
-
}
|
|
33
|
-
if (failed)
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
if (failed)
|
|
37
|
-
continue;
|
|
38
|
-
if (!isKakuroBoard(board))
|
|
39
|
-
throw new Error(JSON.stringify(board));
|
|
40
|
-
const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(KAKURO_MAX_NODES, budget.nodes) });
|
|
41
|
-
budget.nodes -= Math.min(budget.nodes, proof.nodes);
|
|
42
|
-
if (proof.complete && proof.count === 1 && proof.solution) {
|
|
43
|
-
return { ...board, seed, solution: proof.solution };
|
|
23
|
+
for (let aim = KAKURO_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
24
|
+
const aimed = KAKURO_LEVELS[aim];
|
|
25
|
+
const wanted = aimed === "extra-hard" ? 3 : 1;
|
|
26
|
+
let best = null, found = 0;
|
|
27
|
+
for (let attempt = 0; attempt < MOST_ATTEMPTS && found < wanted; attempt += 1) {
|
|
28
|
+
const built = candidateKakuro(size, aimed, random);
|
|
29
|
+
if (!built)
|
|
30
|
+
continue;
|
|
31
|
+
const score = scored(built.board, aimed);
|
|
32
|
+
if (score === null)
|
|
33
|
+
continue;
|
|
34
|
+
found += 1;
|
|
35
|
+
if (!best || score > best.score)
|
|
36
|
+
best = { board: built.board, score };
|
|
44
37
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const used = groups.reduce((sum, size) => sum + size, 0) + groups.length - 1;
|
|
50
|
-
let at = 1 + (8 - used > 0 && random() < .5 ? 8 - used : 0);
|
|
51
|
-
return groups.map(size => { const start = at; at += size + 1; return start; });
|
|
52
|
-
}
|
|
53
|
-
function chooseUniqueTile(width, height, random, budget) {
|
|
54
|
-
for (let attempt = 0; attempt < KAKURO_MAX_ATTEMPTS && budget.attempts > 0 && budget.nodes > 0; attempt += 1) {
|
|
55
|
-
budget.attempts -= 1;
|
|
56
|
-
const candidate = randomRectangle(width, height, random);
|
|
57
|
-
if (!candidate)
|
|
58
|
-
continue;
|
|
59
|
-
const board = tileBoard(candidate);
|
|
60
|
-
const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(25000, budget.nodes) });
|
|
61
|
-
budget.nodes -= Math.min(budget.nodes, proof.nodes);
|
|
62
|
-
if (!budget.nodes)
|
|
63
|
-
return null;
|
|
64
|
-
if (proof.complete && proof.count === 1)
|
|
65
|
-
return proof.solution ? splitSolution(proof.solution, width, height) : candidate;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
function randomRectangle(width, height, random) {
|
|
70
|
-
const rows = [];
|
|
71
|
-
for (let y = 0; y < height; y += 1) {
|
|
72
|
-
let row = null;
|
|
73
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
74
|
-
const available = shuffled([1, 2, 3, 4, 5, 6, 7, 8, 9], random);
|
|
75
|
-
const candidate = available.slice(0, width);
|
|
76
|
-
if (candidate.every((digit, x) => rows.every(previous => previous[x] !== digit))) {
|
|
77
|
-
row = candidate;
|
|
78
|
-
break;
|
|
38
|
+
if (best) {
|
|
39
|
+
const proof = solveKakuro(best.board);
|
|
40
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkKakuro(best.board, proof.solution).ok) {
|
|
41
|
+
return { ...best.board, seed, level, solution: proof.solution };
|
|
79
42
|
}
|
|
80
43
|
}
|
|
81
|
-
if (!row)
|
|
82
|
-
return null;
|
|
83
|
-
rows.push(row);
|
|
84
|
-
}
|
|
85
|
-
return rows;
|
|
86
|
-
}
|
|
87
|
-
function tileBoard(solution) {
|
|
88
|
-
const height = solution.length + 1, width = solution[0].length + 1;
|
|
89
|
-
const cells = Array.from({ length: width * height }, () => ({ kind: "black", across: null, down: null }));
|
|
90
|
-
for (let y = 0; y < solution.length; y += 1)
|
|
91
|
-
for (let x = 0; x < solution[y].length; x += 1)
|
|
92
|
-
cells[(y + 1) * width + x + 1] = { kind: "white" };
|
|
93
|
-
for (let y = 0; y < solution.length; y += 1)
|
|
94
|
-
cells[(y + 1) * width].across = solution[y].reduce((sum, value) => sum + value, 0);
|
|
95
|
-
for (let x = 0; x < solution[0].length; x += 1)
|
|
96
|
-
cells[x + 1].down = solution.reduce((sum, row) => sum + row[x], 0);
|
|
97
|
-
return { width, height, cells };
|
|
98
|
-
}
|
|
99
|
-
function placeTile(board, full, top, left, solution) {
|
|
100
|
-
const width = board.width;
|
|
101
|
-
const cells = board.cells;
|
|
102
|
-
for (let y = 0; y < solution.length; y += 1)
|
|
103
|
-
for (let x = 0; x < solution[y].length; x += 1)
|
|
104
|
-
full[(top + y) * width + left + x] = solution[y][x];
|
|
105
|
-
for (let y = 0; y < solution.length; y += 1) {
|
|
106
|
-
const clue = (top + y) * width + left - 1;
|
|
107
|
-
cells[clue].across = solution[y].reduce((sum, value) => sum + value, 0);
|
|
108
|
-
}
|
|
109
|
-
for (let x = 0; x < solution[0].length; x += 1) {
|
|
110
|
-
const clue = (top - 1) * width + left + x;
|
|
111
|
-
cells[clue].down = solution.reduce((sum, row) => sum + row[x], 0);
|
|
112
|
-
}
|
|
113
|
-
for (let y = 0; y < solution.length; y += 1)
|
|
114
|
-
for (let x = 0; x < solution[y].length; x += 1)
|
|
115
|
-
cells[(top + y) * width + left + x] = { kind: "white" };
|
|
116
|
-
}
|
|
117
|
-
function splitSolution(values, width, height) {
|
|
118
|
-
const result = [];
|
|
119
|
-
for (let y = 1; y <= height; y += 1)
|
|
120
|
-
result.push(values.slice(y * (width + 1) + 1, y * (width + 1) + width + 1));
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
function shuffled(items, random) {
|
|
124
|
-
const result = [...items];
|
|
125
|
-
for (let i = result.length - 1; i > 0; i -= 1) {
|
|
126
|
-
const j = Math.floor(random() * (i + 1));
|
|
127
|
-
[result[i], result[j]] = [result[j], result[i]];
|
|
128
44
|
}
|
|
129
|
-
|
|
45
|
+
if (size === 10)
|
|
46
|
+
return { ...templateKakuro(seed), level };
|
|
47
|
+
throw new Error("No uniquely solvable Kakuro board was found within the generation budget; try another seed");
|
|
48
|
+
}
|
|
49
|
+
/** Whether a board suits a level, and how well: higher is harder. */
|
|
50
|
+
function scored(board, level) {
|
|
51
|
+
const weak = kakuroModel(board, 0), strong = kakuroModel(board, 1);
|
|
52
|
+
if (logicCsp(weak.csp, openSlots(weak.csp), 0).solved)
|
|
53
|
+
return level === "easy" ? 0 : null;
|
|
54
|
+
if (level === "easy")
|
|
55
|
+
return null;
|
|
56
|
+
if (logicCsp(strong.csp, openSlots(strong.csp), 0).solved)
|
|
57
|
+
return level === "medium" ? 0 : null;
|
|
58
|
+
if (level === "medium")
|
|
59
|
+
return null;
|
|
60
|
+
const probing = logicCsp(strong.csp, openSlots(strong.csp), 1);
|
|
61
|
+
return probing.solved ? probing.probes : level === "extra-hard" ? 1000 : null;
|
|
130
62
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { KakuroBoard } from "./kakuro.types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Kakuro as variables: one per white square, nine slots for the digits 1 to 9. A run's pruning keeps only the digit
|
|
5
|
+
* sets that add up to its total and fit what its squares can still be, so a square keeps only digits some such
|
|
6
|
+
* set uses; a digit that every such set uses must appear somewhere (`strength` 1), and a digit that only one square
|
|
7
|
+
* can take is that square's.
|
|
8
|
+
*/
|
|
9
|
+
export declare function kakuroModel(board: KakuroBoard, strength?: 0 | 1, random?: () => number): {
|
|
10
|
+
csp: Csp;
|
|
11
|
+
whites: readonly number[];
|
|
12
|
+
index: Int32Array;
|
|
13
|
+
};
|