@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,267 @@
|
|
|
1
|
+
import { progressSlitherlink, slitherlinkCellEdges, slitherlinkHorizontalCount, slitherlinkVertexEdges, } from "./slitherlinkBoard.js";
|
|
2
|
+
import { decodeSlitherlink, encodeSlitherlink, hintSlitherlink, newSlitherlink, slitherlinkFinished, toggleSlitherlink, undoSlitherlink, } from "./slitherlinkGame.js";
|
|
3
|
+
import { KAZU_STYLE } from "./style.js";
|
|
4
|
+
import { SLITHERLINK_PLAY_STYLE } from "./slitherlinkStyle.js";
|
|
5
|
+
import { slitherlinkWords } from "./slitherlinkStrings.js";
|
|
6
|
+
/** Mounts an accessible dot-and-edge loop player in the given host element. */
|
|
7
|
+
export function mountSlitherlink(host, initial) {
|
|
8
|
+
const doc = host.ownerDocument;
|
|
9
|
+
let options = { ...initial };
|
|
10
|
+
let game = initial.progress ? decodeSlitherlink(initial.progress) : null;
|
|
11
|
+
game ?? (game = newSlitherlink(initial.board));
|
|
12
|
+
const geometry = edgeGeometry(game.board);
|
|
13
|
+
let selected = 0;
|
|
14
|
+
let errors = [];
|
|
15
|
+
let message = "";
|
|
16
|
+
let finished = slitherlinkFinished(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 = "sl-root";
|
|
25
|
+
style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".sl-palette") + SLITHERLINK_PLAY_STYLE;
|
|
26
|
+
board.className = "sl-board";
|
|
27
|
+
status.className = "sl-status";
|
|
28
|
+
status.setAttribute("role", "status");
|
|
29
|
+
tools.className = "sl-tools";
|
|
30
|
+
help.className = "sl-help";
|
|
31
|
+
root.append(style, board, status, tools, help);
|
|
32
|
+
host.append(root);
|
|
33
|
+
const copy = () => ({
|
|
34
|
+
...game,
|
|
35
|
+
board: { ...game.board, clues: [...game.board.clues] },
|
|
36
|
+
edges: [...game.edges],
|
|
37
|
+
history: game.history.map(turn => [...turn]),
|
|
38
|
+
});
|
|
39
|
+
const words = () => slitherlinkWords(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 = slitherlinkFinished(game);
|
|
52
|
+
render();
|
|
53
|
+
options.onChange?.(copy());
|
|
54
|
+
root.dispatchEvent(new CustomEvent("slitherlink-change", { detail: copy(), bubbles: true }));
|
|
55
|
+
if (won && !finished) {
|
|
56
|
+
options.onFinish?.(copy());
|
|
57
|
+
root.dispatchEvent(new CustomEvent("slitherlink-finish", { detail: copy(), bubbles: true }));
|
|
58
|
+
}
|
|
59
|
+
finished = won;
|
|
60
|
+
};
|
|
61
|
+
const toggle = (edge) => {
|
|
62
|
+
if (finished)
|
|
63
|
+
return;
|
|
64
|
+
selected = edge;
|
|
65
|
+
game = toggleSlitherlink(game, edge);
|
|
66
|
+
changed();
|
|
67
|
+
};
|
|
68
|
+
const restart = () => {
|
|
69
|
+
game = newSlitherlink(game.board);
|
|
70
|
+
finished = false;
|
|
71
|
+
selected = 0;
|
|
72
|
+
changed();
|
|
73
|
+
};
|
|
74
|
+
const render = () => {
|
|
75
|
+
if (dead)
|
|
76
|
+
return;
|
|
77
|
+
const focused = board.contains(doc.activeElement);
|
|
78
|
+
const w = words();
|
|
79
|
+
const current = game.board;
|
|
80
|
+
const selectedEdges = new Set(game.edges);
|
|
81
|
+
const clues = progressSlitherlink(current, game.edges);
|
|
82
|
+
board.classList.toggle("sl-palette", (options.material ?? "ivory") === "ivory");
|
|
83
|
+
const material = options.material ?? "ivory";
|
|
84
|
+
const palette = {
|
|
85
|
+
ivory: ["var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"],
|
|
86
|
+
wood: ["#493e2f", "#bca582"],
|
|
87
|
+
slate: ["#ece8dc", "#454a44"],
|
|
88
|
+
}[material];
|
|
89
|
+
board.style.setProperty("--sl-ink", palette[0]);
|
|
90
|
+
board.style.setProperty("--kz-grid", palette[1]);
|
|
91
|
+
board.setAttribute("role", "grid");
|
|
92
|
+
board.setAttribute("aria-label", w.title);
|
|
93
|
+
board.style.gridTemplateColumns = `repeat(${current.width * 2 + 1}, 1fr)`;
|
|
94
|
+
board.style.gridTemplateRows = `repeat(${current.height * 2 + 1}, 1fr)`;
|
|
95
|
+
board.style.aspectRatio = `${current.width * 2 + 1} / ${current.height * 2 + 1}`;
|
|
96
|
+
board.replaceChildren();
|
|
97
|
+
for (let cell = 0; cell < current.clues.length; cell += 1) {
|
|
98
|
+
const clue = current.clues[cell];
|
|
99
|
+
if (clue === null)
|
|
100
|
+
continue;
|
|
101
|
+
const tile = doc.createElement("span");
|
|
102
|
+
tile.className = "sl-cell";
|
|
103
|
+
tile.style.gridColumnStart = String((cell % current.width) * 2 + 2);
|
|
104
|
+
tile.style.gridRowStart = String(Math.floor(cell / current.width) * 2 + 2);
|
|
105
|
+
tile.textContent = String(clue);
|
|
106
|
+
tile.setAttribute("aria-hidden", "true");
|
|
107
|
+
board.append(tile);
|
|
108
|
+
}
|
|
109
|
+
for (const point of gridPoints(current)) {
|
|
110
|
+
const dot = doc.createElement("span");
|
|
111
|
+
dot.className = "sl-dot";
|
|
112
|
+
dot.style.gridColumnStart = String(point.x * 2 + 1);
|
|
113
|
+
dot.style.gridRowStart = String(point.y * 2 + 1);
|
|
114
|
+
dot.setAttribute("aria-hidden", "true");
|
|
115
|
+
board.append(dot);
|
|
116
|
+
}
|
|
117
|
+
for (const edge of geometry) {
|
|
118
|
+
const control = doc.createElement("button");
|
|
119
|
+
const on = selectedEdges.has(edge.id);
|
|
120
|
+
control.type = "button";
|
|
121
|
+
control.className = `sl-edge ${edge.orientation}${on ? " on" : ""}${errors.includes(edge.id) ? " error" : ""}`;
|
|
122
|
+
control.style.gridColumnStart = String(edge.x * 2 + 1);
|
|
123
|
+
control.style.gridRowStart = String(edge.y * 2 + 1);
|
|
124
|
+
if (edge.orientation === "horizontal")
|
|
125
|
+
control.style.gridColumnEnd = "span 3";
|
|
126
|
+
else
|
|
127
|
+
control.style.gridRowEnd = "span 3";
|
|
128
|
+
control.tabIndex = edge.id === selected ? 0 : -1;
|
|
129
|
+
control.dataset.edge = String(edge.id);
|
|
130
|
+
control.setAttribute("aria-pressed", String(on));
|
|
131
|
+
control.setAttribute("aria-label", `${w.edge} ${edge.id + 1}, ${on ? "on" : "off"}`);
|
|
132
|
+
control.onclick = () => toggle(edge.id);
|
|
133
|
+
control.onkeydown = event => {
|
|
134
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
toggle(edge.id);
|
|
137
|
+
}
|
|
138
|
+
else if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
moveFocus(edge.id, event.key);
|
|
141
|
+
}
|
|
142
|
+
else if ((event.key === "Delete" || event.key === "Backspace") && on) {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
toggle(edge.id);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
board.append(control);
|
|
148
|
+
}
|
|
149
|
+
if (focused)
|
|
150
|
+
board.querySelector(`[data-edge="${selected}"]`)?.focus();
|
|
151
|
+
status.textContent = finished ? (game.helped ? w.helped : w.won) : message || w.checked;
|
|
152
|
+
help.textContent = w.help;
|
|
153
|
+
tools.replaceChildren();
|
|
154
|
+
button(w.undo, () => {
|
|
155
|
+
game = undoSlitherlink(game);
|
|
156
|
+
finished = slitherlinkFinished(game);
|
|
157
|
+
changed();
|
|
158
|
+
}, !game.history.length);
|
|
159
|
+
button(w.check, () => {
|
|
160
|
+
errors = progressEdges(current, game.edges, clues.clues, clues.vertices, clues.loops);
|
|
161
|
+
message = slitherlinkFinished(game) ? w.won : w.wrong;
|
|
162
|
+
render();
|
|
163
|
+
});
|
|
164
|
+
button(w.hint, () => {
|
|
165
|
+
const edge = hintSlitherlink(game);
|
|
166
|
+
if (edge === null) {
|
|
167
|
+
message = w.noHint;
|
|
168
|
+
render();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
game = { ...toggleSlitherlink(game, edge), helped: true };
|
|
172
|
+
changed();
|
|
173
|
+
message = w.hintWhy;
|
|
174
|
+
render();
|
|
175
|
+
}, finished);
|
|
176
|
+
button(w.restart, restart);
|
|
177
|
+
button(w.expand, expand);
|
|
178
|
+
};
|
|
179
|
+
function moveFocus(edge, direction) {
|
|
180
|
+
const origin = geometry[edge];
|
|
181
|
+
const dx = direction === "ArrowLeft" ? -1 : direction === "ArrowRight" ? 1 : 0;
|
|
182
|
+
const dy = direction === "ArrowUp" ? -1 : direction === "ArrowDown" ? 1 : 0;
|
|
183
|
+
const candidates = geometry.filter(candidate => {
|
|
184
|
+
const deltaX = candidate.cx - origin.cx;
|
|
185
|
+
const deltaY = candidate.cy - origin.cy;
|
|
186
|
+
return dx ? deltaX * dx > 0 : deltaY * dy > 0;
|
|
187
|
+
});
|
|
188
|
+
candidates.sort((a, b) => {
|
|
189
|
+
const aMajor = dx ? Math.abs(a.cx - origin.cx) : Math.abs(a.cy - origin.cy);
|
|
190
|
+
const bMajor = dx ? Math.abs(b.cx - origin.cx) : Math.abs(b.cy - origin.cy);
|
|
191
|
+
if (aMajor !== bMajor)
|
|
192
|
+
return aMajor - bMajor;
|
|
193
|
+
const aMinor = dx ? Math.abs(a.cy - origin.cy) : Math.abs(a.cx - origin.cx);
|
|
194
|
+
const bMinor = dx ? Math.abs(b.cy - origin.cy) : Math.abs(b.cx - origin.cx);
|
|
195
|
+
return aMinor - bMinor;
|
|
196
|
+
});
|
|
197
|
+
const next = candidates[0];
|
|
198
|
+
if (!next)
|
|
199
|
+
return;
|
|
200
|
+
selected = next.id;
|
|
201
|
+
render();
|
|
202
|
+
}
|
|
203
|
+
function expand() {
|
|
204
|
+
if (root.parentElement !== host)
|
|
205
|
+
return;
|
|
206
|
+
const dialog = doc.createElement("dialog");
|
|
207
|
+
const close = doc.createElement("button");
|
|
208
|
+
dialog.className = "sl-dialog";
|
|
209
|
+
close.className = "sl-close";
|
|
210
|
+
close.textContent = words().close;
|
|
211
|
+
close.onclick = () => dialog.close();
|
|
212
|
+
dialog.append(close, root);
|
|
213
|
+
host.append(dialog);
|
|
214
|
+
dialog.onclose = () => {
|
|
215
|
+
host.append(root);
|
|
216
|
+
dialog.remove();
|
|
217
|
+
};
|
|
218
|
+
dialog.showModal();
|
|
219
|
+
}
|
|
220
|
+
render();
|
|
221
|
+
return {
|
|
222
|
+
game: copy,
|
|
223
|
+
progress: () => encodeSlitherlink(game),
|
|
224
|
+
restart,
|
|
225
|
+
set: (next) => {
|
|
226
|
+
if (next.language && next.language !== options.language)
|
|
227
|
+
message = "";
|
|
228
|
+
options = { ...options, ...next };
|
|
229
|
+
render();
|
|
230
|
+
},
|
|
231
|
+
destroy: () => {
|
|
232
|
+
dead = true;
|
|
233
|
+
const dialog = root.closest("dialog");
|
|
234
|
+
root.remove();
|
|
235
|
+
dialog?.remove();
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function gridPoints(board) {
|
|
240
|
+
return Array.from({ length: (board.width + 1) * (board.height + 1) }, (_, vertex) => ({
|
|
241
|
+
x: vertex % (board.width + 1),
|
|
242
|
+
y: Math.floor(vertex / (board.width + 1)),
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
function edgeGeometry(board) {
|
|
246
|
+
const horizontal = slitherlinkHorizontalCount(board);
|
|
247
|
+
const edges = Array.from({ length: horizontal + board.height * (board.width + 1) }, (_, id) => {
|
|
248
|
+
if (id < horizontal) {
|
|
249
|
+
const x = id % board.width;
|
|
250
|
+
const y = Math.floor(id / board.width);
|
|
251
|
+
return { id, orientation: "horizontal", x, y, cx: x + .5, cy: y };
|
|
252
|
+
}
|
|
253
|
+
const local = id - horizontal;
|
|
254
|
+
const x = local % (board.width + 1);
|
|
255
|
+
const y = Math.floor(local / (board.width + 1));
|
|
256
|
+
return { id, orientation: "vertical", x, y, cx: x, cy: y + .5 };
|
|
257
|
+
});
|
|
258
|
+
return edges;
|
|
259
|
+
}
|
|
260
|
+
function progressEdges(board, edges, clueErrors, vertexErrors, loops) {
|
|
261
|
+
if (loops > 1)
|
|
262
|
+
return [...edges];
|
|
263
|
+
const result = new Set();
|
|
264
|
+
clueErrors.forEach(cell => slitherlinkCellEdges(board, cell)?.forEach(edge => result.add(edge)));
|
|
265
|
+
vertexErrors.forEach(vertex => slitherlinkVertexEdges(board, vertex).forEach(edge => result.add(edge)));
|
|
266
|
+
return [...result];
|
|
267
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SlitherlinkBoard, SlitherlinkGame } from "./slitherlink.types.ts";
|
|
2
|
+
export type SlitherlinkLanguage = "en" | "ja";
|
|
3
|
+
export type SlitherlinkMaterial = "ivory" | "wood" | "slate";
|
|
4
|
+
export type SlitherlinkDrawOptions = {
|
|
5
|
+
edges?: SlitherlinkGame["edges"];
|
|
6
|
+
selected?: number | null;
|
|
7
|
+
errors?: readonly number[];
|
|
8
|
+
material?: SlitherlinkMaterial;
|
|
9
|
+
language?: SlitherlinkLanguage;
|
|
10
|
+
};
|
|
11
|
+
export type SlitherlinkMountOptions = SlitherlinkDrawOptions & {
|
|
12
|
+
board: SlitherlinkBoard;
|
|
13
|
+
progress?: string;
|
|
14
|
+
onChange?: (game: SlitherlinkGame) => void;
|
|
15
|
+
onFinish?: (game: SlitherlinkGame) => void;
|
|
16
|
+
};
|
|
17
|
+
export type SlitherlinkMount = {
|
|
18
|
+
game: () => SlitherlinkGame;
|
|
19
|
+
progress: () => string;
|
|
20
|
+
set: (options: SlitherlinkDrawOptions) => void;
|
|
21
|
+
restart: () => void;
|
|
22
|
+
destroy: () => void;
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SlitherlinkBoard, SlitherlinkRating } from "./slitherlink.types.ts";
|
|
2
|
+
/** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one edge at a time. */
|
|
3
|
+
export declare function rateSlitherlink(board: SlitherlinkBoard): SlitherlinkRating;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isSlitherlinkBoard } from "./slitherlinkBoard.js";
|
|
2
|
+
import { slitherlinkModel } from "./slitherlinkLogic.js";
|
|
3
|
+
import { logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
/** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one edge at a time. */
|
|
5
|
+
export function rateSlitherlink(board) {
|
|
6
|
+
if (!isSlitherlinkBoard(board))
|
|
7
|
+
throw new RangeError("Invalid Slitherlink board");
|
|
8
|
+
const { csp } = slitherlinkModel(board);
|
|
9
|
+
const proof = solveCsp(csp, openSlots(csp), 2, 300000);
|
|
10
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
11
|
+
throw new RangeError("Slitherlink rating needs a board with one answer");
|
|
12
|
+
const plain = logicCsp(csp, openSlots(csp), 0);
|
|
13
|
+
const probing = plain.solved ? plain : logicCsp(csp, openSlots(csp), 1);
|
|
14
|
+
const numbered = board.clues.filter(clue => clue !== null);
|
|
15
|
+
let loop = 0;
|
|
16
|
+
for (let at = 1; at < proof.solution.length; at += 2)
|
|
17
|
+
loop += proof.solution[at];
|
|
18
|
+
return {
|
|
19
|
+
depth: plain.solved ? 0 : probing.solved ? 1 : 2,
|
|
20
|
+
probes: plain.solved ? 0 : probing.probes,
|
|
21
|
+
clues: numbered.length,
|
|
22
|
+
clueShare: numbered.length / board.clues.length,
|
|
23
|
+
zeroShare: numbered.length ? numbered.filter(clue => clue === 0).length / numbered.length : 0,
|
|
24
|
+
loop,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SlitherlinkBoard, SlitherlinkSolve } from "./slitherlink.types.ts";
|
|
2
|
+
/** Counts loop solutions with the pruning in `slitherlinkLogic.ts` under explicit work bounds. */
|
|
3
|
+
export declare function solveSlitherlink(board: SlitherlinkBoard, options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): SlitherlinkSolve;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SLITHERLINK_MOST_NODES } from "./slitherlink.constants.js";
|
|
2
|
+
import { isSlitherlinkBoard, slitherlinkEdgeCount } from "./slitherlinkBoard.js";
|
|
3
|
+
import { slitherlinkModel } from "./slitherlinkLogic.js";
|
|
4
|
+
import { openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Counts loop solutions with the pruning in `slitherlinkLogic.ts` under explicit work bounds. */
|
|
6
|
+
export function solveSlitherlink(board, options = {}) {
|
|
7
|
+
if (!isSlitherlinkBoard(board))
|
|
8
|
+
throw new RangeError("Invalid Slitherlink board");
|
|
9
|
+
const limit = options.limit ?? 2;
|
|
10
|
+
const budget = options.nodes ?? SLITHERLINK_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 } = slitherlinkModel(board);
|
|
15
|
+
const found = solveCsp(csp, openSlots(csp), limit, budget);
|
|
16
|
+
const edges = slitherlinkEdgeCount(board);
|
|
17
|
+
const solution = found.solution ? Array.from({ length: edges }, (_, edge) => edge).filter(edge => found.solution[2 * edge + 1] === 1) : null;
|
|
18
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
19
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { SlitherlinkLanguage } from "./slitherlinkPlay.types.ts";
|
|
2
|
+
export declare const SLITHERLINK_STRINGS: {
|
|
3
|
+
readonly en: {
|
|
4
|
+
readonly title: "Slitherlink";
|
|
5
|
+
readonly help: "Connect adjacent dots with horizontal and vertical edges to make one closed loop. Each number says how many of its four bordering edges are in the loop. Empty cells may have any number of edges. The loop cannot branch or split into separate loops. Click or tap an edge, or focus it and press Enter or Space. Arrow keys move between edges.";
|
|
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: "One closed loop. Solved!";
|
|
13
|
+
readonly helped: "Solved with help.";
|
|
14
|
+
readonly checked: "No rule conflicts so far. Keep drawing the loop.";
|
|
15
|
+
readonly wrong: "Check the numbered cells, branches and loop connections.";
|
|
16
|
+
readonly hintWhy: "This edge is part of the only proved loop.";
|
|
17
|
+
readonly noHint: "No proved hint is available.";
|
|
18
|
+
readonly row: "Row";
|
|
19
|
+
readonly column: "column";
|
|
20
|
+
readonly clue: "number";
|
|
21
|
+
readonly blank: "no number";
|
|
22
|
+
readonly edge: "edge";
|
|
23
|
+
};
|
|
24
|
+
readonly ja: {
|
|
25
|
+
readonly title: "スリザーリンク";
|
|
26
|
+
readonly help: "点を縦横の線でつなぎ、一つの閉じた輪を作ります。数字はそのマスを囲む四辺のうち、輪になる辺の数です。数字のないマスは何辺でも構いません。輪を分岐させたり、別々の輪に分けたりしません。線をタップ、クリック、または選んでEnterかSpaceを押します。矢印キーで辺を移動します。";
|
|
27
|
+
readonly undo: "戻す";
|
|
28
|
+
readonly hint: "ヒント";
|
|
29
|
+
readonly check: "確かめる";
|
|
30
|
+
readonly restart: "やり直す";
|
|
31
|
+
readonly expand: "盤だけ";
|
|
32
|
+
readonly close: "閉じる";
|
|
33
|
+
readonly won: "一つの輪ができました。完成!";
|
|
34
|
+
readonly helped: "ヒントを使って完成!";
|
|
35
|
+
readonly checked: "ここまでルールに合っています。輪を続けましょう。";
|
|
36
|
+
readonly wrong: "数字のマス、分岐、輪のつながりを確かめましょう。";
|
|
37
|
+
readonly hintWhy: "唯一の解答に含まれる辺です。";
|
|
38
|
+
readonly noHint: "確かなヒントがありません。";
|
|
39
|
+
readonly row: "行";
|
|
40
|
+
readonly column: "列";
|
|
41
|
+
readonly clue: "数字";
|
|
42
|
+
readonly blank: "数字なし";
|
|
43
|
+
readonly edge: "辺";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export declare function slitherlinkWords(language?: SlitherlinkLanguage): {
|
|
47
|
+
readonly title: "Slitherlink";
|
|
48
|
+
readonly help: "Connect adjacent dots with horizontal and vertical edges to make one closed loop. Each number says how many of its four bordering edges are in the loop. Empty cells may have any number of edges. The loop cannot branch or split into separate loops. Click or tap an edge, or focus it and press Enter or Space. Arrow keys move between edges.";
|
|
49
|
+
readonly undo: "Undo";
|
|
50
|
+
readonly hint: "Hint";
|
|
51
|
+
readonly check: "Check";
|
|
52
|
+
readonly restart: "Restart";
|
|
53
|
+
readonly expand: "Just the board";
|
|
54
|
+
readonly close: "Close";
|
|
55
|
+
readonly won: "One closed loop. Solved!";
|
|
56
|
+
readonly helped: "Solved with help.";
|
|
57
|
+
readonly checked: "No rule conflicts so far. Keep drawing the loop.";
|
|
58
|
+
readonly wrong: "Check the numbered cells, branches and loop connections.";
|
|
59
|
+
readonly hintWhy: "This edge is part of the only proved loop.";
|
|
60
|
+
readonly noHint: "No proved hint is available.";
|
|
61
|
+
readonly row: "Row";
|
|
62
|
+
readonly column: "column";
|
|
63
|
+
readonly clue: "number";
|
|
64
|
+
readonly blank: "no number";
|
|
65
|
+
readonly edge: "edge";
|
|
66
|
+
} | {
|
|
67
|
+
readonly title: "スリザーリンク";
|
|
68
|
+
readonly help: "点を縦横の線でつなぎ、一つの閉じた輪を作ります。数字はそのマスを囲む四辺のうち、輪になる辺の数です。数字のないマスは何辺でも構いません。輪を分岐させたり、別々の輪に分けたりしません。線をタップ、クリック、または選んでEnterかSpaceを押します。矢印キーで辺を移動します。";
|
|
69
|
+
readonly undo: "戻す";
|
|
70
|
+
readonly hint: "ヒント";
|
|
71
|
+
readonly check: "確かめる";
|
|
72
|
+
readonly restart: "やり直す";
|
|
73
|
+
readonly expand: "盤だけ";
|
|
74
|
+
readonly close: "閉じる";
|
|
75
|
+
readonly won: "一つの輪ができました。完成!";
|
|
76
|
+
readonly helped: "ヒントを使って完成!";
|
|
77
|
+
readonly checked: "ここまでルールに合っています。輪を続けましょう。";
|
|
78
|
+
readonly wrong: "数字のマス、分岐、輪のつながりを確かめましょう。";
|
|
79
|
+
readonly hintWhy: "唯一の解答に含まれる辺です。";
|
|
80
|
+
readonly noHint: "確かなヒントがありません。";
|
|
81
|
+
readonly row: "行";
|
|
82
|
+
readonly column: "列";
|
|
83
|
+
readonly clue: "数字";
|
|
84
|
+
readonly blank: "数字なし";
|
|
85
|
+
readonly edge: "辺";
|
|
86
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const SLITHERLINK_STRINGS = {
|
|
2
|
+
en: {
|
|
3
|
+
title: "Slitherlink",
|
|
4
|
+
help: "Connect adjacent dots with horizontal and vertical edges to make one closed loop. Each number says how many of its four bordering edges are in the loop. Empty cells may have any number of edges. The loop cannot branch or split into separate loops. Click or tap an edge, or focus it and press Enter or Space. Arrow keys move between edges.",
|
|
5
|
+
undo: "Undo",
|
|
6
|
+
hint: "Hint",
|
|
7
|
+
check: "Check",
|
|
8
|
+
restart: "Restart",
|
|
9
|
+
expand: "Just the board",
|
|
10
|
+
close: "Close",
|
|
11
|
+
won: "One closed loop. Solved!",
|
|
12
|
+
helped: "Solved with help.",
|
|
13
|
+
checked: "No rule conflicts so far. Keep drawing the loop.",
|
|
14
|
+
wrong: "Check the numbered cells, branches and loop connections.",
|
|
15
|
+
hintWhy: "This edge is part of the only proved loop.",
|
|
16
|
+
noHint: "No proved hint is available.",
|
|
17
|
+
row: "Row",
|
|
18
|
+
column: "column",
|
|
19
|
+
clue: "number",
|
|
20
|
+
blank: "no number",
|
|
21
|
+
edge: "edge",
|
|
22
|
+
},
|
|
23
|
+
ja: {
|
|
24
|
+
title: "スリザーリンク",
|
|
25
|
+
help: "点を縦横の線でつなぎ、一つの閉じた輪を作ります。数字はそのマスを囲む四辺のうち、輪になる辺の数です。数字のないマスは何辺でも構いません。輪を分岐させたり、別々の輪に分けたりしません。線をタップ、クリック、または選んでEnterかSpaceを押します。矢印キーで辺を移動します。",
|
|
26
|
+
undo: "戻す",
|
|
27
|
+
hint: "ヒント",
|
|
28
|
+
check: "確かめる",
|
|
29
|
+
restart: "やり直す",
|
|
30
|
+
expand: "盤だけ",
|
|
31
|
+
close: "閉じる",
|
|
32
|
+
won: "一つの輪ができました。完成!",
|
|
33
|
+
helped: "ヒントを使って完成!",
|
|
34
|
+
checked: "ここまでルールに合っています。輪を続けましょう。",
|
|
35
|
+
wrong: "数字のマス、分岐、輪のつながりを確かめましょう。",
|
|
36
|
+
hintWhy: "唯一の解答に含まれる辺です。",
|
|
37
|
+
noHint: "確かなヒントがありません。",
|
|
38
|
+
row: "行",
|
|
39
|
+
column: "列",
|
|
40
|
+
clue: "数字",
|
|
41
|
+
blank: "数字なし",
|
|
42
|
+
edge: "辺",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export function slitherlinkWords(language = "en") {
|
|
46
|
+
return SLITHERLINK_STRINGS[language];
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SLITHERLINK_PLAY_STYLE = "\n.sl-root{color:var(--sl-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:760px;margin:auto}\n.sl-board{display:grid;position:relative;gap:0;width:100%;min-width:240px;touch-action:manipulation;user-select:none}\n.sl-cell{position:relative;grid-column:span 2;grid-row:span 2;display:grid;place-items:center;min-width:0;min-height:0;border:0;background:transparent;padding:0;color:var(--sl-ink,#1f2320);font:700 clamp(14px,2vw,22px)/1 system-ui,sans-serif}\n.sl-dot{z-index:2;grid-column:span 1;grid-row:span 1;justify-self:center;align-self:center;width:8px;height:8px;border-radius:50%;background:var(--sl-ink,#1f2320);pointer-events:none}\n.sl-edge{position:relative;z-index:1;grid-column:span 2;grid-row:span 1;min-width:0;min-height:0;border:0;padding:0;background:transparent;cursor:pointer;touch-action:manipulation}\n.sl-edge.vertical{grid-column:span 1;grid-row:span 2}\n.sl-edge::after{content:\"\";position:absolute;background:var(--kz-grid,#cfc6b2);transition:background .12s ease}\n.sl-edge.horizontal::after{left:0;right:0;top:50%;height:5px;transform:translateY(-50%)}\n.sl-edge.vertical::after{top:0;bottom:0;left:50%;width:5px;transform:translateX(-50%)}\n.sl-edge.on::after{background:var(--sl-ink,#1f2320)}\n.sl-edge.error::after{background:var(--kz-bad,#b5452c)}\n.sl-edge:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-2px;border-radius:4px}\n.sl-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}\n.sl-tools button,.sl-close{font:inherit;border:1px solid var(--sl-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--sl-surface,#fbf8f1);color:var(--sl-ink,#1f2320);cursor:pointer}\n.sl-tools button:disabled{opacity:.45;cursor:default}.sl-status{min-height:1.5em}.sl-help{font-size:14px;opacity:.8}\n.sl-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,820px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--sl-ink:var(--ink,#1f2320);--sl-surface:var(--surface,#fbf8f1);--sl-rule:var(--rule,#ddd6c6)}.sl-dialog::backdrop{background:#172c28bb}.sl-close{float:right;margin-bottom:10px}.sl-dialog .sl-root{clear:both}\n";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const SLITHERLINK_PLAY_STYLE = `
|
|
2
|
+
.sl-root{color:var(--sl-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:760px;margin:auto}
|
|
3
|
+
.sl-board{display:grid;position:relative;gap:0;width:100%;min-width:240px;touch-action:manipulation;user-select:none}
|
|
4
|
+
.sl-cell{position:relative;grid-column:span 2;grid-row:span 2;display:grid;place-items:center;min-width:0;min-height:0;border:0;background:transparent;padding:0;color:var(--sl-ink,#1f2320);font:700 clamp(14px,2vw,22px)/1 system-ui,sans-serif}
|
|
5
|
+
.sl-dot{z-index:2;grid-column:span 1;grid-row:span 1;justify-self:center;align-self:center;width:8px;height:8px;border-radius:50%;background:var(--sl-ink,#1f2320);pointer-events:none}
|
|
6
|
+
.sl-edge{position:relative;z-index:1;grid-column:span 2;grid-row:span 1;min-width:0;min-height:0;border:0;padding:0;background:transparent;cursor:pointer;touch-action:manipulation}
|
|
7
|
+
.sl-edge.vertical{grid-column:span 1;grid-row:span 2}
|
|
8
|
+
.sl-edge::after{content:"";position:absolute;background:var(--kz-grid,#cfc6b2);transition:background .12s ease}
|
|
9
|
+
.sl-edge.horizontal::after{left:0;right:0;top:50%;height:5px;transform:translateY(-50%)}
|
|
10
|
+
.sl-edge.vertical::after{top:0;bottom:0;left:50%;width:5px;transform:translateX(-50%)}
|
|
11
|
+
.sl-edge.on::after{background:var(--sl-ink,#1f2320)}
|
|
12
|
+
.sl-edge.error::after{background:var(--kz-bad,#b5452c)}
|
|
13
|
+
.sl-edge:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-2px;border-radius:4px}
|
|
14
|
+
.sl-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}
|
|
15
|
+
.sl-tools button,.sl-close{font:inherit;border:1px solid var(--sl-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--sl-surface,#fbf8f1);color:var(--sl-ink,#1f2320);cursor:pointer}
|
|
16
|
+
.sl-tools button:disabled{opacity:.45;cursor:default}.sl-status{min-height:1.5em}.sl-help{font-size:14px;opacity:.8}
|
|
17
|
+
.sl-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,820px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--sl-ink:var(--ink,#1f2320);--sl-surface:var(--surface,#fbf8f1);--sl-rule:var(--rule,#ddd6c6)}.sl-dialog::backdrop{background:#172c28bb}.sl-close{float:right;margin-bottom:10px}.sl-dialog .sl-root{clear:both}
|
|
18
|
+
`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SlitherlinkPuzzle } from "./slitherlink.types.ts";
|
|
2
|
+
/** The first generator, kept as the fallback that cannot fail: a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
|
|
3
|
+
export declare function templateSlitherlink(width?: number, height?: number, seed?: number): Omit<SlitherlinkPuzzle, "level">;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { SLITHERLINK_MOST_SIDE } from "./slitherlink.constants.js";
|
|
2
|
+
import { checkSlitherlink, isSlitherlinkBoard, slitherlinkCellEdges, } from "./slitherlinkBoard.js";
|
|
3
|
+
import { solveSlitherlink } from "./slitherlinkSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/** The first generator, kept as the fallback that cannot fail: a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
|
|
6
|
+
export function templateSlitherlink(width = 5, height = width, seed = 1) {
|
|
7
|
+
if (![width, height].every(side => Number.isInteger(side) && side >= 2 && side <= SLITHERLINK_MOST_SIDE)
|
|
8
|
+
|| !isKazuSeed(seed))
|
|
9
|
+
throw new RangeError("Invalid Slitherlink settings");
|
|
10
|
+
const random = seededRandom(seed);
|
|
11
|
+
const useL = width >= 3 && height >= 3 && random() < .5;
|
|
12
|
+
const region = [];
|
|
13
|
+
let x0;
|
|
14
|
+
let y0;
|
|
15
|
+
let blockWidth;
|
|
16
|
+
let blockHeight;
|
|
17
|
+
if (useL) {
|
|
18
|
+
blockWidth = 2 + Math.floor(random() * (width - 1));
|
|
19
|
+
blockHeight = 2 + Math.floor(random() * (height - 1));
|
|
20
|
+
x0 = Math.floor(random() * (width - blockWidth + 1));
|
|
21
|
+
y0 = Math.floor(random() * (height - blockHeight + 1));
|
|
22
|
+
for (let y = 0; y < blockHeight; y += 1) {
|
|
23
|
+
for (let x = 0; x < blockWidth; x += 1) {
|
|
24
|
+
if (x === 0 || y === 0)
|
|
25
|
+
region.push((y0 + y) * width + x0 + x);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
blockWidth = 1 + Math.floor(random() * width);
|
|
31
|
+
blockHeight = 1 + Math.floor(random() * height);
|
|
32
|
+
if (blockWidth === 1 && blockHeight === 1)
|
|
33
|
+
blockWidth = 2;
|
|
34
|
+
x0 = Math.floor(random() * (width - blockWidth + 1));
|
|
35
|
+
y0 = Math.floor(random() * (height - blockHeight + 1));
|
|
36
|
+
for (let y = 0; y < blockHeight; y += 1) {
|
|
37
|
+
for (let x = 0; x < blockWidth; x += 1)
|
|
38
|
+
region.push((y0 + y) * width + x0 + x);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const blank = { width, height, clues: Array(width * height).fill(null) };
|
|
42
|
+
const regionSet = new Set(region);
|
|
43
|
+
const edges = new Set();
|
|
44
|
+
for (const cell of region) {
|
|
45
|
+
const [top, right, bottom, left] = slitherlinkCellEdges(blank, cell);
|
|
46
|
+
const x = cell % width;
|
|
47
|
+
if (!regionSet.has(cell - width))
|
|
48
|
+
edges.add(top);
|
|
49
|
+
if (x === width - 1 || !regionSet.has(cell + 1))
|
|
50
|
+
edges.add(right);
|
|
51
|
+
if (!regionSet.has(cell + width))
|
|
52
|
+
edges.add(bottom);
|
|
53
|
+
if (x === 0 || !regionSet.has(cell - 1))
|
|
54
|
+
edges.add(left);
|
|
55
|
+
}
|
|
56
|
+
let board = {
|
|
57
|
+
width,
|
|
58
|
+
height,
|
|
59
|
+
clues: Array.from({ length: width * height }, (_, cell) => slitherlinkCellEdges(blank, cell).filter(edge => edges.has(edge)).length),
|
|
60
|
+
};
|
|
61
|
+
let answer = [...edges];
|
|
62
|
+
// Seeded axis reflections give each selected shape more than one orientation.
|
|
63
|
+
const reflectX = random() < .5;
|
|
64
|
+
const reflectY = random() < .5;
|
|
65
|
+
if (reflectX || reflectY) {
|
|
66
|
+
const horizontalCount = width * (height + 1);
|
|
67
|
+
const mapEdge = (edge) => {
|
|
68
|
+
if (edge < horizontalCount) {
|
|
69
|
+
const x = edge % width;
|
|
70
|
+
const y = Math.floor(edge / width);
|
|
71
|
+
const nextX = reflectX ? width - 1 - x : x;
|
|
72
|
+
const nextY = reflectY ? height - y : y;
|
|
73
|
+
return nextY * width + nextX;
|
|
74
|
+
}
|
|
75
|
+
const local = edge - horizontalCount;
|
|
76
|
+
const x = local % (width + 1);
|
|
77
|
+
const y = Math.floor(local / (width + 1));
|
|
78
|
+
const nextX = reflectX ? width - x : x;
|
|
79
|
+
const nextY = reflectY ? height - 1 - y : y;
|
|
80
|
+
return horizontalCount + nextY * (width + 1) + nextX;
|
|
81
|
+
};
|
|
82
|
+
const reflected = Array(width * height).fill(null);
|
|
83
|
+
board.clues.forEach((clue, cell) => {
|
|
84
|
+
const x = cell % width;
|
|
85
|
+
const y = Math.floor(cell / width);
|
|
86
|
+
const nextX = reflectX ? width - 1 - x : x;
|
|
87
|
+
const nextY = reflectY ? height - 1 - y : y;
|
|
88
|
+
reflected[nextY * width + nextX] = clue;
|
|
89
|
+
});
|
|
90
|
+
board = { width, height, clues: reflected };
|
|
91
|
+
answer = answer.map(mapEdge);
|
|
92
|
+
}
|
|
93
|
+
if (!isSlitherlinkBoard(board) || !checkSlitherlink(board, answer).ok) {
|
|
94
|
+
throw new Error("Unable to construct Slitherlink loop");
|
|
95
|
+
}
|
|
96
|
+
const proof = solveSlitherlink(board, { limit: 2 });
|
|
97
|
+
if (!proof.complete || proof.count !== 1 || !proof.solution) {
|
|
98
|
+
throw new Error("No uniquely solvable Slitherlink found within the generation budget");
|
|
99
|
+
}
|
|
100
|
+
return { ...board, seed, solution: proof.solution };
|
|
101
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export declare const VERSION = "1.
|
|
2
|
+
export declare const VERSION = "1.3.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export const VERSION = "1.
|
|
2
|
+
export const VERSION = "1.3.0";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawYajilin } from "./yajilinDraw.js";
|