@johnmorrisdotca/kazu 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +379 -7
- package/dist/akari-draw-entry.d.ts +2 -0
- package/dist/akari-draw-entry.js +1 -0
- package/dist/akari-entry.d.ts +12 -0
- package/dist/akari-entry.js +10 -0
- package/dist/akari-play-entry.d.ts +2 -0
- package/dist/akari-play-entry.js +1 -0
- package/dist/akari.constants.d.ts +2 -0
- package/dist/akari.constants.js +2 -0
- package/dist/akari.types.d.ts +33 -0
- package/dist/akari.types.js +1 -0
- package/dist/akariBoard.d.ts +11 -0
- package/dist/akariBoard.js +96 -0
- package/dist/akariDraw.d.ts +4 -0
- package/dist/akariDraw.js +38 -0
- package/dist/akariGame.d.ts +15 -0
- package/dist/akariGame.js +71 -0
- package/dist/akariGenerate.d.ts +7 -0
- package/dist/akariGenerate.js +78 -0
- package/dist/akariMount.d.ts +3 -0
- package/dist/akariMount.js +214 -0
- package/dist/akariPlay.types.d.ts +25 -0
- package/dist/akariPlay.types.js +1 -0
- package/dist/akariSolve.d.ts +6 -0
- package/dist/akariSolve.js +106 -0
- package/dist/akariStrings.d.ts +98 -0
- package/dist/akariStrings.js +53 -0
- package/dist/akariStyle.d.ts +1 -0
- package/dist/akariStyle.js +9 -0
- package/dist/fillomino-draw-entry.d.ts +2 -0
- package/dist/fillomino-draw-entry.js +1 -0
- package/dist/fillomino-entry.d.ts +8 -0
- package/dist/fillomino-entry.js +7 -0
- package/dist/fillomino-play-entry.d.ts +4 -0
- package/dist/fillomino-play-entry.js +3 -0
- package/dist/fillomino.constants.d.ts +5 -0
- package/dist/fillomino.constants.js +5 -0
- package/dist/fillomino.types.d.ts +30 -0
- package/dist/fillomino.types.js +1 -0
- package/dist/fillominoBoard.d.ts +6 -0
- package/dist/fillominoBoard.js +84 -0
- package/dist/fillominoDraw.d.ts +3 -0
- package/dist/fillominoDraw.js +31 -0
- package/dist/fillominoGame.d.ts +13 -0
- package/dist/fillominoGame.js +65 -0
- package/dist/fillominoGenerate.d.ts +3 -0
- package/dist/fillominoGenerate.js +125 -0
- package/dist/fillominoMount.d.ts +2 -0
- package/dist/fillominoMount.js +211 -0
- package/dist/fillominoPlay.types.d.ts +25 -0
- package/dist/fillominoPlay.types.js +1 -0
- package/dist/fillominoSolve.d.ts +6 -0
- package/dist/fillominoSolve.js +111 -0
- package/dist/fillominoStrings.d.ts +102 -0
- package/dist/fillominoStrings.js +13 -0
- package/dist/fillominoStyle.d.ts +1 -0
- package/dist/fillominoStyle.js +13 -0
- package/dist/fillominoWorker.d.ts +1 -0
- package/dist/fillominoWorker.js +10 -0
- package/dist/heyawake-draw-entry.d.ts +2 -0
- package/dist/heyawake-draw-entry.js +1 -0
- package/dist/heyawake-entry.d.ts +13 -0
- package/dist/heyawake-entry.js +11 -0
- package/dist/heyawake-play-entry.d.ts +2 -0
- package/dist/heyawake-play-entry.js +1 -0
- package/dist/heyawake.constants.d.ts +5 -0
- package/dist/heyawake.constants.js +5 -0
- package/dist/heyawake.types.d.ts +35 -0
- package/dist/heyawake.types.js +1 -0
- package/dist/heyawakeBoard.d.ts +7 -0
- package/dist/heyawakeBoard.js +116 -0
- package/dist/heyawakeDraw.d.ts +3 -0
- package/dist/heyawakeDraw.js +27 -0
- package/dist/heyawakeGame.d.ts +12 -0
- package/dist/heyawakeGame.js +51 -0
- package/dist/heyawakeGenerate.d.ts +2 -0
- package/dist/heyawakeGenerate.js +114 -0
- package/dist/heyawakeMount.d.ts +2 -0
- package/dist/heyawakeMount.js +106 -0
- package/dist/heyawakePlay.types.d.ts +24 -0
- package/dist/heyawakePlay.types.js +1 -0
- package/dist/heyawakeSolve.d.ts +5 -0
- package/dist/heyawakeSolve.js +52 -0
- package/dist/heyawakeStrings.d.ts +19 -0
- package/dist/heyawakeStrings.js +7 -0
- package/dist/heyawakeStyle.d.ts +1 -0
- package/dist/heyawakeStyle.js +2 -0
- package/dist/heyawakeWorker.d.ts +1 -0
- package/dist/heyawakeWorker.js +10 -0
- package/dist/hitori-draw-entry.d.ts +2 -0
- package/dist/hitori-draw-entry.js +1 -0
- package/dist/hitori-entry.d.ts +6 -0
- package/dist/hitori-entry.js +6 -0
- package/dist/hitori-play-entry.d.ts +4 -0
- package/dist/hitori-play-entry.js +3 -0
- package/dist/hitori.constants.d.ts +2 -0
- package/dist/hitori.constants.js +2 -0
- package/dist/hitori.types.d.ts +20 -0
- package/dist/hitori.types.js +1 -0
- package/dist/hitoriBoard.d.ts +8 -0
- package/dist/hitoriBoard.js +72 -0
- package/dist/hitoriDraw.d.ts +4 -0
- package/dist/hitoriDraw.js +25 -0
- package/dist/hitoriGame.d.ts +10 -0
- package/dist/hitoriGame.js +48 -0
- package/dist/hitoriGenerate.d.ts +3 -0
- package/dist/hitoriGenerate.js +163 -0
- package/dist/hitoriMount.d.ts +3 -0
- package/dist/hitoriMount.js +111 -0
- package/dist/hitoriPlay.types.d.ts +25 -0
- package/dist/hitoriPlay.types.js +1 -0
- package/dist/hitoriSolve.d.ts +6 -0
- package/dist/hitoriSolve.js +69 -0
- package/dist/hitoriStrings.d.ts +70 -0
- package/dist/hitoriStrings.js +17 -0
- package/dist/hitoriStyle.d.ts +1 -0
- package/dist/hitoriStyle.js +9 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/juosan-draw-entry.d.ts +2 -0
- package/dist/juosan-draw-entry.js +1 -0
- package/dist/juosan-entry.d.ts +11 -0
- package/dist/juosan-entry.js +10 -0
- package/dist/juosan-play-entry.d.ts +4 -0
- package/dist/juosan-play-entry.js +3 -0
- package/dist/juosan.constants.d.ts +4 -0
- package/dist/juosan.constants.js +4 -0
- package/dist/juosan.types.d.ts +33 -0
- package/dist/juosan.types.js +1 -0
- package/dist/juosanBoard.d.ts +7 -0
- package/dist/juosanBoard.js +102 -0
- package/dist/juosanDraw.d.ts +4 -0
- package/dist/juosanDraw.js +49 -0
- package/dist/juosanGame.d.ts +20 -0
- package/dist/juosanGame.js +86 -0
- package/dist/juosanGenerate.d.ts +3 -0
- package/dist/juosanGenerate.js +31 -0
- package/dist/juosanMount.d.ts +3 -0
- package/dist/juosanMount.js +206 -0
- package/dist/juosanPlay.types.d.ts +24 -0
- package/dist/juosanPlay.types.js +1 -0
- package/dist/juosanSolve.d.ts +3 -0
- package/dist/juosanSolve.js +82 -0
- package/dist/juosanStrings.d.ts +77 -0
- package/dist/juosanStrings.js +43 -0
- package/dist/juosanStyle.d.ts +1 -0
- package/dist/juosanStyle.js +1 -0
- package/dist/kakuro-draw-entry.d.ts +2 -0
- package/dist/kakuro-draw-entry.js +1 -0
- package/dist/kakuro-entry.d.ts +12 -0
- package/dist/kakuro-entry.js +10 -0
- package/dist/kakuro-play-entry.d.ts +4 -0
- package/dist/kakuro-play-entry.js +3 -0
- package/dist/kakuro.constants.d.ts +6 -0
- package/dist/kakuro.constants.js +6 -0
- package/dist/kakuro.types.d.ts +46 -0
- package/dist/kakuro.types.js +1 -0
- package/dist/kakuroBoard.d.ts +8 -0
- package/dist/kakuroBoard.js +117 -0
- package/dist/kakuroDraw.d.ts +10 -0
- package/dist/kakuroDraw.js +21 -0
- package/dist/kakuroGame.d.ts +12 -0
- package/dist/kakuroGame.js +74 -0
- package/dist/kakuroGenerate.d.ts +3 -0
- package/dist/kakuroGenerate.js +130 -0
- package/dist/kakuroMount.d.ts +2 -0
- package/dist/kakuroMount.js +109 -0
- package/dist/kakuroPlay.types.d.ts +20 -0
- package/dist/kakuroPlay.types.js +1 -0
- package/dist/kakuroSolve.d.ts +6 -0
- package/dist/kakuroSolve.js +85 -0
- package/dist/kakuroStrings.d.ts +82 -0
- package/dist/kakuroStrings.js +5 -0
- package/dist/kakuroStyle.d.ts +1 -0
- package/dist/kakuroStyle.js +1 -0
- package/dist/masyu-draw-entry.d.ts +2 -0
- package/dist/masyu-draw-entry.js +1 -0
- package/dist/masyu-entry.d.ts +6 -0
- package/dist/masyu-entry.js +6 -0
- package/dist/masyu-play-entry.d.ts +4 -0
- package/dist/masyu-play-entry.js +3 -0
- package/dist/masyu.constants.d.ts +2 -0
- package/dist/masyu.constants.js +2 -0
- package/dist/masyu.types.d.ts +46 -0
- package/dist/masyu.types.js +1 -0
- package/dist/masyuBoard.d.ts +10 -0
- package/dist/masyuBoard.js +88 -0
- package/dist/masyuDraw.d.ts +2 -0
- package/dist/masyuDraw.js +33 -0
- package/dist/masyuGame.d.ts +9 -0
- package/dist/masyuGame.js +46 -0
- package/dist/masyuGenerate.d.ts +3 -0
- package/dist/masyuGenerate.js +56 -0
- package/dist/masyuMount.d.ts +2 -0
- package/dist/masyuMount.js +118 -0
- package/dist/masyuPlay.types.d.ts +1 -0
- package/dist/masyuPlay.types.js +1 -0
- package/dist/masyuSolve.d.ts +6 -0
- package/dist/masyuSolve.js +88 -0
- package/dist/masyuStrings.d.ts +28 -0
- package/dist/masyuStrings.js +4 -0
- package/dist/masyuStyle.d.ts +1 -0
- package/dist/masyuStyle.js +11 -0
- package/dist/nurikabe-draw-entry.d.ts +2 -0
- package/dist/nurikabe-draw-entry.js +1 -0
- package/dist/nurikabe-entry.d.ts +6 -0
- package/dist/nurikabe-entry.js +6 -0
- package/dist/nurikabe-play-entry.d.ts +4 -0
- package/dist/nurikabe-play-entry.js +3 -0
- package/dist/nurikabe.constants.d.ts +2 -0
- package/dist/nurikabe.constants.js +2 -0
- package/dist/nurikabe.types.d.ts +20 -0
- package/dist/nurikabe.types.js +1 -0
- package/dist/nurikabeBoard.d.ts +8 -0
- package/dist/nurikabeBoard.js +72 -0
- package/dist/nurikabeDraw.d.ts +4 -0
- package/dist/nurikabeDraw.js +15 -0
- package/dist/nurikabeGame.d.ts +8 -0
- package/dist/nurikabeGame.js +40 -0
- package/dist/nurikabeGenerate.d.ts +3 -0
- package/dist/nurikabeGenerate.js +31 -0
- package/dist/nurikabeMount.d.ts +3 -0
- package/dist/nurikabeMount.js +99 -0
- package/dist/nurikabePlay.types.d.ts +25 -0
- package/dist/nurikabePlay.types.js +1 -0
- package/dist/nurikabeSolve.d.ts +6 -0
- package/dist/nurikabeSolve.js +75 -0
- package/dist/nurikabeStrings.d.ts +70 -0
- package/dist/nurikabeStrings.js +5 -0
- package/dist/nurikabeStyle.d.ts +1 -0
- package/dist/nurikabeStyle.js +9 -0
- package/dist/ripple-draw-entry.d.ts +2 -0
- package/dist/ripple-draw-entry.js +1 -0
- package/dist/ripple-entry.d.ts +12 -0
- package/dist/ripple-entry.js +11 -0
- package/dist/ripple-play-entry.d.ts +4 -0
- package/dist/ripple-play-entry.js +3 -0
- package/dist/ripple.constants.d.ts +4 -0
- package/dist/ripple.constants.js +4 -0
- package/dist/ripple.types.d.ts +34 -0
- package/dist/ripple.types.js +1 -0
- package/dist/rippleBoard.d.ts +7 -0
- package/dist/rippleBoard.js +99 -0
- package/dist/rippleDraw.d.ts +3 -0
- package/dist/rippleDraw.js +32 -0
- package/dist/rippleGame.d.ts +13 -0
- package/dist/rippleGame.js +85 -0
- package/dist/rippleGenerate.d.ts +3 -0
- package/dist/rippleGenerate.js +57 -0
- package/dist/rippleMount.d.ts +2 -0
- package/dist/rippleMount.js +179 -0
- package/dist/ripplePlay.types.d.ts +23 -0
- package/dist/ripplePlay.types.js +1 -0
- package/dist/rippleSolve.d.ts +6 -0
- package/dist/rippleSolve.js +83 -0
- package/dist/rippleStrings.d.ts +86 -0
- package/dist/rippleStrings.js +5 -0
- package/dist/rippleStyle.d.ts +1 -0
- package/dist/rippleStyle.js +1 -0
- package/dist/shikaku-entry.d.ts +1 -0
- package/dist/shikaku-entry.js +1 -0
- package/dist/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -0
- package/dist/slitherlink-draw-entry.d.ts +2 -0
- package/dist/slitherlink-draw-entry.js +1 -0
- package/dist/slitherlink-entry.d.ts +11 -0
- package/dist/slitherlink-entry.js +9 -0
- package/dist/slitherlink-play-entry.d.ts +4 -0
- package/dist/slitherlink-play-entry.js +3 -0
- package/dist/slitherlink.constants.d.ts +2 -0
- package/dist/slitherlink.constants.js +2 -0
- package/dist/slitherlink.types.d.ts +34 -0
- package/dist/slitherlink.types.js +1 -0
- package/dist/slitherlinkBoard.d.ts +12 -0
- package/dist/slitherlinkBoard.js +132 -0
- package/dist/slitherlinkDraw.d.ts +4 -0
- package/dist/slitherlinkDraw.js +51 -0
- package/dist/slitherlinkGame.d.ts +8 -0
- package/dist/slitherlinkGame.js +63 -0
- package/dist/slitherlinkGenerate.d.ts +3 -0
- package/dist/slitherlinkGenerate.js +101 -0
- package/dist/slitherlinkMount.d.ts +3 -0
- package/dist/slitherlinkMount.js +267 -0
- package/dist/slitherlinkPlay.types.d.ts +23 -0
- package/dist/slitherlinkPlay.types.js +1 -0
- package/dist/slitherlinkSolve.d.ts +6 -0
- package/dist/slitherlinkSolve.js +127 -0
- package/dist/slitherlinkStrings.d.ts +86 -0
- package/dist/slitherlinkStrings.js +47 -0
- package/dist/slitherlinkStyle.d.ts +1 -0
- package/dist/slitherlinkStyle.js +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/yajilin-draw-entry.d.ts +2 -0
- package/dist/yajilin-draw-entry.js +1 -0
- package/dist/yajilin-entry.d.ts +6 -0
- package/dist/yajilin-entry.js +6 -0
- package/dist/yajilin-play-entry.d.ts +4 -0
- package/dist/yajilin-play-entry.js +3 -0
- package/dist/yajilin.constants.d.ts +2 -0
- package/dist/yajilin.constants.js +2 -0
- package/dist/yajilin.types.d.ts +63 -0
- package/dist/yajilin.types.js +1 -0
- package/dist/yajilinBoard.d.ts +9 -0
- package/dist/yajilinBoard.js +91 -0
- package/dist/yajilinDraw.d.ts +2 -0
- package/dist/yajilinDraw.js +33 -0
- package/dist/yajilinGame.d.ts +15 -0
- package/dist/yajilinGame.js +58 -0
- package/dist/yajilinGenerate.d.ts +3 -0
- package/dist/yajilinGenerate.js +224 -0
- package/dist/yajilinMount.d.ts +2 -0
- package/dist/yajilinMount.js +162 -0
- package/dist/yajilinPlay.types.d.ts +1 -0
- package/dist/yajilinPlay.types.js +1 -0
- package/dist/yajilinSolve.d.ts +6 -0
- package/dist/yajilinSolve.js +82 -0
- package/dist/yajilinStrings.d.ts +32 -0
- package/dist/yajilinStrings.js +4 -0
- package/dist/yajilinStyle.d.ts +1 -0
- package/dist/yajilinStyle.js +12 -0
- package/package.json +180 -3
|
@@ -0,0 +1,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}`;
|
package/dist/shikaku-entry.d.ts
CHANGED
package/dist/shikaku-entry.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ShikakuLevel, ShikakuPuzzle } from "./shikaku.types.ts";
|
|
2
|
+
export type ShikakuPackKey = "square" | "wide" | "tall";
|
|
3
|
+
export type ShikakuChallenge = {
|
|
4
|
+
pack: ShikakuPackKey;
|
|
5
|
+
packTitle: string;
|
|
6
|
+
name: string;
|
|
7
|
+
level: ShikakuLevel;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
seed: number;
|
|
11
|
+
};
|
|
12
|
+
export type ShikakuChallengePuzzle = ShikakuChallenge & {
|
|
13
|
+
puzzle: ShikakuPuzzle;
|
|
14
|
+
};
|
|
15
|
+
/** Three short, named challenge routes using the regular seeded unique-puzzle generator. */
|
|
16
|
+
export declare const SHIKAKU_CHALLENGE_PACKS: Readonly<Record<ShikakuPackKey, {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
challenges: readonly {
|
|
22
|
+
name: string;
|
|
23
|
+
level: ShikakuLevel;
|
|
24
|
+
seed: number;
|
|
25
|
+
}[];
|
|
26
|
+
}>>;
|
|
27
|
+
export declare function generateShikakuChallenge(pack: ShikakuPackKey, challenge?: number): ShikakuChallengePuzzle;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { generateShikaku } from "./shikakuGenerate.js";
|
|
2
|
+
/** Three short, named challenge routes using the regular seeded unique-puzzle generator. */
|
|
3
|
+
export const SHIKAKU_CHALLENGE_PACKS = {
|
|
4
|
+
square: {
|
|
5
|
+
title: "The Courtyard", description: "A compact square route.", width: 7, height: 7,
|
|
6
|
+
challenges: [{ name: "First Cut", level: "easy", seed: 1 }, { name: "Crossing", level: "medium", seed: 2 }, { name: "Mosaic", level: "hard", seed: 3 }],
|
|
7
|
+
},
|
|
8
|
+
wide: {
|
|
9
|
+
title: "The Long Table", description: "Three puzzles across a wide board.", width: 10, height: 6,
|
|
10
|
+
challenges: [{ name: "Harbour", level: "easy", seed: 1 }, { name: "Causeway", level: "medium", seed: 2 }, { name: "Headland", level: "hard", seed: 3 }],
|
|
11
|
+
},
|
|
12
|
+
tall: {
|
|
13
|
+
title: "The Narrow Garden", description: "A tall route with long edges.", width: 6, height: 10,
|
|
14
|
+
challenges: [{ name: "Gate", level: "easy", seed: 1 }, { name: "Pavilion", level: "medium", seed: 2 }, { name: "Lantern Walk", level: "hard", seed: 3 }],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export function generateShikakuChallenge(pack, challenge = 1) {
|
|
18
|
+
const definition = SHIKAKU_CHALLENGE_PACKS[pack], entry = definition?.challenges[challenge - 1];
|
|
19
|
+
if (!definition || !entry)
|
|
20
|
+
throw new RangeError("Unknown Shikaku challenge");
|
|
21
|
+
return {
|
|
22
|
+
pack, packTitle: definition.title, name: entry.name, level: entry.level,
|
|
23
|
+
width: definition.width, height: definition.height, seed: entry.seed,
|
|
24
|
+
puzzle: generateShikaku(definition.width, definition.height, entry.level, entry.seed),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawSlitherlink } from "./slitherlinkDraw.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Slitherlink uses a dedicated edge model and a single-loop constraint. */
|
|
2
|
+
export * from "./slitherlink.types.ts";
|
|
3
|
+
export * from "./slitherlink.constants.ts";
|
|
4
|
+
export * from "./slitherlinkBoard.ts";
|
|
5
|
+
export * from "./slitherlinkSolve.ts";
|
|
6
|
+
export * from "./slitherlinkGenerate.ts";
|
|
7
|
+
export * from "./slitherlinkGame.ts";
|
|
8
|
+
export { drawSlitherlink } from "./slitherlinkDraw.ts";
|
|
9
|
+
export type { SlitherlinkDrawOptions } from "./slitherlinkPlay.types.ts";
|
|
10
|
+
export { mountSlitherlink } from "./slitherlinkMount.ts";
|
|
11
|
+
export type * from "./slitherlinkPlay.types.ts";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Slitherlink uses a dedicated edge model and a single-loop constraint. */
|
|
2
|
+
export * from "./slitherlink.types.js";
|
|
3
|
+
export * from "./slitherlink.constants.js";
|
|
4
|
+
export * from "./slitherlinkBoard.js";
|
|
5
|
+
export * from "./slitherlinkSolve.js";
|
|
6
|
+
export * from "./slitherlinkGenerate.js";
|
|
7
|
+
export * from "./slitherlinkGame.js";
|
|
8
|
+
export { drawSlitherlink } from "./slitherlinkDraw.js";
|
|
9
|
+
export { mountSlitherlink } from "./slitherlinkMount.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type SlitherlinkBoard = {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
clues: readonly (number | null)[];
|
|
5
|
+
};
|
|
6
|
+
export type SlitherlinkPuzzle = SlitherlinkBoard & {
|
|
7
|
+
seed: number;
|
|
8
|
+
solution: readonly number[];
|
|
9
|
+
};
|
|
10
|
+
export type SlitherlinkCheck = {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
errors: readonly number[];
|
|
13
|
+
clues: readonly number[];
|
|
14
|
+
vertices: readonly number[];
|
|
15
|
+
loops: number;
|
|
16
|
+
};
|
|
17
|
+
export type SlitherlinkProgress = {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
clues: readonly number[];
|
|
20
|
+
vertices: readonly number[];
|
|
21
|
+
loops: number;
|
|
22
|
+
};
|
|
23
|
+
export type SlitherlinkSolve = {
|
|
24
|
+
count: number;
|
|
25
|
+
solution: readonly number[] | null;
|
|
26
|
+
complete: boolean;
|
|
27
|
+
nodes: number;
|
|
28
|
+
};
|
|
29
|
+
export type SlitherlinkGame = {
|
|
30
|
+
board: SlitherlinkBoard;
|
|
31
|
+
edges: readonly number[];
|
|
32
|
+
history: readonly (readonly number[])[];
|
|
33
|
+
helped: boolean;
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SlitherlinkBoard, SlitherlinkCheck, SlitherlinkProgress } from "./slitherlink.types.ts";
|
|
2
|
+
export declare function isSlitherlinkBoard(value: unknown): value is SlitherlinkBoard;
|
|
3
|
+
export declare function slitherlinkHorizontalCount(board: SlitherlinkBoard): number;
|
|
4
|
+
/** An edge index is horizontal row-major first, then vertical row-major. */
|
|
5
|
+
export declare function slitherlinkEdgeCount(board: SlitherlinkBoard): number;
|
|
6
|
+
export declare function slitherlinkCellEdges(board: SlitherlinkBoard, cell: number): [number, number, number, number] | null;
|
|
7
|
+
export declare function slitherlinkVertexEdges(board: SlitherlinkBoard, vertex: number): number[];
|
|
8
|
+
/** Checks all numbered cells, degree-two vertices, and one connected closed loop. */
|
|
9
|
+
export declare function checkSlitherlink(board: SlitherlinkBoard, edges: readonly number[]): SlitherlinkCheck;
|
|
10
|
+
/** Reports overfilled clues and branched vertices during play without requiring a finished loop. */
|
|
11
|
+
export declare function progressSlitherlink(board: SlitherlinkBoard, edges: readonly number[]): SlitherlinkProgress;
|
|
12
|
+
export declare function edgeVertices(board: SlitherlinkBoard, edge: number): [number, number];
|