@johnmorrisdotca/kazu 1.1.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 +39 -0
- package/README.md +424 -10
- package/dist/akari-draw-entry.d.ts +2 -0
- package/dist/akari-draw-entry.js +1 -0
- package/dist/akari-entry.d.ts +13 -0
- package/dist/akari-entry.js +11 -0
- package/dist/akari-play-entry.d.ts +2 -0
- package/dist/akari-play-entry.js +1 -0
- package/dist/akari.constants.d.ts +7 -0
- package/dist/akari.constants.js +7 -0
- package/dist/akari.types.d.ts +52 -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 +10 -0
- package/dist/akariGenerate.js +101 -0
- package/dist/akariLogic.d.ts +10 -0
- package/dist/akariLogic.js +84 -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/akariRate.d.ts +6 -0
- package/dist/akariRate.js +34 -0
- package/dist/akariSolve.d.ts +6 -0
- package/dist/akariSolve.js +18 -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/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-draw-entry.d.ts +2 -0
- package/dist/fillomino-draw-entry.js +1 -0
- package/dist/fillomino-entry.d.ts +9 -0
- package/dist/fillomino-entry.js +8 -0
- package/dist/fillomino-play-entry.d.ts +4 -0
- package/dist/fillomino-play-entry.js +3 -0
- package/dist/fillomino.constants.d.ts +9 -0
- package/dist/fillomino.constants.js +9 -0
- package/dist/fillomino.types.d.ts +47 -0
- package/dist/fillomino.types.js +1 -0
- package/dist/fillominoBoard.d.ts +6 -0
- package/dist/fillominoBoard.js +84 -0
- package/dist/fillominoBuild.d.ts +10 -0
- package/dist/fillominoBuild.js +78 -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 +9 -0
- package/dist/fillominoGenerate.js +93 -0
- package/dist/fillominoLogic.d.ts +14 -0
- package/dist/fillominoLogic.js +177 -0
- package/dist/fillominoMount.d.ts +2 -0
- package/dist/fillominoMount.js +214 -0
- package/dist/fillominoPlay.types.d.ts +25 -0
- package/dist/fillominoPlay.types.js +1 -0
- package/dist/fillominoRate.d.ts +3 -0
- package/dist/fillominoRate.js +57 -0
- package/dist/fillominoSolve.d.ts +6 -0
- package/dist/fillominoSolve.js +38 -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 +7 -0
- package/dist/hitori-entry.js +7 -0
- package/dist/hitori-play-entry.d.ts +4 -0
- package/dist/hitori-play-entry.js +3 -0
- package/dist/hitori.constants.d.ts +8 -0
- package/dist/hitori.constants.js +8 -0
- package/dist/hitori.types.d.ts +37 -0
- package/dist/hitori.types.js +1 -0
- package/dist/hitoriBoard.d.ts +8 -0
- package/dist/hitoriBoard.js +73 -0
- package/dist/hitoriBuild.d.ts +12 -0
- package/dist/hitoriBuild.js +117 -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 +10 -0
- package/dist/hitoriGenerate.js +65 -0
- package/dist/hitoriLogic.d.ts +12 -0
- package/dist/hitoriLogic.js +189 -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/hitoriRate.d.ts +3 -0
- package/dist/hitoriRate.js +32 -0
- package/dist/hitoriSolve.d.ts +9 -0
- package/dist/hitoriSolve.js +18 -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 +13 -0
- package/dist/kakuro-entry.js +11 -0
- package/dist/kakuro-play-entry.d.ts +4 -0
- package/dist/kakuro-play-entry.js +3 -0
- package/dist/kakuro.constants.d.ts +11 -0
- package/dist/kakuro.constants.js +11 -0
- package/dist/kakuro.types.d.ts +65 -0
- package/dist/kakuro.types.js +1 -0
- package/dist/kakuroBoard.d.ts +8 -0
- package/dist/kakuroBoard.js +117 -0
- package/dist/kakuroBuild.d.ts +30 -0
- package/dist/kakuroBuild.js +337 -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 +10 -0
- package/dist/kakuroGenerate.js +62 -0
- package/dist/kakuroLogic.d.ts +13 -0
- package/dist/kakuroLogic.js +153 -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/kakuroRate.d.ts +3 -0
- package/dist/kakuroRate.js +40 -0
- package/dist/kakuroSolve.d.ts +6 -0
- package/dist/kakuroSolve.js +33 -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/kakuroTemplate.d.ts +3 -0
- package/dist/kakuroTemplate.js +130 -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 +2 -0
- package/dist/shikaku-entry.js +2 -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/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -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-draw-entry.d.ts +2 -0
- package/dist/slitherlink-draw-entry.js +1 -0
- package/dist/slitherlink-entry.d.ts +12 -0
- package/dist/slitherlink-entry.js +10 -0
- package/dist/slitherlink-play-entry.d.ts +4 -0
- package/dist/slitherlink-play-entry.js +3 -0
- package/dist/slitherlink.constants.d.ts +7 -0
- package/dist/slitherlink.constants.js +7 -0
- package/dist/slitherlink.types.d.ts +52 -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 +10 -0
- package/dist/slitherlinkGenerate.js +182 -0
- package/dist/slitherlinkLogic.d.ts +10 -0
- package/dist/slitherlinkLogic.js +267 -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/slitherlinkRate.d.ts +3 -0
- package/dist/slitherlinkRate.js +26 -0
- package/dist/slitherlinkSolve.d.ts +6 -0
- package/dist/slitherlinkSolve.js +19 -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/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/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,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, AkariRating } from "./akari.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Rates a board by solving it as a person would: with the rules alone (depth 0), then with one supposition at
|
|
4
|
+
* a time (depth 1), and anything beyond that is depth 2. A board with no single answer cannot be rated.
|
|
5
|
+
*/
|
|
6
|
+
export declare function rateAkari(board: AkariBoard): AkariRating;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isAkariBoard } from "./akariBoard.js";
|
|
2
|
+
import { akariModel } from "./akariLogic.js";
|
|
3
|
+
import { logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
/**
|
|
5
|
+
* Rates a board by solving it as a person would: with the rules alone (depth 0), then with one supposition at
|
|
6
|
+
* a time (depth 1), and anything beyond that is depth 2. A board with no single answer cannot be rated.
|
|
7
|
+
*/
|
|
8
|
+
export function rateAkari(board) {
|
|
9
|
+
if (!isAkariBoard(board))
|
|
10
|
+
throw new RangeError("Invalid Akari board");
|
|
11
|
+
const { csp, whites } = akariModel(board);
|
|
12
|
+
const proof = solveCsp(csp, openSlots(csp), 2, 250000);
|
|
13
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
14
|
+
throw new RangeError("Akari rating needs a board with one answer");
|
|
15
|
+
const plain = logicCsp(csp, openSlots(csp), 0);
|
|
16
|
+
const probing = plain.solved ? plain : logicCsp(csp, openSlots(csp), 1);
|
|
17
|
+
const blacks = board.cells.length - whites.length;
|
|
18
|
+
const clues = board.cells.filter(cell => typeof cell === "number").length;
|
|
19
|
+
const lit = board.cells.map((cell, at) => cell === false || typeof cell === "number" ? at : -1).filter(at => at >= 0);
|
|
20
|
+
const touching = lit.filter(at => {
|
|
21
|
+
const x = at % board.width, y = Math.floor(at / board.width);
|
|
22
|
+
return [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]].some(([nx, ny]) => nx >= 0 && ny >= 0 && nx < board.width && ny < board.height && board.cells[ny * board.width + nx] === null);
|
|
23
|
+
}).length;
|
|
24
|
+
return {
|
|
25
|
+
depth: plain.solved ? 0 : probing.solved ? 1 : 2,
|
|
26
|
+
probes: plain.solved ? 0 : probing.probes,
|
|
27
|
+
whites: whites.length,
|
|
28
|
+
blacks,
|
|
29
|
+
clues,
|
|
30
|
+
bulbs: proof.solution ? whites.filter((_, i) => proof.solution[2 * i + 1] === 1).length : 0,
|
|
31
|
+
clueShare: touching ? clues / touching : 0,
|
|
32
|
+
openShare: whites.length / board.cells.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AkariBoard, AkariSolve } from "./akari.types.ts";
|
|
2
|
+
/** Bounded constraint search over the rules in `akariLogic.ts`; a stopped proof is always marked incomplete. */
|
|
3
|
+
export declare function solveAkari(board: AkariBoard, options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): AkariSolve;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AKARI_MOST_NODES } from "./akari.constants.js";
|
|
2
|
+
import { isAkariBoard } from "./akariBoard.js";
|
|
3
|
+
import { akariModel } from "./akariLogic.js";
|
|
4
|
+
import { openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Bounded constraint search over the rules in `akariLogic.ts`; a stopped proof is always marked incomplete. */
|
|
6
|
+
export function solveAkari(board, options = {}) {
|
|
7
|
+
if (!isAkariBoard(board))
|
|
8
|
+
throw new RangeError("Invalid Akari board");
|
|
9
|
+
const limit = options.limit ?? 2;
|
|
10
|
+
const budget = options.nodes ?? AKARI_MOST_NODES;
|
|
11
|
+
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1) {
|
|
12
|
+
throw new RangeError("Invalid search bounds");
|
|
13
|
+
}
|
|
14
|
+
const { csp, whites } = akariModel(board);
|
|
15
|
+
const found = solveCsp(csp, openSlots(csp), limit, budget);
|
|
16
|
+
const solution = found.solution ? whites.filter((_, i) => found.solution[2 * i + 1] === 1) : null;
|
|
17
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
18
|
+
}
|
|
@@ -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,7 @@
|
|
|
1
|
+
import type { AkariPuzzle } from "./akari.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The first generator, kept as the fallback that cannot fail: 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 templateAkari(width?: number, height?: number, seed?: number): Omit<AkariPuzzle, "level">;
|
|
@@ -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
|
+
* The first generator, kept as the fallback that cannot fail: 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 templateAkari(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
|
+
}
|
package/dist/csp.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE SMALL ENGINE for the puzzles that are solved by narrowing down what each square, edge or rectangle can
|
|
3
|
+
* still be. A puzzle supplies its variables and one function that prunes; this file does the two things
|
|
4
|
+
* every one of them needs: counting answers (a search that stops at a limit) and rating how hard a puzzle
|
|
5
|
+
* is (how deep the reasoning must go before the grid is decided).
|
|
6
|
+
*
|
|
7
|
+
* A variable has a few slots (its possible values). `alive[slot]` is 1 while that value is still possible.
|
|
8
|
+
* A variable is decided when exactly one of its slots is alive and impossible when none is.
|
|
9
|
+
*/
|
|
10
|
+
export type Csp = {
|
|
11
|
+
/** The slots of variable `v` are `starts[v]` up to, not including, `starts[v + 1]`. */
|
|
12
|
+
starts: Int32Array;
|
|
13
|
+
/**
|
|
14
|
+
* Prunes `alive` as far as the rules force and says `false` when they cannot all be kept. Never guesses.
|
|
15
|
+
* `decided` names a variable that was just narrowed from a state that was already pruned, so a rule may
|
|
16
|
+
* look only at what that can change; leaving it out means look at everything.
|
|
17
|
+
*/
|
|
18
|
+
propagate: (alive: Uint8Array, decided?: number) => boolean;
|
|
19
|
+
/** Which variable to branch on in a search; the default is the one with the fewest values left. */
|
|
20
|
+
choose?: (alive: Uint8Array) => number;
|
|
21
|
+
};
|
|
22
|
+
/** How many values a variable still has. */
|
|
23
|
+
export declare function slotsLeft(csp: Csp, alive: Uint8Array, variable: number): number;
|
|
24
|
+
/** Every value of every variable still possible. */
|
|
25
|
+
export declare function openSlots(csp: Csp): Uint8Array;
|
|
26
|
+
/** Leaves `variable` with only `slot` (a slot number, not an offset). */
|
|
27
|
+
export declare function decide(csp: Csp, alive: Uint8Array, variable: number, slot: number): void;
|
|
28
|
+
/** The slot a decided variable has left, or -1 when it has more than one or none. */
|
|
29
|
+
export declare function decidedSlot(csp: Csp, alive: Uint8Array, variable: number): number;
|
|
30
|
+
export type CspCount = {
|
|
31
|
+
count: number;
|
|
32
|
+
solution: Uint8Array | null;
|
|
33
|
+
solutions: Uint8Array[];
|
|
34
|
+
stopped: boolean;
|
|
35
|
+
exhausted: boolean;
|
|
36
|
+
nodes: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Counts the ways to decide every variable, up to `limit`. `exhausted` says the node budget ran out;
|
|
40
|
+
* `stopped` says the limit was reached. Either way the count must not be read as proof of anything
|
|
41
|
+
* beyond what was seen.
|
|
42
|
+
*/
|
|
43
|
+
export declare function countCsp(csp: Csp, start: Uint8Array, limit: number, budget: number): CspCount;
|
|
44
|
+
export type CspLogic = {
|
|
45
|
+
solved: boolean;
|
|
46
|
+
contradiction: boolean;
|
|
47
|
+
probes: number;
|
|
48
|
+
alive: Uint8Array;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Solves by reasoning alone, never by guessing a value and keeping it. `depth` 0 is the rules' own
|
|
52
|
+
* pruning. Depth 1 adds probing: suppose one value, prune, and when that breaks a rule the value is
|
|
53
|
+
* ruled out. Depth 2 lets a probe probe. `probes` counts the values ruled out by supposing, which is how
|
|
54
|
+
* much supposing the puzzle asks of a person.
|
|
55
|
+
*/
|
|
56
|
+
export declare function logicCsp(csp: Csp, start: Uint8Array, depth: number): CspLogic;
|
|
57
|
+
/**
|
|
58
|
+
* Counts answers like `countCsp`, but first tries to settle the puzzle by reasoning (rules, then one level of
|
|
59
|
+
* supposing): reasoning that leaves every variable decided proves exactly one answer, and reasoning that breaks a
|
|
60
|
+
* rule proves none, in far fewer steps than a search. Only a puzzle that reasoning leaves open is searched, from the
|
|
61
|
+
* state reasoning reached.
|
|
62
|
+
*/
|
|
63
|
+
export declare function solveCsp(csp: Csp, start: Uint8Array, limit: number, budget: number, depth?: number): CspCount;
|