@johnmorrisdotca/kazu 1.0.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 +23 -0
- package/README.md +429 -5
- 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/draw-entry.d.ts +1 -0
- package/dist/draw-entry.js +1 -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 +12 -0
- package/dist/index.js +12 -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/play-entry.d.ts +1 -0
- package/dist/play-entry.js +1 -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-draw-entry.d.ts +2 -0
- package/dist/shikaku-draw-entry.js +1 -0
- package/dist/shikaku-entry.d.ts +8 -0
- package/dist/shikaku-entry.js +8 -0
- package/dist/shikaku-play-entry.d.ts +4 -0
- package/dist/shikaku-play-entry.js +3 -0
- package/dist/shikaku.constants.d.ts +5 -0
- package/dist/shikaku.constants.js +5 -0
- package/dist/shikaku.types.d.ts +36 -0
- package/dist/shikaku.types.js +1 -0
- package/dist/shikakuBoard.d.ts +9 -0
- package/dist/shikakuBoard.js +59 -0
- package/dist/shikakuDraw.d.ts +4 -0
- package/dist/shikakuDraw.js +23 -0
- package/dist/shikakuGame.d.ts +14 -0
- package/dist/shikakuGame.js +61 -0
- package/dist/shikakuGenerate.d.ts +3 -0
- package/dist/shikakuGenerate.js +43 -0
- package/dist/shikakuMount.d.ts +3 -0
- package/dist/shikakuMount.js +143 -0
- package/dist/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -0
- package/dist/shikakuPlay.types.d.ts +26 -0
- package/dist/shikakuPlay.types.js +1 -0
- package/dist/shikakuSolve.d.ts +6 -0
- package/dist/shikakuSolve.js +54 -0
- package/dist/shikakuStrings.d.ts +102 -0
- package/dist/shikakuStrings.js +5 -0
- package/dist/shikakuStyle.d.ts +1 -0
- package/dist/shikakuStyle.js +12 -0
- package/dist/shikakuWorker.d.ts +1 -0
- package/dist/shikakuWorker.js +10 -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 +196 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { KAZU_STYLE } from "./style.js";
|
|
2
|
+
import { checkRipple, progressRipple, rippleRoomSizes } from "./rippleBoard.js";
|
|
3
|
+
import { decodeRipple, encodeRipple, hintRipple, newRipple, rippleFinished, setRippleValue, toggleRippleNote, undoRipple } from "./rippleGame.js";
|
|
4
|
+
import { RIPPLE_PLAY_STYLE } from "./rippleStyle.js";
|
|
5
|
+
import { rippleWords } from "./rippleStrings.js";
|
|
6
|
+
export function mountRipple(host, initial) {
|
|
7
|
+
const doc = host.ownerDocument;
|
|
8
|
+
let options = { ...initial };
|
|
9
|
+
let game = initial.progress ? decodeRipple(initial.progress) : null;
|
|
10
|
+
game ?? (game = newRipple(initial.board));
|
|
11
|
+
let selected = game.values.findIndex((value, cell) => game.board.clues[cell] === null);
|
|
12
|
+
if (selected < 0)
|
|
13
|
+
selected = 0;
|
|
14
|
+
let pencil = false, errors = [], message = "", dead = false;
|
|
15
|
+
const root = doc.createElement("section");
|
|
16
|
+
root.className = "rp-root";
|
|
17
|
+
const style = doc.createElement("style");
|
|
18
|
+
style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".rp-palette") + RIPPLE_PLAY_STYLE;
|
|
19
|
+
const board = doc.createElement("div");
|
|
20
|
+
board.className = "rp-board";
|
|
21
|
+
board.setAttribute("role", "grid");
|
|
22
|
+
const status = doc.createElement("p");
|
|
23
|
+
status.className = "rp-status";
|
|
24
|
+
status.setAttribute("role", "status");
|
|
25
|
+
const help = doc.createElement("p");
|
|
26
|
+
help.className = "rp-help";
|
|
27
|
+
const controls = doc.createElement("div");
|
|
28
|
+
controls.className = "rp-controls";
|
|
29
|
+
const numpad = doc.createElement("div");
|
|
30
|
+
numpad.className = "rp-numpad";
|
|
31
|
+
numpad.setAttribute("aria-label", "Numbers");
|
|
32
|
+
root.append(style, board, status, help, numpad, controls);
|
|
33
|
+
host.append(root);
|
|
34
|
+
const words = () => rippleWords(options.language);
|
|
35
|
+
const copy = () => ({ ...game, board: { ...game.board, rooms: [...game.board.rooms], clues: [...game.board.clues] }, values: [...game.values], notes: game.notes.map(list => [...list]), history: game.history.map(turn => ({ values: [...turn.values], notes: turn.notes.map(list => [...list]) })) });
|
|
36
|
+
const changed = () => {
|
|
37
|
+
errors = [];
|
|
38
|
+
message = "";
|
|
39
|
+
render();
|
|
40
|
+
const snapshot = copy();
|
|
41
|
+
options.onChange?.(snapshot);
|
|
42
|
+
root.dispatchEvent(new CustomEvent("ripple-change", { detail: snapshot, bubbles: true }));
|
|
43
|
+
if (rippleFinished(game)) {
|
|
44
|
+
options.onFinish?.(snapshot);
|
|
45
|
+
root.dispatchEvent(new CustomEvent("ripple-finish", { detail: snapshot, bubbles: true }));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const enter = (value) => {
|
|
49
|
+
if (game.board.clues[selected] !== null)
|
|
50
|
+
return;
|
|
51
|
+
game = pencil ? toggleRippleNote(game, selected, value) : setRippleValue(game, selected, value);
|
|
52
|
+
changed();
|
|
53
|
+
};
|
|
54
|
+
const button = (label, action, disabled = false) => {
|
|
55
|
+
const item = doc.createElement("button");
|
|
56
|
+
item.type = "button";
|
|
57
|
+
item.className = "rp-tool";
|
|
58
|
+
item.textContent = label;
|
|
59
|
+
item.disabled = disabled;
|
|
60
|
+
item.onclick = action;
|
|
61
|
+
controls.append(item);
|
|
62
|
+
return item;
|
|
63
|
+
};
|
|
64
|
+
const render = () => {
|
|
65
|
+
if (dead)
|
|
66
|
+
return;
|
|
67
|
+
const focused = board.contains(doc.activeElement);
|
|
68
|
+
const w = words(), current = game.board, sizes = rippleRoomSizes(current), validation = progressRipple(current, game.values);
|
|
69
|
+
root.dataset.material = options.material ?? "ivory";
|
|
70
|
+
root.dataset.pieces = options.pieces ?? "ink";
|
|
71
|
+
root.classList.toggle("rp-tiles", options.pieces === "tiles");
|
|
72
|
+
root.style.setProperty("--rp-width", String(current.width));
|
|
73
|
+
board.setAttribute("aria-label", w.title);
|
|
74
|
+
board.replaceChildren();
|
|
75
|
+
for (let cell = 0; cell < current.rooms.length; cell += 1) {
|
|
76
|
+
const x = cell % current.width, y = Math.floor(cell / current.width), room = current.rooms[cell];
|
|
77
|
+
const control = doc.createElement("button");
|
|
78
|
+
control.type = "button";
|
|
79
|
+
control.className = "rp-cell";
|
|
80
|
+
if (x + 1 < current.width && current.rooms[cell + 1] !== room)
|
|
81
|
+
control.classList.add("rp-room-right");
|
|
82
|
+
if (y + 1 < current.height && current.rooms[cell + current.width] !== room)
|
|
83
|
+
control.classList.add("rp-room-bottom");
|
|
84
|
+
const clue = current.clues[cell] !== null, value = game.values[cell];
|
|
85
|
+
control.dataset.cell = String(cell);
|
|
86
|
+
control.dataset.clue = String(clue);
|
|
87
|
+
control.dataset.error = String(errors.includes(cell));
|
|
88
|
+
control.dataset.selected = String(cell === selected);
|
|
89
|
+
control.setAttribute("aria-disabled", String(clue));
|
|
90
|
+
control.tabIndex = cell === selected ? 0 : -1;
|
|
91
|
+
control.setAttribute("role", "gridcell");
|
|
92
|
+
control.setAttribute("aria-label", `${w.cell.replace("{row}", String(y + 1)).replace("{column}", String(x + 1))}, ${clue ? w.clue.replace("{value}", String(current.clues[cell])) : value ? w.entry.replace("{value}", String(value)) : game.notes[cell].length ? w.note.replace("{values}", game.notes[cell].join(", ")) : w.empty}`);
|
|
93
|
+
if (clue)
|
|
94
|
+
control.textContent = String(current.clues[cell]);
|
|
95
|
+
else if (value)
|
|
96
|
+
control.textContent = String(value);
|
|
97
|
+
else if (game.notes[cell].length) {
|
|
98
|
+
const mini = doc.createElement("span");
|
|
99
|
+
mini.className = "rp-notes";
|
|
100
|
+
for (let digit = 1; digit <= sizes[room]; digit += 1) {
|
|
101
|
+
const span = doc.createElement("span");
|
|
102
|
+
span.textContent = game.notes[cell].includes(digit) ? String(digit) : "";
|
|
103
|
+
mini.append(span);
|
|
104
|
+
}
|
|
105
|
+
control.append(mini);
|
|
106
|
+
}
|
|
107
|
+
control.onclick = () => { selected = cell; render(); };
|
|
108
|
+
control.onkeydown = event => {
|
|
109
|
+
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
move(cell, event.key);
|
|
112
|
+
}
|
|
113
|
+
else if (/^[1-9]$/.test(event.key)) {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
enter(Number(event.key));
|
|
116
|
+
}
|
|
117
|
+
else if (event.key === "Backspace" || event.key === "Delete") {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
enter(0);
|
|
120
|
+
}
|
|
121
|
+
else if (event.key.toLowerCase() === "p") {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
pencil = !pencil;
|
|
124
|
+
render();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
board.append(control);
|
|
128
|
+
}
|
|
129
|
+
if (focused)
|
|
130
|
+
board.querySelector(`[data-cell="${selected}"]`)?.focus({ preventScroll: true });
|
|
131
|
+
numpad.replaceChildren();
|
|
132
|
+
for (let digit = 1; digit <= Math.max(...sizes); digit += 1) {
|
|
133
|
+
const key = doc.createElement("button");
|
|
134
|
+
key.type = "button";
|
|
135
|
+
key.className = "rp-key";
|
|
136
|
+
key.textContent = String(digit);
|
|
137
|
+
key.disabled = digit > sizes[current.rooms[selected]];
|
|
138
|
+
key.onclick = () => enter(digit);
|
|
139
|
+
numpad.append(key);
|
|
140
|
+
}
|
|
141
|
+
status.textContent = rippleFinished(game) ? (game.helped ? `${w.won} · ${w.hint}` : w.won) : message || (validation.ok ? w.won : w.checked);
|
|
142
|
+
help.textContent = w.help;
|
|
143
|
+
controls.replaceChildren();
|
|
144
|
+
button(w.undo, () => { game = undoRipple(game); changed(); }, !game.history.length);
|
|
145
|
+
button(w.check, () => { errors = [...checkRipple(current, game.values).errors]; message = checkRipple(current, game.values).ok ? w.won : w.wrong; render(); });
|
|
146
|
+
button(w.hint, () => { const hint = hintRipple(game); if (!hint) {
|
|
147
|
+
message = w.noHint;
|
|
148
|
+
render();
|
|
149
|
+
return;
|
|
150
|
+
} game = { ...setRippleValue(game, hint.cell, hint.value), helped: true }; selected = hint.cell; changed(); message = w.hintWhy; render(); });
|
|
151
|
+
button(pencil ? `${w.pencil} ✓` : w.pencil, () => { pencil = !pencil; render(); });
|
|
152
|
+
button(w.restart, restart);
|
|
153
|
+
button(w.view, expand);
|
|
154
|
+
};
|
|
155
|
+
function move(cell, direction) {
|
|
156
|
+
const dx = direction === "ArrowLeft" ? -1 : direction === "ArrowRight" ? 1 : 0;
|
|
157
|
+
const dy = direction === "ArrowUp" ? -1 : direction === "ArrowDown" ? 1 : 0;
|
|
158
|
+
const x = cell % game.board.width, y = Math.floor(cell / game.board.width);
|
|
159
|
+
const nx = Math.min(game.board.width - 1, Math.max(0, x + dx)), ny = Math.min(game.board.height - 1, Math.max(0, y + dy));
|
|
160
|
+
selected = ny * game.board.width + nx;
|
|
161
|
+
render();
|
|
162
|
+
board.querySelector(`[data-cell="${selected}"]`)?.focus();
|
|
163
|
+
}
|
|
164
|
+
function restart() { game = newRipple(game.board); selected = game.values.findIndex((value, cell) => game.board.clues[cell] === null); changed(); }
|
|
165
|
+
function expand() {
|
|
166
|
+
const dialog = doc.createElement("dialog"), close = doc.createElement("button");
|
|
167
|
+
dialog.className = "rp-dialog";
|
|
168
|
+
close.type = "button";
|
|
169
|
+
close.className = "rp-tool rp-close";
|
|
170
|
+
close.textContent = words().close;
|
|
171
|
+
close.onclick = () => dialog.close();
|
|
172
|
+
dialog.append(close, root);
|
|
173
|
+
host.append(dialog);
|
|
174
|
+
dialog.onclose = () => { host.append(root); dialog.remove(); };
|
|
175
|
+
dialog.showModal();
|
|
176
|
+
}
|
|
177
|
+
render();
|
|
178
|
+
return { game: copy, progress: () => encodeRipple(game), restart, set: (next) => { options = { ...options, ...next }; render(); }, destroy: () => { dead = true; root.remove(); root.closest("dialog")?.remove(); } };
|
|
179
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RippleBoard, RippleGame, RippleLanguage, RippleMaterial, RipplePieces } from "./ripple.types.ts";
|
|
2
|
+
export type RippleDrawOptions = {
|
|
3
|
+
values?: readonly number[];
|
|
4
|
+
notes?: readonly (readonly number[])[];
|
|
5
|
+
errors?: readonly number[];
|
|
6
|
+
selected?: number | null;
|
|
7
|
+
material?: RippleMaterial;
|
|
8
|
+
pieces?: RipplePieces;
|
|
9
|
+
language?: RippleLanguage;
|
|
10
|
+
};
|
|
11
|
+
export type RippleMountOptions = RippleDrawOptions & {
|
|
12
|
+
board: RippleBoard;
|
|
13
|
+
progress?: string;
|
|
14
|
+
onChange?: (game: RippleGame) => void;
|
|
15
|
+
onFinish?: (game: RippleGame) => void;
|
|
16
|
+
};
|
|
17
|
+
export type RippleMount = {
|
|
18
|
+
game: () => RippleGame;
|
|
19
|
+
progress: () => string;
|
|
20
|
+
set: (options: RippleDrawOptions) => void;
|
|
21
|
+
restart: () => void;
|
|
22
|
+
destroy: () => void;
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RIPPLE_MAX_NODES } from "./ripple.constants.js";
|
|
2
|
+
import { isRippleBoard, rippleRoomSizes } from "./rippleBoard.js";
|
|
3
|
+
/** Counts rule-valid fillings within an explicit node budget. */
|
|
4
|
+
export function solveRipple(board, options = {}) {
|
|
5
|
+
if (!isRippleBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Ripple Effect board");
|
|
7
|
+
const limit = options.limit ?? 2, budget = options.nodes ?? RIPPLE_MAX_NODES;
|
|
8
|
+
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1)
|
|
9
|
+
throw new RangeError("Invalid search bounds");
|
|
10
|
+
const sizes = rippleRoomSizes(board);
|
|
11
|
+
const values = board.clues.map(clue => clue ?? 0);
|
|
12
|
+
const roomUsed = sizes.map(() => new Set());
|
|
13
|
+
let count = 0, nodes = 0, complete = true, solution = null;
|
|
14
|
+
for (let cell = 0; cell < values.length; cell += 1) {
|
|
15
|
+
const value = values[cell];
|
|
16
|
+
if (!value)
|
|
17
|
+
continue;
|
|
18
|
+
const room = board.rooms[cell];
|
|
19
|
+
if (roomUsed[room].has(value) || conflicts(cell, value, values, board)) {
|
|
20
|
+
return { count: 0, solution: null, complete: true, nodes: 0 };
|
|
21
|
+
}
|
|
22
|
+
roomUsed[room].add(value);
|
|
23
|
+
}
|
|
24
|
+
const visit = () => {
|
|
25
|
+
if (count >= limit || !complete) {
|
|
26
|
+
complete = false;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (++nodes > budget) {
|
|
30
|
+
complete = false;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let bestCell = -1, bestValues = [];
|
|
34
|
+
for (let cell = 0; cell < values.length; cell += 1) {
|
|
35
|
+
if (values[cell])
|
|
36
|
+
continue;
|
|
37
|
+
const room = board.rooms[cell];
|
|
38
|
+
const candidates = [];
|
|
39
|
+
for (let value = 1; value <= sizes[room]; value += 1) {
|
|
40
|
+
if (!roomUsed[room].has(value) && !conflicts(cell, value, values, board))
|
|
41
|
+
candidates.push(value);
|
|
42
|
+
}
|
|
43
|
+
if (!candidates.length)
|
|
44
|
+
return;
|
|
45
|
+
if (bestCell < 0 || candidates.length < bestValues.length) {
|
|
46
|
+
bestCell = cell;
|
|
47
|
+
bestValues = candidates;
|
|
48
|
+
if (candidates.length === 1)
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (bestCell < 0) {
|
|
53
|
+
count += 1;
|
|
54
|
+
solution ?? (solution = [...values]);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const room = board.rooms[bestCell];
|
|
58
|
+
for (const value of bestValues) {
|
|
59
|
+
values[bestCell] = value;
|
|
60
|
+
roomUsed[room].add(value);
|
|
61
|
+
visit();
|
|
62
|
+
roomUsed[room].delete(value);
|
|
63
|
+
values[bestCell] = 0;
|
|
64
|
+
if (!complete || count >= limit) {
|
|
65
|
+
complete = false;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
visit();
|
|
71
|
+
return { count, solution, complete, nodes };
|
|
72
|
+
}
|
|
73
|
+
function conflicts(cell, value, values, board) {
|
|
74
|
+
const x = cell % board.width, y = Math.floor(cell / board.width);
|
|
75
|
+
for (let other = 0; other < values.length; other += 1) {
|
|
76
|
+
if (values[other] !== value || other === cell)
|
|
77
|
+
continue;
|
|
78
|
+
const ox = other % board.width, oy = Math.floor(other / board.width);
|
|
79
|
+
if ((oy === y && Math.abs(ox - x) <= value) || (ox === x && Math.abs(oy - y) <= value))
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { RippleLanguage } from "./ripple.types.ts";
|
|
2
|
+
export declare const RIPPLE_STRINGS: {
|
|
3
|
+
readonly en: {
|
|
4
|
+
readonly title: "Ripple Effect";
|
|
5
|
+
readonly help: "Fill each room with 1 through its size. Equal numbers in a row or column need at least that many cells between them.";
|
|
6
|
+
readonly undo: "Undo";
|
|
7
|
+
readonly check: "Check";
|
|
8
|
+
readonly hint: "Hint";
|
|
9
|
+
readonly restart: "Restart";
|
|
10
|
+
readonly pencil: "Pencil";
|
|
11
|
+
readonly won: "Every room and ripple rule is correct.";
|
|
12
|
+
readonly wrong: "Some entries break a room or ripple rule.";
|
|
13
|
+
readonly checked: "Fill the rooms and check the spacing.";
|
|
14
|
+
readonly noHint: "No single proved answer is available yet.";
|
|
15
|
+
readonly hintWhy: "This value follows from the room and spacing rules.";
|
|
16
|
+
readonly view: "Just the board";
|
|
17
|
+
readonly close: "Close";
|
|
18
|
+
readonly cell: "Row {row}, column {column}";
|
|
19
|
+
readonly clue: "clue {value}";
|
|
20
|
+
readonly empty: "empty";
|
|
21
|
+
readonly entry: "{value}";
|
|
22
|
+
readonly note: "notes {values}";
|
|
23
|
+
};
|
|
24
|
+
readonly ja: {
|
|
25
|
+
readonly title: "波及効果";
|
|
26
|
+
readonly help: "部屋には1から部屋の大きさまでの数字を一つずつ入れます。同じ数字を同じ行・列に置くときは、その数字以上のマスを間に空けます。";
|
|
27
|
+
readonly undo: "戻す";
|
|
28
|
+
readonly check: "確認";
|
|
29
|
+
readonly hint: "ヒント";
|
|
30
|
+
readonly restart: "やり直す";
|
|
31
|
+
readonly pencil: "メモ";
|
|
32
|
+
readonly won: "部屋と間隔のルールをすべて満たしました。";
|
|
33
|
+
readonly wrong: "部屋か数字の間隔に誤りがあります。";
|
|
34
|
+
readonly checked: "部屋を埋めて間隔を確認してください。";
|
|
35
|
+
readonly noHint: "確定できるヒントはまだありません。";
|
|
36
|
+
readonly hintWhy: "部屋と間隔のルールから確定する数字です。";
|
|
37
|
+
readonly view: "盤だけ";
|
|
38
|
+
readonly close: "閉じる";
|
|
39
|
+
readonly cell: "{row}行{column}列";
|
|
40
|
+
readonly clue: "手がかり {value}";
|
|
41
|
+
readonly empty: "空き";
|
|
42
|
+
readonly entry: "{value}";
|
|
43
|
+
readonly note: "メモ {values}";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export declare function rippleWords(language?: RippleLanguage): {
|
|
47
|
+
readonly title: "Ripple Effect";
|
|
48
|
+
readonly help: "Fill each room with 1 through its size. Equal numbers in a row or column need at least that many cells between them.";
|
|
49
|
+
readonly undo: "Undo";
|
|
50
|
+
readonly check: "Check";
|
|
51
|
+
readonly hint: "Hint";
|
|
52
|
+
readonly restart: "Restart";
|
|
53
|
+
readonly pencil: "Pencil";
|
|
54
|
+
readonly won: "Every room and ripple rule is correct.";
|
|
55
|
+
readonly wrong: "Some entries break a room or ripple rule.";
|
|
56
|
+
readonly checked: "Fill the rooms and check the spacing.";
|
|
57
|
+
readonly noHint: "No single proved answer is available yet.";
|
|
58
|
+
readonly hintWhy: "This value follows from the room and spacing rules.";
|
|
59
|
+
readonly view: "Just the board";
|
|
60
|
+
readonly close: "Close";
|
|
61
|
+
readonly cell: "Row {row}, column {column}";
|
|
62
|
+
readonly clue: "clue {value}";
|
|
63
|
+
readonly empty: "empty";
|
|
64
|
+
readonly entry: "{value}";
|
|
65
|
+
readonly note: "notes {values}";
|
|
66
|
+
} | {
|
|
67
|
+
readonly title: "波及効果";
|
|
68
|
+
readonly help: "部屋には1から部屋の大きさまでの数字を一つずつ入れます。同じ数字を同じ行・列に置くときは、その数字以上のマスを間に空けます。";
|
|
69
|
+
readonly undo: "戻す";
|
|
70
|
+
readonly check: "確認";
|
|
71
|
+
readonly hint: "ヒント";
|
|
72
|
+
readonly restart: "やり直す";
|
|
73
|
+
readonly pencil: "メモ";
|
|
74
|
+
readonly won: "部屋と間隔のルールをすべて満たしました。";
|
|
75
|
+
readonly wrong: "部屋か数字の間隔に誤りがあります。";
|
|
76
|
+
readonly checked: "部屋を埋めて間隔を確認してください。";
|
|
77
|
+
readonly noHint: "確定できるヒントはまだありません。";
|
|
78
|
+
readonly hintWhy: "部屋と間隔のルールから確定する数字です。";
|
|
79
|
+
readonly view: "盤だけ";
|
|
80
|
+
readonly close: "閉じる";
|
|
81
|
+
readonly cell: "{row}行{column}列";
|
|
82
|
+
readonly clue: "手がかり {value}";
|
|
83
|
+
readonly empty: "空き";
|
|
84
|
+
readonly entry: "{value}";
|
|
85
|
+
readonly note: "メモ {values}";
|
|
86
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const RIPPLE_STRINGS = {
|
|
2
|
+
en: { title: "Ripple Effect", help: "Fill each room with 1 through its size. Equal numbers in a row or column need at least that many cells between them.", undo: "Undo", check: "Check", hint: "Hint", restart: "Restart", pencil: "Pencil", won: "Every room and ripple rule is correct.", wrong: "Some entries break a room or ripple rule.", checked: "Fill the rooms and check the spacing.", noHint: "No single proved answer is available yet.", hintWhy: "This value follows from the room and spacing rules.", view: "Just the board", close: "Close", cell: "Row {row}, column {column}", clue: "clue {value}", empty: "empty", entry: "{value}", note: "notes {values}" },
|
|
3
|
+
ja: { title: "波及効果", help: "部屋には1から部屋の大きさまでの数字を一つずつ入れます。同じ数字を同じ行・列に置くときは、その数字以上のマスを間に空けます。", undo: "戻す", check: "確認", hint: "ヒント", restart: "やり直す", pencil: "メモ", won: "部屋と間隔のルールをすべて満たしました。", wrong: "部屋か数字の間隔に誤りがあります。", checked: "部屋を埋めて間隔を確認してください。", noHint: "確定できるヒントはまだありません。", hintWhy: "部屋と間隔のルールから確定する数字です。", view: "盤だけ", close: "閉じる", cell: "{row}行{column}列", clue: "手がかり {value}", empty: "空き", entry: "{value}", note: "メモ {values}" },
|
|
4
|
+
};
|
|
5
|
+
export function rippleWords(language = "en") { return RIPPLE_STRINGS[language]; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const RIPPLE_PLAY_STYLE = ".rp-root{color:var(--kz-ink,#242621);font:inherit}.rp-board{display:grid;grid-template-columns:repeat(var(--rp-width),minmax(36px,1fr));max-width:620px;margin:auto;touch-action:manipulation}.rp-cell{position:relative;aspect-ratio:1;border:1px solid var(--rp-line);background:var(--rp-surface);color:var(--rp-ink);font:500 clamp(16px,4vw,25px) system-ui;cursor:pointer;padding:0}.rp-cell[data-clue=true]{font-weight:700}.rp-cell[data-error=true]{box-shadow:inset 0 0 0 3px #bd493f}.rp-cell[data-selected=true]{background:#dce9c9;color:#1f3021}.rp-cell:focus-visible,.rp-key:focus-visible,.rp-tool:focus-visible{outline:3px solid #466f41;outline-offset:2px;z-index:1}.rp-room-right{border-right:3px solid var(--rp-ink)}.rp-room-bottom{border-bottom:3px solid var(--rp-ink)}.rp-notes{position:absolute;inset:2px;display:grid;grid-template-columns:repeat(3,1fr);font-size:10px;line-height:1;align-items:center}.rp-controls,.rp-numpad{display:flex;gap:7px;justify-content:center;flex-wrap:wrap;margin:12px auto}.rp-tool,.rp-key{font:inherit;min-height:44px;min-width:44px;border:1px solid var(--rp-line);border-radius:10px;background:var(--rp-surface);color:var(--rp-ink);cursor:pointer}.rp-tool:disabled{opacity:.5}.rp-status{min-height:1.5em;text-align:center}.rp-help{text-align:center;font-size:.9em}.rp-dialog{border:0;border-radius:16px;padding:18px;max-width:min(94vw,720px);background:#f7f3e9}.rp-close{margin-bottom:10px}.rp-tiles .rp-cell[data-clue=true]{box-shadow:inset 0 0 0 4px #c2a875}.rp-palette{display:grid;gap:8px}.rp-root[data-material=wood]{--rp-surface:#e0bf88;--rp-line:#907044;--rp-ink:#392d1f}.rp-root[data-material=slate]{--rp-surface:#364348;--rp-line:#839397;--rp-ink:#f1eee5}.rp-root[data-material=ivory]{--rp-surface:#faf7ef;--rp-line:#cfc6b2;--rp-ink:#252824}";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RIPPLE_PLAY_STYLE = `.rp-root{color:var(--kz-ink,#242621);font:inherit}.rp-board{display:grid;grid-template-columns:repeat(var(--rp-width),minmax(36px,1fr));max-width:620px;margin:auto;touch-action:manipulation}.rp-cell{position:relative;aspect-ratio:1;border:1px solid var(--rp-line);background:var(--rp-surface);color:var(--rp-ink);font:500 clamp(16px,4vw,25px) system-ui;cursor:pointer;padding:0}.rp-cell[data-clue=true]{font-weight:700}.rp-cell[data-error=true]{box-shadow:inset 0 0 0 3px #bd493f}.rp-cell[data-selected=true]{background:#dce9c9;color:#1f3021}.rp-cell:focus-visible,.rp-key:focus-visible,.rp-tool:focus-visible{outline:3px solid #466f41;outline-offset:2px;z-index:1}.rp-room-right{border-right:3px solid var(--rp-ink)}.rp-room-bottom{border-bottom:3px solid var(--rp-ink)}.rp-notes{position:absolute;inset:2px;display:grid;grid-template-columns:repeat(3,1fr);font-size:10px;line-height:1;align-items:center}.rp-controls,.rp-numpad{display:flex;gap:7px;justify-content:center;flex-wrap:wrap;margin:12px auto}.rp-tool,.rp-key{font:inherit;min-height:44px;min-width:44px;border:1px solid var(--rp-line);border-radius:10px;background:var(--rp-surface);color:var(--rp-ink);cursor:pointer}.rp-tool:disabled{opacity:.5}.rp-status{min-height:1.5em;text-align:center}.rp-help{text-align:center;font-size:.9em}.rp-dialog{border:0;border-radius:16px;padding:18px;max-width:min(94vw,720px);background:#f7f3e9}.rp-close{margin-bottom:10px}.rp-tiles .rp-cell[data-clue=true]{box-shadow:inset 0 0 0 4px #c2a875}.rp-palette{display:grid;gap:8px}.rp-root[data-material=wood]{--rp-surface:#e0bf88;--rp-line:#907044;--rp-ink:#392d1f}.rp-root[data-material=slate]{--rp-surface:#364348;--rp-line:#839397;--rp-ink:#f1eee5}.rp-root[data-material=ivory]{--rp-surface:#faf7ef;--rp-line:#cfc6b2;--rp-ink:#252824}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawShikaku } from "./shikakuDraw.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Shikaku shares Kazu's number puzzle package, with its own rectangle model. */
|
|
2
|
+
export * from "./shikaku.types.ts";
|
|
3
|
+
export * from "./shikaku.constants.ts";
|
|
4
|
+
export * from "./shikakuBoard.ts";
|
|
5
|
+
export * from "./shikakuSolve.ts";
|
|
6
|
+
export * from "./shikakuGenerate.ts";
|
|
7
|
+
export * from "./shikakuGame.ts";
|
|
8
|
+
export * from "./shikakuPacks.ts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Shikaku shares Kazu's number puzzle package, with its own rectangle model. */
|
|
2
|
+
export * from "./shikaku.types.js";
|
|
3
|
+
export * from "./shikaku.constants.js";
|
|
4
|
+
export * from "./shikakuBoard.js";
|
|
5
|
+
export * from "./shikakuSolve.js";
|
|
6
|
+
export * from "./shikakuGenerate.js";
|
|
7
|
+
export * from "./shikakuGame.js";
|
|
8
|
+
export * from "./shikakuPacks.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Bounds keep custom boards and searches finite. */
|
|
2
|
+
export declare const SHIKAKU_MOST_SIDE = 16;
|
|
3
|
+
export declare const SHIKAKU_MOST_NODES = 100000;
|
|
4
|
+
export declare const SHIKAKU_MOST_ATTEMPTS = 200;
|
|
5
|
+
export declare const SHIKAKU_LEVELS: readonly ["easy", "medium", "hard"];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** A rectangle uses zero-based column and row coordinates. */
|
|
2
|
+
export type ShikakuRectangle = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
/** One area clue per rectangle; zero is an unnumbered cell. */
|
|
9
|
+
export type ShikakuBoard = {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
clues: readonly number[];
|
|
13
|
+
};
|
|
14
|
+
export type ShikakuLevel = "easy" | "medium" | "hard";
|
|
15
|
+
export type ShikakuPuzzle = ShikakuBoard & {
|
|
16
|
+
seed: number;
|
|
17
|
+
level: ShikakuLevel;
|
|
18
|
+
solution: readonly ShikakuRectangle[];
|
|
19
|
+
};
|
|
20
|
+
export type ShikakuCheck = {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
covered: number;
|
|
23
|
+
errors: readonly number[];
|
|
24
|
+
};
|
|
25
|
+
export type ShikakuSolve = {
|
|
26
|
+
count: number;
|
|
27
|
+
solution: readonly ShikakuRectangle[] | null;
|
|
28
|
+
complete: boolean;
|
|
29
|
+
nodes: number;
|
|
30
|
+
};
|
|
31
|
+
export type ShikakuGame = {
|
|
32
|
+
board: ShikakuBoard;
|
|
33
|
+
rectangles: readonly ShikakuRectangle[];
|
|
34
|
+
history: readonly (readonly ShikakuRectangle[])[];
|
|
35
|
+
helped: boolean;
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ShikakuBoard, ShikakuCheck, ShikakuRectangle } from "./shikaku.types.ts";
|
|
2
|
+
/** Whether dimensions and area clues describe a possible partition. */
|
|
3
|
+
export declare function isShikakuBoard(value: unknown): value is ShikakuBoard;
|
|
4
|
+
/** Cells of a bounded rectangle, or null for invalid coordinates. */
|
|
5
|
+
export declare function shikakuCells(board: ShikakuBoard, r: ShikakuRectangle): number[] | null;
|
|
6
|
+
/** A finished partition is checked against the rules, never against a stored answer. */
|
|
7
|
+
export declare function checkShikaku(board: ShikakuBoard, rectangles: readonly ShikakuRectangle[]): ShikakuCheck;
|
|
8
|
+
/** All legal rectangles for a numbered cell, in a stable order. */
|
|
9
|
+
export declare function shikakuCandidates(board: ShikakuBoard, cell: number): ShikakuRectangle[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SHIKAKU_MOST_SIDE } from "./shikaku.constants.js";
|
|
2
|
+
/** Whether dimensions and area clues describe a possible partition. */
|
|
3
|
+
export function isShikakuBoard(value) {
|
|
4
|
+
if (!value || typeof value !== "object")
|
|
5
|
+
return false;
|
|
6
|
+
const b = value;
|
|
7
|
+
return Number.isInteger(b.width) && b.width >= 2 && b.width <= SHIKAKU_MOST_SIDE
|
|
8
|
+
&& Number.isInteger(b.height) && b.height >= 2 && b.height <= SHIKAKU_MOST_SIDE
|
|
9
|
+
&& Array.isArray(b.clues) && b.clues.length === b.width * b.height
|
|
10
|
+
&& b.clues.every(n => Number.isInteger(n) && n >= 0 && n <= b.clues.length)
|
|
11
|
+
&& b.clues.reduce((a, n) => a + n, 0) === b.clues.length;
|
|
12
|
+
}
|
|
13
|
+
/** Cells of a bounded rectangle, or null for invalid coordinates. */
|
|
14
|
+
export function shikakuCells(board, r) {
|
|
15
|
+
if (!r || ![r.x, r.y, r.width, r.height].every(Number.isInteger)
|
|
16
|
+
|| r.x < 0 || r.y < 0 || r.width < 1 || r.height < 1
|
|
17
|
+
|| r.x + r.width > board.width || r.y + r.height > board.height)
|
|
18
|
+
return null;
|
|
19
|
+
return Array.from({ length: r.width * r.height }, (_, i) => (r.y + Math.floor(i / r.width)) * board.width + r.x + i % r.width);
|
|
20
|
+
}
|
|
21
|
+
/** A finished partition is checked against the rules, never against a stored answer. */
|
|
22
|
+
export function checkShikaku(board, rectangles) {
|
|
23
|
+
if (!isShikakuBoard(board))
|
|
24
|
+
throw new RangeError("Invalid Shikaku board");
|
|
25
|
+
const occupied = new Set(), errors = [];
|
|
26
|
+
rectangles.forEach((r, index) => {
|
|
27
|
+
const cells = shikakuCells(board, r);
|
|
28
|
+
const clues = cells?.filter(c => board.clues[c] > 0) ?? [];
|
|
29
|
+
if (!cells || clues.length !== 1 || board.clues[clues[0]] !== cells.length
|
|
30
|
+
|| cells.some(c => occupied.has(c)))
|
|
31
|
+
errors.push(index);
|
|
32
|
+
cells?.forEach(c => occupied.add(c));
|
|
33
|
+
});
|
|
34
|
+
return { ok: errors.length === 0 && occupied.size === board.clues.length, covered: occupied.size, errors };
|
|
35
|
+
}
|
|
36
|
+
/** All legal rectangles for a numbered cell, in a stable order. */
|
|
37
|
+
export function shikakuCandidates(board, cell) {
|
|
38
|
+
if (!isShikakuBoard(board))
|
|
39
|
+
throw new RangeError("Invalid Shikaku board");
|
|
40
|
+
const area = board.clues[cell], out = [];
|
|
41
|
+
if (!area)
|
|
42
|
+
return out;
|
|
43
|
+
const cx = cell % board.width, cy = Math.floor(cell / board.width);
|
|
44
|
+
for (let w = 1; w <= board.width; w += 1) {
|
|
45
|
+
if (area % w)
|
|
46
|
+
continue;
|
|
47
|
+
const h = area / w;
|
|
48
|
+
if (h > board.height)
|
|
49
|
+
continue;
|
|
50
|
+
for (let y = Math.max(0, cy - h + 1); y <= Math.min(cy, board.height - h); y += 1) {
|
|
51
|
+
for (let x = Math.max(0, cx - w + 1); x <= Math.min(cx, board.width - w); x += 1) {
|
|
52
|
+
const r = { x, y, width: w, height: h };
|
|
53
|
+
if (shikakuCells(board, r).every(c => c === cell || board.clues[c] === 0))
|
|
54
|
+
out.push(r);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ShikakuBoard } from "./shikaku.types.ts";
|
|
2
|
+
import type { ShikakuDrawOptions } from "./shikakuPlay.types.ts";
|
|
3
|
+
/** SVG text with only the public clues and the player's rectangles. */
|
|
4
|
+
export declare function drawShikaku(board: ShikakuBoard, options?: ShikakuDrawOptions): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isShikakuBoard, shikakuCells } from "./shikakuBoard.js";
|
|
2
|
+
import { shikakuWords } from "./shikakuStrings.js";
|
|
3
|
+
/** SVG text with only the public clues and the player's rectangles. */
|
|
4
|
+
export function drawShikaku(board, options = {}) {
|
|
5
|
+
if (!isShikakuBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Shikaku board");
|
|
7
|
+
const words = shikakuWords(options.language), size = 48;
|
|
8
|
+
const palettes = { ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"], wood: ["#e5cda6", "#493e2f", "#bca582"], slate: ["#262a27", "#ece8dc", "#454a44"] };
|
|
9
|
+
const [paper, ink, grid] = palettes[options.material ?? "ivory"];
|
|
10
|
+
const marks = [];
|
|
11
|
+
for (const [index, r] of (options.rectangles ?? []).entries()) {
|
|
12
|
+
if (!shikakuCells(board, r))
|
|
13
|
+
continue;
|
|
14
|
+
const colour = options.errors?.includes(index) ? "var(--kz-bad,#b5452c)" : ink;
|
|
15
|
+
marks.push(`<rect x="${r.x * size + 3}" y="${r.y * size + 3}" width="${r.width * size - 6}" height="${r.height * size - 6}" rx="3" fill="${colour}" fill-opacity=".10" stroke="${colour}" stroke-width="2.5"/>`);
|
|
16
|
+
}
|
|
17
|
+
const cells = board.clues.map((n, c) => {
|
|
18
|
+
const x = c % board.width * size, y = Math.floor(c / board.width) * size;
|
|
19
|
+
const active = c === options.selected || c === options.anchor;
|
|
20
|
+
return `<g data-shikaku-cell="${c}"><rect x="${x}" y="${y}" width="48" height="48" fill="${active ? ink : "transparent"}" fill-opacity="${active ? ".15" : "1"}" stroke="${grid}" stroke-width="1"/>${n ? `${options.pieces === "tiles" ? `<rect x="${x + 10}" y="${y + 10}" width="28" height="28" rx="6" fill="${paper}" stroke="${grid}"/>` : ""}<text x="${x + 24}" y="${y + 25}" dominant-baseline="middle" text-anchor="middle" fill="${ink}" font-size="21" font-weight="700" font-family="system-ui,sans-serif">${n}</text>` : ""}</g>`;
|
|
21
|
+
}).join("");
|
|
22
|
+
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}${marks.join("")}<rect width="${board.width * size}" height="${board.height * size}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="2"/></svg>`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShikakuBoard, ShikakuGame, ShikakuRectangle } from "./shikaku.types.ts";
|
|
2
|
+
/** Begin with a copy of just the public clues; a generated answer is never kept in play. */
|
|
3
|
+
export declare function newShikaku(board: ShikakuBoard): ShikakuGame;
|
|
4
|
+
/** Place a rectangle, replacing any it touches. Local rule errors are allowed for the player to check. */
|
|
5
|
+
export declare function placeShikaku(game: ShikakuGame, rectangle: ShikakuRectangle): ShikakuGame;
|
|
6
|
+
/** Remove the rectangle containing a cell. */
|
|
7
|
+
export declare function removeShikaku(game: ShikakuGame, cell: number): ShikakuGame;
|
|
8
|
+
export declare function undoShikaku(game: ShikakuGame): ShikakuGame;
|
|
9
|
+
/** A rectangle is offered only when the remaining partition has one proved answer. */
|
|
10
|
+
export declare function hintShikaku(game: ShikakuGame): ShikakuRectangle | null;
|
|
11
|
+
export declare function shikakuFinished(game: ShikakuGame): boolean;
|
|
12
|
+
/** Versioned public puzzle and placement data, with no hidden answer. */
|
|
13
|
+
export declare function encodeShikaku(game: ShikakuGame): string;
|
|
14
|
+
export declare function decodeShikaku(code: string): ShikakuGame | null;
|