@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
package/dist/shikakuSolve.js
CHANGED
|
@@ -1,54 +1,33 @@
|
|
|
1
1
|
import { SHIKAKU_MOST_NODES } from "./shikaku.constants.js";
|
|
2
|
-
import { checkShikaku, isShikakuBoard
|
|
3
|
-
|
|
2
|
+
import { checkShikaku, isShikakuBoard } from "./shikakuBoard.js";
|
|
3
|
+
import { shikakuModel } from "./shikakuLogic.js";
|
|
4
|
+
import { decide, openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Exact cover over squares. A bounded search reports incompleteness instead of claiming uniqueness. */
|
|
4
6
|
export function solveShikaku(board, placed = [], options = {}) {
|
|
5
7
|
if (!isShikakuBoard(board))
|
|
6
8
|
throw new RangeError("Invalid Shikaku board");
|
|
7
9
|
const limit = options.limit ?? 2, budget = options.nodes ?? SHIKAKU_MOST_NODES;
|
|
8
10
|
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1)
|
|
9
11
|
throw new RangeError("Invalid search bounds");
|
|
10
|
-
const maskOf = (r) => shikakuCells(board, r).reduce((m, c) => m | 1n << BigInt(c), 0n);
|
|
11
12
|
if (checkShikaku(board, placed).errors.length)
|
|
12
13
|
return { count: 0, solution: null, complete: true, nodes: 0 };
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
for (const r of placed)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
if (mask === all) {
|
|
26
|
-
count += 1;
|
|
27
|
-
solution ?? (solution = rectangles.map(r => ({ ...r })));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
let choices = null;
|
|
31
|
-
for (let c = 0; c < board.clues.length; c += 1) {
|
|
32
|
-
if ((mask & 1n << BigInt(c)) !== 0n)
|
|
33
|
-
continue;
|
|
34
|
-
const available = byCell[c].filter(r => (mask & r.mask) === 0n);
|
|
35
|
-
if (!available.length)
|
|
36
|
-
return;
|
|
37
|
-
if (!choices || available.length < choices.length)
|
|
38
|
-
choices = available;
|
|
39
|
-
if (choices.length === 1)
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
for (const next of choices ?? []) {
|
|
43
|
-
visit(mask | next.mask, [...rectangles, next.r]);
|
|
44
|
-
if (!complete)
|
|
45
|
-
return;
|
|
46
|
-
if (count >= limit) {
|
|
47
|
-
complete = false;
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
visit(occupied, placed);
|
|
53
|
-
return { count, solution, complete, nodes };
|
|
14
|
+
const { csp, rectangles, clues } = shikakuModel(board);
|
|
15
|
+
const start = openSlots(csp);
|
|
16
|
+
for (const r of placed) {
|
|
17
|
+
const slot = rectangles.findIndex(c => c.x === r.x && c.y === r.y && c.width === r.width && c.height === r.height);
|
|
18
|
+
if (slot < 0)
|
|
19
|
+
return { count: 0, solution: null, complete: true, nodes: 0 };
|
|
20
|
+
decide(csp, start, clues.indexOf(clueOf(board, r)), slot);
|
|
21
|
+
}
|
|
22
|
+
const found = solveCsp(csp, start, limit, budget);
|
|
23
|
+
const solution = found.solution ? [...placed.map(r => ({ ...r })), ...rectangles.filter((_, slot) => found.solution[slot] === 1 && !placed.some(r => sameRectangle(r, rectangles[slot])))] : null;
|
|
24
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
54
25
|
}
|
|
26
|
+
const sameRectangle = (a, b) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
27
|
+
const clueOf = (board, r) => {
|
|
28
|
+
for (let y = r.y; y < r.y + r.height; y += 1)
|
|
29
|
+
for (let x = r.x; x < r.x + r.width; x += 1)
|
|
30
|
+
if (board.clues[y * board.width + x])
|
|
31
|
+
return y * board.width + x;
|
|
32
|
+
return -1;
|
|
33
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ShikakuLevel, ShikakuPuzzle } from "./shikaku.types.ts";
|
|
2
|
+
/** The first generator, kept as the fallback that cannot fail on a board it can make: seeded cuts into rectangles, accepted when the answer is single. */
|
|
3
|
+
export declare function templateShikaku(width?: number, height?: number, level?: ShikakuLevel, seed?: number): ShikakuPuzzle;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SHIKAKU_LEVELS, SHIKAKU_MOST_ATTEMPTS } from "./shikaku.constants.js";
|
|
2
|
+
import { isShikakuBoard, shikakuCells } from "./shikakuBoard.js";
|
|
3
|
+
import { solveShikaku } from "./shikakuSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/** The first generator, kept as the fallback that cannot fail on a board it can make: seeded cuts into rectangles, accepted when the answer is single. */
|
|
6
|
+
export function templateShikaku(width = 7, height = width, level = "medium", seed = 1) {
|
|
7
|
+
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= 16))
|
|
8
|
+
throw new RangeError("Invalid Shikaku dimensions");
|
|
9
|
+
const empty = { width, height, clues: Array(width * height).fill(0) };
|
|
10
|
+
if (!isKazuSeed(seed) || !SHIKAKU_LEVELS.includes(level)
|
|
11
|
+
|| !isShikakuBoard({ ...empty, clues: [width * height, ...empty.clues.slice(1)] }))
|
|
12
|
+
throw new RangeError("Invalid Shikaku settings");
|
|
13
|
+
const random = seededRandom(seed), maximum = { easy: 5, medium: 9, hard: 15, "extra-hard": 20 }[level];
|
|
14
|
+
for (let attempt = 0; attempt < SHIKAKU_MOST_ATTEMPTS; attempt += 1) {
|
|
15
|
+
const solution = [];
|
|
16
|
+
const split = (r) => {
|
|
17
|
+
if (r.width * r.height <= maximum && (r.width * r.height < 3 || random() < .6)) {
|
|
18
|
+
solution.push(r);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const vertical = r.height === 1 || (r.width > 1 && random() < .5);
|
|
22
|
+
const cut = 1 + Math.floor(random() * ((vertical ? r.width : r.height) - 1));
|
|
23
|
+
if (vertical) {
|
|
24
|
+
split({ ...r, width: cut });
|
|
25
|
+
split({ ...r, x: r.x + cut, width: r.width - cut });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
split({ ...r, height: cut });
|
|
29
|
+
split({ ...r, y: r.y + cut, height: r.height - cut });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
split({ x: 0, y: 0, width, height });
|
|
33
|
+
const clues = [...empty.clues];
|
|
34
|
+
for (const r of solution) {
|
|
35
|
+
const cells = shikakuCells(empty, r);
|
|
36
|
+
clues[cells[Math.floor(random() * cells.length)]] = cells.length;
|
|
37
|
+
}
|
|
38
|
+
const board = { width, height, clues }, counted = solveShikaku(board);
|
|
39
|
+
if (counted.complete && counted.count === 1)
|
|
40
|
+
return { ...board, seed, level, solution: counted.solution };
|
|
41
|
+
}
|
|
42
|
+
throw new Error("No unique Shikaku found within the generation budget; try another seed");
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawSlitherlink } from "./slitherlinkDraw.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Slitherlink uses a dedicated edge model and a single-loop constraint. */
|
|
2
|
+
export * from "./slitherlink.types.ts";
|
|
3
|
+
export * from "./slitherlink.constants.ts";
|
|
4
|
+
export * from "./slitherlinkBoard.ts";
|
|
5
|
+
export * from "./slitherlinkSolve.ts";
|
|
6
|
+
export * from "./slitherlinkGenerate.ts";
|
|
7
|
+
export * from "./slitherlinkRate.ts";
|
|
8
|
+
export * from "./slitherlinkGame.ts";
|
|
9
|
+
export { drawSlitherlink } from "./slitherlinkDraw.ts";
|
|
10
|
+
export type { SlitherlinkDrawOptions } from "./slitherlinkPlay.types.ts";
|
|
11
|
+
export { mountSlitherlink } from "./slitherlinkMount.ts";
|
|
12
|
+
export type * from "./slitherlinkPlay.types.ts";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Slitherlink uses a dedicated edge model and a single-loop constraint. */
|
|
2
|
+
export * from "./slitherlink.types.js";
|
|
3
|
+
export * from "./slitherlink.constants.js";
|
|
4
|
+
export * from "./slitherlinkBoard.js";
|
|
5
|
+
export * from "./slitherlinkSolve.js";
|
|
6
|
+
export * from "./slitherlinkGenerate.js";
|
|
7
|
+
export * from "./slitherlinkRate.js";
|
|
8
|
+
export * from "./slitherlinkGame.js";
|
|
9
|
+
export { drawSlitherlink } from "./slitherlinkDraw.js";
|
|
10
|
+
export { mountSlitherlink } from "./slitherlinkMount.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SLITHERLINK_MOST_SIDE = 10;
|
|
2
|
+
export declare const SLITHERLINK_MOST_NODES = 300000;
|
|
3
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateSlitherlink`. */
|
|
4
|
+
export declare const SLITHERLINK_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
|
|
5
|
+
/** The sides on offer for a square board; any side from 2 to `SLITHERLINK_MOST_SIDE` can be made. */
|
|
6
|
+
export declare const SLITHERLINK_SIZES: readonly [5, 7, 10];
|
|
7
|
+
export declare const SLITHERLINK_MOST_ATTEMPTS = 40;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const SLITHERLINK_MOST_SIDE = 10;
|
|
2
|
+
export const SLITHERLINK_MOST_NODES = 300000;
|
|
3
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateSlitherlink`. */
|
|
4
|
+
export const SLITHERLINK_LEVELS = ["easy", "medium", "hard", "extra-hard"];
|
|
5
|
+
/** The sides on offer for a square board; any side from 2 to `SLITHERLINK_MOST_SIDE` can be made. */
|
|
6
|
+
export const SLITHERLINK_SIZES = [5, 7, 10];
|
|
7
|
+
export const SLITHERLINK_MOST_ATTEMPTS = 40;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type SlitherlinkBoard = {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
clues: readonly (number | null)[];
|
|
5
|
+
};
|
|
6
|
+
export type SlitherlinkLevel = "easy" | "medium" | "hard" | "extra-hard";
|
|
7
|
+
export type SlitherlinkPuzzle = SlitherlinkBoard & {
|
|
8
|
+
seed: number;
|
|
9
|
+
level: SlitherlinkLevel;
|
|
10
|
+
solution: readonly number[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* How hard a board is, measured by solving it. `depth` 0 means the plain rules solve it, 1 that somebody has to
|
|
14
|
+
* suppose an edge drawn or crossed and watch it break, 2 that more than that is needed; `probes` is how many
|
|
15
|
+
* suppositions depth 1 needed. The rest describes the board.
|
|
16
|
+
*/
|
|
17
|
+
export type SlitherlinkRating = {
|
|
18
|
+
depth: 0 | 1 | 2;
|
|
19
|
+
probes: number;
|
|
20
|
+
clues: number;
|
|
21
|
+
/** Numbered squares as a share of all squares. */
|
|
22
|
+
clueShare: number;
|
|
23
|
+
/** Squares numbered 0 as a share of the numbered ones. */
|
|
24
|
+
zeroShare: number;
|
|
25
|
+
/** Edges in the loop. */
|
|
26
|
+
loop: number;
|
|
27
|
+
};
|
|
28
|
+
export type SlitherlinkCheck = {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
errors: readonly number[];
|
|
31
|
+
clues: readonly number[];
|
|
32
|
+
vertices: readonly number[];
|
|
33
|
+
loops: number;
|
|
34
|
+
};
|
|
35
|
+
export type SlitherlinkProgress = {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
clues: readonly number[];
|
|
38
|
+
vertices: readonly number[];
|
|
39
|
+
loops: number;
|
|
40
|
+
};
|
|
41
|
+
export type SlitherlinkSolve = {
|
|
42
|
+
count: number;
|
|
43
|
+
solution: readonly number[] | null;
|
|
44
|
+
complete: boolean;
|
|
45
|
+
nodes: number;
|
|
46
|
+
};
|
|
47
|
+
export type SlitherlinkGame = {
|
|
48
|
+
board: SlitherlinkBoard;
|
|
49
|
+
edges: readonly number[];
|
|
50
|
+
history: readonly (readonly number[])[];
|
|
51
|
+
helped: boolean;
|
|
52
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SlitherlinkBoard, SlitherlinkCheck, SlitherlinkProgress } from "./slitherlink.types.ts";
|
|
2
|
+
export declare function isSlitherlinkBoard(value: unknown): value is SlitherlinkBoard;
|
|
3
|
+
export declare function slitherlinkHorizontalCount(board: SlitherlinkBoard): number;
|
|
4
|
+
/** An edge index is horizontal row-major first, then vertical row-major. */
|
|
5
|
+
export declare function slitherlinkEdgeCount(board: SlitherlinkBoard): number;
|
|
6
|
+
export declare function slitherlinkCellEdges(board: SlitherlinkBoard, cell: number): [number, number, number, number] | null;
|
|
7
|
+
export declare function slitherlinkVertexEdges(board: SlitherlinkBoard, vertex: number): number[];
|
|
8
|
+
/** Checks all numbered cells, degree-two vertices, and one connected closed loop. */
|
|
9
|
+
export declare function checkSlitherlink(board: SlitherlinkBoard, edges: readonly number[]): SlitherlinkCheck;
|
|
10
|
+
/** Reports overfilled clues and branched vertices during play without requiring a finished loop. */
|
|
11
|
+
export declare function progressSlitherlink(board: SlitherlinkBoard, edges: readonly number[]): SlitherlinkProgress;
|
|
12
|
+
export declare function edgeVertices(board: SlitherlinkBoard, edge: number): [number, number];
|
|
@@ -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,10 @@
|
|
|
1
|
+
import type { SlitherlinkLevel, SlitherlinkPuzzle } from "./slitherlink.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Makes a seeded puzzle and proves it has one answer. A random winding loop is grown cell by cell, every square is
|
|
4
|
+
* numbered with how many of its edges the loop uses, and then numbers are taken away for as long as the puzzle can
|
|
5
|
+
* still be solved the way the level asks: easy and medium by the rules alone (easy keeps most numbers), hard by
|
|
6
|
+
* supposing one edge at a time, extra-hard the most-supposing of several such boards. Numbers that say 0 are
|
|
7
|
+
* taken away first, because they say least. If no board of the level is found within the attempts the next level
|
|
8
|
+
* down is tried, and the first generator, which cannot fail, is the last.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateSlitherlink(width?: number, height?: number, seed?: number, level?: SlitherlinkLevel): SlitherlinkPuzzle;
|