@johnmorrisdotca/kazu 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +379 -7
- package/dist/akari-draw-entry.d.ts +2 -0
- package/dist/akari-draw-entry.js +1 -0
- package/dist/akari-entry.d.ts +12 -0
- package/dist/akari-entry.js +10 -0
- package/dist/akari-play-entry.d.ts +2 -0
- package/dist/akari-play-entry.js +1 -0
- package/dist/akari.constants.d.ts +2 -0
- package/dist/akari.constants.js +2 -0
- package/dist/akari.types.d.ts +33 -0
- package/dist/akari.types.js +1 -0
- package/dist/akariBoard.d.ts +11 -0
- package/dist/akariBoard.js +96 -0
- package/dist/akariDraw.d.ts +4 -0
- package/dist/akariDraw.js +38 -0
- package/dist/akariGame.d.ts +15 -0
- package/dist/akariGame.js +71 -0
- package/dist/akariGenerate.d.ts +7 -0
- package/dist/akariGenerate.js +78 -0
- package/dist/akariMount.d.ts +3 -0
- package/dist/akariMount.js +214 -0
- package/dist/akariPlay.types.d.ts +25 -0
- package/dist/akariPlay.types.js +1 -0
- package/dist/akariSolve.d.ts +6 -0
- package/dist/akariSolve.js +106 -0
- package/dist/akariStrings.d.ts +98 -0
- package/dist/akariStrings.js +53 -0
- package/dist/akariStyle.d.ts +1 -0
- package/dist/akariStyle.js +9 -0
- package/dist/fillomino-draw-entry.d.ts +2 -0
- package/dist/fillomino-draw-entry.js +1 -0
- package/dist/fillomino-entry.d.ts +8 -0
- package/dist/fillomino-entry.js +7 -0
- package/dist/fillomino-play-entry.d.ts +4 -0
- package/dist/fillomino-play-entry.js +3 -0
- package/dist/fillomino.constants.d.ts +5 -0
- package/dist/fillomino.constants.js +5 -0
- package/dist/fillomino.types.d.ts +30 -0
- package/dist/fillomino.types.js +1 -0
- package/dist/fillominoBoard.d.ts +6 -0
- package/dist/fillominoBoard.js +84 -0
- package/dist/fillominoDraw.d.ts +3 -0
- package/dist/fillominoDraw.js +31 -0
- package/dist/fillominoGame.d.ts +13 -0
- package/dist/fillominoGame.js +65 -0
- package/dist/fillominoGenerate.d.ts +3 -0
- package/dist/fillominoGenerate.js +125 -0
- package/dist/fillominoMount.d.ts +2 -0
- package/dist/fillominoMount.js +211 -0
- package/dist/fillominoPlay.types.d.ts +25 -0
- package/dist/fillominoPlay.types.js +1 -0
- package/dist/fillominoSolve.d.ts +6 -0
- package/dist/fillominoSolve.js +111 -0
- package/dist/fillominoStrings.d.ts +102 -0
- package/dist/fillominoStrings.js +13 -0
- package/dist/fillominoStyle.d.ts +1 -0
- package/dist/fillominoStyle.js +13 -0
- package/dist/fillominoWorker.d.ts +1 -0
- package/dist/fillominoWorker.js +10 -0
- package/dist/heyawake-draw-entry.d.ts +2 -0
- package/dist/heyawake-draw-entry.js +1 -0
- package/dist/heyawake-entry.d.ts +13 -0
- package/dist/heyawake-entry.js +11 -0
- package/dist/heyawake-play-entry.d.ts +2 -0
- package/dist/heyawake-play-entry.js +1 -0
- package/dist/heyawake.constants.d.ts +5 -0
- package/dist/heyawake.constants.js +5 -0
- package/dist/heyawake.types.d.ts +35 -0
- package/dist/heyawake.types.js +1 -0
- package/dist/heyawakeBoard.d.ts +7 -0
- package/dist/heyawakeBoard.js +116 -0
- package/dist/heyawakeDraw.d.ts +3 -0
- package/dist/heyawakeDraw.js +27 -0
- package/dist/heyawakeGame.d.ts +12 -0
- package/dist/heyawakeGame.js +51 -0
- package/dist/heyawakeGenerate.d.ts +2 -0
- package/dist/heyawakeGenerate.js +114 -0
- package/dist/heyawakeMount.d.ts +2 -0
- package/dist/heyawakeMount.js +106 -0
- package/dist/heyawakePlay.types.d.ts +24 -0
- package/dist/heyawakePlay.types.js +1 -0
- package/dist/heyawakeSolve.d.ts +5 -0
- package/dist/heyawakeSolve.js +52 -0
- package/dist/heyawakeStrings.d.ts +19 -0
- package/dist/heyawakeStrings.js +7 -0
- package/dist/heyawakeStyle.d.ts +1 -0
- package/dist/heyawakeStyle.js +2 -0
- package/dist/heyawakeWorker.d.ts +1 -0
- package/dist/heyawakeWorker.js +10 -0
- package/dist/hitori-draw-entry.d.ts +2 -0
- package/dist/hitori-draw-entry.js +1 -0
- package/dist/hitori-entry.d.ts +6 -0
- package/dist/hitori-entry.js +6 -0
- package/dist/hitori-play-entry.d.ts +4 -0
- package/dist/hitori-play-entry.js +3 -0
- package/dist/hitori.constants.d.ts +2 -0
- package/dist/hitori.constants.js +2 -0
- package/dist/hitori.types.d.ts +20 -0
- package/dist/hitori.types.js +1 -0
- package/dist/hitoriBoard.d.ts +8 -0
- package/dist/hitoriBoard.js +72 -0
- package/dist/hitoriDraw.d.ts +4 -0
- package/dist/hitoriDraw.js +25 -0
- package/dist/hitoriGame.d.ts +10 -0
- package/dist/hitoriGame.js +48 -0
- package/dist/hitoriGenerate.d.ts +3 -0
- package/dist/hitoriGenerate.js +163 -0
- package/dist/hitoriMount.d.ts +3 -0
- package/dist/hitoriMount.js +111 -0
- package/dist/hitoriPlay.types.d.ts +25 -0
- package/dist/hitoriPlay.types.js +1 -0
- package/dist/hitoriSolve.d.ts +6 -0
- package/dist/hitoriSolve.js +69 -0
- package/dist/hitoriStrings.d.ts +70 -0
- package/dist/hitoriStrings.js +17 -0
- package/dist/hitoriStyle.d.ts +1 -0
- package/dist/hitoriStyle.js +9 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/juosan-draw-entry.d.ts +2 -0
- package/dist/juosan-draw-entry.js +1 -0
- package/dist/juosan-entry.d.ts +11 -0
- package/dist/juosan-entry.js +10 -0
- package/dist/juosan-play-entry.d.ts +4 -0
- package/dist/juosan-play-entry.js +3 -0
- package/dist/juosan.constants.d.ts +4 -0
- package/dist/juosan.constants.js +4 -0
- package/dist/juosan.types.d.ts +33 -0
- package/dist/juosan.types.js +1 -0
- package/dist/juosanBoard.d.ts +7 -0
- package/dist/juosanBoard.js +102 -0
- package/dist/juosanDraw.d.ts +4 -0
- package/dist/juosanDraw.js +49 -0
- package/dist/juosanGame.d.ts +20 -0
- package/dist/juosanGame.js +86 -0
- package/dist/juosanGenerate.d.ts +3 -0
- package/dist/juosanGenerate.js +31 -0
- package/dist/juosanMount.d.ts +3 -0
- package/dist/juosanMount.js +206 -0
- package/dist/juosanPlay.types.d.ts +24 -0
- package/dist/juosanPlay.types.js +1 -0
- package/dist/juosanSolve.d.ts +3 -0
- package/dist/juosanSolve.js +82 -0
- package/dist/juosanStrings.d.ts +77 -0
- package/dist/juosanStrings.js +43 -0
- package/dist/juosanStyle.d.ts +1 -0
- package/dist/juosanStyle.js +1 -0
- package/dist/kakuro-draw-entry.d.ts +2 -0
- package/dist/kakuro-draw-entry.js +1 -0
- package/dist/kakuro-entry.d.ts +12 -0
- package/dist/kakuro-entry.js +10 -0
- package/dist/kakuro-play-entry.d.ts +4 -0
- package/dist/kakuro-play-entry.js +3 -0
- package/dist/kakuro.constants.d.ts +6 -0
- package/dist/kakuro.constants.js +6 -0
- package/dist/kakuro.types.d.ts +46 -0
- package/dist/kakuro.types.js +1 -0
- package/dist/kakuroBoard.d.ts +8 -0
- package/dist/kakuroBoard.js +117 -0
- package/dist/kakuroDraw.d.ts +10 -0
- package/dist/kakuroDraw.js +21 -0
- package/dist/kakuroGame.d.ts +12 -0
- package/dist/kakuroGame.js +74 -0
- package/dist/kakuroGenerate.d.ts +3 -0
- package/dist/kakuroGenerate.js +130 -0
- package/dist/kakuroMount.d.ts +2 -0
- package/dist/kakuroMount.js +109 -0
- package/dist/kakuroPlay.types.d.ts +20 -0
- package/dist/kakuroPlay.types.js +1 -0
- package/dist/kakuroSolve.d.ts +6 -0
- package/dist/kakuroSolve.js +85 -0
- package/dist/kakuroStrings.d.ts +82 -0
- package/dist/kakuroStrings.js +5 -0
- package/dist/kakuroStyle.d.ts +1 -0
- package/dist/kakuroStyle.js +1 -0
- package/dist/masyu-draw-entry.d.ts +2 -0
- package/dist/masyu-draw-entry.js +1 -0
- package/dist/masyu-entry.d.ts +6 -0
- package/dist/masyu-entry.js +6 -0
- package/dist/masyu-play-entry.d.ts +4 -0
- package/dist/masyu-play-entry.js +3 -0
- package/dist/masyu.constants.d.ts +2 -0
- package/dist/masyu.constants.js +2 -0
- package/dist/masyu.types.d.ts +46 -0
- package/dist/masyu.types.js +1 -0
- package/dist/masyuBoard.d.ts +10 -0
- package/dist/masyuBoard.js +88 -0
- package/dist/masyuDraw.d.ts +2 -0
- package/dist/masyuDraw.js +33 -0
- package/dist/masyuGame.d.ts +9 -0
- package/dist/masyuGame.js +46 -0
- package/dist/masyuGenerate.d.ts +3 -0
- package/dist/masyuGenerate.js +56 -0
- package/dist/masyuMount.d.ts +2 -0
- package/dist/masyuMount.js +118 -0
- package/dist/masyuPlay.types.d.ts +1 -0
- package/dist/masyuPlay.types.js +1 -0
- package/dist/masyuSolve.d.ts +6 -0
- package/dist/masyuSolve.js +88 -0
- package/dist/masyuStrings.d.ts +28 -0
- package/dist/masyuStrings.js +4 -0
- package/dist/masyuStyle.d.ts +1 -0
- package/dist/masyuStyle.js +11 -0
- package/dist/nurikabe-draw-entry.d.ts +2 -0
- package/dist/nurikabe-draw-entry.js +1 -0
- package/dist/nurikabe-entry.d.ts +6 -0
- package/dist/nurikabe-entry.js +6 -0
- package/dist/nurikabe-play-entry.d.ts +4 -0
- package/dist/nurikabe-play-entry.js +3 -0
- package/dist/nurikabe.constants.d.ts +2 -0
- package/dist/nurikabe.constants.js +2 -0
- package/dist/nurikabe.types.d.ts +20 -0
- package/dist/nurikabe.types.js +1 -0
- package/dist/nurikabeBoard.d.ts +8 -0
- package/dist/nurikabeBoard.js +72 -0
- package/dist/nurikabeDraw.d.ts +4 -0
- package/dist/nurikabeDraw.js +15 -0
- package/dist/nurikabeGame.d.ts +8 -0
- package/dist/nurikabeGame.js +40 -0
- package/dist/nurikabeGenerate.d.ts +3 -0
- package/dist/nurikabeGenerate.js +31 -0
- package/dist/nurikabeMount.d.ts +3 -0
- package/dist/nurikabeMount.js +99 -0
- package/dist/nurikabePlay.types.d.ts +25 -0
- package/dist/nurikabePlay.types.js +1 -0
- package/dist/nurikabeSolve.d.ts +6 -0
- package/dist/nurikabeSolve.js +75 -0
- package/dist/nurikabeStrings.d.ts +70 -0
- package/dist/nurikabeStrings.js +5 -0
- package/dist/nurikabeStyle.d.ts +1 -0
- package/dist/nurikabeStyle.js +9 -0
- package/dist/ripple-draw-entry.d.ts +2 -0
- package/dist/ripple-draw-entry.js +1 -0
- package/dist/ripple-entry.d.ts +12 -0
- package/dist/ripple-entry.js +11 -0
- package/dist/ripple-play-entry.d.ts +4 -0
- package/dist/ripple-play-entry.js +3 -0
- package/dist/ripple.constants.d.ts +4 -0
- package/dist/ripple.constants.js +4 -0
- package/dist/ripple.types.d.ts +34 -0
- package/dist/ripple.types.js +1 -0
- package/dist/rippleBoard.d.ts +7 -0
- package/dist/rippleBoard.js +99 -0
- package/dist/rippleDraw.d.ts +3 -0
- package/dist/rippleDraw.js +32 -0
- package/dist/rippleGame.d.ts +13 -0
- package/dist/rippleGame.js +85 -0
- package/dist/rippleGenerate.d.ts +3 -0
- package/dist/rippleGenerate.js +57 -0
- package/dist/rippleMount.d.ts +2 -0
- package/dist/rippleMount.js +179 -0
- package/dist/ripplePlay.types.d.ts +23 -0
- package/dist/ripplePlay.types.js +1 -0
- package/dist/rippleSolve.d.ts +6 -0
- package/dist/rippleSolve.js +83 -0
- package/dist/rippleStrings.d.ts +86 -0
- package/dist/rippleStrings.js +5 -0
- package/dist/rippleStyle.d.ts +1 -0
- package/dist/rippleStyle.js +1 -0
- package/dist/shikaku-entry.d.ts +1 -0
- package/dist/shikaku-entry.js +1 -0
- package/dist/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -0
- package/dist/slitherlink-draw-entry.d.ts +2 -0
- package/dist/slitherlink-draw-entry.js +1 -0
- package/dist/slitherlink-entry.d.ts +11 -0
- package/dist/slitherlink-entry.js +9 -0
- package/dist/slitherlink-play-entry.d.ts +4 -0
- package/dist/slitherlink-play-entry.js +3 -0
- package/dist/slitherlink.constants.d.ts +2 -0
- package/dist/slitherlink.constants.js +2 -0
- package/dist/slitherlink.types.d.ts +34 -0
- package/dist/slitherlink.types.js +1 -0
- package/dist/slitherlinkBoard.d.ts +12 -0
- package/dist/slitherlinkBoard.js +132 -0
- package/dist/slitherlinkDraw.d.ts +4 -0
- package/dist/slitherlinkDraw.js +51 -0
- package/dist/slitherlinkGame.d.ts +8 -0
- package/dist/slitherlinkGame.js +63 -0
- package/dist/slitherlinkGenerate.d.ts +3 -0
- package/dist/slitherlinkGenerate.js +101 -0
- package/dist/slitherlinkMount.d.ts +3 -0
- package/dist/slitherlinkMount.js +267 -0
- package/dist/slitherlinkPlay.types.d.ts +23 -0
- package/dist/slitherlinkPlay.types.js +1 -0
- package/dist/slitherlinkSolve.d.ts +6 -0
- package/dist/slitherlinkSolve.js +127 -0
- package/dist/slitherlinkStrings.d.ts +86 -0
- package/dist/slitherlinkStrings.js +47 -0
- package/dist/slitherlinkStyle.d.ts +1 -0
- package/dist/slitherlinkStyle.js +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/yajilin-draw-entry.d.ts +2 -0
- package/dist/yajilin-draw-entry.js +1 -0
- package/dist/yajilin-entry.d.ts +6 -0
- package/dist/yajilin-entry.js +6 -0
- package/dist/yajilin-play-entry.d.ts +4 -0
- package/dist/yajilin-play-entry.js +3 -0
- package/dist/yajilin.constants.d.ts +2 -0
- package/dist/yajilin.constants.js +2 -0
- package/dist/yajilin.types.d.ts +63 -0
- package/dist/yajilin.types.js +1 -0
- package/dist/yajilinBoard.d.ts +9 -0
- package/dist/yajilinBoard.js +91 -0
- package/dist/yajilinDraw.d.ts +2 -0
- package/dist/yajilinDraw.js +33 -0
- package/dist/yajilinGame.d.ts +15 -0
- package/dist/yajilinGame.js +58 -0
- package/dist/yajilinGenerate.d.ts +3 -0
- package/dist/yajilinGenerate.js +224 -0
- package/dist/yajilinMount.d.ts +2 -0
- package/dist/yajilinMount.js +162 -0
- package/dist/yajilinPlay.types.d.ts +1 -0
- package/dist/yajilinPlay.types.js +1 -0
- package/dist/yajilinSolve.d.ts +6 -0
- package/dist/yajilinSolve.js +82 -0
- package/dist/yajilinStrings.d.ts +32 -0
- package/dist/yajilinStrings.js +4 -0
- package/dist/yajilinStyle.d.ts +1 -0
- package/dist/yajilinStyle.js +12 -0
- package/package.json +180 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { NurikabeLanguage } from "./nurikabePlay.types.ts";
|
|
2
|
+
export declare const NURIKABE_STRINGS: {
|
|
3
|
+
readonly en: {
|
|
4
|
+
readonly title: "Nurikabe";
|
|
5
|
+
readonly help: "Shade cells to build one connected wall. Black cells cannot form a 2×2 square. Each remaining white region contains one number and has that exact area. Tap or press Space to shade; arrows move.";
|
|
6
|
+
readonly undo: "Undo";
|
|
7
|
+
readonly hint: "Hint";
|
|
8
|
+
readonly check: "Check";
|
|
9
|
+
readonly restart: "Restart";
|
|
10
|
+
readonly won: "Solved!";
|
|
11
|
+
readonly helped: "Solved with help.";
|
|
12
|
+
readonly wrong: "Some rules are broken.";
|
|
13
|
+
readonly good: "No rule errors yet. Keep going.";
|
|
14
|
+
readonly noHint: "No proved hint is available.";
|
|
15
|
+
readonly row: "Row";
|
|
16
|
+
readonly column: "column";
|
|
17
|
+
readonly sea: "sea";
|
|
18
|
+
readonly white: "island";
|
|
19
|
+
};
|
|
20
|
+
readonly ja: {
|
|
21
|
+
readonly title: "ぬりかべ";
|
|
22
|
+
readonly help: "黒マスをつなげて一つの壁にします。黒マスで2×2の四角形を作れません。白い島には数字が一つだけあり、島の面積はその数字と同じです。マスを押すかスペースで黒くし、矢印キーで移動します。";
|
|
23
|
+
readonly undo: "戻す";
|
|
24
|
+
readonly hint: "ヒント";
|
|
25
|
+
readonly check: "確認";
|
|
26
|
+
readonly restart: "やり直す";
|
|
27
|
+
readonly won: "完成!";
|
|
28
|
+
readonly helped: "ヒントを使って完成。";
|
|
29
|
+
readonly wrong: "ルールに合わないところがあります。";
|
|
30
|
+
readonly good: "今のところルール違反はありません。";
|
|
31
|
+
readonly noHint: "確かなヒントはありません。";
|
|
32
|
+
readonly row: "行";
|
|
33
|
+
readonly column: "列";
|
|
34
|
+
readonly sea: "海";
|
|
35
|
+
readonly white: "島";
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare function nurikabeWords(language?: NurikabeLanguage): {
|
|
39
|
+
readonly title: "Nurikabe";
|
|
40
|
+
readonly help: "Shade cells to build one connected wall. Black cells cannot form a 2×2 square. Each remaining white region contains one number and has that exact area. Tap or press Space to shade; arrows move.";
|
|
41
|
+
readonly undo: "Undo";
|
|
42
|
+
readonly hint: "Hint";
|
|
43
|
+
readonly check: "Check";
|
|
44
|
+
readonly restart: "Restart";
|
|
45
|
+
readonly won: "Solved!";
|
|
46
|
+
readonly helped: "Solved with help.";
|
|
47
|
+
readonly wrong: "Some rules are broken.";
|
|
48
|
+
readonly good: "No rule errors yet. Keep going.";
|
|
49
|
+
readonly noHint: "No proved hint is available.";
|
|
50
|
+
readonly row: "Row";
|
|
51
|
+
readonly column: "column";
|
|
52
|
+
readonly sea: "sea";
|
|
53
|
+
readonly white: "island";
|
|
54
|
+
} | {
|
|
55
|
+
readonly title: "ぬりかべ";
|
|
56
|
+
readonly help: "黒マスをつなげて一つの壁にします。黒マスで2×2の四角形を作れません。白い島には数字が一つだけあり、島の面積はその数字と同じです。マスを押すかスペースで黒くし、矢印キーで移動します。";
|
|
57
|
+
readonly undo: "戻す";
|
|
58
|
+
readonly hint: "ヒント";
|
|
59
|
+
readonly check: "確認";
|
|
60
|
+
readonly restart: "やり直す";
|
|
61
|
+
readonly won: "完成!";
|
|
62
|
+
readonly helped: "ヒントを使って完成。";
|
|
63
|
+
readonly wrong: "ルールに合わないところがあります。";
|
|
64
|
+
readonly good: "今のところルール違反はありません。";
|
|
65
|
+
readonly noHint: "確かなヒントはありません。";
|
|
66
|
+
readonly row: "行";
|
|
67
|
+
readonly column: "列";
|
|
68
|
+
readonly sea: "海";
|
|
69
|
+
readonly white: "島";
|
|
70
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const NURIKABE_STRINGS = {
|
|
2
|
+
en: { title: "Nurikabe", help: "Shade cells to build one connected wall. Black cells cannot form a 2×2 square. Each remaining white region contains one number and has that exact area. Tap or press Space to shade; arrows move.", undo: "Undo", hint: "Hint", check: "Check", restart: "Restart", won: "Solved!", helped: "Solved with help.", wrong: "Some rules are broken.", good: "No rule errors yet. Keep going.", noHint: "No proved hint is available.", row: "Row", column: "column", sea: "sea", white: "island" },
|
|
3
|
+
ja: { title: "ぬりかべ", help: "黒マスをつなげて一つの壁にします。黒マスで2×2の四角形を作れません。白い島には数字が一つだけあり、島の面積はその数字と同じです。マスを押すかスペースで黒くし、矢印キーで移動します。", undo: "戻す", hint: "ヒント", check: "確認", restart: "やり直す", won: "完成!", helped: "ヒントを使って完成。", wrong: "ルールに合わないところがあります。", good: "今のところルール違反はありません。", noHint: "確かなヒントはありません。", row: "行", column: "列", sea: "海", white: "島" },
|
|
4
|
+
};
|
|
5
|
+
export function nurikabeWords(language = "en") { return NURIKABE_STRINGS[language]; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const NURIKABE_PLAY_STYLE = "\n.kn-root{font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto;color:var(--ks-ink,#1f2320)}\n.kn-board{position:relative;width:100%;touch-action:manipulation}\n.kn-cells{position:absolute;inset:0;display:grid;padding:2px}\n.kn-cell{border:0;background:transparent;min-width:0;min-height:0;padding:0;cursor:pointer;touch-action:manipulation}\n.kn-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px}\n.kn-tools{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}\n.kn-tools button{font:inherit;border:1px solid var(--ks-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--ks-surface,#fbf8f1);color:inherit;cursor:pointer}\n.kn-tools button:disabled{opacity:.45}.kn-status{min-height:1.5em}.kn-help{font-size:14px;opacity:.8}";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const NURIKABE_PLAY_STYLE = `
|
|
2
|
+
.kn-root{font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto;color:var(--ks-ink,#1f2320)}
|
|
3
|
+
.kn-board{position:relative;width:100%;touch-action:manipulation}
|
|
4
|
+
.kn-cells{position:absolute;inset:0;display:grid;padding:2px}
|
|
5
|
+
.kn-cell{border:0;background:transparent;min-width:0;min-height:0;padding:0;cursor:pointer;touch-action:manipulation}
|
|
6
|
+
.kn-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px}
|
|
7
|
+
.kn-tools{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
|
|
8
|
+
.kn-tools button{font:inherit;border:1px solid var(--ks-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--ks-surface,#fbf8f1);color:inherit;cursor:pointer}
|
|
9
|
+
.kn-tools button:disabled{opacity:.45}.kn-status{min-height:1.5em}.kn-help{font-size:14px;opacity:.8}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawRipple } from "./rippleDraw.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Ripple Effect has its own room and spacing model. */
|
|
2
|
+
export * from "./ripple.types.ts";
|
|
3
|
+
export * from "./ripple.constants.ts";
|
|
4
|
+
export * from "./rippleBoard.ts";
|
|
5
|
+
export * from "./rippleSolve.ts";
|
|
6
|
+
export * from "./rippleGenerate.ts";
|
|
7
|
+
export * from "./rippleGame.ts";
|
|
8
|
+
export { drawRipple } from "./rippleDraw.ts";
|
|
9
|
+
export { mountRipple } from "./rippleMount.ts";
|
|
10
|
+
export { RIPPLE_PLAY_STYLE } from "./rippleStyle.ts";
|
|
11
|
+
export { RIPPLE_STRINGS } from "./rippleStrings.ts";
|
|
12
|
+
export type * from "./ripplePlay.types.ts";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Ripple Effect has its own room and spacing model. */
|
|
2
|
+
export * from "./ripple.types.js";
|
|
3
|
+
export * from "./ripple.constants.js";
|
|
4
|
+
export * from "./rippleBoard.js";
|
|
5
|
+
export * from "./rippleSolve.js";
|
|
6
|
+
export * from "./rippleGenerate.js";
|
|
7
|
+
export * from "./rippleGame.js";
|
|
8
|
+
export { drawRipple } from "./rippleDraw.js";
|
|
9
|
+
export { mountRipple } from "./rippleMount.js";
|
|
10
|
+
export { RIPPLE_PLAY_STYLE } from "./rippleStyle.js";
|
|
11
|
+
export { RIPPLE_STRINGS } from "./rippleStrings.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RippleBoard = {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
rooms: readonly number[];
|
|
5
|
+
clues: readonly (number | null)[];
|
|
6
|
+
};
|
|
7
|
+
export type RipplePuzzle = RippleBoard & {
|
|
8
|
+
seed: number;
|
|
9
|
+
solution: readonly number[];
|
|
10
|
+
};
|
|
11
|
+
export type RippleCheck = {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
errors: readonly number[];
|
|
14
|
+
complete: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type RippleSolve = {
|
|
17
|
+
count: number;
|
|
18
|
+
solution: readonly number[] | null;
|
|
19
|
+
complete: boolean;
|
|
20
|
+
nodes: number;
|
|
21
|
+
};
|
|
22
|
+
export type RippleGame = {
|
|
23
|
+
board: RippleBoard;
|
|
24
|
+
values: readonly number[];
|
|
25
|
+
notes: readonly (readonly number[])[];
|
|
26
|
+
history: readonly {
|
|
27
|
+
values: readonly number[];
|
|
28
|
+
notes: readonly (readonly number[])[];
|
|
29
|
+
}[];
|
|
30
|
+
helped: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type RippleLanguage = "en" | "ja";
|
|
33
|
+
export type RippleMaterial = "ivory" | "wood" | "slate";
|
|
34
|
+
export type RipplePieces = "ink" | "tiles";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RippleBoard, RippleCheck } from "./ripple.types.ts";
|
|
2
|
+
export declare function isRippleBoard(value: unknown): value is RippleBoard;
|
|
3
|
+
export declare function rippleRoomSizes(board: RippleBoard): number[];
|
|
4
|
+
/** Checks room permutations and the at-least-N-cell gap between equal N clues in a line. */
|
|
5
|
+
export declare function checkRipple(board: RippleBoard, values: readonly number[]): RippleCheck;
|
|
6
|
+
/** Partial progress check; missing entries are allowed, but impossible room/ripple pairs are flagged. */
|
|
7
|
+
export declare function progressRipple(board: RippleBoard, values: readonly number[]): RippleCheck;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RIPPLE_MAX_SIDE, RIPPLE_MIN_SIDE } from "./ripple.constants.js";
|
|
2
|
+
export function isRippleBoard(value) {
|
|
3
|
+
if (!value || typeof value !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
const board = value;
|
|
6
|
+
if (!Number.isInteger(board.width) || board.width < RIPPLE_MIN_SIDE || board.width > RIPPLE_MAX_SIDE
|
|
7
|
+
|| !Number.isInteger(board.height) || board.height < RIPPLE_MIN_SIDE || board.height > RIPPLE_MAX_SIDE
|
|
8
|
+
|| !Array.isArray(board.rooms) || !Array.isArray(board.clues)
|
|
9
|
+
|| board.rooms.length !== board.width * board.height || board.clues.length !== board.rooms.length
|
|
10
|
+
|| board.rooms.some(id => !Number.isInteger(id) || id < 0))
|
|
11
|
+
return false;
|
|
12
|
+
const members = new Map();
|
|
13
|
+
board.rooms.forEach((id, cell) => members.set(id, [...(members.get(id) ?? []), cell]));
|
|
14
|
+
const ids = [...members.keys()].sort((a, b) => a - b);
|
|
15
|
+
if (ids.some((id, index) => id !== index))
|
|
16
|
+
return false;
|
|
17
|
+
const sizes = new Map([...members].map(([id, cells]) => [id, cells.length]));
|
|
18
|
+
if (board.clues.some((clue, cell) => clue !== null && (!Number.isInteger(clue) || clue < 1 || clue > sizes.get(board.rooms[cell]))))
|
|
19
|
+
return false;
|
|
20
|
+
return [...members].every(([id, cells]) => connected(board, id, cells));
|
|
21
|
+
}
|
|
22
|
+
function connected(board, room, cells) {
|
|
23
|
+
const reached = new Set([cells[0]]);
|
|
24
|
+
const queue = [cells[0]];
|
|
25
|
+
while (queue.length) {
|
|
26
|
+
const cell = queue.pop();
|
|
27
|
+
for (const next of [cell - board.width, cell + board.width, cell - 1, cell + 1]) {
|
|
28
|
+
if (next < 0 || next >= board.rooms.length || (Math.abs(next - cell) === 1 && Math.floor(next / board.width) !== Math.floor(cell / board.width)))
|
|
29
|
+
continue;
|
|
30
|
+
if (board.rooms[next] === room && !reached.has(next)) {
|
|
31
|
+
reached.add(next);
|
|
32
|
+
queue.push(next);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return reached.size === cells.length;
|
|
37
|
+
}
|
|
38
|
+
export function rippleRoomSizes(board) {
|
|
39
|
+
const sizes = [];
|
|
40
|
+
board.rooms.forEach(id => { sizes[id] = (sizes[id] ?? 0) + 1; });
|
|
41
|
+
return sizes;
|
|
42
|
+
}
|
|
43
|
+
/** Checks room permutations and the at-least-N-cell gap between equal N clues in a line. */
|
|
44
|
+
export function checkRipple(board, values) {
|
|
45
|
+
if (!isRippleBoard(board))
|
|
46
|
+
throw new RangeError("Invalid Ripple Effect board");
|
|
47
|
+
if (values.length !== board.rooms.length)
|
|
48
|
+
throw new RangeError("Invalid Ripple Effect values");
|
|
49
|
+
const errors = new Set();
|
|
50
|
+
const sizes = rippleRoomSizes(board);
|
|
51
|
+
const seen = new Map();
|
|
52
|
+
values.forEach((value, cell) => {
|
|
53
|
+
const clue = board.clues[cell];
|
|
54
|
+
if (!Number.isInteger(value) || value < 0 || value > sizes[board.rooms[cell]]
|
|
55
|
+
|| clue !== null && value !== clue) {
|
|
56
|
+
errors.add(cell);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (value > 0)
|
|
60
|
+
seen.set(board.rooms[cell], [...(seen.get(board.rooms[cell]) ?? []), value]);
|
|
61
|
+
});
|
|
62
|
+
for (const room of seen.keys()) {
|
|
63
|
+
const cells = values.flatMap((value, cell) => board.rooms[cell] === room && value > 0 ? [value] : []);
|
|
64
|
+
const duplicates = new Set(cells.filter((value, index) => cells.indexOf(value) !== index));
|
|
65
|
+
if (duplicates.size)
|
|
66
|
+
values.forEach((value, cell) => { if (board.rooms[cell] === room && duplicates.has(value))
|
|
67
|
+
errors.add(cell); });
|
|
68
|
+
}
|
|
69
|
+
const lines = [...Array.from({ length: board.height }, (_, y) => Array.from({ length: board.width }, (_, x) => y * board.width + x)),
|
|
70
|
+
...Array.from({ length: board.width }, (_, x) => Array.from({ length: board.height }, (_, y) => y * board.width + x))];
|
|
71
|
+
for (const line of lines)
|
|
72
|
+
for (let a = 0; a < line.length; a += 1) {
|
|
73
|
+
const cell = line[a], value = values[cell];
|
|
74
|
+
if (!value)
|
|
75
|
+
continue;
|
|
76
|
+
for (let b = a + 1; b < line.length; b += 1) {
|
|
77
|
+
const other = line[b];
|
|
78
|
+
if (values[other] === value && b - a <= value) {
|
|
79
|
+
errors.add(cell);
|
|
80
|
+
errors.add(other);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const complete = values.every(value => value > 0);
|
|
85
|
+
if (complete) {
|
|
86
|
+
for (let room = 0; room < sizes.length; room += 1) {
|
|
87
|
+
const list = values.flatMap((value, cell) => board.rooms[cell] === room ? [value] : []).sort((a, b) => a - b);
|
|
88
|
+
if (list.some((value, index) => value !== index + 1))
|
|
89
|
+
board.rooms.forEach((id, cell) => { if (id === room)
|
|
90
|
+
errors.add(cell); });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { ok: errors.size === 0 && complete, errors: [...errors].sort((a, b) => a - b), complete };
|
|
94
|
+
}
|
|
95
|
+
/** Partial progress check; missing entries are allowed, but impossible room/ripple pairs are flagged. */
|
|
96
|
+
export function progressRipple(board, values) {
|
|
97
|
+
const result = checkRipple(board, values);
|
|
98
|
+
return { ...result, ok: result.errors.length === 0 };
|
|
99
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { rippleRoomSizes } from "./rippleBoard.js";
|
|
2
|
+
export function drawRipple(board, options = {}) {
|
|
3
|
+
const size = 56, margin = 8, width = board.width * size, height = board.height * size;
|
|
4
|
+
const ink = options.material === "slate" ? "#f1eee5" : options.material === "wood" ? "#382d20" : "#252824";
|
|
5
|
+
const line = options.material === "slate" ? "#899697" : options.material === "wood" ? "#80613c" : "#8b8578";
|
|
6
|
+
const surface = options.material === "slate" ? "#354247" : options.material === "wood" ? "#dfc18f" : "#faf7ef";
|
|
7
|
+
const values = options.values ?? board.clues.map(clue => clue ?? 0);
|
|
8
|
+
const notes = options.notes ?? board.rooms.map(() => []);
|
|
9
|
+
const sizes = rippleRoomSizes(board);
|
|
10
|
+
const cells = Array.from({ length: board.rooms.length }, (_, cell) => {
|
|
11
|
+
const x = cell % board.width * size, y = Math.floor(cell / board.width) * size;
|
|
12
|
+
const room = board.rooms[cell];
|
|
13
|
+
const clue = board.clues[cell];
|
|
14
|
+
const value = values[cell] ?? 0;
|
|
15
|
+
const text = clue !== null ? `<text class="rp-clue" x="${x + size / 2}" y="${y + size * .64}">${clue}</text>`
|
|
16
|
+
: value ? `<text class="rp-value" x="${x + size / 2}" y="${y + size * .64}">${value}</text>`
|
|
17
|
+
: (notes[cell] ?? []).map((note, i) => `<text class="rp-note" x="${x + 14 + i % 3 * 14}" y="${y + 18 + Math.floor(i / 3) * 14}">${note}</text>`).join("");
|
|
18
|
+
const selected = options.selected === cell;
|
|
19
|
+
const piece = options.pieces === "tiles" ? `<rect class="rp-piece" x="${x + 9}" y="${y + 9}" width="${size - 18}" height="${size - 18}" rx="6"/>` : "";
|
|
20
|
+
const error = options.errors?.includes(cell) ?? false;
|
|
21
|
+
return `<rect x="${x}" y="${y}" width="${size}" height="${size}" fill="${selected ? "#dce9c9" : surface}" stroke="${error ? "#c34b3e" : line}" stroke-width="1"/>${piece}${text}<title>${Math.floor(cell / board.width) + 1}, ${cell % board.width + 1}: ${clue !== null ? `clue ${clue}` : value ? `entry ${value}` : `room of ${sizes[room]}`}</title>`;
|
|
22
|
+
}).join("");
|
|
23
|
+
const walls = board.rooms.flatMap((room, cell) => {
|
|
24
|
+
const x = cell % board.width, y = Math.floor(cell / board.width), edges = [];
|
|
25
|
+
if (x + 1 < board.width && board.rooms[cell + 1] !== room)
|
|
26
|
+
edges.push(`<path d="M${(x + 1) * size} ${y * size}v${size}"/>`);
|
|
27
|
+
if (y + 1 < board.height && board.rooms[cell + board.width] !== room)
|
|
28
|
+
edges.push(`<path d="M${x * size} ${(y + 1) * size}h${size}"/>`);
|
|
29
|
+
return edges;
|
|
30
|
+
}).join("");
|
|
31
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Ripple Effect ${board.width} by ${board.height}" viewBox="-${margin} -${margin} ${width + margin * 2} ${height + margin * 2}" style="color:${ink}"><style>.rp-clue,.rp-value{font:500 27px system-ui;text-anchor:middle;fill:${ink}}.rp-clue{font-weight:700}.rp-note{font:12px system-ui;text-anchor:middle;fill:${ink}}.rp-piece{fill:none;stroke:${line};stroke-width:2}</style>${cells}<g fill="none" stroke="${ink}" stroke-width="3">${walls}</g></svg>`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RippleBoard, RippleGame } from "./ripple.types.ts";
|
|
2
|
+
export declare function newRipple(board: RippleBoard): RippleGame;
|
|
3
|
+
export declare function setRippleValue(game: RippleGame, cell: number, value: number): RippleGame;
|
|
4
|
+
export declare function toggleRippleNote(game: RippleGame, cell: number, value: number): RippleGame;
|
|
5
|
+
export declare function undoRipple(game: RippleGame): RippleGame;
|
|
6
|
+
export declare function rippleFinished(game: RippleGame): boolean;
|
|
7
|
+
/** Offers a cell only when the current public entries leave one proved completion. */
|
|
8
|
+
export declare function hintRipple(game: RippleGame): {
|
|
9
|
+
cell: number;
|
|
10
|
+
value: number;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function encodeRipple(game: RippleGame): string;
|
|
13
|
+
export declare function decodeRipple(code: string): RippleGame | null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isRippleBoard, checkRipple, rippleRoomSizes } from "./rippleBoard.js";
|
|
2
|
+
import { solveRipple } from "./rippleSolve.js";
|
|
3
|
+
export function newRipple(board) {
|
|
4
|
+
if (!isRippleBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Ripple Effect board");
|
|
6
|
+
return { board: { width: board.width, height: board.height, rooms: [...board.rooms], clues: [...board.clues] },
|
|
7
|
+
values: board.clues.map(clue => clue ?? 0), notes: board.rooms.map(() => []), history: [], helped: false };
|
|
8
|
+
}
|
|
9
|
+
export function setRippleValue(game, cell, value) {
|
|
10
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= game.values.length || game.board.clues[cell] !== null
|
|
11
|
+
|| !Number.isInteger(value) || value < 0 || value > rippleRoomSizes(game.board)[game.board.rooms[cell]])
|
|
12
|
+
return game;
|
|
13
|
+
if (game.values[cell] === value)
|
|
14
|
+
return game;
|
|
15
|
+
const values = [...game.values];
|
|
16
|
+
values[cell] = value;
|
|
17
|
+
const notes = game.notes.map((list, index) => index === cell ? [] : [...list]);
|
|
18
|
+
return { ...game, values, notes, history: [...game.history, snapshot(game)] };
|
|
19
|
+
}
|
|
20
|
+
export function toggleRippleNote(game, cell, value) {
|
|
21
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= game.values.length || game.board.clues[cell] !== null || game.values[cell] !== 0
|
|
22
|
+
|| !Number.isInteger(value) || value < 1 || value > rippleRoomSizes(game.board)[game.board.rooms[cell]])
|
|
23
|
+
return game;
|
|
24
|
+
const notes = game.notes.map(list => [...list]);
|
|
25
|
+
const current = new Set(notes[cell]);
|
|
26
|
+
if (current.has(value))
|
|
27
|
+
current.delete(value);
|
|
28
|
+
else
|
|
29
|
+
current.add(value);
|
|
30
|
+
notes[cell] = [...current].sort((a, b) => a - b);
|
|
31
|
+
return { ...game, notes, history: [...game.history, snapshot(game)] };
|
|
32
|
+
}
|
|
33
|
+
export function undoRipple(game) {
|
|
34
|
+
const previous = game.history.at(-1);
|
|
35
|
+
return previous ? { ...game, values: [...previous.values], notes: previous.notes.map(list => [...list]), history: game.history.slice(0, -1) } : game;
|
|
36
|
+
}
|
|
37
|
+
export function rippleFinished(game) { return checkRipple(game.board, game.values).ok; }
|
|
38
|
+
/** Offers a cell only when the current public entries leave one proved completion. */
|
|
39
|
+
export function hintRipple(game) {
|
|
40
|
+
const clues = game.values.map((value, cell) => game.board.clues[cell] ?? (value || null));
|
|
41
|
+
const solved = solveRipple({ ...game.board, clues });
|
|
42
|
+
if (!solved.complete || solved.count !== 1 || !solved.solution)
|
|
43
|
+
return null;
|
|
44
|
+
const cell = solved.solution.findIndex((value, index) => game.values[index] === 0 && value > 0);
|
|
45
|
+
return cell < 0 ? null : { cell, value: solved.solution[cell] };
|
|
46
|
+
}
|
|
47
|
+
export function encodeRipple(game) {
|
|
48
|
+
return JSON.stringify({ version: 1, board: game.board, values: game.values, notes: game.notes, helped: game.helped });
|
|
49
|
+
}
|
|
50
|
+
export function decodeRipple(code) {
|
|
51
|
+
try {
|
|
52
|
+
if (code.length > 40000)
|
|
53
|
+
return null;
|
|
54
|
+
const data = JSON.parse(code);
|
|
55
|
+
if (data.version !== 1 || !isRippleBoard(data.board) || !Array.isArray(data.values)
|
|
56
|
+
|| data.values.length !== data.board.rooms.length || !Array.isArray(data.notes)
|
|
57
|
+
|| data.notes.length !== data.board.rooms.length || typeof data.helped !== "boolean")
|
|
58
|
+
return null;
|
|
59
|
+
let game = newRipple(data.board);
|
|
60
|
+
for (let cell = 0; cell < data.values.length; cell += 1) {
|
|
61
|
+
const value = data.values[cell];
|
|
62
|
+
if (game.board.clues[cell] !== null && value !== game.board.clues[cell])
|
|
63
|
+
return null;
|
|
64
|
+
if (game.board.clues[cell] === null && value) {
|
|
65
|
+
const next = setRippleValue(game, cell, value);
|
|
66
|
+
if (next === game)
|
|
67
|
+
return null;
|
|
68
|
+
game = next;
|
|
69
|
+
}
|
|
70
|
+
const list = data.notes[cell];
|
|
71
|
+
if (!Array.isArray(list) || new Set(list).size !== list.length
|
|
72
|
+
|| list.some(note => !Number.isInteger(note) || note < 1 || note > rippleRoomSizes(game.board)[game.board.rooms[cell]]))
|
|
73
|
+
return null;
|
|
74
|
+
for (const note of list)
|
|
75
|
+
game = toggleRippleNote(game, cell, note);
|
|
76
|
+
}
|
|
77
|
+
return { ...game, history: [], helped: data.helped };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function snapshot(game) {
|
|
84
|
+
return { values: [...game.values], notes: game.notes.map(list => [...list]) };
|
|
85
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { RIPPLE_MAX_ATTEMPTS } from "./ripple.constants.js";
|
|
2
|
+
import { checkRipple } from "./rippleBoard.js";
|
|
3
|
+
import { solveRipple } from "./rippleSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/** Builds a seeded block-room family and accepts it only after a bounded uniqueness proof. */
|
|
6
|
+
export function generateRipple(width = 9, height = width, seed = 1) {
|
|
7
|
+
if (width !== 9 || height !== 9 || !isKazuSeed(seed)) {
|
|
8
|
+
throw new RangeError("Ripple Effect currently supports original 9×9 boards and valid integer seeds.");
|
|
9
|
+
}
|
|
10
|
+
const random = seededRandom(seed);
|
|
11
|
+
for (let attempt = 0; attempt < RIPPLE_MAX_ATTEMPTS; attempt += 1) {
|
|
12
|
+
const rooms = Array.from({ length: 81 }, (_, cell) => Math.floor(Math.floor(cell / 9) / 3) * 3 + Math.floor(cell % 9 / 3));
|
|
13
|
+
const solution = blockSolution(random);
|
|
14
|
+
const clues = [...solution];
|
|
15
|
+
const order = Array.from({ length: clues.length }, (_, cell) => cell);
|
|
16
|
+
order.splice(0, order.length, ...shuffled(order, random));
|
|
17
|
+
const target = 42 + Math.floor(random() * 7);
|
|
18
|
+
let clueCount = clues.length;
|
|
19
|
+
for (const cell of order) {
|
|
20
|
+
if (clueCount <= target)
|
|
21
|
+
break;
|
|
22
|
+
const previous = clues[cell];
|
|
23
|
+
clues[cell] = null;
|
|
24
|
+
const board = { width, height, rooms, clues };
|
|
25
|
+
const proof = solveRipple(board, { limit: 2 });
|
|
26
|
+
if (!proof.complete || proof.count !== 1) {
|
|
27
|
+
clues[cell] = previous;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
clueCount -= 1;
|
|
31
|
+
}
|
|
32
|
+
if (clueCount > 58 || clueCount >= clues.length)
|
|
33
|
+
continue;
|
|
34
|
+
const board = { width, height, rooms, clues };
|
|
35
|
+
const proof = solveRipple(board, { limit: 2 });
|
|
36
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkRipple(board, proof.solution).ok) {
|
|
37
|
+
return { ...board, seed, solution: proof.solution };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error("No uniquely solvable Ripple Effect board was proved within the generation budget.");
|
|
41
|
+
}
|
|
42
|
+
function blockSolution(random) {
|
|
43
|
+
const bands = shuffled([0, 1, 2], random);
|
|
44
|
+
const stacks = shuffled([0, 1, 2], random);
|
|
45
|
+
const rows = bands.flatMap(band => shuffled([0, 1, 2], random).map(row => band * 3 + row));
|
|
46
|
+
const columns = stacks.flatMap(stack => shuffled([0, 1, 2], random).map(column => stack * 3 + column));
|
|
47
|
+
const digits = shuffled([1, 2, 3, 4, 5, 6, 7, 8, 9], random);
|
|
48
|
+
return rows.flatMap(row => columns.map(column => digits[(row * 3 + Math.floor(row / 3) + column) % 9]));
|
|
49
|
+
}
|
|
50
|
+
function shuffled(items, random) {
|
|
51
|
+
const result = [...items];
|
|
52
|
+
for (let i = result.length - 1; i > 0; i -= 1) {
|
|
53
|
+
const j = Math.floor(random() * (i + 1));
|
|
54
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|