@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,96 @@
|
|
|
1
|
+
import { AKARI_MOST_SIDE } from "./akari.constants.js";
|
|
2
|
+
/** Validates dimensions and the three public square kinds. */
|
|
3
|
+
export function isAkariBoard(value) {
|
|
4
|
+
if (!value || typeof value !== "object")
|
|
5
|
+
return false;
|
|
6
|
+
const board = value;
|
|
7
|
+
return Number.isInteger(board.width) && board.width >= 2 && board.width <= AKARI_MOST_SIDE
|
|
8
|
+
&& Number.isInteger(board.height) && board.height >= 2 && board.height <= AKARI_MOST_SIDE
|
|
9
|
+
&& Array.isArray(board.cells) && board.cells.length === board.width * board.height
|
|
10
|
+
&& board.cells.every(cell => cell === null || cell === false
|
|
11
|
+
|| Number.isInteger(cell) && cell >= 0 && cell <= 4)
|
|
12
|
+
&& board.cells.some(cell => cell === null);
|
|
13
|
+
}
|
|
14
|
+
/** Returns the orthogonally adjacent cells to a board position. */
|
|
15
|
+
export function akariNeighbours(board, cell) {
|
|
16
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= board.cells.length)
|
|
17
|
+
return [];
|
|
18
|
+
const x = cell % board.width;
|
|
19
|
+
const y = Math.floor(cell / board.width);
|
|
20
|
+
const coordinates = [[x, y - 1], [x + 1, y], [x, y + 1], [x - 1, y]];
|
|
21
|
+
return coordinates
|
|
22
|
+
.filter(([cx, cy]) => cx >= 0 && cx < board.width && cy >= 0 && cy < board.height)
|
|
23
|
+
.map(([cx, cy]) => cy * board.width + cx);
|
|
24
|
+
}
|
|
25
|
+
/** White squares visible from a cell, including itself, stopping at black squares and edges. */
|
|
26
|
+
export function akariVisible(board, cell) {
|
|
27
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= board.cells.length || board.cells[cell] !== null)
|
|
28
|
+
return [];
|
|
29
|
+
const x = cell % board.width;
|
|
30
|
+
const y = Math.floor(cell / board.width);
|
|
31
|
+
const visible = [cell];
|
|
32
|
+
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
|
33
|
+
let cx = x + dx;
|
|
34
|
+
let cy = y + dy;
|
|
35
|
+
while (cx >= 0 && cx < board.width && cy >= 0 && cy < board.height) {
|
|
36
|
+
const next = cy * board.width + cx;
|
|
37
|
+
if (board.cells[next] !== null)
|
|
38
|
+
break;
|
|
39
|
+
visible.push(next);
|
|
40
|
+
cx += dx;
|
|
41
|
+
cy += dy;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return visible;
|
|
45
|
+
}
|
|
46
|
+
function evaluate(board, bulbs, complete) {
|
|
47
|
+
if (!isAkariBoard(board))
|
|
48
|
+
throw new RangeError("Invalid Akari board");
|
|
49
|
+
const errors = [];
|
|
50
|
+
const conflicts = [];
|
|
51
|
+
const numbered = [];
|
|
52
|
+
const dark = [];
|
|
53
|
+
const unique = new Set();
|
|
54
|
+
bulbs.forEach((cell, index) => {
|
|
55
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= board.cells.length
|
|
56
|
+
|| board.cells[cell] !== null || unique.has(cell)) {
|
|
57
|
+
errors.push(index);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
unique.add(cell);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const lit = new Set();
|
|
64
|
+
for (const bulb of unique) {
|
|
65
|
+
const ray = akariVisible(board, bulb);
|
|
66
|
+
ray.forEach(cell => lit.add(cell));
|
|
67
|
+
if (ray.some(cell => cell !== bulb && unique.has(cell)))
|
|
68
|
+
conflicts.push(bulb);
|
|
69
|
+
}
|
|
70
|
+
for (let cell = 0; cell < board.cells.length; cell += 1) {
|
|
71
|
+
if (board.cells[cell] === null && !lit.has(cell))
|
|
72
|
+
dark.push(cell);
|
|
73
|
+
const clue = board.cells[cell];
|
|
74
|
+
if (typeof clue !== "number")
|
|
75
|
+
continue;
|
|
76
|
+
const adjacent = akariNeighbours(board, cell).filter(neighbour => unique.has(neighbour)).length;
|
|
77
|
+
if (complete ? adjacent !== clue : adjacent > clue)
|
|
78
|
+
numbered.push(cell);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ok: !errors.length && !conflicts.length && !numbered.length && (!complete || !dark.length),
|
|
82
|
+
illuminated: lit.size,
|
|
83
|
+
errors,
|
|
84
|
+
dark,
|
|
85
|
+
numbered,
|
|
86
|
+
conflicts,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Checks a complete placement from the public rules, without consulting a stored answer. */
|
|
90
|
+
export function checkAkari(board, bulbs) {
|
|
91
|
+
return evaluate(board, bulbs, true);
|
|
92
|
+
}
|
|
93
|
+
/** Reports immediate contradictions while allowing required clues to remain unsatisfied during play. */
|
|
94
|
+
export function progressAkari(board, bulbs) {
|
|
95
|
+
return evaluate(board, bulbs, false);
|
|
96
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { akariVisible, isAkariBoard } from "./akariBoard.js";
|
|
2
|
+
import { akariWords } from "./akariStrings.js";
|
|
3
|
+
/** Draws the public clues and current bulbs as SVG. */
|
|
4
|
+
export function drawAkari(board, options = {}) {
|
|
5
|
+
if (!isAkariBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Akari board");
|
|
7
|
+
const words = akariWords(options.language);
|
|
8
|
+
const size = 48;
|
|
9
|
+
const palettes = {
|
|
10
|
+
ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)", "#252923"],
|
|
11
|
+
wood: ["#e5cda6", "#493e2f", "#bca582", "#493e2f"],
|
|
12
|
+
slate: ["#262a27", "#ece8dc", "#454a44", "#111412"],
|
|
13
|
+
};
|
|
14
|
+
const [paper, ink, grid, black] = palettes[options.material ?? "ivory"];
|
|
15
|
+
const bulbs = new Set(options.bulbs ?? []);
|
|
16
|
+
const lit = new Set();
|
|
17
|
+
bulbs.forEach(cell => akariVisible(board, cell).forEach(illuminated => lit.add(illuminated)));
|
|
18
|
+
const cells = board.cells.map((clue, cell) => {
|
|
19
|
+
const x = cell % board.width * size;
|
|
20
|
+
const y = Math.floor(cell / board.width) * size;
|
|
21
|
+
const active = cell === options.selected;
|
|
22
|
+
const bad = options.errors?.includes(cell);
|
|
23
|
+
const bulb = bulbs.has(cell)
|
|
24
|
+
? `${options.pieces === "tiles" ? `<rect x="${x + 10}" y="${y + 10}" width="28" height="28" rx="6" fill="${paper}" stroke="${grid}"/>` : ""}
|
|
25
|
+
<circle cx="${x + 24}" cy="${y + 24}" r="11" fill="${ink}"/>
|
|
26
|
+
<path d="M${x + 18} ${y + 31}h12m-10 4h8" stroke="${ink}" stroke-width="2"/>`
|
|
27
|
+
: "";
|
|
28
|
+
const body = clue === null
|
|
29
|
+
? `<rect x="${x}" y="${y}" width="48" height="48" fill="${lit.has(cell) ? paper : "var(--kz-grid,#cfc6b2)"}" fill-opacity=".95"/>${bulb}`
|
|
30
|
+
: `<rect x="${x}" y="${y}" width="48" height="48" fill="${black}"/>${typeof clue === "number"
|
|
31
|
+
? `<text x="${x + 24}" y="${y + 25}" dominant-baseline="middle" text-anchor="middle" fill="${paper}" font-size="21" font-weight="700" font-family="system-ui,sans-serif">${clue}</text>`
|
|
32
|
+
: ""}`;
|
|
33
|
+
const stroke = bad ? "var(--kz-bad,#b5452c)" : grid;
|
|
34
|
+
const strokeWidth = bad ? 3 : 1;
|
|
35
|
+
return `<g data-akari-cell="${cell}">${body}<rect x="${x}" y="${y}" width="48" height="48" fill="${active ? ink : "transparent"}" fill-opacity="${active ? ".16" : "1"}" stroke="${stroke}" stroke-width="${strokeWidth}"/></g>`;
|
|
36
|
+
}).join("");
|
|
37
|
+
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>`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AkariBoard, AkariGame } from "./akari.types.ts";
|
|
2
|
+
/** Starts a game with only its public board data. */
|
|
3
|
+
export declare function newAkari(board: AkariBoard): AkariGame;
|
|
4
|
+
/** Toggles a bulb on a white square and keeps one prior turn for undo. */
|
|
5
|
+
export declare function toggleAkari(game: AkariGame, cell: number): AkariGame;
|
|
6
|
+
/** Restores the last bulb placement. */
|
|
7
|
+
export declare function undoAkari(game: AkariGame): AkariGame;
|
|
8
|
+
/** A game is finished only when its bulbs satisfy every rule and light every white square. */
|
|
9
|
+
export declare function akariFinished(game: AkariGame): boolean;
|
|
10
|
+
/** Returns a missing bulb, or an extra bulb to remove, only after uniqueness is proved. */
|
|
11
|
+
export declare function hintAkari(game: AkariGame): number | null;
|
|
12
|
+
/** Encodes only public clues and player placements. */
|
|
13
|
+
export declare function encodeAkari(game: AkariGame): string;
|
|
14
|
+
/** Decodes a bounded save after validating the board and each placement. */
|
|
15
|
+
export declare function decodeAkari(code: string): AkariGame | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
2
|
+
import { solveAkari } from "./akariSolve.js";
|
|
3
|
+
/** Starts a game with only its public board data. */
|
|
4
|
+
export function newAkari(board) {
|
|
5
|
+
if (!isAkariBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Akari board");
|
|
7
|
+
return {
|
|
8
|
+
board: { width: board.width, height: board.height, cells: [...board.cells] },
|
|
9
|
+
bulbs: [],
|
|
10
|
+
history: [],
|
|
11
|
+
helped: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Toggles a bulb on a white square and keeps one prior turn for undo. */
|
|
15
|
+
export function toggleAkari(game, cell) {
|
|
16
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= game.board.cells.length || game.board.cells[cell] !== null) {
|
|
17
|
+
return game;
|
|
18
|
+
}
|
|
19
|
+
const bulbs = game.bulbs.includes(cell)
|
|
20
|
+
? game.bulbs.filter(placed => placed !== cell)
|
|
21
|
+
: [...game.bulbs, cell];
|
|
22
|
+
return { ...game, bulbs, history: [...game.history, [...game.bulbs]] };
|
|
23
|
+
}
|
|
24
|
+
/** Restores the last bulb placement. */
|
|
25
|
+
export function undoAkari(game) {
|
|
26
|
+
const previous = game.history.at(-1);
|
|
27
|
+
return previous
|
|
28
|
+
? { ...game, bulbs: [...previous], history: game.history.slice(0, -1) }
|
|
29
|
+
: game;
|
|
30
|
+
}
|
|
31
|
+
/** A game is finished only when its bulbs satisfy every rule and light every white square. */
|
|
32
|
+
export function akariFinished(game) {
|
|
33
|
+
return checkAkari(game.board, game.bulbs).ok;
|
|
34
|
+
}
|
|
35
|
+
/** Returns a missing bulb, or an extra bulb to remove, only after uniqueness is proved. */
|
|
36
|
+
export function hintAkari(game) {
|
|
37
|
+
const solved = solveAkari(game.board);
|
|
38
|
+
if (!solved.complete || solved.count !== 1 || !solved.solution)
|
|
39
|
+
return null;
|
|
40
|
+
return solved.solution.find(cell => !game.bulbs.includes(cell))
|
|
41
|
+
?? game.bulbs.find(cell => !solved.solution.includes(cell))
|
|
42
|
+
?? null;
|
|
43
|
+
}
|
|
44
|
+
/** Encodes only public clues and player placements. */
|
|
45
|
+
export function encodeAkari(game) {
|
|
46
|
+
return JSON.stringify({ version: 1, board: game.board, bulbs: game.bulbs, helped: game.helped });
|
|
47
|
+
}
|
|
48
|
+
/** Decodes a bounded save after validating the board and each placement. */
|
|
49
|
+
export function decodeAkari(code) {
|
|
50
|
+
try {
|
|
51
|
+
if (code.length > 40000)
|
|
52
|
+
return null;
|
|
53
|
+
const value = JSON.parse(code);
|
|
54
|
+
if (value.version !== 1 || !isAkariBoard(value.board) || !Array.isArray(value.bulbs)
|
|
55
|
+
|| value.bulbs.length > value.board.cells.length || typeof value.helped !== "boolean")
|
|
56
|
+
return null;
|
|
57
|
+
if (new Set(value.bulbs).size !== value.bulbs.length)
|
|
58
|
+
return null;
|
|
59
|
+
let game = newAkari(value.board);
|
|
60
|
+
for (const cell of value.bulbs) {
|
|
61
|
+
const next = toggleAkari(game, cell);
|
|
62
|
+
if (next === game)
|
|
63
|
+
return null;
|
|
64
|
+
game = next;
|
|
65
|
+
}
|
|
66
|
+
return { ...game, history: [], helped: value.helped };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AkariPuzzle } from "./akari.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Makes a seeded board from two original room-grid families: horizontal or vertical paired
|
|
4
|
+
* corridors, with isolated singleton rooms at the edges. Seeded bulb direction and reflections
|
|
5
|
+
* vary each layout. Generation returns only after the independent counter proves uniqueness.
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateAkari(width?: number, height?: number, seed?: number): AkariPuzzle;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AKARI_MOST_SIDE } from "./akari.constants.js";
|
|
2
|
+
import { checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
3
|
+
import { solveAkari } from "./akariSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/**
|
|
6
|
+
* Makes a seeded board from two original room-grid families: horizontal or vertical paired
|
|
7
|
+
* corridors, with isolated singleton rooms at the edges. Seeded bulb direction and reflections
|
|
8
|
+
* vary each layout. Generation returns only after the independent counter proves uniqueness.
|
|
9
|
+
*/
|
|
10
|
+
export function generateAkari(width = 7, height = width, seed = 1) {
|
|
11
|
+
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= AKARI_MOST_SIDE)
|
|
12
|
+
|| !isKazuSeed(seed))
|
|
13
|
+
throw new RangeError("Invalid Akari settings");
|
|
14
|
+
const random = seededRandom(seed);
|
|
15
|
+
const cells = Array(width * height).fill(false);
|
|
16
|
+
const bulbs = [];
|
|
17
|
+
const horizontal = random() < .5;
|
|
18
|
+
if (horizontal) {
|
|
19
|
+
const firstRow = height > 2 ? 1 : 0;
|
|
20
|
+
for (let y = firstRow; y < height; y += 3) {
|
|
21
|
+
const clueY = y > 0 ? y - 1 : y + 1;
|
|
22
|
+
for (let x = 0; x < width; x += 3) {
|
|
23
|
+
const room = y * width + x;
|
|
24
|
+
if (x + 1 < width) {
|
|
25
|
+
const bulbX = random() < .5 ? x : x + 1;
|
|
26
|
+
cells[room] = null;
|
|
27
|
+
cells[room + 1] = null;
|
|
28
|
+
cells[clueY * width + bulbX] = 1;
|
|
29
|
+
bulbs.push(y * width + bulbX);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
cells[room] = null;
|
|
33
|
+
cells[clueY * width + x] = 1;
|
|
34
|
+
bulbs.push(room);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const firstColumn = width > 2 ? 1 : 0;
|
|
41
|
+
for (let x = firstColumn; x < width; x += 3) {
|
|
42
|
+
const clueX = x > 0 ? x - 1 : x + 1;
|
|
43
|
+
for (let y = 0; y < height; y += 3) {
|
|
44
|
+
const room = y * width + x;
|
|
45
|
+
if (y + 1 < height) {
|
|
46
|
+
const bulbY = random() < .5 ? y : y + 1;
|
|
47
|
+
cells[room] = null;
|
|
48
|
+
cells[room + width] = null;
|
|
49
|
+
cells[bulbY * width + clueX] = 1;
|
|
50
|
+
bulbs.push(bulbY * width + x);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
cells[room] = null;
|
|
54
|
+
cells[y * width + clueX] = 1;
|
|
55
|
+
bulbs.push(room);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Reflections are applied to the board and the construction answer together.
|
|
61
|
+
const reflectX = random() < .5, reflectY = random() < .5;
|
|
62
|
+
const original = [...cells];
|
|
63
|
+
const map = (cell) => {
|
|
64
|
+
const x = cell % width, y = Math.floor(cell / width);
|
|
65
|
+
return (reflectY ? height - 1 - y : y) * width + (reflectX ? width - 1 - x : x);
|
|
66
|
+
};
|
|
67
|
+
for (let cell = 0; cell < original.length; cell += 1)
|
|
68
|
+
cells[map(cell)] = original[cell];
|
|
69
|
+
const answer = bulbs.map(map);
|
|
70
|
+
const board = { width, height, cells };
|
|
71
|
+
if (!isAkariBoard(board) || !checkAkari(board, answer).ok)
|
|
72
|
+
throw new Error("Unable to construct Akari puzzle");
|
|
73
|
+
const proof = solveAkari(board, { limit: 2 });
|
|
74
|
+
if (!proof.complete || proof.count !== 1 || !proof.solution) {
|
|
75
|
+
throw new Error("No uniquely solvable Akari found within the generation budget");
|
|
76
|
+
}
|
|
77
|
+
return { ...board, seed, solution: proof.solution };
|
|
78
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { akariVisible, checkAkari } from "./akariBoard.js";
|
|
2
|
+
import { akariFinished, decodeAkari, encodeAkari, hintAkari, newAkari, toggleAkari, undoAkari, } from "./akariGame.js";
|
|
3
|
+
import { drawAkari } from "./akariDraw.js";
|
|
4
|
+
import { KAZU_STYLE } from "./style.js";
|
|
5
|
+
import { AKARI_PLAY_STYLE } from "./akariStyle.js";
|
|
6
|
+
import { akariWords } from "./akariStrings.js";
|
|
7
|
+
/** Mounts the player in a host element; call destroy when the host is removed. */
|
|
8
|
+
export function mountAkari(host, initial) {
|
|
9
|
+
const doc = host.ownerDocument;
|
|
10
|
+
let options = { ...initial };
|
|
11
|
+
let game = initial.progress ? decodeAkari(initial.progress) : null;
|
|
12
|
+
game ?? (game = newAkari(initial.board));
|
|
13
|
+
let selected = game.board.cells.findIndex(cell => cell === null);
|
|
14
|
+
let errors = [];
|
|
15
|
+
let message = "";
|
|
16
|
+
let finished = akariFinished(game);
|
|
17
|
+
let dead = false;
|
|
18
|
+
const root = doc.createElement("div");
|
|
19
|
+
const style = doc.createElement("style");
|
|
20
|
+
const board = doc.createElement("div");
|
|
21
|
+
const status = doc.createElement("p");
|
|
22
|
+
const tools = doc.createElement("div");
|
|
23
|
+
const help = doc.createElement("p");
|
|
24
|
+
root.className = "ka-root";
|
|
25
|
+
style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".ka-palette") + AKARI_PLAY_STYLE;
|
|
26
|
+
board.className = "ka-board";
|
|
27
|
+
status.className = "ka-status";
|
|
28
|
+
status.setAttribute("role", "status");
|
|
29
|
+
tools.className = "ka-tools";
|
|
30
|
+
help.className = "ka-help";
|
|
31
|
+
root.append(style, board, status, tools, help);
|
|
32
|
+
host.append(root);
|
|
33
|
+
const copy = () => ({
|
|
34
|
+
...game,
|
|
35
|
+
board: { ...game.board, cells: [...game.board.cells] },
|
|
36
|
+
bulbs: [...game.bulbs],
|
|
37
|
+
history: game.history.map(turn => [...turn]),
|
|
38
|
+
});
|
|
39
|
+
const words = () => akariWords(options.language);
|
|
40
|
+
const button = (label, action, disabled = false) => {
|
|
41
|
+
const control = doc.createElement("button");
|
|
42
|
+
control.type = "button";
|
|
43
|
+
control.textContent = label;
|
|
44
|
+
control.disabled = disabled;
|
|
45
|
+
control.onclick = action;
|
|
46
|
+
tools.append(control);
|
|
47
|
+
};
|
|
48
|
+
const changed = () => {
|
|
49
|
+
errors = [];
|
|
50
|
+
message = "";
|
|
51
|
+
const won = akariFinished(game);
|
|
52
|
+
render();
|
|
53
|
+
options.onChange?.(copy());
|
|
54
|
+
root.dispatchEvent(new CustomEvent("akari-change", { detail: copy(), bubbles: true }));
|
|
55
|
+
if (won && !finished) {
|
|
56
|
+
options.onFinish?.(copy());
|
|
57
|
+
root.dispatchEvent(new CustomEvent("akari-finish", { detail: copy(), bubbles: true }));
|
|
58
|
+
}
|
|
59
|
+
finished = won;
|
|
60
|
+
};
|
|
61
|
+
const toggle = (cell) => {
|
|
62
|
+
if (finished || game.board.cells[cell] !== null)
|
|
63
|
+
return;
|
|
64
|
+
selected = cell;
|
|
65
|
+
game = toggleAkari(game, cell);
|
|
66
|
+
changed();
|
|
67
|
+
};
|
|
68
|
+
const restart = () => {
|
|
69
|
+
game = newAkari(game.board);
|
|
70
|
+
finished = false;
|
|
71
|
+
selected = game.board.cells.findIndex(cell => cell === null);
|
|
72
|
+
changed();
|
|
73
|
+
};
|
|
74
|
+
const render = () => {
|
|
75
|
+
if (dead)
|
|
76
|
+
return;
|
|
77
|
+
const w = words();
|
|
78
|
+
const current = game.board;
|
|
79
|
+
const focused = board.contains(doc.activeElement);
|
|
80
|
+
const check = checkAkari(current, game.bulbs);
|
|
81
|
+
board.classList.toggle("ka-palette", (options.material ?? "ivory") === "ivory");
|
|
82
|
+
board.innerHTML = drawAkari(current, { ...options, bulbs: game.bulbs, selected, errors });
|
|
83
|
+
const cells = doc.createElement("div");
|
|
84
|
+
cells.className = "ka-cells";
|
|
85
|
+
cells.setAttribute("role", "grid");
|
|
86
|
+
cells.style.gridTemplateColumns = `repeat(${current.width},1fr)`;
|
|
87
|
+
for (let cellIndex = 0; cellIndex < current.cells.length; cellIndex += 1) {
|
|
88
|
+
const white = current.cells[cellIndex] === null;
|
|
89
|
+
const bulb = game.bulbs.includes(cellIndex);
|
|
90
|
+
const lit = white && game.bulbs.some(on => akariVisible(current, on).includes(cellIndex));
|
|
91
|
+
const control = doc.createElement("button");
|
|
92
|
+
const row = Math.floor(cellIndex / current.width) + 1;
|
|
93
|
+
const column = cellIndex % current.width + 1;
|
|
94
|
+
control.type = "button";
|
|
95
|
+
control.className = "ka-cell";
|
|
96
|
+
control.dataset.cell = String(cellIndex);
|
|
97
|
+
control.tabIndex = white && cellIndex === selected ? 0 : -1;
|
|
98
|
+
control.disabled = !white;
|
|
99
|
+
control.setAttribute("role", "gridcell");
|
|
100
|
+
control.setAttribute("aria-pressed", String(bulb));
|
|
101
|
+
control.setAttribute("aria-label", cellLabel(w, current.cells[cellIndex], row, column, lit, bulb, white));
|
|
102
|
+
control.onclick = () => toggle(cellIndex);
|
|
103
|
+
control.onkeydown = event => handleKey(event, cellIndex, current.width, current.cells);
|
|
104
|
+
cells.append(control);
|
|
105
|
+
}
|
|
106
|
+
board.append(cells);
|
|
107
|
+
if (focused)
|
|
108
|
+
board.querySelector(`[data-cell="${selected}"]`)?.focus();
|
|
109
|
+
status.textContent = check.ok ? (game.helped ? w.helped : w.won) : message || w.checked;
|
|
110
|
+
help.textContent = w.help;
|
|
111
|
+
tools.replaceChildren();
|
|
112
|
+
button(w.undo, () => {
|
|
113
|
+
game = undoAkari(game);
|
|
114
|
+
finished = akariFinished(game);
|
|
115
|
+
changed();
|
|
116
|
+
}, !game.history.length);
|
|
117
|
+
button(w.check, () => {
|
|
118
|
+
errors = [...new Set([...check.dark, ...check.numbered, ...check.conflicts])];
|
|
119
|
+
message = check.ok ? w.won : w.wrong;
|
|
120
|
+
render();
|
|
121
|
+
});
|
|
122
|
+
button(w.hint, () => {
|
|
123
|
+
const cell = hintAkari(game);
|
|
124
|
+
if (cell === null) {
|
|
125
|
+
message = w.noHint;
|
|
126
|
+
render();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
game = { ...toggleAkari(game, cell), helped: true };
|
|
130
|
+
changed();
|
|
131
|
+
message = w.hintWhy;
|
|
132
|
+
render();
|
|
133
|
+
}, finished);
|
|
134
|
+
button(w.restart, restart);
|
|
135
|
+
button(w.expand, expand);
|
|
136
|
+
};
|
|
137
|
+
function cellLabel(w, value, row, column, lit, bulb, white) {
|
|
138
|
+
const position = `${w.row} ${row}, ${w.column} ${column}`;
|
|
139
|
+
if (!white)
|
|
140
|
+
return `${position}: ${w.black}${typeof value === "number" && value ? ` ${value}` : ""}`;
|
|
141
|
+
return `${position}: ${w.white}${lit ? `, ${w.lit}` : `, ${w.dark}`}${bulb ? `, ${w.bulb}` : ""}`;
|
|
142
|
+
}
|
|
143
|
+
function handleKey(event, cell, width, values) {
|
|
144
|
+
const steps = {
|
|
145
|
+
ArrowLeft: -1,
|
|
146
|
+
ArrowRight: 1,
|
|
147
|
+
ArrowUp: -width,
|
|
148
|
+
ArrowDown: width,
|
|
149
|
+
};
|
|
150
|
+
if (event.key in steps) {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
const delta = steps[event.key];
|
|
153
|
+
let next = cell;
|
|
154
|
+
while (true) {
|
|
155
|
+
const candidate = next + delta;
|
|
156
|
+
if (candidate < 0 || candidate >= values.length)
|
|
157
|
+
break;
|
|
158
|
+
if (Math.abs(delta) === 1 && Math.floor(candidate / width) !== Math.floor(next / width))
|
|
159
|
+
break;
|
|
160
|
+
next = candidate;
|
|
161
|
+
if (values[next] === null)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (values[next] === null) {
|
|
165
|
+
selected = next;
|
|
166
|
+
render();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (event.key === "Enter" || event.key === " ") {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
toggle(cell);
|
|
172
|
+
}
|
|
173
|
+
else if (event.key === "Delete" || event.key === "Backspace") {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
if (game.bulbs.includes(cell))
|
|
176
|
+
toggle(cell);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function expand() {
|
|
180
|
+
if (root.parentElement !== host)
|
|
181
|
+
return;
|
|
182
|
+
const dialog = doc.createElement("dialog");
|
|
183
|
+
const close = doc.createElement("button");
|
|
184
|
+
dialog.className = "ka-dialog";
|
|
185
|
+
close.className = "ka-close";
|
|
186
|
+
close.textContent = words().close;
|
|
187
|
+
close.onclick = () => dialog.close();
|
|
188
|
+
dialog.append(close, root);
|
|
189
|
+
host.append(dialog);
|
|
190
|
+
dialog.onclose = () => {
|
|
191
|
+
host.append(root);
|
|
192
|
+
dialog.remove();
|
|
193
|
+
};
|
|
194
|
+
dialog.showModal();
|
|
195
|
+
}
|
|
196
|
+
render();
|
|
197
|
+
return {
|
|
198
|
+
game: copy,
|
|
199
|
+
progress: () => encodeAkari(game),
|
|
200
|
+
restart,
|
|
201
|
+
set: (next) => {
|
|
202
|
+
if (next.language && next.language !== options.language)
|
|
203
|
+
message = "";
|
|
204
|
+
options = { ...options, ...next };
|
|
205
|
+
render();
|
|
206
|
+
},
|
|
207
|
+
destroy: () => {
|
|
208
|
+
dead = true;
|
|
209
|
+
const dialog = root.closest("dialog");
|
|
210
|
+
root.remove();
|
|
211
|
+
dialog?.remove();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AkariBoard, AkariGame } from "./akari.types.ts";
|
|
2
|
+
export type AkariMaterial = "ivory" | "wood" | "slate";
|
|
3
|
+
export type AkariPieces = "ink" | "tiles";
|
|
4
|
+
export type AkariLanguage = "en" | "ja";
|
|
5
|
+
export type AkariDrawOptions = {
|
|
6
|
+
bulbs?: AkariGame["bulbs"];
|
|
7
|
+
selected?: number | null;
|
|
8
|
+
errors?: readonly number[];
|
|
9
|
+
material?: AkariMaterial;
|
|
10
|
+
pieces?: AkariPieces;
|
|
11
|
+
language?: AkariLanguage;
|
|
12
|
+
};
|
|
13
|
+
export type AkariMountOptions = AkariDrawOptions & {
|
|
14
|
+
board: AkariBoard;
|
|
15
|
+
progress?: string;
|
|
16
|
+
onChange?: (game: AkariGame) => void;
|
|
17
|
+
onFinish?: (game: AkariGame) => void;
|
|
18
|
+
};
|
|
19
|
+
export type AkariMount = {
|
|
20
|
+
game: () => AkariGame;
|
|
21
|
+
progress: () => string;
|
|
22
|
+
set: (options: AkariDrawOptions) => void;
|
|
23
|
+
restart: () => void;
|
|
24
|
+
destroy: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AkariBoard, AkariSolve } from "./akari.types.ts";
|
|
2
|
+
/** Bounded binary constraint search; a stopped proof is always marked incomplete. */
|
|
3
|
+
export declare function solveAkari(board: AkariBoard, options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): AkariSolve;
|