@johnmorrisdotca/kazu 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +429 -5
- package/dist/akari-draw-entry.d.ts +2 -0
- package/dist/akari-draw-entry.js +1 -0
- package/dist/akari-entry.d.ts +12 -0
- package/dist/akari-entry.js +10 -0
- package/dist/akari-play-entry.d.ts +2 -0
- package/dist/akari-play-entry.js +1 -0
- package/dist/akari.constants.d.ts +2 -0
- package/dist/akari.constants.js +2 -0
- package/dist/akari.types.d.ts +33 -0
- package/dist/akari.types.js +1 -0
- package/dist/akariBoard.d.ts +11 -0
- package/dist/akariBoard.js +96 -0
- package/dist/akariDraw.d.ts +4 -0
- package/dist/akariDraw.js +38 -0
- package/dist/akariGame.d.ts +15 -0
- package/dist/akariGame.js +71 -0
- package/dist/akariGenerate.d.ts +7 -0
- package/dist/akariGenerate.js +78 -0
- package/dist/akariMount.d.ts +3 -0
- package/dist/akariMount.js +214 -0
- package/dist/akariPlay.types.d.ts +25 -0
- package/dist/akariPlay.types.js +1 -0
- package/dist/akariSolve.d.ts +6 -0
- package/dist/akariSolve.js +106 -0
- package/dist/akariStrings.d.ts +98 -0
- package/dist/akariStrings.js +53 -0
- package/dist/akariStyle.d.ts +1 -0
- package/dist/akariStyle.js +9 -0
- package/dist/draw-entry.d.ts +1 -0
- package/dist/draw-entry.js +1 -0
- package/dist/fillomino-draw-entry.d.ts +2 -0
- package/dist/fillomino-draw-entry.js +1 -0
- package/dist/fillomino-entry.d.ts +8 -0
- package/dist/fillomino-entry.js +7 -0
- package/dist/fillomino-play-entry.d.ts +4 -0
- package/dist/fillomino-play-entry.js +3 -0
- package/dist/fillomino.constants.d.ts +5 -0
- package/dist/fillomino.constants.js +5 -0
- package/dist/fillomino.types.d.ts +30 -0
- package/dist/fillomino.types.js +1 -0
- package/dist/fillominoBoard.d.ts +6 -0
- package/dist/fillominoBoard.js +84 -0
- package/dist/fillominoDraw.d.ts +3 -0
- package/dist/fillominoDraw.js +31 -0
- package/dist/fillominoGame.d.ts +13 -0
- package/dist/fillominoGame.js +65 -0
- package/dist/fillominoGenerate.d.ts +3 -0
- package/dist/fillominoGenerate.js +125 -0
- package/dist/fillominoMount.d.ts +2 -0
- package/dist/fillominoMount.js +211 -0
- package/dist/fillominoPlay.types.d.ts +25 -0
- package/dist/fillominoPlay.types.js +1 -0
- package/dist/fillominoSolve.d.ts +6 -0
- package/dist/fillominoSolve.js +111 -0
- package/dist/fillominoStrings.d.ts +102 -0
- package/dist/fillominoStrings.js +13 -0
- package/dist/fillominoStyle.d.ts +1 -0
- package/dist/fillominoStyle.js +13 -0
- package/dist/fillominoWorker.d.ts +1 -0
- package/dist/fillominoWorker.js +10 -0
- package/dist/heyawake-draw-entry.d.ts +2 -0
- package/dist/heyawake-draw-entry.js +1 -0
- package/dist/heyawake-entry.d.ts +13 -0
- package/dist/heyawake-entry.js +11 -0
- package/dist/heyawake-play-entry.d.ts +2 -0
- package/dist/heyawake-play-entry.js +1 -0
- package/dist/heyawake.constants.d.ts +5 -0
- package/dist/heyawake.constants.js +5 -0
- package/dist/heyawake.types.d.ts +35 -0
- package/dist/heyawake.types.js +1 -0
- package/dist/heyawakeBoard.d.ts +7 -0
- package/dist/heyawakeBoard.js +116 -0
- package/dist/heyawakeDraw.d.ts +3 -0
- package/dist/heyawakeDraw.js +27 -0
- package/dist/heyawakeGame.d.ts +12 -0
- package/dist/heyawakeGame.js +51 -0
- package/dist/heyawakeGenerate.d.ts +2 -0
- package/dist/heyawakeGenerate.js +114 -0
- package/dist/heyawakeMount.d.ts +2 -0
- package/dist/heyawakeMount.js +106 -0
- package/dist/heyawakePlay.types.d.ts +24 -0
- package/dist/heyawakePlay.types.js +1 -0
- package/dist/heyawakeSolve.d.ts +5 -0
- package/dist/heyawakeSolve.js +52 -0
- package/dist/heyawakeStrings.d.ts +19 -0
- package/dist/heyawakeStrings.js +7 -0
- package/dist/heyawakeStyle.d.ts +1 -0
- package/dist/heyawakeStyle.js +2 -0
- package/dist/heyawakeWorker.d.ts +1 -0
- package/dist/heyawakeWorker.js +10 -0
- package/dist/hitori-draw-entry.d.ts +2 -0
- package/dist/hitori-draw-entry.js +1 -0
- package/dist/hitori-entry.d.ts +6 -0
- package/dist/hitori-entry.js +6 -0
- package/dist/hitori-play-entry.d.ts +4 -0
- package/dist/hitori-play-entry.js +3 -0
- package/dist/hitori.constants.d.ts +2 -0
- package/dist/hitori.constants.js +2 -0
- package/dist/hitori.types.d.ts +20 -0
- package/dist/hitori.types.js +1 -0
- package/dist/hitoriBoard.d.ts +8 -0
- package/dist/hitoriBoard.js +72 -0
- package/dist/hitoriDraw.d.ts +4 -0
- package/dist/hitoriDraw.js +25 -0
- package/dist/hitoriGame.d.ts +10 -0
- package/dist/hitoriGame.js +48 -0
- package/dist/hitoriGenerate.d.ts +3 -0
- package/dist/hitoriGenerate.js +163 -0
- package/dist/hitoriMount.d.ts +3 -0
- package/dist/hitoriMount.js +111 -0
- package/dist/hitoriPlay.types.d.ts +25 -0
- package/dist/hitoriPlay.types.js +1 -0
- package/dist/hitoriSolve.d.ts +6 -0
- package/dist/hitoriSolve.js +69 -0
- package/dist/hitoriStrings.d.ts +70 -0
- package/dist/hitoriStrings.js +17 -0
- package/dist/hitoriStyle.d.ts +1 -0
- package/dist/hitoriStyle.js +9 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/juosan-draw-entry.d.ts +2 -0
- package/dist/juosan-draw-entry.js +1 -0
- package/dist/juosan-entry.d.ts +11 -0
- package/dist/juosan-entry.js +10 -0
- package/dist/juosan-play-entry.d.ts +4 -0
- package/dist/juosan-play-entry.js +3 -0
- package/dist/juosan.constants.d.ts +4 -0
- package/dist/juosan.constants.js +4 -0
- package/dist/juosan.types.d.ts +33 -0
- package/dist/juosan.types.js +1 -0
- package/dist/juosanBoard.d.ts +7 -0
- package/dist/juosanBoard.js +102 -0
- package/dist/juosanDraw.d.ts +4 -0
- package/dist/juosanDraw.js +49 -0
- package/dist/juosanGame.d.ts +20 -0
- package/dist/juosanGame.js +86 -0
- package/dist/juosanGenerate.d.ts +3 -0
- package/dist/juosanGenerate.js +31 -0
- package/dist/juosanMount.d.ts +3 -0
- package/dist/juosanMount.js +206 -0
- package/dist/juosanPlay.types.d.ts +24 -0
- package/dist/juosanPlay.types.js +1 -0
- package/dist/juosanSolve.d.ts +3 -0
- package/dist/juosanSolve.js +82 -0
- package/dist/juosanStrings.d.ts +77 -0
- package/dist/juosanStrings.js +43 -0
- package/dist/juosanStyle.d.ts +1 -0
- package/dist/juosanStyle.js +1 -0
- package/dist/kakuro-draw-entry.d.ts +2 -0
- package/dist/kakuro-draw-entry.js +1 -0
- package/dist/kakuro-entry.d.ts +12 -0
- package/dist/kakuro-entry.js +10 -0
- package/dist/kakuro-play-entry.d.ts +4 -0
- package/dist/kakuro-play-entry.js +3 -0
- package/dist/kakuro.constants.d.ts +6 -0
- package/dist/kakuro.constants.js +6 -0
- package/dist/kakuro.types.d.ts +46 -0
- package/dist/kakuro.types.js +1 -0
- package/dist/kakuroBoard.d.ts +8 -0
- package/dist/kakuroBoard.js +117 -0
- package/dist/kakuroDraw.d.ts +10 -0
- package/dist/kakuroDraw.js +21 -0
- package/dist/kakuroGame.d.ts +12 -0
- package/dist/kakuroGame.js +74 -0
- package/dist/kakuroGenerate.d.ts +3 -0
- package/dist/kakuroGenerate.js +130 -0
- package/dist/kakuroMount.d.ts +2 -0
- package/dist/kakuroMount.js +109 -0
- package/dist/kakuroPlay.types.d.ts +20 -0
- package/dist/kakuroPlay.types.js +1 -0
- package/dist/kakuroSolve.d.ts +6 -0
- package/dist/kakuroSolve.js +85 -0
- package/dist/kakuroStrings.d.ts +82 -0
- package/dist/kakuroStrings.js +5 -0
- package/dist/kakuroStyle.d.ts +1 -0
- package/dist/kakuroStyle.js +1 -0
- package/dist/masyu-draw-entry.d.ts +2 -0
- package/dist/masyu-draw-entry.js +1 -0
- package/dist/masyu-entry.d.ts +6 -0
- package/dist/masyu-entry.js +6 -0
- package/dist/masyu-play-entry.d.ts +4 -0
- package/dist/masyu-play-entry.js +3 -0
- package/dist/masyu.constants.d.ts +2 -0
- package/dist/masyu.constants.js +2 -0
- package/dist/masyu.types.d.ts +46 -0
- package/dist/masyu.types.js +1 -0
- package/dist/masyuBoard.d.ts +10 -0
- package/dist/masyuBoard.js +88 -0
- package/dist/masyuDraw.d.ts +2 -0
- package/dist/masyuDraw.js +33 -0
- package/dist/masyuGame.d.ts +9 -0
- package/dist/masyuGame.js +46 -0
- package/dist/masyuGenerate.d.ts +3 -0
- package/dist/masyuGenerate.js +56 -0
- package/dist/masyuMount.d.ts +2 -0
- package/dist/masyuMount.js +118 -0
- package/dist/masyuPlay.types.d.ts +1 -0
- package/dist/masyuPlay.types.js +1 -0
- package/dist/masyuSolve.d.ts +6 -0
- package/dist/masyuSolve.js +88 -0
- package/dist/masyuStrings.d.ts +28 -0
- package/dist/masyuStrings.js +4 -0
- package/dist/masyuStyle.d.ts +1 -0
- package/dist/masyuStyle.js +11 -0
- package/dist/nurikabe-draw-entry.d.ts +2 -0
- package/dist/nurikabe-draw-entry.js +1 -0
- package/dist/nurikabe-entry.d.ts +6 -0
- package/dist/nurikabe-entry.js +6 -0
- package/dist/nurikabe-play-entry.d.ts +4 -0
- package/dist/nurikabe-play-entry.js +3 -0
- package/dist/nurikabe.constants.d.ts +2 -0
- package/dist/nurikabe.constants.js +2 -0
- package/dist/nurikabe.types.d.ts +20 -0
- package/dist/nurikabe.types.js +1 -0
- package/dist/nurikabeBoard.d.ts +8 -0
- package/dist/nurikabeBoard.js +72 -0
- package/dist/nurikabeDraw.d.ts +4 -0
- package/dist/nurikabeDraw.js +15 -0
- package/dist/nurikabeGame.d.ts +8 -0
- package/dist/nurikabeGame.js +40 -0
- package/dist/nurikabeGenerate.d.ts +3 -0
- package/dist/nurikabeGenerate.js +31 -0
- package/dist/nurikabeMount.d.ts +3 -0
- package/dist/nurikabeMount.js +99 -0
- package/dist/nurikabePlay.types.d.ts +25 -0
- package/dist/nurikabePlay.types.js +1 -0
- package/dist/nurikabeSolve.d.ts +6 -0
- package/dist/nurikabeSolve.js +75 -0
- package/dist/nurikabeStrings.d.ts +70 -0
- package/dist/nurikabeStrings.js +5 -0
- package/dist/nurikabeStyle.d.ts +1 -0
- package/dist/nurikabeStyle.js +9 -0
- package/dist/play-entry.d.ts +1 -0
- package/dist/play-entry.js +1 -0
- package/dist/ripple-draw-entry.d.ts +2 -0
- package/dist/ripple-draw-entry.js +1 -0
- package/dist/ripple-entry.d.ts +12 -0
- package/dist/ripple-entry.js +11 -0
- package/dist/ripple-play-entry.d.ts +4 -0
- package/dist/ripple-play-entry.js +3 -0
- package/dist/ripple.constants.d.ts +4 -0
- package/dist/ripple.constants.js +4 -0
- package/dist/ripple.types.d.ts +34 -0
- package/dist/ripple.types.js +1 -0
- package/dist/rippleBoard.d.ts +7 -0
- package/dist/rippleBoard.js +99 -0
- package/dist/rippleDraw.d.ts +3 -0
- package/dist/rippleDraw.js +32 -0
- package/dist/rippleGame.d.ts +13 -0
- package/dist/rippleGame.js +85 -0
- package/dist/rippleGenerate.d.ts +3 -0
- package/dist/rippleGenerate.js +57 -0
- package/dist/rippleMount.d.ts +2 -0
- package/dist/rippleMount.js +179 -0
- package/dist/ripplePlay.types.d.ts +23 -0
- package/dist/ripplePlay.types.js +1 -0
- package/dist/rippleSolve.d.ts +6 -0
- package/dist/rippleSolve.js +83 -0
- package/dist/rippleStrings.d.ts +86 -0
- package/dist/rippleStrings.js +5 -0
- package/dist/rippleStyle.d.ts +1 -0
- package/dist/rippleStyle.js +1 -0
- package/dist/shikaku-draw-entry.d.ts +2 -0
- package/dist/shikaku-draw-entry.js +1 -0
- package/dist/shikaku-entry.d.ts +8 -0
- package/dist/shikaku-entry.js +8 -0
- package/dist/shikaku-play-entry.d.ts +4 -0
- package/dist/shikaku-play-entry.js +3 -0
- package/dist/shikaku.constants.d.ts +5 -0
- package/dist/shikaku.constants.js +5 -0
- package/dist/shikaku.types.d.ts +36 -0
- package/dist/shikaku.types.js +1 -0
- package/dist/shikakuBoard.d.ts +9 -0
- package/dist/shikakuBoard.js +59 -0
- package/dist/shikakuDraw.d.ts +4 -0
- package/dist/shikakuDraw.js +23 -0
- package/dist/shikakuGame.d.ts +14 -0
- package/dist/shikakuGame.js +61 -0
- package/dist/shikakuGenerate.d.ts +3 -0
- package/dist/shikakuGenerate.js +43 -0
- package/dist/shikakuMount.d.ts +3 -0
- package/dist/shikakuMount.js +143 -0
- package/dist/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -0
- package/dist/shikakuPlay.types.d.ts +26 -0
- package/dist/shikakuPlay.types.js +1 -0
- package/dist/shikakuSolve.d.ts +6 -0
- package/dist/shikakuSolve.js +54 -0
- package/dist/shikakuStrings.d.ts +102 -0
- package/dist/shikakuStrings.js +5 -0
- package/dist/shikakuStyle.d.ts +1 -0
- package/dist/shikakuStyle.js +12 -0
- package/dist/shikakuWorker.d.ts +1 -0
- package/dist/shikakuWorker.js +10 -0
- package/dist/slitherlink-draw-entry.d.ts +2 -0
- package/dist/slitherlink-draw-entry.js +1 -0
- package/dist/slitherlink-entry.d.ts +11 -0
- package/dist/slitherlink-entry.js +9 -0
- package/dist/slitherlink-play-entry.d.ts +4 -0
- package/dist/slitherlink-play-entry.js +3 -0
- package/dist/slitherlink.constants.d.ts +2 -0
- package/dist/slitherlink.constants.js +2 -0
- package/dist/slitherlink.types.d.ts +34 -0
- package/dist/slitherlink.types.js +1 -0
- package/dist/slitherlinkBoard.d.ts +12 -0
- package/dist/slitherlinkBoard.js +132 -0
- package/dist/slitherlinkDraw.d.ts +4 -0
- package/dist/slitherlinkDraw.js +51 -0
- package/dist/slitherlinkGame.d.ts +8 -0
- package/dist/slitherlinkGame.js +63 -0
- package/dist/slitherlinkGenerate.d.ts +3 -0
- package/dist/slitherlinkGenerate.js +101 -0
- package/dist/slitherlinkMount.d.ts +3 -0
- package/dist/slitherlinkMount.js +267 -0
- package/dist/slitherlinkPlay.types.d.ts +23 -0
- package/dist/slitherlinkPlay.types.js +1 -0
- package/dist/slitherlinkSolve.d.ts +6 -0
- package/dist/slitherlinkSolve.js +127 -0
- package/dist/slitherlinkStrings.d.ts +86 -0
- package/dist/slitherlinkStrings.js +47 -0
- package/dist/slitherlinkStyle.d.ts +1 -0
- package/dist/slitherlinkStyle.js +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/yajilin-draw-entry.d.ts +2 -0
- package/dist/yajilin-draw-entry.js +1 -0
- package/dist/yajilin-entry.d.ts +6 -0
- package/dist/yajilin-entry.js +6 -0
- package/dist/yajilin-play-entry.d.ts +4 -0
- package/dist/yajilin-play-entry.js +3 -0
- package/dist/yajilin.constants.d.ts +2 -0
- package/dist/yajilin.constants.js +2 -0
- package/dist/yajilin.types.d.ts +63 -0
- package/dist/yajilin.types.js +1 -0
- package/dist/yajilinBoard.d.ts +9 -0
- package/dist/yajilinBoard.js +91 -0
- package/dist/yajilinDraw.d.ts +2 -0
- package/dist/yajilinDraw.js +33 -0
- package/dist/yajilinGame.d.ts +15 -0
- package/dist/yajilinGame.js +58 -0
- package/dist/yajilinGenerate.d.ts +3 -0
- package/dist/yajilinGenerate.js +224 -0
- package/dist/yajilinMount.d.ts +2 -0
- package/dist/yajilinMount.js +162 -0
- package/dist/yajilinPlay.types.d.ts +1 -0
- package/dist/yajilinPlay.types.js +1 -0
- package/dist/yajilinSolve.d.ts +6 -0
- package/dist/yajilinSolve.js +82 -0
- package/dist/yajilinStrings.d.ts +32 -0
- package/dist/yajilinStrings.js +4 -0
- package/dist/yajilinStyle.d.ts +1 -0
- package/dist/yajilinStyle.js +12 -0
- package/package.json +196 -3
|
@@ -0,0 +1,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];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SLITHERLINK_MOST_SIDE } from "./slitherlink.constants.js";
|
|
2
|
+
export function isSlitherlinkBoard(value) {
|
|
3
|
+
if (!value || typeof value !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
const board = value;
|
|
6
|
+
return Number.isInteger(board.width) && board.width >= 2 && board.width <= SLITHERLINK_MOST_SIDE
|
|
7
|
+
&& Number.isInteger(board.height) && board.height >= 2 && board.height <= SLITHERLINK_MOST_SIDE
|
|
8
|
+
&& Array.isArray(board.clues) && board.clues.length === board.width * board.height
|
|
9
|
+
&& board.clues.every(clue => clue === null || Number.isInteger(clue) && clue >= 0 && clue <= 3);
|
|
10
|
+
}
|
|
11
|
+
export function slitherlinkHorizontalCount(board) {
|
|
12
|
+
return board.width * (board.height + 1);
|
|
13
|
+
}
|
|
14
|
+
/** An edge index is horizontal row-major first, then vertical row-major. */
|
|
15
|
+
export function slitherlinkEdgeCount(board) {
|
|
16
|
+
return slitherlinkHorizontalCount(board) + (board.width + 1) * board.height;
|
|
17
|
+
}
|
|
18
|
+
export function slitherlinkCellEdges(board, cell) {
|
|
19
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= board.width * board.height)
|
|
20
|
+
return null;
|
|
21
|
+
const x = cell % board.width;
|
|
22
|
+
const y = Math.floor(cell / board.width);
|
|
23
|
+
const horizontal = slitherlinkHorizontalCount(board);
|
|
24
|
+
const top = y * board.width + x;
|
|
25
|
+
const bottom = (y + 1) * board.width + x;
|
|
26
|
+
const left = horizontal + y * (board.width + 1) + x;
|
|
27
|
+
const right = left + 1;
|
|
28
|
+
return [top, right, bottom, left];
|
|
29
|
+
}
|
|
30
|
+
export function slitherlinkVertexEdges(board, vertex) {
|
|
31
|
+
const vertexWidth = board.width + 1;
|
|
32
|
+
if (!Number.isInteger(vertex) || vertex < 0 || vertex >= vertexWidth * (board.height + 1))
|
|
33
|
+
return [];
|
|
34
|
+
const x = vertex % vertexWidth;
|
|
35
|
+
const y = Math.floor(vertex / vertexWidth);
|
|
36
|
+
const horizontal = slitherlinkHorizontalCount(board);
|
|
37
|
+
const edges = [];
|
|
38
|
+
if (x > 0)
|
|
39
|
+
edges.push(y * board.width + x - 1);
|
|
40
|
+
if (x < board.width)
|
|
41
|
+
edges.push(y * board.width + x);
|
|
42
|
+
if (y > 0)
|
|
43
|
+
edges.push(horizontal + (y - 1) * vertexWidth + x);
|
|
44
|
+
if (y < board.height)
|
|
45
|
+
edges.push(horizontal + y * vertexWidth + x);
|
|
46
|
+
return edges;
|
|
47
|
+
}
|
|
48
|
+
function evaluate(board, selected, complete) {
|
|
49
|
+
if (!isSlitherlinkBoard(board))
|
|
50
|
+
throw new RangeError("Invalid Slitherlink board");
|
|
51
|
+
const edgeCount = slitherlinkEdgeCount(board);
|
|
52
|
+
const errors = selected.filter((edge, index) => !Number.isInteger(edge)
|
|
53
|
+
|| edge < 0 || edge >= edgeCount || selected.indexOf(edge) !== index);
|
|
54
|
+
const edges = [...new Set(selected.filter(edge => Number.isInteger(edge) && edge >= 0 && edge < edgeCount))];
|
|
55
|
+
const edgeSet = new Set(edges);
|
|
56
|
+
const vertexDegrees = Array.from({ length: (board.width + 1) * (board.height + 1) }, (_, vertex) => slitherlinkVertexEdges(board, vertex).filter(edge => edgeSet.has(edge)).length);
|
|
57
|
+
const vertices = vertexDegrees.flatMap((degree, vertex) => degree > 2 || complete && degree === 1 ? [vertex] : []);
|
|
58
|
+
const clues = board.clues.flatMap((clue, cell) => {
|
|
59
|
+
if (clue === null)
|
|
60
|
+
return [];
|
|
61
|
+
const count = slitherlinkCellEdges(board, cell).filter(edge => edgeSet.has(edge)).length;
|
|
62
|
+
return complete ? count !== clue ? [cell] : [] : count > clue ? [cell] : [];
|
|
63
|
+
});
|
|
64
|
+
const closed = closedComponents(board, edgeSet);
|
|
65
|
+
const loops = closed.count;
|
|
66
|
+
const detachedClosedLoop = loops > 0 && closed.edges !== edges.length;
|
|
67
|
+
const loopError = complete
|
|
68
|
+
? edges.length === 0 || loops !== 1
|
|
69
|
+
: loops > 1 || detachedClosedLoop;
|
|
70
|
+
return {
|
|
71
|
+
ok: !errors.length && !vertices.length && !clues.length && !loopError,
|
|
72
|
+
errors,
|
|
73
|
+
clues,
|
|
74
|
+
vertices,
|
|
75
|
+
loops,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Checks all numbered cells, degree-two vertices, and one connected closed loop. */
|
|
79
|
+
export function checkSlitherlink(board, edges) {
|
|
80
|
+
return evaluate(board, edges, true);
|
|
81
|
+
}
|
|
82
|
+
/** Reports overfilled clues and branched vertices during play without requiring a finished loop. */
|
|
83
|
+
export function progressSlitherlink(board, edges) {
|
|
84
|
+
const result = evaluate(board, edges, false);
|
|
85
|
+
return { ok: result.ok, clues: result.clues, vertices: result.vertices, loops: result.loops };
|
|
86
|
+
}
|
|
87
|
+
function closedComponents(board, edges) {
|
|
88
|
+
if (!edges.size)
|
|
89
|
+
return { count: 0, edges: 0 };
|
|
90
|
+
const unseen = new Set(edges);
|
|
91
|
+
let count = 0;
|
|
92
|
+
let closedEdges = 0;
|
|
93
|
+
while (unseen.size) {
|
|
94
|
+
const first = unseen.values().next().value;
|
|
95
|
+
const component = new Set([first]);
|
|
96
|
+
const queue = [first];
|
|
97
|
+
unseen.delete(first);
|
|
98
|
+
while (queue.length) {
|
|
99
|
+
const edge = queue.pop();
|
|
100
|
+
for (const vertex of edgeVertices(board, edge)) {
|
|
101
|
+
for (const neighbour of slitherlinkVertexEdges(board, vertex)) {
|
|
102
|
+
if (!unseen.has(neighbour))
|
|
103
|
+
continue;
|
|
104
|
+
unseen.delete(neighbour);
|
|
105
|
+
component.add(neighbour);
|
|
106
|
+
queue.push(neighbour);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const degreeTwo = [...component].every(edge => edgeVertices(board, edge)
|
|
111
|
+
.every(vertex => slitherlinkVertexEdges(board, vertex).filter(adjacent => component.has(adjacent)).length === 2));
|
|
112
|
+
if (degreeTwo) {
|
|
113
|
+
count += 1;
|
|
114
|
+
closedEdges += component.size;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { count, edges: closedEdges };
|
|
118
|
+
}
|
|
119
|
+
export function edgeVertices(board, edge) {
|
|
120
|
+
const horizontal = slitherlinkHorizontalCount(board);
|
|
121
|
+
if (edge < horizontal) {
|
|
122
|
+
const x = edge % board.width;
|
|
123
|
+
const y = Math.floor(edge / board.width);
|
|
124
|
+
const left = y * (board.width + 1) + x;
|
|
125
|
+
return [left, left + 1];
|
|
126
|
+
}
|
|
127
|
+
const local = edge - horizontal;
|
|
128
|
+
const x = local % (board.width + 1);
|
|
129
|
+
const y = Math.floor(local / (board.width + 1));
|
|
130
|
+
const top = y * (board.width + 1) + x;
|
|
131
|
+
return [top, top + board.width + 1];
|
|
132
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SlitherlinkBoard } from "./slitherlink.types.ts";
|
|
2
|
+
import type { SlitherlinkDrawOptions } from "./slitherlinkPlay.types.ts";
|
|
3
|
+
/** Draws public clues and selected loop edges as an SVG board. */
|
|
4
|
+
export declare function drawSlitherlink(board: SlitherlinkBoard, options?: SlitherlinkDrawOptions): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { edgeVertices, isSlitherlinkBoard, } from "./slitherlinkBoard.js";
|
|
2
|
+
import { slitherlinkWords } from "./slitherlinkStrings.js";
|
|
3
|
+
/** Draws public clues and selected loop edges as an SVG board. */
|
|
4
|
+
export function drawSlitherlink(board, options = {}) {
|
|
5
|
+
if (!isSlitherlinkBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Slitherlink board");
|
|
7
|
+
const words = slitherlinkWords(options.language);
|
|
8
|
+
const size = 48;
|
|
9
|
+
const colours = {
|
|
10
|
+
ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"],
|
|
11
|
+
wood: ["#e5cda6", "#493e2f", "#bca582"],
|
|
12
|
+
slate: ["#262a27", "#ece8dc", "#454a44"],
|
|
13
|
+
};
|
|
14
|
+
const [paper, ink, grid] = colours[options.material ?? "ivory"];
|
|
15
|
+
const selected = new Set(options.edges ?? []);
|
|
16
|
+
const clueText = board.clues.map((clue, cell) => {
|
|
17
|
+
if (clue === null)
|
|
18
|
+
return "";
|
|
19
|
+
const x = (cell % board.width + .5) * size;
|
|
20
|
+
const y = (Math.floor(cell / board.width) + .53) * size;
|
|
21
|
+
return `<text x="${x}" y="${y}" dominant-baseline="middle" text-anchor="middle" fill="${ink}" font-size="20" font-weight="700" font-family="system-ui,sans-serif">${clue}</text>`;
|
|
22
|
+
}).join("");
|
|
23
|
+
const lines = [...selected].map(edge => {
|
|
24
|
+
const [a, b] = edgeVertices(board, edge);
|
|
25
|
+
const x1 = (a % (board.width + 1)) * size;
|
|
26
|
+
const y1 = Math.floor(a / (board.width + 1)) * size;
|
|
27
|
+
const x2 = (b % (board.width + 1)) * size;
|
|
28
|
+
const y2 = Math.floor(b / (board.width + 1)) * size;
|
|
29
|
+
const bad = options.errors?.includes(edge);
|
|
30
|
+
const active = options.selected === edge;
|
|
31
|
+
const colour = bad ? "var(--kz-bad,#b5452c)" : active ? "var(--kz-frame,#a98954)" : ink;
|
|
32
|
+
return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${colour}" stroke-width="${active ? 6 : 4}" stroke-linecap="round"/>`;
|
|
33
|
+
}).join("");
|
|
34
|
+
const dots = Array.from({ length: (board.width + 1) * (board.height + 1) }, (_, vertex) => {
|
|
35
|
+
const x = (vertex % (board.width + 1)) * size;
|
|
36
|
+
const y = Math.floor(vertex / (board.width + 1)) * size;
|
|
37
|
+
return `<circle cx="${x}" cy="${y}" r="3.2" fill="${ink}"/>`;
|
|
38
|
+
}).join("");
|
|
39
|
+
const gridLines = [];
|
|
40
|
+
for (let y = 0; y <= board.height; y += 1) {
|
|
41
|
+
for (let x = 0; x < board.width; x += 1) {
|
|
42
|
+
gridLines.push(`<line x1="${x * size}" y1="${y * size}" x2="${(x + 1) * size}" y2="${y * size}" stroke="${grid}" stroke-width="1"/>`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (let y = 0; y < board.height; y += 1) {
|
|
46
|
+
for (let x = 0; x <= board.width; x += 1) {
|
|
47
|
+
gridLines.push(`<line x1="${x * size}" y1="${y * size}" x2="${x * size}" y2="${(y + 1) * size}" stroke="${grid}" stroke-width="1"/>`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 ${board.width * size + 8} ${board.height * size + 8}" role="img" aria-label="${words.title}" style="display:block;width:100%;height:auto;background:${paper}">${gridLines.join("")}${clueText}${lines}${dots}<rect width="${board.width * size}" height="${board.height * size}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="2"/></svg>`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SlitherlinkBoard, SlitherlinkGame } from "./slitherlink.types.ts";
|
|
2
|
+
export declare function newSlitherlink(board: SlitherlinkBoard): SlitherlinkGame;
|
|
3
|
+
export declare function toggleSlitherlink(game: SlitherlinkGame, edge: number): SlitherlinkGame;
|
|
4
|
+
export declare function undoSlitherlink(game: SlitherlinkGame): SlitherlinkGame;
|
|
5
|
+
export declare function slitherlinkFinished(game: SlitherlinkGame): boolean;
|
|
6
|
+
export declare function hintSlitherlink(game: SlitherlinkGame): number | null;
|
|
7
|
+
export declare function encodeSlitherlink(game: SlitherlinkGame): string;
|
|
8
|
+
export declare function decodeSlitherlink(code: string): SlitherlinkGame | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { checkSlitherlink, isSlitherlinkBoard } from "./slitherlinkBoard.js";
|
|
2
|
+
import { solveSlitherlink } from "./slitherlinkSolve.js";
|
|
3
|
+
export function newSlitherlink(board) {
|
|
4
|
+
if (!isSlitherlinkBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Slitherlink board");
|
|
6
|
+
return {
|
|
7
|
+
board: { width: board.width, height: board.height, clues: [...board.clues] },
|
|
8
|
+
edges: [],
|
|
9
|
+
history: [],
|
|
10
|
+
helped: false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function toggleSlitherlink(game, edge) {
|
|
14
|
+
const maximum = (game.board.height + 1) * game.board.width + game.board.height * (game.board.width + 1);
|
|
15
|
+
if (!Number.isInteger(edge) || edge < 0 || edge >= maximum)
|
|
16
|
+
return game;
|
|
17
|
+
const edges = game.edges.includes(edge)
|
|
18
|
+
? game.edges.filter(selected => selected !== edge)
|
|
19
|
+
: [...game.edges, edge];
|
|
20
|
+
return { ...game, edges, history: [...game.history, [...game.edges]] };
|
|
21
|
+
}
|
|
22
|
+
export function undoSlitherlink(game) {
|
|
23
|
+
const previous = game.history.at(-1);
|
|
24
|
+
return previous
|
|
25
|
+
? { ...game, edges: [...previous], history: game.history.slice(0, -1) }
|
|
26
|
+
: game;
|
|
27
|
+
}
|
|
28
|
+
export function slitherlinkFinished(game) {
|
|
29
|
+
return checkSlitherlink(game.board, game.edges).ok;
|
|
30
|
+
}
|
|
31
|
+
export function hintSlitherlink(game) {
|
|
32
|
+
const solved = solveSlitherlink(game.board);
|
|
33
|
+
if (!solved.complete || solved.count !== 1 || !solved.solution)
|
|
34
|
+
return null;
|
|
35
|
+
return solved.solution.find(edge => !game.edges.includes(edge))
|
|
36
|
+
?? game.edges.find(edge => !solved.solution.includes(edge))
|
|
37
|
+
?? null;
|
|
38
|
+
}
|
|
39
|
+
export function encodeSlitherlink(game) {
|
|
40
|
+
return JSON.stringify({ version: 1, board: game.board, edges: game.edges, helped: game.helped });
|
|
41
|
+
}
|
|
42
|
+
export function decodeSlitherlink(code) {
|
|
43
|
+
try {
|
|
44
|
+
if (code.length > 40000)
|
|
45
|
+
return null;
|
|
46
|
+
const value = JSON.parse(code);
|
|
47
|
+
if (value.version !== 1 || !isSlitherlinkBoard(value.board) || !Array.isArray(value.edges)
|
|
48
|
+
|| value.edges.length > (value.board.width + 1) * (value.board.height + 1) * 2
|
|
49
|
+
|| typeof value.helped !== "boolean" || new Set(value.edges).size !== value.edges.length)
|
|
50
|
+
return null;
|
|
51
|
+
let game = newSlitherlink(value.board);
|
|
52
|
+
for (const edge of value.edges) {
|
|
53
|
+
const next = toggleSlitherlink(game, edge);
|
|
54
|
+
if (next === game)
|
|
55
|
+
return null;
|
|
56
|
+
game = next;
|
|
57
|
+
}
|
|
58
|
+
return { ...game, history: [], helped: value.helped };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -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
|
+
/** Makes a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
|
|
6
|
+
export function generateSlitherlink(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
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SlitherlinkMount, SlitherlinkMountOptions } from "./slitherlinkPlay.types.ts";
|
|
2
|
+
/** Mounts an accessible dot-and-edge loop player in the given host element. */
|
|
3
|
+
export declare function mountSlitherlink(host: HTMLElement, initial: SlitherlinkMountOptions): SlitherlinkMount;
|
|
@@ -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
|
+
}
|