@johnmorrisdotca/kazu 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +424 -10
- package/dist/akari-draw-entry.d.ts +2 -0
- package/dist/akari-draw-entry.js +1 -0
- package/dist/akari-entry.d.ts +13 -0
- package/dist/akari-entry.js +11 -0
- package/dist/akari-play-entry.d.ts +2 -0
- package/dist/akari-play-entry.js +1 -0
- package/dist/akari.constants.d.ts +7 -0
- package/dist/akari.constants.js +7 -0
- package/dist/akari.types.d.ts +52 -0
- package/dist/akari.types.js +1 -0
- package/dist/akariBoard.d.ts +11 -0
- package/dist/akariBoard.js +96 -0
- package/dist/akariDraw.d.ts +4 -0
- package/dist/akariDraw.js +38 -0
- package/dist/akariGame.d.ts +15 -0
- package/dist/akariGame.js +71 -0
- package/dist/akariGenerate.d.ts +10 -0
- package/dist/akariGenerate.js +101 -0
- package/dist/akariLogic.d.ts +10 -0
- package/dist/akariLogic.js +84 -0
- package/dist/akariMount.d.ts +3 -0
- package/dist/akariMount.js +214 -0
- package/dist/akariPlay.types.d.ts +25 -0
- package/dist/akariPlay.types.js +1 -0
- package/dist/akariRate.d.ts +6 -0
- package/dist/akariRate.js +34 -0
- package/dist/akariSolve.d.ts +6 -0
- package/dist/akariSolve.js +18 -0
- package/dist/akariStrings.d.ts +98 -0
- package/dist/akariStrings.js +53 -0
- package/dist/akariStyle.d.ts +1 -0
- package/dist/akariStyle.js +9 -0
- package/dist/akariTemplate.d.ts +7 -0
- package/dist/akariTemplate.js +78 -0
- package/dist/csp.d.ts +63 -0
- package/dist/csp.js +162 -0
- package/dist/fillomino-draw-entry.d.ts +2 -0
- package/dist/fillomino-draw-entry.js +1 -0
- package/dist/fillomino-entry.d.ts +9 -0
- package/dist/fillomino-entry.js +8 -0
- package/dist/fillomino-play-entry.d.ts +4 -0
- package/dist/fillomino-play-entry.js +3 -0
- package/dist/fillomino.constants.d.ts +9 -0
- package/dist/fillomino.constants.js +9 -0
- package/dist/fillomino.types.d.ts +47 -0
- package/dist/fillomino.types.js +1 -0
- package/dist/fillominoBoard.d.ts +6 -0
- package/dist/fillominoBoard.js +84 -0
- package/dist/fillominoBuild.d.ts +10 -0
- package/dist/fillominoBuild.js +78 -0
- package/dist/fillominoDraw.d.ts +3 -0
- package/dist/fillominoDraw.js +31 -0
- package/dist/fillominoGame.d.ts +13 -0
- package/dist/fillominoGame.js +65 -0
- package/dist/fillominoGenerate.d.ts +9 -0
- package/dist/fillominoGenerate.js +93 -0
- package/dist/fillominoLogic.d.ts +14 -0
- package/dist/fillominoLogic.js +177 -0
- package/dist/fillominoMount.d.ts +2 -0
- package/dist/fillominoMount.js +214 -0
- package/dist/fillominoPlay.types.d.ts +25 -0
- package/dist/fillominoPlay.types.js +1 -0
- package/dist/fillominoRate.d.ts +3 -0
- package/dist/fillominoRate.js +57 -0
- package/dist/fillominoSolve.d.ts +6 -0
- package/dist/fillominoSolve.js +38 -0
- package/dist/fillominoStrings.d.ts +102 -0
- package/dist/fillominoStrings.js +13 -0
- package/dist/fillominoStyle.d.ts +1 -0
- package/dist/fillominoStyle.js +13 -0
- package/dist/fillominoWorker.d.ts +1 -0
- package/dist/fillominoWorker.js +10 -0
- package/dist/heyawake-draw-entry.d.ts +2 -0
- package/dist/heyawake-draw-entry.js +1 -0
- package/dist/heyawake-entry.d.ts +13 -0
- package/dist/heyawake-entry.js +11 -0
- package/dist/heyawake-play-entry.d.ts +2 -0
- package/dist/heyawake-play-entry.js +1 -0
- package/dist/heyawake.constants.d.ts +5 -0
- package/dist/heyawake.constants.js +5 -0
- package/dist/heyawake.types.d.ts +35 -0
- package/dist/heyawake.types.js +1 -0
- package/dist/heyawakeBoard.d.ts +7 -0
- package/dist/heyawakeBoard.js +116 -0
- package/dist/heyawakeDraw.d.ts +3 -0
- package/dist/heyawakeDraw.js +27 -0
- package/dist/heyawakeGame.d.ts +12 -0
- package/dist/heyawakeGame.js +51 -0
- package/dist/heyawakeGenerate.d.ts +2 -0
- package/dist/heyawakeGenerate.js +114 -0
- package/dist/heyawakeMount.d.ts +2 -0
- package/dist/heyawakeMount.js +106 -0
- package/dist/heyawakePlay.types.d.ts +24 -0
- package/dist/heyawakePlay.types.js +1 -0
- package/dist/heyawakeSolve.d.ts +5 -0
- package/dist/heyawakeSolve.js +52 -0
- package/dist/heyawakeStrings.d.ts +19 -0
- package/dist/heyawakeStrings.js +7 -0
- package/dist/heyawakeStyle.d.ts +1 -0
- package/dist/heyawakeStyle.js +2 -0
- package/dist/heyawakeWorker.d.ts +1 -0
- package/dist/heyawakeWorker.js +10 -0
- package/dist/hitori-draw-entry.d.ts +2 -0
- package/dist/hitori-draw-entry.js +1 -0
- package/dist/hitori-entry.d.ts +7 -0
- package/dist/hitori-entry.js +7 -0
- package/dist/hitori-play-entry.d.ts +4 -0
- package/dist/hitori-play-entry.js +3 -0
- package/dist/hitori.constants.d.ts +8 -0
- package/dist/hitori.constants.js +8 -0
- package/dist/hitori.types.d.ts +37 -0
- package/dist/hitori.types.js +1 -0
- package/dist/hitoriBoard.d.ts +8 -0
- package/dist/hitoriBoard.js +73 -0
- package/dist/hitoriBuild.d.ts +12 -0
- package/dist/hitoriBuild.js +117 -0
- package/dist/hitoriDraw.d.ts +4 -0
- package/dist/hitoriDraw.js +25 -0
- package/dist/hitoriGame.d.ts +10 -0
- package/dist/hitoriGame.js +48 -0
- package/dist/hitoriGenerate.d.ts +10 -0
- package/dist/hitoriGenerate.js +65 -0
- package/dist/hitoriLogic.d.ts +12 -0
- package/dist/hitoriLogic.js +189 -0
- package/dist/hitoriMount.d.ts +3 -0
- package/dist/hitoriMount.js +111 -0
- package/dist/hitoriPlay.types.d.ts +25 -0
- package/dist/hitoriPlay.types.js +1 -0
- package/dist/hitoriRate.d.ts +3 -0
- package/dist/hitoriRate.js +32 -0
- package/dist/hitoriSolve.d.ts +9 -0
- package/dist/hitoriSolve.js +18 -0
- package/dist/hitoriStrings.d.ts +70 -0
- package/dist/hitoriStrings.js +17 -0
- package/dist/hitoriStyle.d.ts +1 -0
- package/dist/hitoriStyle.js +9 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/juosan-draw-entry.d.ts +2 -0
- package/dist/juosan-draw-entry.js +1 -0
- package/dist/juosan-entry.d.ts +11 -0
- package/dist/juosan-entry.js +10 -0
- package/dist/juosan-play-entry.d.ts +4 -0
- package/dist/juosan-play-entry.js +3 -0
- package/dist/juosan.constants.d.ts +4 -0
- package/dist/juosan.constants.js +4 -0
- package/dist/juosan.types.d.ts +33 -0
- package/dist/juosan.types.js +1 -0
- package/dist/juosanBoard.d.ts +7 -0
- package/dist/juosanBoard.js +102 -0
- package/dist/juosanDraw.d.ts +4 -0
- package/dist/juosanDraw.js +49 -0
- package/dist/juosanGame.d.ts +20 -0
- package/dist/juosanGame.js +86 -0
- package/dist/juosanGenerate.d.ts +3 -0
- package/dist/juosanGenerate.js +31 -0
- package/dist/juosanMount.d.ts +3 -0
- package/dist/juosanMount.js +206 -0
- package/dist/juosanPlay.types.d.ts +24 -0
- package/dist/juosanPlay.types.js +1 -0
- package/dist/juosanSolve.d.ts +3 -0
- package/dist/juosanSolve.js +82 -0
- package/dist/juosanStrings.d.ts +77 -0
- package/dist/juosanStrings.js +43 -0
- package/dist/juosanStyle.d.ts +1 -0
- package/dist/juosanStyle.js +1 -0
- package/dist/kakuro-draw-entry.d.ts +2 -0
- package/dist/kakuro-draw-entry.js +1 -0
- package/dist/kakuro-entry.d.ts +13 -0
- package/dist/kakuro-entry.js +11 -0
- package/dist/kakuro-play-entry.d.ts +4 -0
- package/dist/kakuro-play-entry.js +3 -0
- package/dist/kakuro.constants.d.ts +11 -0
- package/dist/kakuro.constants.js +11 -0
- package/dist/kakuro.types.d.ts +65 -0
- package/dist/kakuro.types.js +1 -0
- package/dist/kakuroBoard.d.ts +8 -0
- package/dist/kakuroBoard.js +117 -0
- package/dist/kakuroBuild.d.ts +30 -0
- package/dist/kakuroBuild.js +337 -0
- package/dist/kakuroDraw.d.ts +10 -0
- package/dist/kakuroDraw.js +21 -0
- package/dist/kakuroGame.d.ts +12 -0
- package/dist/kakuroGame.js +74 -0
- package/dist/kakuroGenerate.d.ts +10 -0
- package/dist/kakuroGenerate.js +62 -0
- package/dist/kakuroLogic.d.ts +13 -0
- package/dist/kakuroLogic.js +153 -0
- package/dist/kakuroMount.d.ts +2 -0
- package/dist/kakuroMount.js +109 -0
- package/dist/kakuroPlay.types.d.ts +20 -0
- package/dist/kakuroPlay.types.js +1 -0
- package/dist/kakuroRate.d.ts +3 -0
- package/dist/kakuroRate.js +40 -0
- package/dist/kakuroSolve.d.ts +6 -0
- package/dist/kakuroSolve.js +33 -0
- package/dist/kakuroStrings.d.ts +82 -0
- package/dist/kakuroStrings.js +5 -0
- package/dist/kakuroStyle.d.ts +1 -0
- package/dist/kakuroStyle.js +1 -0
- package/dist/kakuroTemplate.d.ts +3 -0
- package/dist/kakuroTemplate.js +130 -0
- package/dist/masyu-draw-entry.d.ts +2 -0
- package/dist/masyu-draw-entry.js +1 -0
- package/dist/masyu-entry.d.ts +6 -0
- package/dist/masyu-entry.js +6 -0
- package/dist/masyu-play-entry.d.ts +4 -0
- package/dist/masyu-play-entry.js +3 -0
- package/dist/masyu.constants.d.ts +2 -0
- package/dist/masyu.constants.js +2 -0
- package/dist/masyu.types.d.ts +46 -0
- package/dist/masyu.types.js +1 -0
- package/dist/masyuBoard.d.ts +10 -0
- package/dist/masyuBoard.js +88 -0
- package/dist/masyuDraw.d.ts +2 -0
- package/dist/masyuDraw.js +33 -0
- package/dist/masyuGame.d.ts +9 -0
- package/dist/masyuGame.js +46 -0
- package/dist/masyuGenerate.d.ts +3 -0
- package/dist/masyuGenerate.js +56 -0
- package/dist/masyuMount.d.ts +2 -0
- package/dist/masyuMount.js +118 -0
- package/dist/masyuPlay.types.d.ts +1 -0
- package/dist/masyuPlay.types.js +1 -0
- package/dist/masyuSolve.d.ts +6 -0
- package/dist/masyuSolve.js +88 -0
- package/dist/masyuStrings.d.ts +28 -0
- package/dist/masyuStrings.js +4 -0
- package/dist/masyuStyle.d.ts +1 -0
- package/dist/masyuStyle.js +11 -0
- package/dist/nurikabe-draw-entry.d.ts +2 -0
- package/dist/nurikabe-draw-entry.js +1 -0
- package/dist/nurikabe-entry.d.ts +6 -0
- package/dist/nurikabe-entry.js +6 -0
- package/dist/nurikabe-play-entry.d.ts +4 -0
- package/dist/nurikabe-play-entry.js +3 -0
- package/dist/nurikabe.constants.d.ts +2 -0
- package/dist/nurikabe.constants.js +2 -0
- package/dist/nurikabe.types.d.ts +20 -0
- package/dist/nurikabe.types.js +1 -0
- package/dist/nurikabeBoard.d.ts +8 -0
- package/dist/nurikabeBoard.js +72 -0
- package/dist/nurikabeDraw.d.ts +4 -0
- package/dist/nurikabeDraw.js +15 -0
- package/dist/nurikabeGame.d.ts +8 -0
- package/dist/nurikabeGame.js +40 -0
- package/dist/nurikabeGenerate.d.ts +3 -0
- package/dist/nurikabeGenerate.js +31 -0
- package/dist/nurikabeMount.d.ts +3 -0
- package/dist/nurikabeMount.js +99 -0
- package/dist/nurikabePlay.types.d.ts +25 -0
- package/dist/nurikabePlay.types.js +1 -0
- package/dist/nurikabeSolve.d.ts +6 -0
- package/dist/nurikabeSolve.js +75 -0
- package/dist/nurikabeStrings.d.ts +70 -0
- package/dist/nurikabeStrings.js +5 -0
- package/dist/nurikabeStyle.d.ts +1 -0
- package/dist/nurikabeStyle.js +9 -0
- package/dist/ripple-draw-entry.d.ts +2 -0
- package/dist/ripple-draw-entry.js +1 -0
- package/dist/ripple-entry.d.ts +12 -0
- package/dist/ripple-entry.js +11 -0
- package/dist/ripple-play-entry.d.ts +4 -0
- package/dist/ripple-play-entry.js +3 -0
- package/dist/ripple.constants.d.ts +4 -0
- package/dist/ripple.constants.js +4 -0
- package/dist/ripple.types.d.ts +34 -0
- package/dist/ripple.types.js +1 -0
- package/dist/rippleBoard.d.ts +7 -0
- package/dist/rippleBoard.js +99 -0
- package/dist/rippleDraw.d.ts +3 -0
- package/dist/rippleDraw.js +32 -0
- package/dist/rippleGame.d.ts +13 -0
- package/dist/rippleGame.js +85 -0
- package/dist/rippleGenerate.d.ts +3 -0
- package/dist/rippleGenerate.js +57 -0
- package/dist/rippleMount.d.ts +2 -0
- package/dist/rippleMount.js +179 -0
- package/dist/ripplePlay.types.d.ts +23 -0
- package/dist/ripplePlay.types.js +1 -0
- package/dist/rippleSolve.d.ts +6 -0
- package/dist/rippleSolve.js +83 -0
- package/dist/rippleStrings.d.ts +86 -0
- package/dist/rippleStrings.js +5 -0
- package/dist/rippleStyle.d.ts +1 -0
- package/dist/rippleStyle.js +1 -0
- package/dist/shikaku-entry.d.ts +2 -0
- package/dist/shikaku-entry.js +2 -0
- package/dist/shikaku.constants.d.ts +5 -2
- package/dist/shikaku.constants.js +5 -2
- package/dist/shikaku.types.d.ts +18 -1
- package/dist/shikakuBuild.d.ts +21 -0
- package/dist/shikakuBuild.js +135 -0
- package/dist/shikakuGenerate.d.ts +6 -1
- package/dist/shikakuGenerate.js +50 -35
- package/dist/shikakuLogic.d.ts +13 -0
- package/dist/shikakuLogic.js +84 -0
- package/dist/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -0
- package/dist/shikakuRate.d.ts +3 -0
- package/dist/shikakuRate.js +27 -0
- package/dist/shikakuSolve.d.ts +1 -1
- package/dist/shikakuSolve.js +23 -44
- package/dist/shikakuTemplate.d.ts +3 -0
- package/dist/shikakuTemplate.js +43 -0
- package/dist/slitherlink-draw-entry.d.ts +2 -0
- package/dist/slitherlink-draw-entry.js +1 -0
- package/dist/slitherlink-entry.d.ts +12 -0
- package/dist/slitherlink-entry.js +10 -0
- package/dist/slitherlink-play-entry.d.ts +4 -0
- package/dist/slitherlink-play-entry.js +3 -0
- package/dist/slitherlink.constants.d.ts +7 -0
- package/dist/slitherlink.constants.js +7 -0
- package/dist/slitherlink.types.d.ts +52 -0
- package/dist/slitherlink.types.js +1 -0
- package/dist/slitherlinkBoard.d.ts +12 -0
- package/dist/slitherlinkBoard.js +132 -0
- package/dist/slitherlinkDraw.d.ts +4 -0
- package/dist/slitherlinkDraw.js +51 -0
- package/dist/slitherlinkGame.d.ts +8 -0
- package/dist/slitherlinkGame.js +63 -0
- package/dist/slitherlinkGenerate.d.ts +10 -0
- package/dist/slitherlinkGenerate.js +182 -0
- package/dist/slitherlinkLogic.d.ts +10 -0
- package/dist/slitherlinkLogic.js +267 -0
- package/dist/slitherlinkMount.d.ts +3 -0
- package/dist/slitherlinkMount.js +267 -0
- package/dist/slitherlinkPlay.types.d.ts +23 -0
- package/dist/slitherlinkPlay.types.js +1 -0
- package/dist/slitherlinkRate.d.ts +3 -0
- package/dist/slitherlinkRate.js +26 -0
- package/dist/slitherlinkSolve.d.ts +6 -0
- package/dist/slitherlinkSolve.js +19 -0
- package/dist/slitherlinkStrings.d.ts +86 -0
- package/dist/slitherlinkStrings.js +47 -0
- package/dist/slitherlinkStyle.d.ts +1 -0
- package/dist/slitherlinkStyle.js +18 -0
- package/dist/slitherlinkTemplate.d.ts +3 -0
- package/dist/slitherlinkTemplate.js +101 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/yajilin-draw-entry.d.ts +2 -0
- package/dist/yajilin-draw-entry.js +1 -0
- package/dist/yajilin-entry.d.ts +6 -0
- package/dist/yajilin-entry.js +6 -0
- package/dist/yajilin-play-entry.d.ts +4 -0
- package/dist/yajilin-play-entry.js +3 -0
- package/dist/yajilin.constants.d.ts +2 -0
- package/dist/yajilin.constants.js +2 -0
- package/dist/yajilin.types.d.ts +63 -0
- package/dist/yajilin.types.js +1 -0
- package/dist/yajilinBoard.d.ts +9 -0
- package/dist/yajilinBoard.js +91 -0
- package/dist/yajilinDraw.d.ts +2 -0
- package/dist/yajilinDraw.js +33 -0
- package/dist/yajilinGame.d.ts +15 -0
- package/dist/yajilinGame.js +58 -0
- package/dist/yajilinGenerate.d.ts +3 -0
- package/dist/yajilinGenerate.js +224 -0
- package/dist/yajilinMount.d.ts +2 -0
- package/dist/yajilinMount.js +162 -0
- package/dist/yajilinPlay.types.d.ts +1 -0
- package/dist/yajilinPlay.types.js +1 -0
- package/dist/yajilinSolve.d.ts +6 -0
- package/dist/yajilinSolve.js +82 -0
- package/dist/yajilinStrings.d.ts +32 -0
- package/dist/yajilinStrings.js +4 -0
- package/dist/yajilinStyle.d.ts +1 -0
- package/dist/yajilinStyle.js +12 -0
- package/package.json +180 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type NurikabeBoard = {
|
|
2
|
+
size: 5;
|
|
3
|
+
clues: readonly number[];
|
|
4
|
+
};
|
|
5
|
+
export type NurikabePuzzle = NurikabeBoard & {
|
|
6
|
+
seed: number;
|
|
7
|
+
solution: readonly boolean[];
|
|
8
|
+
};
|
|
9
|
+
export type NurikabeSolve = {
|
|
10
|
+
count: number;
|
|
11
|
+
solution: readonly boolean[] | null;
|
|
12
|
+
complete: boolean;
|
|
13
|
+
nodes: number;
|
|
14
|
+
};
|
|
15
|
+
export type NurikabeGame = {
|
|
16
|
+
board: NurikabeBoard;
|
|
17
|
+
sea: readonly boolean[];
|
|
18
|
+
history: readonly (readonly boolean[])[];
|
|
19
|
+
helped: boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NurikabeBoard } from "./nurikabe.types.ts";
|
|
2
|
+
export declare function isNurikabeBoard(board: NurikabeBoard): boolean;
|
|
3
|
+
export declare function nurikabeNeighbors(size: number, cell: number): number[];
|
|
4
|
+
/** Checks island areas and separation, the connected sea, and the ban on 2×2 sea blocks. */
|
|
5
|
+
export declare function checkNurikabe(board: NurikabeBoard, sea: readonly boolean[]): {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
errors: readonly number[];
|
|
8
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function isNurikabeBoard(board) {
|
|
2
|
+
return !!board && board.size === 5 && Array.isArray(board.clues)
|
|
3
|
+
&& board.clues.length === board.size * board.size
|
|
4
|
+
&& board.clues.every(value => Number.isInteger(value) && value >= 0 && value <= board.size ** 2)
|
|
5
|
+
&& board.clues.some(value => value > 0)
|
|
6
|
+
&& board.clues.reduce((sum, value) => sum + value, 0) < board.size * board.size;
|
|
7
|
+
}
|
|
8
|
+
export function nurikabeNeighbors(size, cell) {
|
|
9
|
+
const x = cell % size, y = Math.floor(cell / size), neighbors = [];
|
|
10
|
+
if (x > 0)
|
|
11
|
+
neighbors.push(cell - 1);
|
|
12
|
+
if (x < size - 1)
|
|
13
|
+
neighbors.push(cell + 1);
|
|
14
|
+
if (y > 0)
|
|
15
|
+
neighbors.push(cell - size);
|
|
16
|
+
if (y < size - 1)
|
|
17
|
+
neighbors.push(cell + size);
|
|
18
|
+
return neighbors;
|
|
19
|
+
}
|
|
20
|
+
/** Checks island areas and separation, the connected sea, and the ban on 2×2 sea blocks. */
|
|
21
|
+
export function checkNurikabe(board, sea) {
|
|
22
|
+
if (!isNurikabeBoard(board) || sea.length !== board.size * board.size || !sea.every(value => typeof value === "boolean"))
|
|
23
|
+
return { ok: false, errors: [] };
|
|
24
|
+
const size = board.size, total = size * size, errors = new Set();
|
|
25
|
+
for (let cell = 0; cell < total; cell += 1)
|
|
26
|
+
if (board.clues[cell] && sea[cell])
|
|
27
|
+
errors.add(cell);
|
|
28
|
+
for (let y = 0; y < size - 1; y += 1)
|
|
29
|
+
for (let x = 0; x < size - 1; x += 1) {
|
|
30
|
+
const square = [y * size + x, y * size + x + 1, (y + 1) * size + x, (y + 1) * size + x + 1];
|
|
31
|
+
if (square.every(cell => sea[cell]))
|
|
32
|
+
square.forEach(cell => errors.add(cell));
|
|
33
|
+
}
|
|
34
|
+
const visited = new Set(), islands = [];
|
|
35
|
+
for (let start = 0; start < total; start += 1)
|
|
36
|
+
if (!sea[start] && !visited.has(start)) {
|
|
37
|
+
const stack = [start], cells = [], clues = [];
|
|
38
|
+
visited.add(start);
|
|
39
|
+
while (stack.length) {
|
|
40
|
+
const cell = stack.pop();
|
|
41
|
+
cells.push(cell);
|
|
42
|
+
if (board.clues[cell])
|
|
43
|
+
clues.push(cell);
|
|
44
|
+
for (const next of nurikabeNeighbors(size, cell))
|
|
45
|
+
if (!sea[next] && !visited.has(next)) {
|
|
46
|
+
visited.add(next);
|
|
47
|
+
stack.push(next);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
islands.push({ cells, clues });
|
|
51
|
+
if (clues.length !== 1 || board.clues[clues[0]] !== cells.length)
|
|
52
|
+
cells.forEach(cell => errors.add(cell));
|
|
53
|
+
}
|
|
54
|
+
for (let cell = 0; cell < total; cell += 1)
|
|
55
|
+
if (board.clues[cell] && !islands.some(island => island.clues[0] === cell))
|
|
56
|
+
errors.add(cell);
|
|
57
|
+
const black = sea.map((value, cell) => value ? cell : -1).filter(cell => cell >= 0), connected = new Set();
|
|
58
|
+
if (black.length) {
|
|
59
|
+
const stack = [black[0]];
|
|
60
|
+
connected.add(black[0]);
|
|
61
|
+
while (stack.length)
|
|
62
|
+
for (const next of nurikabeNeighbors(size, stack.pop()))
|
|
63
|
+
if (sea[next] && !connected.has(next)) {
|
|
64
|
+
connected.add(next);
|
|
65
|
+
stack.push(next);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const cell of black)
|
|
69
|
+
if (!connected.has(cell))
|
|
70
|
+
errors.add(cell);
|
|
71
|
+
return { ok: errors.size === 0, errors: [...errors].sort((a, b) => a - b) };
|
|
72
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NurikabeBoard } from "./nurikabe.types.ts";
|
|
2
|
+
import type { NurikabeDrawOptions } from "./nurikabePlay.types.ts";
|
|
3
|
+
/** SVG text contains the numbered clues and the player's sea marks only. */
|
|
4
|
+
export declare function drawNurikabe(board: NurikabeBoard, options?: NurikabeDrawOptions): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isNurikabeBoard } from "./nurikabeBoard.js";
|
|
2
|
+
/** SVG text contains the numbered clues and the player's sea marks only. */
|
|
3
|
+
export function drawNurikabe(board, options = {}) {
|
|
4
|
+
if (!isNurikabeBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Nurikabe board");
|
|
6
|
+
const n = board.size, cell = 52;
|
|
7
|
+
const palettes = { ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"], wood: ["#e5cda6", "#493e2f", "#bca582"], slate: ["#333a42", "#f1eee7", "#707984"] };
|
|
8
|
+
const [paper, ink, grid] = palettes[options.material ?? "ivory"], sea = options.sea ?? [];
|
|
9
|
+
const cells = board.clues.map((clue, c) => {
|
|
10
|
+
const x = c % n * cell, y = Math.floor(c / n) * cell, black = sea[c], selected = options.selected === c, error = options.errors?.includes(c);
|
|
11
|
+
const tile = options.pieces === "tiles" && clue && !black ? `<rect x="${x + 10}" y="${y + 10}" width="32" height="32" rx="6" fill="${paper}" stroke="${grid}"/>` : "";
|
|
12
|
+
return `<g><rect x="${x}" y="${y}" width="${cell}" height="${cell}" fill="${black ? ink : selected ? `${ink}22` : paper}" stroke="${grid}" stroke-width="1"/>${tile}${clue ? `<text x="${x + cell / 2}" y="${y + cell / 2}" text-anchor="middle" dominant-baseline="middle" fill="${black ? paper : error ? "#b5452c" : ink}" font-size="22" font-weight="700" font-family="system-ui,sans-serif">${clue}</text>` : ""}</g>`;
|
|
13
|
+
}).join("");
|
|
14
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 ${n * cell + 4} ${n * cell + 4}" role="img" aria-label="Nurikabe" style="display:block;width:100%;height:auto;background:${paper}">${cells}<rect width="${n * cell}" height="${n * cell}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="2"/></svg>`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NurikabeBoard, NurikabeGame } from "./nurikabe.types.ts";
|
|
2
|
+
export declare function newNurikabe(board: NurikabeBoard): NurikabeGame;
|
|
3
|
+
export declare function markNurikabeSea(game: NurikabeGame, cell: number): NurikabeGame;
|
|
4
|
+
export declare function undoNurikabe(game: NurikabeGame): NurikabeGame;
|
|
5
|
+
export declare function restartNurikabe(game: NurikabeGame): NurikabeGame;
|
|
6
|
+
export declare function hintNurikabe(game: NurikabeGame): number | null;
|
|
7
|
+
export declare function encodeNurikabe(game: NurikabeGame): string;
|
|
8
|
+
export declare function decodeNurikabe(code: string): NurikabeGame | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isNurikabeBoard } from "./nurikabeBoard.js";
|
|
2
|
+
import { solveNurikabe } from "./nurikabeSolve.js";
|
|
3
|
+
export function newNurikabe(board) {
|
|
4
|
+
if (!isNurikabeBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Nurikabe board");
|
|
6
|
+
return { board: { size: board.size, clues: [...board.clues] }, sea: Array(board.size ** 2).fill(false), history: [], helped: false };
|
|
7
|
+
}
|
|
8
|
+
export function markNurikabeSea(game, cell) {
|
|
9
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= game.sea.length || game.board.clues[cell])
|
|
10
|
+
return game;
|
|
11
|
+
const sea = [...game.sea];
|
|
12
|
+
sea[cell] = !sea[cell];
|
|
13
|
+
return { ...game, sea, history: [...game.history, [...game.sea]] };
|
|
14
|
+
}
|
|
15
|
+
export function undoNurikabe(game) {
|
|
16
|
+
const prior = game.history.at(-1);
|
|
17
|
+
return prior ? { ...game, sea: [...prior], history: game.history.slice(0, -1) } : game;
|
|
18
|
+
}
|
|
19
|
+
export function restartNurikabe(game) { return { ...game, sea: Array(game.sea.length).fill(false), history: [], helped: false }; }
|
|
20
|
+
export function hintNurikabe(game) {
|
|
21
|
+
const solved = solveNurikabe(game.board);
|
|
22
|
+
if (!solved.complete || solved.count !== 1 || !solved.solution)
|
|
23
|
+
return null;
|
|
24
|
+
const cell = solved.solution.findIndex((value, i) => value !== game.sea[i]);
|
|
25
|
+
return cell < 0 ? null : cell;
|
|
26
|
+
}
|
|
27
|
+
export function encodeNurikabe(game) { return JSON.stringify({ version: 1, board: game.board, sea: game.sea, helped: game.helped }); }
|
|
28
|
+
export function decodeNurikabe(code) {
|
|
29
|
+
try {
|
|
30
|
+
if (code.length > 10000)
|
|
31
|
+
return null;
|
|
32
|
+
const value = JSON.parse(code);
|
|
33
|
+
if (value.version !== 1 || !isNurikabeBoard(value.board) || !Array.isArray(value.sea) || value.sea.length !== value.board.size ** 2 || !value.sea.every((item) => typeof item === "boolean") || typeof value.helped !== "boolean")
|
|
34
|
+
return null;
|
|
35
|
+
return { ...newNurikabe(value.board), sea: [...value.sea], helped: value.helped };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NURIKABE_MAX_NODES } from "./nurikabe.constants.js";
|
|
2
|
+
import { checkNurikabe } from "./nurikabeBoard.js";
|
|
3
|
+
import { solveNurikabe } from "./nurikabeSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
const originals = [
|
|
6
|
+
{ clues: [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 7, 0, 0], solution: [false, true, true, false, true, true, false, true, true, true, true, true, true, false, true, false, true, false, false, false, true, true, false, false, false] },
|
|
7
|
+
{ clues: [0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3], solution: [true, true, true, true, true, true, false, false, true, false, false, true, true, true, true, false, true, false, true, false, true, true, true, false, false] },
|
|
8
|
+
{ clues: [1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 1], solution: [false, true, true, true, true, true, true, false, true, false, false, true, false, true, false, false, true, false, true, true, false, false, true, true, false] },
|
|
9
|
+
{ clues: [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 0], solution: [false, true, true, true, false, false, true, false, true, true, false, true, false, false, true, true, true, true, false, true, false, false, true, false, false] },
|
|
10
|
+
{ clues: [0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 1, 0, 0, 0], solution: [true, true, true, false, true, true, false, false, true, true, true, false, true, true, false, true, true, true, false, false, true, false, true, false, false] },
|
|
11
|
+
];
|
|
12
|
+
/** Seeded symmetries of original 5×5 layouts are returned only after the solver proves uniqueness. */
|
|
13
|
+
export function generateNurikabe(seed = 1) {
|
|
14
|
+
if (!isKazuSeed(seed))
|
|
15
|
+
throw new RangeError("Invalid Nurikabe seed");
|
|
16
|
+
const random = seededRandom(seed), sample = originals[Math.floor(random() * originals.length)], rotation = Math.floor(random() * 4), reflect = random() < 0.5;
|
|
17
|
+
const size = 5, clues = Array(size * size).fill(0), solution = Array(size * size).fill(false);
|
|
18
|
+
for (let y = 0; y < size; y += 1)
|
|
19
|
+
for (let x = 0; x < size; x += 1) {
|
|
20
|
+
let sx = reflect ? size - 1 - x : x, sy = y;
|
|
21
|
+
for (let turn = 0; turn < rotation; turn += 1)
|
|
22
|
+
[sx, sy] = [size - 1 - sy, sx];
|
|
23
|
+
const from = sy * size + sx, to = y * size + x;
|
|
24
|
+
clues[to] = sample.clues[from];
|
|
25
|
+
solution[to] = sample.solution[from];
|
|
26
|
+
}
|
|
27
|
+
const board = { size: 5, clues }, proof = solveNurikabe(board, { nodes: NURIKABE_MAX_NODES });
|
|
28
|
+
if (!checkNurikabe(board, solution).ok || !proof.complete || proof.count !== 1 || !proof.solution)
|
|
29
|
+
throw new Error("No uniquely solvable Nurikabe found within the generation budget; try another seed");
|
|
30
|
+
return { ...board, seed, solution: proof.solution };
|
|
31
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { checkNurikabe } from "./nurikabeBoard.js";
|
|
2
|
+
import { decodeNurikabe, encodeNurikabe, hintNurikabe, markNurikabeSea, newNurikabe, restartNurikabe, undoNurikabe } from "./nurikabeGame.js";
|
|
3
|
+
import { drawNurikabe } from "./nurikabeDraw.js";
|
|
4
|
+
import { KAZU_STYLE } from "./style.js";
|
|
5
|
+
import { NURIKABE_PLAY_STYLE } from "./nurikabeStyle.js";
|
|
6
|
+
import { nurikabeWords } from "./nurikabeStrings.js";
|
|
7
|
+
/** A keyboard- and touch-friendly Nurikabe player, mounted in any host element. */
|
|
8
|
+
export function mountNurikabe(host, initial) {
|
|
9
|
+
const doc = host.ownerDocument;
|
|
10
|
+
let options = { ...initial }, game = initial.progress ? decodeNurikabe(initial.progress) : null;
|
|
11
|
+
game ?? (game = newNurikabe(initial.board));
|
|
12
|
+
let selected = 0, errors = [], message = "", finished = checkNurikabe(game.board, game.sea).ok, dead = false;
|
|
13
|
+
const root = doc.createElement("div"), style = doc.createElement("style"), board = doc.createElement("div"), status = doc.createElement("p"), tools = doc.createElement("div"), help = doc.createElement("p");
|
|
14
|
+
root.className = "kn-root";
|
|
15
|
+
style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".ks-palette") + NURIKABE_PLAY_STYLE;
|
|
16
|
+
board.className = "kn-board";
|
|
17
|
+
status.className = "kn-status";
|
|
18
|
+
status.setAttribute("role", "status");
|
|
19
|
+
tools.className = "kn-tools";
|
|
20
|
+
help.className = "kn-help";
|
|
21
|
+
root.append(style, board, status, tools, help);
|
|
22
|
+
host.append(root);
|
|
23
|
+
const copy = () => ({ ...game, board: { ...game.board, clues: [...game.board.clues] }, sea: [...game.sea], history: game.history.map(turn => [...turn]) });
|
|
24
|
+
const button = (label, action, disabled = false) => {
|
|
25
|
+
const control = doc.createElement("button");
|
|
26
|
+
control.type = "button";
|
|
27
|
+
control.textContent = label;
|
|
28
|
+
control.disabled = disabled;
|
|
29
|
+
control.onclick = action;
|
|
30
|
+
tools.append(control);
|
|
31
|
+
};
|
|
32
|
+
const changed = () => {
|
|
33
|
+
errors = [];
|
|
34
|
+
message = "";
|
|
35
|
+
const wasFinished = finished;
|
|
36
|
+
finished = checkNurikabe(game.board, game.sea).ok;
|
|
37
|
+
render();
|
|
38
|
+
options.onChange?.(copy());
|
|
39
|
+
if (finished && !wasFinished)
|
|
40
|
+
options.onFinish?.(copy());
|
|
41
|
+
};
|
|
42
|
+
const mark = (cell) => { if (finished)
|
|
43
|
+
return; selected = cell; game = markNurikabeSea(game, cell); changed(); };
|
|
44
|
+
const render = () => {
|
|
45
|
+
if (dead)
|
|
46
|
+
return;
|
|
47
|
+
const words = nurikabeWords(options.language), focused = board.contains(doc.activeElement);
|
|
48
|
+
board.innerHTML = drawNurikabe(game.board, { ...options, sea: game.sea, selected, errors });
|
|
49
|
+
const cells = doc.createElement("div");
|
|
50
|
+
cells.className = "kn-cells";
|
|
51
|
+
cells.style.gridTemplateColumns = `repeat(${game.board.size},1fr)`;
|
|
52
|
+
for (let cell = 0; cell < game.sea.length; cell += 1) {
|
|
53
|
+
const control = doc.createElement("button");
|
|
54
|
+
control.type = "button";
|
|
55
|
+
control.className = "kn-cell";
|
|
56
|
+
control.dataset.cell = String(cell);
|
|
57
|
+
control.tabIndex = cell === selected ? 0 : -1;
|
|
58
|
+
control.setAttribute("aria-label", `${words.row} ${Math.floor(cell / game.board.size) + 1}, ${words.column} ${cell % game.board.size + 1}: ${game.board.clues[cell] || (game.sea[cell] ? words.sea : words.white)}`);
|
|
59
|
+
control.onclick = () => mark(cell);
|
|
60
|
+
control.onkeydown = event => {
|
|
61
|
+
const n = game.board.size, moves = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -n, ArrowDown: n };
|
|
62
|
+
if (event.key in moves) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
selected = Math.max(0, Math.min(game.sea.length - 1, cell + moves[event.key]));
|
|
65
|
+
render();
|
|
66
|
+
}
|
|
67
|
+
else if (event.key === " " || event.key === "Enter") {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
mark(cell);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
cells.append(control);
|
|
73
|
+
}
|
|
74
|
+
board.append(cells);
|
|
75
|
+
if (focused)
|
|
76
|
+
board.querySelector(`[data-cell="${selected}"]`)?.focus();
|
|
77
|
+
status.textContent = finished ? (game.helped ? words.helped : words.won) : message;
|
|
78
|
+
help.textContent = words.help;
|
|
79
|
+
tools.replaceChildren();
|
|
80
|
+
button(words.undo, () => { game = undoNurikabe(game); changed(); }, !game.history.length);
|
|
81
|
+
button(words.check, () => { errors = checkNurikabe(game.board, game.sea).errors; message = errors.length ? words.wrong : words.good; render(); });
|
|
82
|
+
button(words.hint, () => {
|
|
83
|
+
const cell = hintNurikabe(game);
|
|
84
|
+
if (cell === null) {
|
|
85
|
+
message = words.noHint;
|
|
86
|
+
render();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
game = { ...markNurikabeSea(game, cell), helped: true };
|
|
90
|
+
changed();
|
|
91
|
+
}, finished);
|
|
92
|
+
button(words.restart, () => { game = restartNurikabe(game); finished = false; changed(); });
|
|
93
|
+
};
|
|
94
|
+
render();
|
|
95
|
+
return {
|
|
96
|
+
game: copy, progress: () => encodeNurikabe(game), restart: () => { game = restartNurikabe(game); finished = false; changed(); },
|
|
97
|
+
set: (next) => { options = { ...options, ...next }; render(); }, destroy: () => { dead = true; root.remove(); },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { NurikabeBoard, NurikabeGame } from "./nurikabe.types.ts";
|
|
2
|
+
export type NurikabeLanguage = "en" | "ja";
|
|
3
|
+
export type NurikabeMaterial = "ivory" | "wood" | "slate";
|
|
4
|
+
export type NurikabePieces = "ink" | "tiles";
|
|
5
|
+
export type NurikabeDrawOptions = {
|
|
6
|
+
sea?: readonly boolean[];
|
|
7
|
+
selected?: number | null;
|
|
8
|
+
errors?: readonly number[];
|
|
9
|
+
material?: NurikabeMaterial;
|
|
10
|
+
pieces?: NurikabePieces;
|
|
11
|
+
language?: NurikabeLanguage;
|
|
12
|
+
};
|
|
13
|
+
export type NurikabeMountOptions = NurikabeDrawOptions & {
|
|
14
|
+
board: NurikabeBoard;
|
|
15
|
+
progress?: string;
|
|
16
|
+
onChange?: (game: NurikabeGame) => void;
|
|
17
|
+
onFinish?: (game: NurikabeGame) => void;
|
|
18
|
+
};
|
|
19
|
+
export type NurikabeMount = {
|
|
20
|
+
game: () => NurikabeGame;
|
|
21
|
+
progress: () => string;
|
|
22
|
+
restart: () => void;
|
|
23
|
+
set: (options: NurikabeDrawOptions) => void;
|
|
24
|
+
destroy: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NurikabeBoard, NurikabeSolve } from "./nurikabe.types.ts";
|
|
2
|
+
/** Counts valid island placements and checks the remaining cells as the sea under a finite budget. */
|
|
3
|
+
export declare function solveNurikabe(board: NurikabeBoard, options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): NurikabeSolve;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NURIKABE_MAX_NODES } from "./nurikabe.constants.js";
|
|
2
|
+
import { checkNurikabe, isNurikabeBoard, nurikabeNeighbors } from "./nurikabeBoard.js";
|
|
3
|
+
function islandShapes(board, clue) {
|
|
4
|
+
let shapes = new Map([[String(clue.cell), new Set([clue.cell])]]);
|
|
5
|
+
for (let area = 1; area < clue.size; area += 1) {
|
|
6
|
+
const next = new Map();
|
|
7
|
+
for (const shape of shapes.values()) {
|
|
8
|
+
const fringe = new Set();
|
|
9
|
+
for (const cell of shape)
|
|
10
|
+
for (const neighbor of nurikabeNeighbors(board.size, cell)) {
|
|
11
|
+
if ((!board.clues[neighbor] || neighbor === clue.cell) && !shape.has(neighbor))
|
|
12
|
+
fringe.add(neighbor);
|
|
13
|
+
}
|
|
14
|
+
for (const cell of fringe) {
|
|
15
|
+
const grown = new Set(shape);
|
|
16
|
+
grown.add(cell);
|
|
17
|
+
const key = [...grown].sort((a, b) => a - b).join(",");
|
|
18
|
+
next.set(key, grown);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
shapes = next;
|
|
22
|
+
}
|
|
23
|
+
return [...shapes.values()].map(shape => [...shape]);
|
|
24
|
+
}
|
|
25
|
+
/** Counts valid island placements and checks the remaining cells as the sea under a finite budget. */
|
|
26
|
+
export function solveNurikabe(board, options = {}) {
|
|
27
|
+
if (!isNurikabeBoard(board))
|
|
28
|
+
throw new RangeError("Invalid Nurikabe board");
|
|
29
|
+
const total = board.size ** 2, limit = Math.max(1, Math.floor(options.limit ?? 2)), budget = Math.max(1, Math.floor(options.nodes ?? NURIKABE_MAX_NODES));
|
|
30
|
+
const clues = board.clues.map((size, cell) => size ? { cell, size } : null).filter((clue) => !!clue);
|
|
31
|
+
const choices = clues.map(clue => islandShapes(board, clue));
|
|
32
|
+
const placed = [];
|
|
33
|
+
let nodes = 0, count = 0, solution = null, stopped = false;
|
|
34
|
+
const visit = (depth) => {
|
|
35
|
+
if (count >= limit || stopped)
|
|
36
|
+
return;
|
|
37
|
+
nodes += 1;
|
|
38
|
+
if (nodes > budget) {
|
|
39
|
+
stopped = true;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (depth === clues.length) {
|
|
43
|
+
const sea = Array(total).fill(true);
|
|
44
|
+
for (const island of placed)
|
|
45
|
+
for (const cell of island)
|
|
46
|
+
sea[cell] = false;
|
|
47
|
+
if (checkNurikabe(board, sea).ok) {
|
|
48
|
+
count += 1;
|
|
49
|
+
solution ?? (solution = sea);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let selected = -1, candidates = null;
|
|
54
|
+
for (let clue = 0; clue < clues.length; clue += 1)
|
|
55
|
+
if (!placed[clue]) {
|
|
56
|
+
const available = choices[clue].filter(shape => placed.every(other => !other
|
|
57
|
+
|| !shape.some(cell => other.some(neighbor => cell === neighbor || nurikabeNeighbors(board.size, cell).includes(neighbor)))));
|
|
58
|
+
if (!candidates || available.length < candidates.length) {
|
|
59
|
+
selected = clue;
|
|
60
|
+
candidates = available;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (selected < 0 || !candidates?.length)
|
|
64
|
+
return;
|
|
65
|
+
for (const shape of candidates) {
|
|
66
|
+
placed[selected] = shape;
|
|
67
|
+
visit(depth + 1);
|
|
68
|
+
delete placed[selected];
|
|
69
|
+
if (count >= limit || stopped)
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
visit(0);
|
|
74
|
+
return { count, solution, complete: !stopped && count < limit, nodes: Math.min(nodes, budget) };
|
|
75
|
+
}
|
|
@@ -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;
|