@johnmorrisdotca/kazu 1.1.0 → 1.2.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 +16 -0
- package/README.md +379 -7
- package/dist/akari-draw-entry.d.ts +2 -0
- package/dist/akari-draw-entry.js +1 -0
- package/dist/akari-entry.d.ts +12 -0
- package/dist/akari-entry.js +10 -0
- package/dist/akari-play-entry.d.ts +2 -0
- package/dist/akari-play-entry.js +1 -0
- package/dist/akari.constants.d.ts +2 -0
- package/dist/akari.constants.js +2 -0
- package/dist/akari.types.d.ts +33 -0
- package/dist/akari.types.js +1 -0
- package/dist/akariBoard.d.ts +11 -0
- package/dist/akariBoard.js +96 -0
- package/dist/akariDraw.d.ts +4 -0
- package/dist/akariDraw.js +38 -0
- package/dist/akariGame.d.ts +15 -0
- package/dist/akariGame.js +71 -0
- package/dist/akariGenerate.d.ts +7 -0
- package/dist/akariGenerate.js +78 -0
- package/dist/akariMount.d.ts +3 -0
- package/dist/akariMount.js +214 -0
- package/dist/akariPlay.types.d.ts +25 -0
- package/dist/akariPlay.types.js +1 -0
- package/dist/akariSolve.d.ts +6 -0
- package/dist/akariSolve.js +106 -0
- package/dist/akariStrings.d.ts +98 -0
- package/dist/akariStrings.js +53 -0
- package/dist/akariStyle.d.ts +1 -0
- package/dist/akariStyle.js +9 -0
- package/dist/fillomino-draw-entry.d.ts +2 -0
- package/dist/fillomino-draw-entry.js +1 -0
- package/dist/fillomino-entry.d.ts +8 -0
- package/dist/fillomino-entry.js +7 -0
- package/dist/fillomino-play-entry.d.ts +4 -0
- package/dist/fillomino-play-entry.js +3 -0
- package/dist/fillomino.constants.d.ts +5 -0
- package/dist/fillomino.constants.js +5 -0
- package/dist/fillomino.types.d.ts +30 -0
- package/dist/fillomino.types.js +1 -0
- package/dist/fillominoBoard.d.ts +6 -0
- package/dist/fillominoBoard.js +84 -0
- package/dist/fillominoDraw.d.ts +3 -0
- package/dist/fillominoDraw.js +31 -0
- package/dist/fillominoGame.d.ts +13 -0
- package/dist/fillominoGame.js +65 -0
- package/dist/fillominoGenerate.d.ts +3 -0
- package/dist/fillominoGenerate.js +125 -0
- package/dist/fillominoMount.d.ts +2 -0
- package/dist/fillominoMount.js +211 -0
- package/dist/fillominoPlay.types.d.ts +25 -0
- package/dist/fillominoPlay.types.js +1 -0
- package/dist/fillominoSolve.d.ts +6 -0
- package/dist/fillominoSolve.js +111 -0
- package/dist/fillominoStrings.d.ts +102 -0
- package/dist/fillominoStrings.js +13 -0
- package/dist/fillominoStyle.d.ts +1 -0
- package/dist/fillominoStyle.js +13 -0
- package/dist/fillominoWorker.d.ts +1 -0
- package/dist/fillominoWorker.js +10 -0
- package/dist/heyawake-draw-entry.d.ts +2 -0
- package/dist/heyawake-draw-entry.js +1 -0
- package/dist/heyawake-entry.d.ts +13 -0
- package/dist/heyawake-entry.js +11 -0
- package/dist/heyawake-play-entry.d.ts +2 -0
- package/dist/heyawake-play-entry.js +1 -0
- package/dist/heyawake.constants.d.ts +5 -0
- package/dist/heyawake.constants.js +5 -0
- package/dist/heyawake.types.d.ts +35 -0
- package/dist/heyawake.types.js +1 -0
- package/dist/heyawakeBoard.d.ts +7 -0
- package/dist/heyawakeBoard.js +116 -0
- package/dist/heyawakeDraw.d.ts +3 -0
- package/dist/heyawakeDraw.js +27 -0
- package/dist/heyawakeGame.d.ts +12 -0
- package/dist/heyawakeGame.js +51 -0
- package/dist/heyawakeGenerate.d.ts +2 -0
- package/dist/heyawakeGenerate.js +114 -0
- package/dist/heyawakeMount.d.ts +2 -0
- package/dist/heyawakeMount.js +106 -0
- package/dist/heyawakePlay.types.d.ts +24 -0
- package/dist/heyawakePlay.types.js +1 -0
- package/dist/heyawakeSolve.d.ts +5 -0
- package/dist/heyawakeSolve.js +52 -0
- package/dist/heyawakeStrings.d.ts +19 -0
- package/dist/heyawakeStrings.js +7 -0
- package/dist/heyawakeStyle.d.ts +1 -0
- package/dist/heyawakeStyle.js +2 -0
- package/dist/heyawakeWorker.d.ts +1 -0
- package/dist/heyawakeWorker.js +10 -0
- package/dist/hitori-draw-entry.d.ts +2 -0
- package/dist/hitori-draw-entry.js +1 -0
- package/dist/hitori-entry.d.ts +6 -0
- package/dist/hitori-entry.js +6 -0
- package/dist/hitori-play-entry.d.ts +4 -0
- package/dist/hitori-play-entry.js +3 -0
- package/dist/hitori.constants.d.ts +2 -0
- package/dist/hitori.constants.js +2 -0
- package/dist/hitori.types.d.ts +20 -0
- package/dist/hitori.types.js +1 -0
- package/dist/hitoriBoard.d.ts +8 -0
- package/dist/hitoriBoard.js +72 -0
- package/dist/hitoriDraw.d.ts +4 -0
- package/dist/hitoriDraw.js +25 -0
- package/dist/hitoriGame.d.ts +10 -0
- package/dist/hitoriGame.js +48 -0
- package/dist/hitoriGenerate.d.ts +3 -0
- package/dist/hitoriGenerate.js +163 -0
- package/dist/hitoriMount.d.ts +3 -0
- package/dist/hitoriMount.js +111 -0
- package/dist/hitoriPlay.types.d.ts +25 -0
- package/dist/hitoriPlay.types.js +1 -0
- package/dist/hitoriSolve.d.ts +6 -0
- package/dist/hitoriSolve.js +69 -0
- package/dist/hitoriStrings.d.ts +70 -0
- package/dist/hitoriStrings.js +17 -0
- package/dist/hitoriStyle.d.ts +1 -0
- package/dist/hitoriStyle.js +9 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/juosan-draw-entry.d.ts +2 -0
- package/dist/juosan-draw-entry.js +1 -0
- package/dist/juosan-entry.d.ts +11 -0
- package/dist/juosan-entry.js +10 -0
- package/dist/juosan-play-entry.d.ts +4 -0
- package/dist/juosan-play-entry.js +3 -0
- package/dist/juosan.constants.d.ts +4 -0
- package/dist/juosan.constants.js +4 -0
- package/dist/juosan.types.d.ts +33 -0
- package/dist/juosan.types.js +1 -0
- package/dist/juosanBoard.d.ts +7 -0
- package/dist/juosanBoard.js +102 -0
- package/dist/juosanDraw.d.ts +4 -0
- package/dist/juosanDraw.js +49 -0
- package/dist/juosanGame.d.ts +20 -0
- package/dist/juosanGame.js +86 -0
- package/dist/juosanGenerate.d.ts +3 -0
- package/dist/juosanGenerate.js +31 -0
- package/dist/juosanMount.d.ts +3 -0
- package/dist/juosanMount.js +206 -0
- package/dist/juosanPlay.types.d.ts +24 -0
- package/dist/juosanPlay.types.js +1 -0
- package/dist/juosanSolve.d.ts +3 -0
- package/dist/juosanSolve.js +82 -0
- package/dist/juosanStrings.d.ts +77 -0
- package/dist/juosanStrings.js +43 -0
- package/dist/juosanStyle.d.ts +1 -0
- package/dist/juosanStyle.js +1 -0
- package/dist/kakuro-draw-entry.d.ts +2 -0
- package/dist/kakuro-draw-entry.js +1 -0
- package/dist/kakuro-entry.d.ts +12 -0
- package/dist/kakuro-entry.js +10 -0
- package/dist/kakuro-play-entry.d.ts +4 -0
- package/dist/kakuro-play-entry.js +3 -0
- package/dist/kakuro.constants.d.ts +6 -0
- package/dist/kakuro.constants.js +6 -0
- package/dist/kakuro.types.d.ts +46 -0
- package/dist/kakuro.types.js +1 -0
- package/dist/kakuroBoard.d.ts +8 -0
- package/dist/kakuroBoard.js +117 -0
- package/dist/kakuroDraw.d.ts +10 -0
- package/dist/kakuroDraw.js +21 -0
- package/dist/kakuroGame.d.ts +12 -0
- package/dist/kakuroGame.js +74 -0
- package/dist/kakuroGenerate.d.ts +3 -0
- package/dist/kakuroGenerate.js +130 -0
- package/dist/kakuroMount.d.ts +2 -0
- package/dist/kakuroMount.js +109 -0
- package/dist/kakuroPlay.types.d.ts +20 -0
- package/dist/kakuroPlay.types.js +1 -0
- package/dist/kakuroSolve.d.ts +6 -0
- package/dist/kakuroSolve.js +85 -0
- package/dist/kakuroStrings.d.ts +82 -0
- package/dist/kakuroStrings.js +5 -0
- package/dist/kakuroStyle.d.ts +1 -0
- package/dist/kakuroStyle.js +1 -0
- package/dist/masyu-draw-entry.d.ts +2 -0
- package/dist/masyu-draw-entry.js +1 -0
- package/dist/masyu-entry.d.ts +6 -0
- package/dist/masyu-entry.js +6 -0
- package/dist/masyu-play-entry.d.ts +4 -0
- package/dist/masyu-play-entry.js +3 -0
- package/dist/masyu.constants.d.ts +2 -0
- package/dist/masyu.constants.js +2 -0
- package/dist/masyu.types.d.ts +46 -0
- package/dist/masyu.types.js +1 -0
- package/dist/masyuBoard.d.ts +10 -0
- package/dist/masyuBoard.js +88 -0
- package/dist/masyuDraw.d.ts +2 -0
- package/dist/masyuDraw.js +33 -0
- package/dist/masyuGame.d.ts +9 -0
- package/dist/masyuGame.js +46 -0
- package/dist/masyuGenerate.d.ts +3 -0
- package/dist/masyuGenerate.js +56 -0
- package/dist/masyuMount.d.ts +2 -0
- package/dist/masyuMount.js +118 -0
- package/dist/masyuPlay.types.d.ts +1 -0
- package/dist/masyuPlay.types.js +1 -0
- package/dist/masyuSolve.d.ts +6 -0
- package/dist/masyuSolve.js +88 -0
- package/dist/masyuStrings.d.ts +28 -0
- package/dist/masyuStrings.js +4 -0
- package/dist/masyuStyle.d.ts +1 -0
- package/dist/masyuStyle.js +11 -0
- package/dist/nurikabe-draw-entry.d.ts +2 -0
- package/dist/nurikabe-draw-entry.js +1 -0
- package/dist/nurikabe-entry.d.ts +6 -0
- package/dist/nurikabe-entry.js +6 -0
- package/dist/nurikabe-play-entry.d.ts +4 -0
- package/dist/nurikabe-play-entry.js +3 -0
- package/dist/nurikabe.constants.d.ts +2 -0
- package/dist/nurikabe.constants.js +2 -0
- package/dist/nurikabe.types.d.ts +20 -0
- package/dist/nurikabe.types.js +1 -0
- package/dist/nurikabeBoard.d.ts +8 -0
- package/dist/nurikabeBoard.js +72 -0
- package/dist/nurikabeDraw.d.ts +4 -0
- package/dist/nurikabeDraw.js +15 -0
- package/dist/nurikabeGame.d.ts +8 -0
- package/dist/nurikabeGame.js +40 -0
- package/dist/nurikabeGenerate.d.ts +3 -0
- package/dist/nurikabeGenerate.js +31 -0
- package/dist/nurikabeMount.d.ts +3 -0
- package/dist/nurikabeMount.js +99 -0
- package/dist/nurikabePlay.types.d.ts +25 -0
- package/dist/nurikabePlay.types.js +1 -0
- package/dist/nurikabeSolve.d.ts +6 -0
- package/dist/nurikabeSolve.js +75 -0
- package/dist/nurikabeStrings.d.ts +70 -0
- package/dist/nurikabeStrings.js +5 -0
- package/dist/nurikabeStyle.d.ts +1 -0
- package/dist/nurikabeStyle.js +9 -0
- package/dist/ripple-draw-entry.d.ts +2 -0
- package/dist/ripple-draw-entry.js +1 -0
- package/dist/ripple-entry.d.ts +12 -0
- package/dist/ripple-entry.js +11 -0
- package/dist/ripple-play-entry.d.ts +4 -0
- package/dist/ripple-play-entry.js +3 -0
- package/dist/ripple.constants.d.ts +4 -0
- package/dist/ripple.constants.js +4 -0
- package/dist/ripple.types.d.ts +34 -0
- package/dist/ripple.types.js +1 -0
- package/dist/rippleBoard.d.ts +7 -0
- package/dist/rippleBoard.js +99 -0
- package/dist/rippleDraw.d.ts +3 -0
- package/dist/rippleDraw.js +32 -0
- package/dist/rippleGame.d.ts +13 -0
- package/dist/rippleGame.js +85 -0
- package/dist/rippleGenerate.d.ts +3 -0
- package/dist/rippleGenerate.js +57 -0
- package/dist/rippleMount.d.ts +2 -0
- package/dist/rippleMount.js +179 -0
- package/dist/ripplePlay.types.d.ts +23 -0
- package/dist/ripplePlay.types.js +1 -0
- package/dist/rippleSolve.d.ts +6 -0
- package/dist/rippleSolve.js +83 -0
- package/dist/rippleStrings.d.ts +86 -0
- package/dist/rippleStrings.js +5 -0
- package/dist/rippleStyle.d.ts +1 -0
- package/dist/rippleStyle.js +1 -0
- package/dist/shikaku-entry.d.ts +1 -0
- package/dist/shikaku-entry.js +1 -0
- package/dist/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -0
- package/dist/slitherlink-draw-entry.d.ts +2 -0
- package/dist/slitherlink-draw-entry.js +1 -0
- package/dist/slitherlink-entry.d.ts +11 -0
- package/dist/slitherlink-entry.js +9 -0
- package/dist/slitherlink-play-entry.d.ts +4 -0
- package/dist/slitherlink-play-entry.js +3 -0
- package/dist/slitherlink.constants.d.ts +2 -0
- package/dist/slitherlink.constants.js +2 -0
- package/dist/slitherlink.types.d.ts +34 -0
- package/dist/slitherlink.types.js +1 -0
- package/dist/slitherlinkBoard.d.ts +12 -0
- package/dist/slitherlinkBoard.js +132 -0
- package/dist/slitherlinkDraw.d.ts +4 -0
- package/dist/slitherlinkDraw.js +51 -0
- package/dist/slitherlinkGame.d.ts +8 -0
- package/dist/slitherlinkGame.js +63 -0
- package/dist/slitherlinkGenerate.d.ts +3 -0
- package/dist/slitherlinkGenerate.js +101 -0
- package/dist/slitherlinkMount.d.ts +3 -0
- package/dist/slitherlinkMount.js +267 -0
- package/dist/slitherlinkPlay.types.d.ts +23 -0
- package/dist/slitherlinkPlay.types.js +1 -0
- package/dist/slitherlinkSolve.d.ts +6 -0
- package/dist/slitherlinkSolve.js +127 -0
- package/dist/slitherlinkStrings.d.ts +86 -0
- package/dist/slitherlinkStrings.js +47 -0
- package/dist/slitherlinkStyle.d.ts +1 -0
- package/dist/slitherlinkStyle.js +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/yajilin-draw-entry.d.ts +2 -0
- package/dist/yajilin-draw-entry.js +1 -0
- package/dist/yajilin-entry.d.ts +6 -0
- package/dist/yajilin-entry.js +6 -0
- package/dist/yajilin-play-entry.d.ts +4 -0
- package/dist/yajilin-play-entry.js +3 -0
- package/dist/yajilin.constants.d.ts +2 -0
- package/dist/yajilin.constants.js +2 -0
- package/dist/yajilin.types.d.ts +63 -0
- package/dist/yajilin.types.js +1 -0
- package/dist/yajilinBoard.d.ts +9 -0
- package/dist/yajilinBoard.js +91 -0
- package/dist/yajilinDraw.d.ts +2 -0
- package/dist/yajilinDraw.js +33 -0
- package/dist/yajilinGame.d.ts +15 -0
- package/dist/yajilinGame.js +58 -0
- package/dist/yajilinGenerate.d.ts +3 -0
- package/dist/yajilinGenerate.js +224 -0
- package/dist/yajilinMount.d.ts +2 -0
- package/dist/yajilinMount.js +162 -0
- package/dist/yajilinPlay.types.d.ts +1 -0
- package/dist/yajilinPlay.types.js +1 -0
- package/dist/yajilinSolve.d.ts +6 -0
- package/dist/yajilinSolve.js +82 -0
- package/dist/yajilinStrings.d.ts +32 -0
- package/dist/yajilinStrings.js +4 -0
- package/dist/yajilinStyle.d.ts +1 -0
- package/dist/yajilinStyle.js +12 -0
- package/package.json +180 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { AKARI_MOST_NODES } from "./akari.constants.js";
|
|
2
|
+
import { akariNeighbours, akariVisible, checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
3
|
+
/** Bounded binary constraint search; a stopped proof is always marked incomplete. */
|
|
4
|
+
export function solveAkari(board, options = {}) {
|
|
5
|
+
if (!isAkariBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Akari board");
|
|
7
|
+
const limit = options.limit ?? 2;
|
|
8
|
+
const budget = options.nodes ?? AKARI_MOST_NODES;
|
|
9
|
+
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1) {
|
|
10
|
+
throw new RangeError("Invalid search bounds");
|
|
11
|
+
}
|
|
12
|
+
const whites = board.cells.flatMap((value, cell) => value === null ? [cell] : []);
|
|
13
|
+
const visible = whites.map(cell => akariVisible(board, cell));
|
|
14
|
+
let nodes = 0;
|
|
15
|
+
let count = 0;
|
|
16
|
+
let complete = true;
|
|
17
|
+
let solution = null;
|
|
18
|
+
const search = (start) => {
|
|
19
|
+
if (!complete)
|
|
20
|
+
return;
|
|
21
|
+
nodes += 1;
|
|
22
|
+
if (nodes > budget) {
|
|
23
|
+
complete = false;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const assigned = start.slice();
|
|
27
|
+
let changed = true;
|
|
28
|
+
while (changed) {
|
|
29
|
+
changed = false;
|
|
30
|
+
// A placed bulb rules out every other white square in its four rays.
|
|
31
|
+
for (let i = 0; i < whites.length; i += 1) {
|
|
32
|
+
if (assigned[i] !== 1)
|
|
33
|
+
continue;
|
|
34
|
+
for (const cell of visible[i]) {
|
|
35
|
+
if (cell === whites[i])
|
|
36
|
+
continue;
|
|
37
|
+
const other = whites.indexOf(cell);
|
|
38
|
+
if (assigned[other] === 1)
|
|
39
|
+
return;
|
|
40
|
+
if (assigned[other] < 0) {
|
|
41
|
+
assigned[other] = 0;
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Numbered black squares provide lower and upper bounds on adjacent bulbs.
|
|
47
|
+
for (let cell = 0; cell < board.cells.length; cell += 1) {
|
|
48
|
+
const clue = board.cells[cell];
|
|
49
|
+
if (typeof clue !== "number")
|
|
50
|
+
continue;
|
|
51
|
+
const nearby = akariNeighbours(board, cell)
|
|
52
|
+
.map(neighbour => whites.indexOf(neighbour))
|
|
53
|
+
.filter(index => index >= 0);
|
|
54
|
+
const on = nearby.filter(index => assigned[index] === 1).length;
|
|
55
|
+
const unknown = nearby.filter(index => assigned[index] < 0);
|
|
56
|
+
if (on > clue || on + unknown.length < clue)
|
|
57
|
+
return;
|
|
58
|
+
if (on === clue) {
|
|
59
|
+
for (const index of unknown) {
|
|
60
|
+
assigned[index] = 0;
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (on + unknown.length === clue) {
|
|
65
|
+
for (const index of unknown) {
|
|
66
|
+
assigned[index] = 1;
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Every white cell still needs a possible source of light.
|
|
72
|
+
for (let i = 0; i < whites.length; i += 1) {
|
|
73
|
+
const ray = visible[i].map(cell => whites.indexOf(cell));
|
|
74
|
+
if (ray.some(index => assigned[index] === 1))
|
|
75
|
+
continue;
|
|
76
|
+
const unknown = ray.filter(index => assigned[index] < 0);
|
|
77
|
+
if (!unknown.length)
|
|
78
|
+
return;
|
|
79
|
+
if (unknown.length === 1) {
|
|
80
|
+
assigned[unknown[0]] = 1;
|
|
81
|
+
changed = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const branch = assigned.findIndex(value => value < 0);
|
|
86
|
+
if (branch < 0) {
|
|
87
|
+
const bulbs = whites.filter((_, index) => assigned[index] === 1);
|
|
88
|
+
if (!checkAkari(board, bulbs).ok)
|
|
89
|
+
return;
|
|
90
|
+
count += 1;
|
|
91
|
+
solution ?? (solution = bulbs);
|
|
92
|
+
if (count >= limit)
|
|
93
|
+
complete = false;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const value of [1, 0]) {
|
|
97
|
+
const next = assigned.slice();
|
|
98
|
+
next[branch] = value;
|
|
99
|
+
search(next);
|
|
100
|
+
if (!complete)
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
search(new Int8Array(whites.length).fill(-1));
|
|
105
|
+
return { count, solution, complete, nodes };
|
|
106
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AkariLanguage } from "./akariPlay.types.ts";
|
|
2
|
+
export declare const AKARI_STRINGS: {
|
|
3
|
+
readonly en: {
|
|
4
|
+
readonly title: "Akari";
|
|
5
|
+
readonly help: "Place bulbs on white squares. Each bulb lights along its row and column until a black square or the edge. Light every white square; bulbs cannot see one another. Numbered black squares touch exactly that many bulbs. Click, tap or press Enter or Space to toggle a bulb; arrows move; Delete removes it.";
|
|
6
|
+
readonly undo: "Undo";
|
|
7
|
+
readonly hint: "Hint";
|
|
8
|
+
readonly check: "Check";
|
|
9
|
+
readonly restart: "Restart";
|
|
10
|
+
readonly expand: "Just the board";
|
|
11
|
+
readonly close: "Close";
|
|
12
|
+
readonly won: "Every square is lit. Solved!";
|
|
13
|
+
readonly helped: "Solved with help.";
|
|
14
|
+
readonly checked: "No rule conflicts so far. Keep lighting the board.";
|
|
15
|
+
readonly wrong: "Check the dark squares, bulb lines and numbered squares.";
|
|
16
|
+
readonly hintWhy: "A bulb from the only proved solution was placed.";
|
|
17
|
+
readonly noHint: "No proved hint is available.";
|
|
18
|
+
readonly row: "Row";
|
|
19
|
+
readonly column: "column";
|
|
20
|
+
readonly white: "white square";
|
|
21
|
+
readonly bulb: "bulb";
|
|
22
|
+
readonly lit: "lit";
|
|
23
|
+
readonly dark: "dark";
|
|
24
|
+
readonly black: "black square";
|
|
25
|
+
readonly clue: "number";
|
|
26
|
+
};
|
|
27
|
+
readonly ja: {
|
|
28
|
+
readonly title: "美術館";
|
|
29
|
+
readonly help: "白マスに電球を置きます。電球は黒マスか盤の端まで、縦横に光ります。白マスすべてを照らし、電球同士が見えないようにします。数字のある黒マスには、その数だけ電球を隣接させます。タップ、クリック、Enter、Spaceで電球を切り替えます。矢印キーで移動し、Deleteで消します。";
|
|
30
|
+
readonly undo: "戻す";
|
|
31
|
+
readonly hint: "ヒント";
|
|
32
|
+
readonly check: "確かめる";
|
|
33
|
+
readonly restart: "やり直す";
|
|
34
|
+
readonly expand: "盤だけ";
|
|
35
|
+
readonly close: "閉じる";
|
|
36
|
+
readonly won: "全部照らしました。完成!";
|
|
37
|
+
readonly helped: "ヒントを使って完成!";
|
|
38
|
+
readonly checked: "ここまでルールに合っています。残りも照らしましょう。";
|
|
39
|
+
readonly wrong: "暗いマス、電球の見通し、数字のマスを確かめましょう。";
|
|
40
|
+
readonly hintWhy: "唯一の解答で必要な電球を置きました。";
|
|
41
|
+
readonly noHint: "確かなヒントがありません。";
|
|
42
|
+
readonly row: "行";
|
|
43
|
+
readonly column: "列";
|
|
44
|
+
readonly white: "白マス";
|
|
45
|
+
readonly bulb: "電球";
|
|
46
|
+
readonly lit: "照明あり";
|
|
47
|
+
readonly dark: "暗い";
|
|
48
|
+
readonly black: "黒マス";
|
|
49
|
+
readonly clue: "数字";
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export declare function akariWords(language?: AkariLanguage): {
|
|
53
|
+
readonly title: "Akari";
|
|
54
|
+
readonly help: "Place bulbs on white squares. Each bulb lights along its row and column until a black square or the edge. Light every white square; bulbs cannot see one another. Numbered black squares touch exactly that many bulbs. Click, tap or press Enter or Space to toggle a bulb; arrows move; Delete removes it.";
|
|
55
|
+
readonly undo: "Undo";
|
|
56
|
+
readonly hint: "Hint";
|
|
57
|
+
readonly check: "Check";
|
|
58
|
+
readonly restart: "Restart";
|
|
59
|
+
readonly expand: "Just the board";
|
|
60
|
+
readonly close: "Close";
|
|
61
|
+
readonly won: "Every square is lit. Solved!";
|
|
62
|
+
readonly helped: "Solved with help.";
|
|
63
|
+
readonly checked: "No rule conflicts so far. Keep lighting the board.";
|
|
64
|
+
readonly wrong: "Check the dark squares, bulb lines and numbered squares.";
|
|
65
|
+
readonly hintWhy: "A bulb from the only proved solution was placed.";
|
|
66
|
+
readonly noHint: "No proved hint is available.";
|
|
67
|
+
readonly row: "Row";
|
|
68
|
+
readonly column: "column";
|
|
69
|
+
readonly white: "white square";
|
|
70
|
+
readonly bulb: "bulb";
|
|
71
|
+
readonly lit: "lit";
|
|
72
|
+
readonly dark: "dark";
|
|
73
|
+
readonly black: "black square";
|
|
74
|
+
readonly clue: "number";
|
|
75
|
+
} | {
|
|
76
|
+
readonly title: "美術館";
|
|
77
|
+
readonly help: "白マスに電球を置きます。電球は黒マスか盤の端まで、縦横に光ります。白マスすべてを照らし、電球同士が見えないようにします。数字のある黒マスには、その数だけ電球を隣接させます。タップ、クリック、Enter、Spaceで電球を切り替えます。矢印キーで移動し、Deleteで消します。";
|
|
78
|
+
readonly undo: "戻す";
|
|
79
|
+
readonly hint: "ヒント";
|
|
80
|
+
readonly check: "確かめる";
|
|
81
|
+
readonly restart: "やり直す";
|
|
82
|
+
readonly expand: "盤だけ";
|
|
83
|
+
readonly close: "閉じる";
|
|
84
|
+
readonly won: "全部照らしました。完成!";
|
|
85
|
+
readonly helped: "ヒントを使って完成!";
|
|
86
|
+
readonly checked: "ここまでルールに合っています。残りも照らしましょう。";
|
|
87
|
+
readonly wrong: "暗いマス、電球の見通し、数字のマスを確かめましょう。";
|
|
88
|
+
readonly hintWhy: "唯一の解答で必要な電球を置きました。";
|
|
89
|
+
readonly noHint: "確かなヒントがありません。";
|
|
90
|
+
readonly row: "行";
|
|
91
|
+
readonly column: "列";
|
|
92
|
+
readonly white: "白マス";
|
|
93
|
+
readonly bulb: "電球";
|
|
94
|
+
readonly lit: "照明あり";
|
|
95
|
+
readonly dark: "暗い";
|
|
96
|
+
readonly black: "黒マス";
|
|
97
|
+
readonly clue: "数字";
|
|
98
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const AKARI_STRINGS = {
|
|
2
|
+
en: {
|
|
3
|
+
title: "Akari",
|
|
4
|
+
help: "Place bulbs on white squares. Each bulb lights along its row and column until a black square or the edge. Light every white square; bulbs cannot see one another. Numbered black squares touch exactly that many bulbs. Click, tap or press Enter or Space to toggle a bulb; arrows move; Delete removes it.",
|
|
5
|
+
undo: "Undo",
|
|
6
|
+
hint: "Hint",
|
|
7
|
+
check: "Check",
|
|
8
|
+
restart: "Restart",
|
|
9
|
+
expand: "Just the board",
|
|
10
|
+
close: "Close",
|
|
11
|
+
won: "Every square is lit. Solved!",
|
|
12
|
+
helped: "Solved with help.",
|
|
13
|
+
checked: "No rule conflicts so far. Keep lighting the board.",
|
|
14
|
+
wrong: "Check the dark squares, bulb lines and numbered squares.",
|
|
15
|
+
hintWhy: "A bulb from the only proved solution was placed.",
|
|
16
|
+
noHint: "No proved hint is available.",
|
|
17
|
+
row: "Row",
|
|
18
|
+
column: "column",
|
|
19
|
+
white: "white square",
|
|
20
|
+
bulb: "bulb",
|
|
21
|
+
lit: "lit",
|
|
22
|
+
dark: "dark",
|
|
23
|
+
black: "black square",
|
|
24
|
+
clue: "number",
|
|
25
|
+
},
|
|
26
|
+
ja: {
|
|
27
|
+
title: "美術館",
|
|
28
|
+
help: "白マスに電球を置きます。電球は黒マスか盤の端まで、縦横に光ります。白マスすべてを照らし、電球同士が見えないようにします。数字のある黒マスには、その数だけ電球を隣接させます。タップ、クリック、Enter、Spaceで電球を切り替えます。矢印キーで移動し、Deleteで消します。",
|
|
29
|
+
undo: "戻す",
|
|
30
|
+
hint: "ヒント",
|
|
31
|
+
check: "確かめる",
|
|
32
|
+
restart: "やり直す",
|
|
33
|
+
expand: "盤だけ",
|
|
34
|
+
close: "閉じる",
|
|
35
|
+
won: "全部照らしました。完成!",
|
|
36
|
+
helped: "ヒントを使って完成!",
|
|
37
|
+
checked: "ここまでルールに合っています。残りも照らしましょう。",
|
|
38
|
+
wrong: "暗いマス、電球の見通し、数字のマスを確かめましょう。",
|
|
39
|
+
hintWhy: "唯一の解答で必要な電球を置きました。",
|
|
40
|
+
noHint: "確かなヒントがありません。",
|
|
41
|
+
row: "行",
|
|
42
|
+
column: "列",
|
|
43
|
+
white: "白マス",
|
|
44
|
+
bulb: "電球",
|
|
45
|
+
lit: "照明あり",
|
|
46
|
+
dark: "暗い",
|
|
47
|
+
black: "黒マス",
|
|
48
|
+
clue: "数字",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export function akariWords(language = "en") {
|
|
52
|
+
return AKARI_STRINGS[language];
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AKARI_PLAY_STYLE = "\n.ka-root{color:var(--ka-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto}\n.ka-board{position:relative;width:100%;min-width:240px;touch-action:manipulation}.ka-cells{position:absolute;inset:0;display:grid;padding:2px}\n.ka-cell{border:0;min-width:0;min-height:0;padding:0;cursor:pointer;touch-action:manipulation;font:inherit;color:inherit;background:transparent}\n.ka-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px;border-radius:4px}.ka-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}\n.ka-tools button,.ka-close{font:inherit;border:1px solid var(--ka-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--ka-surface,#fbf8f1);color:var(--ka-ink,#1f2320);cursor:pointer}\n.ka-tools button:disabled{opacity:.45;cursor:default}.ka-status{min-height:1.5em}.ka-help{font-size:14px;opacity:.8}\n.ka-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,760px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--ka-ink:var(--ink,#1f2320);--ka-surface:var(--surface,#fbf8f1);--ka-rule:var(--rule,#ddd6c6)}.ka-dialog::backdrop{background:#172c28bb}.ka-close{float:right;margin-bottom:10px}.ka-dialog .ka-root{clear:both}\n";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const AKARI_PLAY_STYLE = `
|
|
2
|
+
.ka-root{color:var(--ka-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto}
|
|
3
|
+
.ka-board{position:relative;width:100%;min-width:240px;touch-action:manipulation}.ka-cells{position:absolute;inset:0;display:grid;padding:2px}
|
|
4
|
+
.ka-cell{border:0;min-width:0;min-height:0;padding:0;cursor:pointer;touch-action:manipulation;font:inherit;color:inherit;background:transparent}
|
|
5
|
+
.ka-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px;border-radius:4px}.ka-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}
|
|
6
|
+
.ka-tools button,.ka-close{font:inherit;border:1px solid var(--ka-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--ka-surface,#fbf8f1);color:var(--ka-ink,#1f2320);cursor:pointer}
|
|
7
|
+
.ka-tools button:disabled{opacity:.45;cursor:default}.ka-status{min-height:1.5em}.ka-help{font-size:14px;opacity:.8}
|
|
8
|
+
.ka-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,760px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--ka-ink:var(--ink,#1f2320);--ka-surface:var(--surface,#fbf8f1);--ka-rule:var(--rule,#ddd6c6)}.ka-dialog::backdrop{background:#172c28bb}.ka-close{float:right;margin-bottom:10px}.ka-dialog .ka-root{clear:both}
|
|
9
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawFillomino } from "./fillominoDraw.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./fillomino.types.ts";
|
|
2
|
+
export * from "./fillomino.constants.ts";
|
|
3
|
+
export * from "./fillominoBoard.ts";
|
|
4
|
+
export * from "./fillominoSolve.ts";
|
|
5
|
+
export * from "./fillominoGenerate.ts";
|
|
6
|
+
export * from "./fillominoGame.ts";
|
|
7
|
+
export { drawFillomino } from "./fillominoDraw.ts";
|
|
8
|
+
export type { FillominoDrawOptions } from "./fillominoPlay.types.ts";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./fillomino.types.js";
|
|
2
|
+
export * from "./fillomino.constants.js";
|
|
3
|
+
export * from "./fillominoBoard.js";
|
|
4
|
+
export * from "./fillominoSolve.js";
|
|
5
|
+
export * from "./fillominoGenerate.js";
|
|
6
|
+
export * from "./fillominoGame.js";
|
|
7
|
+
export { drawFillomino } from "./fillominoDraw.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const FILLOMINO_MOST_SIDE = 12;
|
|
2
|
+
export declare const FILLOMINO_GENERATOR_MOST_CELLS = 36;
|
|
3
|
+
export declare const FILLOMINO_MOST_NODES = 80000;
|
|
4
|
+
export declare const FILLOMINO_MOST_ATTEMPTS = 120;
|
|
5
|
+
export declare const FILLOMINO_LEVELS: readonly ["easy", "medium", "hard"];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type FillominoBoard = {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
givens: readonly number[];
|
|
5
|
+
};
|
|
6
|
+
export type FillominoPuzzle = FillominoBoard & {
|
|
7
|
+
seed: number;
|
|
8
|
+
level: FillominoLevel;
|
|
9
|
+
solution: readonly number[];
|
|
10
|
+
};
|
|
11
|
+
export type FillominoLevel = "easy" | "medium" | "hard";
|
|
12
|
+
export type FillominoCheck = {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
complete: boolean;
|
|
15
|
+
filled: number;
|
|
16
|
+
regions: number;
|
|
17
|
+
errors: readonly number[];
|
|
18
|
+
};
|
|
19
|
+
export type FillominoSolve = {
|
|
20
|
+
count: number;
|
|
21
|
+
solution: readonly number[] | null;
|
|
22
|
+
complete: boolean;
|
|
23
|
+
nodes: number;
|
|
24
|
+
};
|
|
25
|
+
export type FillominoGame = {
|
|
26
|
+
board: FillominoBoard;
|
|
27
|
+
entries: readonly number[];
|
|
28
|
+
history: readonly (readonly number[])[];
|
|
29
|
+
helped: boolean;
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FillominoBoard, FillominoCheck } from "./fillomino.types.ts";
|
|
2
|
+
export declare function isFillominoBoard(value: unknown): value is FillominoBoard;
|
|
3
|
+
export declare function fillominoNeighbours(board: FillominoBoard, cell: number): number[];
|
|
4
|
+
export declare function fillominoRegions(board: FillominoBoard, entries: readonly number[]): number[][];
|
|
5
|
+
/** Checks region connectivity and area without consulting a generated answer. */
|
|
6
|
+
export declare function checkFillomino(board: FillominoBoard, entries: readonly number[]): FillominoCheck;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { FILLOMINO_MOST_SIDE } from "./fillomino.constants.js";
|
|
2
|
+
export function isFillominoBoard(value) {
|
|
3
|
+
if (!value || typeof value !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
const board = value;
|
|
6
|
+
const cells = board.width * board.height;
|
|
7
|
+
return Number.isInteger(board.width) && board.width >= 2 && board.width <= FILLOMINO_MOST_SIDE
|
|
8
|
+
&& Number.isInteger(board.height) && board.height >= 2 && board.height <= FILLOMINO_MOST_SIDE
|
|
9
|
+
&& Array.isArray(board.givens) && board.givens.length === cells
|
|
10
|
+
&& board.givens.every(value => Number.isInteger(value) && value >= 0 && value <= cells);
|
|
11
|
+
}
|
|
12
|
+
export function fillominoNeighbours(board, cell) {
|
|
13
|
+
const x = cell % board.width;
|
|
14
|
+
const y = Math.floor(cell / board.width);
|
|
15
|
+
const neighbours = [];
|
|
16
|
+
if (x > 0)
|
|
17
|
+
neighbours.push(cell - 1);
|
|
18
|
+
if (x + 1 < board.width)
|
|
19
|
+
neighbours.push(cell + 1);
|
|
20
|
+
if (y > 0)
|
|
21
|
+
neighbours.push(cell - board.width);
|
|
22
|
+
if (y + 1 < board.height)
|
|
23
|
+
neighbours.push(cell + board.width);
|
|
24
|
+
return neighbours;
|
|
25
|
+
}
|
|
26
|
+
export function fillominoRegions(board, entries) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const regions = [];
|
|
29
|
+
for (let start = 0; start < entries.length; start += 1) {
|
|
30
|
+
if (!entries[start] || seen.has(start))
|
|
31
|
+
continue;
|
|
32
|
+
const value = entries[start];
|
|
33
|
+
const region = [];
|
|
34
|
+
const pending = [start];
|
|
35
|
+
seen.add(start);
|
|
36
|
+
while (pending.length) {
|
|
37
|
+
const cell = pending.pop();
|
|
38
|
+
region.push(cell);
|
|
39
|
+
for (const next of fillominoNeighbours(board, cell)) {
|
|
40
|
+
if (!seen.has(next) && entries[next] === value) {
|
|
41
|
+
seen.add(next);
|
|
42
|
+
pending.push(next);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
regions.push(region);
|
|
47
|
+
}
|
|
48
|
+
return regions;
|
|
49
|
+
}
|
|
50
|
+
/** Checks region connectivity and area without consulting a generated answer. */
|
|
51
|
+
export function checkFillomino(board, entries) {
|
|
52
|
+
if (!isFillominoBoard(board))
|
|
53
|
+
throw new RangeError("Invalid Fillomino board");
|
|
54
|
+
if (!Array.isArray(entries) || entries.length !== board.givens.length)
|
|
55
|
+
throw new RangeError("Invalid Fillomino entries");
|
|
56
|
+
const errors = new Set();
|
|
57
|
+
let filled = 0;
|
|
58
|
+
entries.forEach((value, cell) => {
|
|
59
|
+
if (!Number.isInteger(value) || value < 0 || value > entries.length) {
|
|
60
|
+
errors.add(cell);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (value > 0)
|
|
64
|
+
filled += 1;
|
|
65
|
+
if (board.givens[cell] && value !== board.givens[cell])
|
|
66
|
+
errors.add(cell);
|
|
67
|
+
});
|
|
68
|
+
const regions = fillominoRegions(board, entries);
|
|
69
|
+
for (const region of regions) {
|
|
70
|
+
const number = entries[region[0]];
|
|
71
|
+
if (region.length > number)
|
|
72
|
+
region.forEach(cell => errors.add(cell));
|
|
73
|
+
}
|
|
74
|
+
const componentErrors = errors.size > 0;
|
|
75
|
+
const complete = filled === entries.length;
|
|
76
|
+
const validAreas = regions.every(region => region.length === entries[region[0]]);
|
|
77
|
+
return {
|
|
78
|
+
ok: complete && !componentErrors && validAreas,
|
|
79
|
+
complete,
|
|
80
|
+
filled,
|
|
81
|
+
regions: regions.length,
|
|
82
|
+
errors: [...errors].sort((a, b) => a - b),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isFillominoBoard } from "./fillominoBoard.js";
|
|
2
|
+
import { fillominoWords } from "./fillominoStrings.js";
|
|
3
|
+
export function drawFillomino(board, options = {}) {
|
|
4
|
+
if (!isFillominoBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Fillomino board");
|
|
6
|
+
const words = fillominoWords(options.language);
|
|
7
|
+
const size = 48;
|
|
8
|
+
const [paper, ink, grid] = {
|
|
9
|
+
ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"],
|
|
10
|
+
wood: ["#e5cda6", "#493e2f", "#bca582"],
|
|
11
|
+
slate: ["#262a27", "#ece8dc", "#454a44"],
|
|
12
|
+
}[options.material ?? "ivory"];
|
|
13
|
+
const entries = options.entries ?? board.givens;
|
|
14
|
+
const markedErrors = new Set(options.errors ?? []);
|
|
15
|
+
const cells = board.givens.map((given, cell) => {
|
|
16
|
+
const x = cell % board.width * size;
|
|
17
|
+
const y = Math.floor(cell / board.width) * size;
|
|
18
|
+
const value = entries[cell] ?? 0;
|
|
19
|
+
const active = options.selected === cell;
|
|
20
|
+
const error = markedErrors.has(cell);
|
|
21
|
+
const fill = error ? "var(--kz-bad,#b5452c)" : active ? "var(--kz-focus,#d8bd68)" : "transparent";
|
|
22
|
+
const tile = value && options.pieces === "tiles"
|
|
23
|
+
? `<rect x="${x + 10}" y="${y + 10}" width="28" height="28" rx="6" fill="${paper}" stroke="${grid}"/>`
|
|
24
|
+
: "";
|
|
25
|
+
const text = value
|
|
26
|
+
? `<text x="${x + 24}" y="${y + 25}" dominant-baseline="middle" text-anchor="middle" fill="${ink}" font-size="21" font-weight="${given ? 750 : 500}" font-family="system-ui,sans-serif">${value}</text>`
|
|
27
|
+
: "";
|
|
28
|
+
return `<g data-fillomino-cell="${cell}"><rect x="${x}" y="${y}" width="${size}" height="${size}" fill="${fill}" fill-opacity="${active || error ? ".28" : "1"}" stroke="${grid}"/>${tile}${text}</g>`;
|
|
29
|
+
}).join("");
|
|
30
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 ${board.width * size + 4} ${board.height * size + 4}" role="img" aria-label="${words.title}" style="display:block;width:100%;height:auto;background:${paper}">${cells}<rect width="${board.width * size}" height="${board.height * size}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="2"/></svg>`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FillominoBoard, FillominoGame } from "./fillomino.types.ts";
|
|
2
|
+
export declare function newFillomino(board: FillominoBoard): FillominoGame;
|
|
3
|
+
export declare function setFillominoCell(game: FillominoGame, cell: number, value: number): FillominoGame;
|
|
4
|
+
export declare function undoFillomino(game: FillominoGame): FillominoGame;
|
|
5
|
+
export declare function hintFillomino(game: FillominoGame): {
|
|
6
|
+
cell: number;
|
|
7
|
+
value: number;
|
|
8
|
+
why: string;
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function fillominoFinished(game: FillominoGame): boolean;
|
|
11
|
+
export declare function restartFillomino(game: FillominoGame): FillominoGame;
|
|
12
|
+
export declare function encodeFillomino(game: FillominoGame): string;
|
|
13
|
+
export declare function decodeFillomino(value: string): FillominoGame | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { checkFillomino, isFillominoBoard } from "./fillominoBoard.js";
|
|
2
|
+
import { solveFillomino } from "./fillominoSolve.js";
|
|
3
|
+
export function newFillomino(board) {
|
|
4
|
+
if (!isFillominoBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Fillomino board");
|
|
6
|
+
return {
|
|
7
|
+
board: { width: board.width, height: board.height, givens: [...board.givens] },
|
|
8
|
+
entries: [...board.givens],
|
|
9
|
+
history: [],
|
|
10
|
+
helped: false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function setFillominoCell(game, cell, value) {
|
|
14
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= game.entries.length
|
|
15
|
+
|| !Number.isInteger(value) || value < 0 || value > game.entries.length
|
|
16
|
+
|| game.board.givens[cell])
|
|
17
|
+
return game;
|
|
18
|
+
if (game.entries[cell] === value)
|
|
19
|
+
return game;
|
|
20
|
+
const entries = [...game.entries];
|
|
21
|
+
entries[cell] = value;
|
|
22
|
+
return { ...game, entries, history: [...game.history, [...game.entries]] };
|
|
23
|
+
}
|
|
24
|
+
export function undoFillomino(game) {
|
|
25
|
+
const previous = game.history.at(-1);
|
|
26
|
+
return previous
|
|
27
|
+
? { ...game, entries: [...previous], history: game.history.slice(0, -1) }
|
|
28
|
+
: game;
|
|
29
|
+
}
|
|
30
|
+
export function hintFillomino(game) {
|
|
31
|
+
const result = solveFillomino(game.board, game.entries);
|
|
32
|
+
if (!result.complete || result.count !== 1 || !result.solution)
|
|
33
|
+
return null;
|
|
34
|
+
const cell = result.solution.findIndex((value, index) => !game.entries[index] && value > 0);
|
|
35
|
+
return cell < 0 ? null : { cell, value: result.solution[cell], why: "unique-completion" };
|
|
36
|
+
}
|
|
37
|
+
export function fillominoFinished(game) {
|
|
38
|
+
return checkFillomino(game.board, game.entries).ok;
|
|
39
|
+
}
|
|
40
|
+
export function restartFillomino(game) {
|
|
41
|
+
return { ...newFillomino(game.board), helped: game.helped };
|
|
42
|
+
}
|
|
43
|
+
export function encodeFillomino(game) {
|
|
44
|
+
return JSON.stringify({ version: 1, board: game.board, entries: game.entries, helped: game.helped });
|
|
45
|
+
}
|
|
46
|
+
export function decodeFillomino(value) {
|
|
47
|
+
try {
|
|
48
|
+
if (value.length > 40000)
|
|
49
|
+
return null;
|
|
50
|
+
const parsed = JSON.parse(value);
|
|
51
|
+
if (parsed.version !== 1 || !isFillominoBoard(parsed.board) || !Array.isArray(parsed.entries)
|
|
52
|
+
|| typeof parsed.helped !== "boolean" || parsed.entries.length !== parsed.board.givens.length) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const entries = parsed.entries;
|
|
56
|
+
if (entries.some(entry => !Number.isInteger(entry) || Number(entry) < 0 || Number(entry) > entries.length))
|
|
57
|
+
return null;
|
|
58
|
+
if (parsed.board.givens.some((given, cell) => given > 0 && entries[cell] !== given))
|
|
59
|
+
return null;
|
|
60
|
+
return { ...newFillomino(parsed.board), entries: [...entries], helped: parsed.helped };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FillominoLevel, FillominoPuzzle } from "./fillomino.types.ts";
|
|
2
|
+
/** Makes an original connected-region partition, then keeps only independently proved unique clues. */
|
|
3
|
+
export declare function generateFillomino(width?: number, height?: number, level?: FillominoLevel, seed?: number): FillominoPuzzle;
|