@johnmorrisdotca/kazu 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +429 -5
- package/dist/akari-draw-entry.d.ts +2 -0
- package/dist/akari-draw-entry.js +1 -0
- package/dist/akari-entry.d.ts +12 -0
- package/dist/akari-entry.js +10 -0
- package/dist/akari-play-entry.d.ts +2 -0
- package/dist/akari-play-entry.js +1 -0
- package/dist/akari.constants.d.ts +2 -0
- package/dist/akari.constants.js +2 -0
- package/dist/akari.types.d.ts +33 -0
- package/dist/akari.types.js +1 -0
- package/dist/akariBoard.d.ts +11 -0
- package/dist/akariBoard.js +96 -0
- package/dist/akariDraw.d.ts +4 -0
- package/dist/akariDraw.js +38 -0
- package/dist/akariGame.d.ts +15 -0
- package/dist/akariGame.js +71 -0
- package/dist/akariGenerate.d.ts +7 -0
- package/dist/akariGenerate.js +78 -0
- package/dist/akariMount.d.ts +3 -0
- package/dist/akariMount.js +214 -0
- package/dist/akariPlay.types.d.ts +25 -0
- package/dist/akariPlay.types.js +1 -0
- package/dist/akariSolve.d.ts +6 -0
- package/dist/akariSolve.js +106 -0
- package/dist/akariStrings.d.ts +98 -0
- package/dist/akariStrings.js +53 -0
- package/dist/akariStyle.d.ts +1 -0
- package/dist/akariStyle.js +9 -0
- package/dist/draw-entry.d.ts +1 -0
- package/dist/draw-entry.js +1 -0
- package/dist/fillomino-draw-entry.d.ts +2 -0
- package/dist/fillomino-draw-entry.js +1 -0
- package/dist/fillomino-entry.d.ts +8 -0
- package/dist/fillomino-entry.js +7 -0
- package/dist/fillomino-play-entry.d.ts +4 -0
- package/dist/fillomino-play-entry.js +3 -0
- package/dist/fillomino.constants.d.ts +5 -0
- package/dist/fillomino.constants.js +5 -0
- package/dist/fillomino.types.d.ts +30 -0
- package/dist/fillomino.types.js +1 -0
- package/dist/fillominoBoard.d.ts +6 -0
- package/dist/fillominoBoard.js +84 -0
- package/dist/fillominoDraw.d.ts +3 -0
- package/dist/fillominoDraw.js +31 -0
- package/dist/fillominoGame.d.ts +13 -0
- package/dist/fillominoGame.js +65 -0
- package/dist/fillominoGenerate.d.ts +3 -0
- package/dist/fillominoGenerate.js +125 -0
- package/dist/fillominoMount.d.ts +2 -0
- package/dist/fillominoMount.js +211 -0
- package/dist/fillominoPlay.types.d.ts +25 -0
- package/dist/fillominoPlay.types.js +1 -0
- package/dist/fillominoSolve.d.ts +6 -0
- package/dist/fillominoSolve.js +111 -0
- package/dist/fillominoStrings.d.ts +102 -0
- package/dist/fillominoStrings.js +13 -0
- package/dist/fillominoStyle.d.ts +1 -0
- package/dist/fillominoStyle.js +13 -0
- package/dist/fillominoWorker.d.ts +1 -0
- package/dist/fillominoWorker.js +10 -0
- package/dist/heyawake-draw-entry.d.ts +2 -0
- package/dist/heyawake-draw-entry.js +1 -0
- package/dist/heyawake-entry.d.ts +13 -0
- package/dist/heyawake-entry.js +11 -0
- package/dist/heyawake-play-entry.d.ts +2 -0
- package/dist/heyawake-play-entry.js +1 -0
- package/dist/heyawake.constants.d.ts +5 -0
- package/dist/heyawake.constants.js +5 -0
- package/dist/heyawake.types.d.ts +35 -0
- package/dist/heyawake.types.js +1 -0
- package/dist/heyawakeBoard.d.ts +7 -0
- package/dist/heyawakeBoard.js +116 -0
- package/dist/heyawakeDraw.d.ts +3 -0
- package/dist/heyawakeDraw.js +27 -0
- package/dist/heyawakeGame.d.ts +12 -0
- package/dist/heyawakeGame.js +51 -0
- package/dist/heyawakeGenerate.d.ts +2 -0
- package/dist/heyawakeGenerate.js +114 -0
- package/dist/heyawakeMount.d.ts +2 -0
- package/dist/heyawakeMount.js +106 -0
- package/dist/heyawakePlay.types.d.ts +24 -0
- package/dist/heyawakePlay.types.js +1 -0
- package/dist/heyawakeSolve.d.ts +5 -0
- package/dist/heyawakeSolve.js +52 -0
- package/dist/heyawakeStrings.d.ts +19 -0
- package/dist/heyawakeStrings.js +7 -0
- package/dist/heyawakeStyle.d.ts +1 -0
- package/dist/heyawakeStyle.js +2 -0
- package/dist/heyawakeWorker.d.ts +1 -0
- package/dist/heyawakeWorker.js +10 -0
- package/dist/hitori-draw-entry.d.ts +2 -0
- package/dist/hitori-draw-entry.js +1 -0
- package/dist/hitori-entry.d.ts +6 -0
- package/dist/hitori-entry.js +6 -0
- package/dist/hitori-play-entry.d.ts +4 -0
- package/dist/hitori-play-entry.js +3 -0
- package/dist/hitori.constants.d.ts +2 -0
- package/dist/hitori.constants.js +2 -0
- package/dist/hitori.types.d.ts +20 -0
- package/dist/hitori.types.js +1 -0
- package/dist/hitoriBoard.d.ts +8 -0
- package/dist/hitoriBoard.js +72 -0
- package/dist/hitoriDraw.d.ts +4 -0
- package/dist/hitoriDraw.js +25 -0
- package/dist/hitoriGame.d.ts +10 -0
- package/dist/hitoriGame.js +48 -0
- package/dist/hitoriGenerate.d.ts +3 -0
- package/dist/hitoriGenerate.js +163 -0
- package/dist/hitoriMount.d.ts +3 -0
- package/dist/hitoriMount.js +111 -0
- package/dist/hitoriPlay.types.d.ts +25 -0
- package/dist/hitoriPlay.types.js +1 -0
- package/dist/hitoriSolve.d.ts +6 -0
- package/dist/hitoriSolve.js +69 -0
- package/dist/hitoriStrings.d.ts +70 -0
- package/dist/hitoriStrings.js +17 -0
- package/dist/hitoriStyle.d.ts +1 -0
- package/dist/hitoriStyle.js +9 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/juosan-draw-entry.d.ts +2 -0
- package/dist/juosan-draw-entry.js +1 -0
- package/dist/juosan-entry.d.ts +11 -0
- package/dist/juosan-entry.js +10 -0
- package/dist/juosan-play-entry.d.ts +4 -0
- package/dist/juosan-play-entry.js +3 -0
- package/dist/juosan.constants.d.ts +4 -0
- package/dist/juosan.constants.js +4 -0
- package/dist/juosan.types.d.ts +33 -0
- package/dist/juosan.types.js +1 -0
- package/dist/juosanBoard.d.ts +7 -0
- package/dist/juosanBoard.js +102 -0
- package/dist/juosanDraw.d.ts +4 -0
- package/dist/juosanDraw.js +49 -0
- package/dist/juosanGame.d.ts +20 -0
- package/dist/juosanGame.js +86 -0
- package/dist/juosanGenerate.d.ts +3 -0
- package/dist/juosanGenerate.js +31 -0
- package/dist/juosanMount.d.ts +3 -0
- package/dist/juosanMount.js +206 -0
- package/dist/juosanPlay.types.d.ts +24 -0
- package/dist/juosanPlay.types.js +1 -0
- package/dist/juosanSolve.d.ts +3 -0
- package/dist/juosanSolve.js +82 -0
- package/dist/juosanStrings.d.ts +77 -0
- package/dist/juosanStrings.js +43 -0
- package/dist/juosanStyle.d.ts +1 -0
- package/dist/juosanStyle.js +1 -0
- package/dist/kakuro-draw-entry.d.ts +2 -0
- package/dist/kakuro-draw-entry.js +1 -0
- package/dist/kakuro-entry.d.ts +12 -0
- package/dist/kakuro-entry.js +10 -0
- package/dist/kakuro-play-entry.d.ts +4 -0
- package/dist/kakuro-play-entry.js +3 -0
- package/dist/kakuro.constants.d.ts +6 -0
- package/dist/kakuro.constants.js +6 -0
- package/dist/kakuro.types.d.ts +46 -0
- package/dist/kakuro.types.js +1 -0
- package/dist/kakuroBoard.d.ts +8 -0
- package/dist/kakuroBoard.js +117 -0
- package/dist/kakuroDraw.d.ts +10 -0
- package/dist/kakuroDraw.js +21 -0
- package/dist/kakuroGame.d.ts +12 -0
- package/dist/kakuroGame.js +74 -0
- package/dist/kakuroGenerate.d.ts +3 -0
- package/dist/kakuroGenerate.js +130 -0
- package/dist/kakuroMount.d.ts +2 -0
- package/dist/kakuroMount.js +109 -0
- package/dist/kakuroPlay.types.d.ts +20 -0
- package/dist/kakuroPlay.types.js +1 -0
- package/dist/kakuroSolve.d.ts +6 -0
- package/dist/kakuroSolve.js +85 -0
- package/dist/kakuroStrings.d.ts +82 -0
- package/dist/kakuroStrings.js +5 -0
- package/dist/kakuroStyle.d.ts +1 -0
- package/dist/kakuroStyle.js +1 -0
- package/dist/masyu-draw-entry.d.ts +2 -0
- package/dist/masyu-draw-entry.js +1 -0
- package/dist/masyu-entry.d.ts +6 -0
- package/dist/masyu-entry.js +6 -0
- package/dist/masyu-play-entry.d.ts +4 -0
- package/dist/masyu-play-entry.js +3 -0
- package/dist/masyu.constants.d.ts +2 -0
- package/dist/masyu.constants.js +2 -0
- package/dist/masyu.types.d.ts +46 -0
- package/dist/masyu.types.js +1 -0
- package/dist/masyuBoard.d.ts +10 -0
- package/dist/masyuBoard.js +88 -0
- package/dist/masyuDraw.d.ts +2 -0
- package/dist/masyuDraw.js +33 -0
- package/dist/masyuGame.d.ts +9 -0
- package/dist/masyuGame.js +46 -0
- package/dist/masyuGenerate.d.ts +3 -0
- package/dist/masyuGenerate.js +56 -0
- package/dist/masyuMount.d.ts +2 -0
- package/dist/masyuMount.js +118 -0
- package/dist/masyuPlay.types.d.ts +1 -0
- package/dist/masyuPlay.types.js +1 -0
- package/dist/masyuSolve.d.ts +6 -0
- package/dist/masyuSolve.js +88 -0
- package/dist/masyuStrings.d.ts +28 -0
- package/dist/masyuStrings.js +4 -0
- package/dist/masyuStyle.d.ts +1 -0
- package/dist/masyuStyle.js +11 -0
- package/dist/nurikabe-draw-entry.d.ts +2 -0
- package/dist/nurikabe-draw-entry.js +1 -0
- package/dist/nurikabe-entry.d.ts +6 -0
- package/dist/nurikabe-entry.js +6 -0
- package/dist/nurikabe-play-entry.d.ts +4 -0
- package/dist/nurikabe-play-entry.js +3 -0
- package/dist/nurikabe.constants.d.ts +2 -0
- package/dist/nurikabe.constants.js +2 -0
- package/dist/nurikabe.types.d.ts +20 -0
- package/dist/nurikabe.types.js +1 -0
- package/dist/nurikabeBoard.d.ts +8 -0
- package/dist/nurikabeBoard.js +72 -0
- package/dist/nurikabeDraw.d.ts +4 -0
- package/dist/nurikabeDraw.js +15 -0
- package/dist/nurikabeGame.d.ts +8 -0
- package/dist/nurikabeGame.js +40 -0
- package/dist/nurikabeGenerate.d.ts +3 -0
- package/dist/nurikabeGenerate.js +31 -0
- package/dist/nurikabeMount.d.ts +3 -0
- package/dist/nurikabeMount.js +99 -0
- package/dist/nurikabePlay.types.d.ts +25 -0
- package/dist/nurikabePlay.types.js +1 -0
- package/dist/nurikabeSolve.d.ts +6 -0
- package/dist/nurikabeSolve.js +75 -0
- package/dist/nurikabeStrings.d.ts +70 -0
- package/dist/nurikabeStrings.js +5 -0
- package/dist/nurikabeStyle.d.ts +1 -0
- package/dist/nurikabeStyle.js +9 -0
- package/dist/play-entry.d.ts +1 -0
- package/dist/play-entry.js +1 -0
- package/dist/ripple-draw-entry.d.ts +2 -0
- package/dist/ripple-draw-entry.js +1 -0
- package/dist/ripple-entry.d.ts +12 -0
- package/dist/ripple-entry.js +11 -0
- package/dist/ripple-play-entry.d.ts +4 -0
- package/dist/ripple-play-entry.js +3 -0
- package/dist/ripple.constants.d.ts +4 -0
- package/dist/ripple.constants.js +4 -0
- package/dist/ripple.types.d.ts +34 -0
- package/dist/ripple.types.js +1 -0
- package/dist/rippleBoard.d.ts +7 -0
- package/dist/rippleBoard.js +99 -0
- package/dist/rippleDraw.d.ts +3 -0
- package/dist/rippleDraw.js +32 -0
- package/dist/rippleGame.d.ts +13 -0
- package/dist/rippleGame.js +85 -0
- package/dist/rippleGenerate.d.ts +3 -0
- package/dist/rippleGenerate.js +57 -0
- package/dist/rippleMount.d.ts +2 -0
- package/dist/rippleMount.js +179 -0
- package/dist/ripplePlay.types.d.ts +23 -0
- package/dist/ripplePlay.types.js +1 -0
- package/dist/rippleSolve.d.ts +6 -0
- package/dist/rippleSolve.js +83 -0
- package/dist/rippleStrings.d.ts +86 -0
- package/dist/rippleStrings.js +5 -0
- package/dist/rippleStyle.d.ts +1 -0
- package/dist/rippleStyle.js +1 -0
- package/dist/shikaku-draw-entry.d.ts +2 -0
- package/dist/shikaku-draw-entry.js +1 -0
- package/dist/shikaku-entry.d.ts +8 -0
- package/dist/shikaku-entry.js +8 -0
- package/dist/shikaku-play-entry.d.ts +4 -0
- package/dist/shikaku-play-entry.js +3 -0
- package/dist/shikaku.constants.d.ts +5 -0
- package/dist/shikaku.constants.js +5 -0
- package/dist/shikaku.types.d.ts +36 -0
- package/dist/shikaku.types.js +1 -0
- package/dist/shikakuBoard.d.ts +9 -0
- package/dist/shikakuBoard.js +59 -0
- package/dist/shikakuDraw.d.ts +4 -0
- package/dist/shikakuDraw.js +23 -0
- package/dist/shikakuGame.d.ts +14 -0
- package/dist/shikakuGame.js +61 -0
- package/dist/shikakuGenerate.d.ts +3 -0
- package/dist/shikakuGenerate.js +43 -0
- package/dist/shikakuMount.d.ts +3 -0
- package/dist/shikakuMount.js +143 -0
- package/dist/shikakuPacks.d.ts +27 -0
- package/dist/shikakuPacks.js +26 -0
- package/dist/shikakuPlay.types.d.ts +26 -0
- package/dist/shikakuPlay.types.js +1 -0
- package/dist/shikakuSolve.d.ts +6 -0
- package/dist/shikakuSolve.js +54 -0
- package/dist/shikakuStrings.d.ts +102 -0
- package/dist/shikakuStrings.js +5 -0
- package/dist/shikakuStyle.d.ts +1 -0
- package/dist/shikakuStyle.js +12 -0
- package/dist/shikakuWorker.d.ts +1 -0
- package/dist/shikakuWorker.js +10 -0
- package/dist/slitherlink-draw-entry.d.ts +2 -0
- package/dist/slitherlink-draw-entry.js +1 -0
- package/dist/slitherlink-entry.d.ts +11 -0
- package/dist/slitherlink-entry.js +9 -0
- package/dist/slitherlink-play-entry.d.ts +4 -0
- package/dist/slitherlink-play-entry.js +3 -0
- package/dist/slitherlink.constants.d.ts +2 -0
- package/dist/slitherlink.constants.js +2 -0
- package/dist/slitherlink.types.d.ts +34 -0
- package/dist/slitherlink.types.js +1 -0
- package/dist/slitherlinkBoard.d.ts +12 -0
- package/dist/slitherlinkBoard.js +132 -0
- package/dist/slitherlinkDraw.d.ts +4 -0
- package/dist/slitherlinkDraw.js +51 -0
- package/dist/slitherlinkGame.d.ts +8 -0
- package/dist/slitherlinkGame.js +63 -0
- package/dist/slitherlinkGenerate.d.ts +3 -0
- package/dist/slitherlinkGenerate.js +101 -0
- package/dist/slitherlinkMount.d.ts +3 -0
- package/dist/slitherlinkMount.js +267 -0
- package/dist/slitherlinkPlay.types.d.ts +23 -0
- package/dist/slitherlinkPlay.types.js +1 -0
- package/dist/slitherlinkSolve.d.ts +6 -0
- package/dist/slitherlinkSolve.js +127 -0
- package/dist/slitherlinkStrings.d.ts +86 -0
- package/dist/slitherlinkStrings.js +47 -0
- package/dist/slitherlinkStyle.d.ts +1 -0
- package/dist/slitherlinkStyle.js +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/yajilin-draw-entry.d.ts +2 -0
- package/dist/yajilin-draw-entry.js +1 -0
- package/dist/yajilin-entry.d.ts +6 -0
- package/dist/yajilin-entry.js +6 -0
- package/dist/yajilin-play-entry.d.ts +4 -0
- package/dist/yajilin-play-entry.js +3 -0
- package/dist/yajilin.constants.d.ts +2 -0
- package/dist/yajilin.constants.js +2 -0
- package/dist/yajilin.types.d.ts +63 -0
- package/dist/yajilin.types.js +1 -0
- package/dist/yajilinBoard.d.ts +9 -0
- package/dist/yajilinBoard.js +91 -0
- package/dist/yajilinDraw.d.ts +2 -0
- package/dist/yajilinDraw.js +33 -0
- package/dist/yajilinGame.d.ts +15 -0
- package/dist/yajilinGame.js +58 -0
- package/dist/yajilinGenerate.d.ts +3 -0
- package/dist/yajilinGenerate.js +224 -0
- package/dist/yajilinMount.d.ts +2 -0
- package/dist/yajilinMount.js +162 -0
- package/dist/yajilinPlay.types.d.ts +1 -0
- package/dist/yajilinPlay.types.js +1 -0
- package/dist/yajilinSolve.d.ts +6 -0
- package/dist/yajilinSolve.js +82 -0
- package/dist/yajilinStrings.d.ts +32 -0
- package/dist/yajilinStrings.js +4 -0
- package/dist/yajilinStyle.d.ts +1 -0
- package/dist/yajilinStyle.js +12 -0
- package/package.json +196 -3
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Heyawake has its own room model alongside Kazu's number puzzles. */
|
|
2
|
+
export * from "./heyawake.types.js";
|
|
3
|
+
export * from "./heyawake.constants.js";
|
|
4
|
+
export * from "./heyawakeBoard.js";
|
|
5
|
+
export * from "./heyawakeSolve.js";
|
|
6
|
+
export * from "./heyawakeGenerate.js";
|
|
7
|
+
export * from "./heyawakeGame.js";
|
|
8
|
+
export { drawHeyawake } from "./heyawakeDraw.js";
|
|
9
|
+
export { mountHeyawake } from "./heyawakeMount.js";
|
|
10
|
+
export { HEYAWAKE_PLAY_STYLE } from "./heyawakeStyle.js";
|
|
11
|
+
export { heyawakeWords } from "./heyawakeStrings.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { mountHeyawake } from "./heyawakeMount.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const HEYAWAKE_MOST_SIDE = 12;
|
|
2
|
+
export declare const HEYAWAKE_GENERATOR_MOST_CELLS = 25;
|
|
3
|
+
export declare const HEYAWAKE_MOST_NODES = 250000;
|
|
4
|
+
export declare const HEYAWAKE_MOST_ATTEMPTS = 500;
|
|
5
|
+
export declare const HEYAWAKE_LEVELS: readonly ["easy", "medium", "hard"];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type HeyawakeRoom = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
blacks: number | null;
|
|
7
|
+
};
|
|
8
|
+
export type HeyawakeBoard = {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
rooms: readonly HeyawakeRoom[];
|
|
12
|
+
};
|
|
13
|
+
export type HeyawakeLevel = "easy" | "medium" | "hard";
|
|
14
|
+
export type HeyawakePuzzle = HeyawakeBoard & {
|
|
15
|
+
seed: number;
|
|
16
|
+
level: HeyawakeLevel;
|
|
17
|
+
solution: readonly boolean[];
|
|
18
|
+
};
|
|
19
|
+
export type HeyawakeCheck = {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
complete: boolean;
|
|
22
|
+
errors: readonly number[];
|
|
23
|
+
};
|
|
24
|
+
export type HeyawakeSolve = {
|
|
25
|
+
count: number;
|
|
26
|
+
solution: readonly boolean[] | null;
|
|
27
|
+
complete: boolean;
|
|
28
|
+
nodes: number;
|
|
29
|
+
};
|
|
30
|
+
export type HeyawakeGame = {
|
|
31
|
+
board: HeyawakeBoard;
|
|
32
|
+
entries: readonly (boolean | null)[];
|
|
33
|
+
history: readonly (readonly (boolean | null)[])[];
|
|
34
|
+
helped: boolean;
|
|
35
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HeyawakeBoard, HeyawakeCheck, HeyawakeRoom } from "./heyawake.types.ts";
|
|
2
|
+
export declare function heyawakeCells(board: HeyawakeBoard, room: HeyawakeRoom): number[] | null;
|
|
3
|
+
export declare function isHeyawakeBoard(value: unknown): value is HeyawakeBoard;
|
|
4
|
+
export declare function heyawakeNeighbours(board: HeyawakeBoard, cell: number): number[];
|
|
5
|
+
export declare function roomForCell(board: HeyawakeBoard): number[];
|
|
6
|
+
export declare function checkHeyawake(board: HeyawakeBoard, entries: readonly (boolean | null)[]): HeyawakeCheck;
|
|
7
|
+
export declare function checkRoomSpans(board: HeyawakeBoard, entries: readonly (boolean | null)[], roomIds?: number[], errors?: Set<number>): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { HEYAWAKE_MOST_SIDE } from "./heyawake.constants.js";
|
|
2
|
+
export function heyawakeCells(board, room) {
|
|
3
|
+
if (!room || ![room.x, room.y, room.width, room.height].every(Number.isInteger)
|
|
4
|
+
|| room.x < 0 || room.y < 0 || room.width < 1 || room.height < 1
|
|
5
|
+
|| room.x + room.width > board.width || room.y + room.height > board.height)
|
|
6
|
+
return null;
|
|
7
|
+
return Array.from({ length: room.width * room.height }, (_, index) => (room.y + Math.floor(index / room.width)) * board.width + room.x + index % room.width);
|
|
8
|
+
}
|
|
9
|
+
export function isHeyawakeBoard(value) {
|
|
10
|
+
if (!value || typeof value !== "object")
|
|
11
|
+
return false;
|
|
12
|
+
const board = value;
|
|
13
|
+
if (!Number.isInteger(board.width) || board.width < 2 || board.width > HEYAWAKE_MOST_SIDE
|
|
14
|
+
|| !Number.isInteger(board.height) || board.height < 2 || board.height > HEYAWAKE_MOST_SIDE
|
|
15
|
+
|| !Array.isArray(board.rooms) || board.rooms.length === 0)
|
|
16
|
+
return false;
|
|
17
|
+
const occupied = new Set();
|
|
18
|
+
for (const room of board.rooms) {
|
|
19
|
+
if (!room || typeof room !== "object"
|
|
20
|
+
|| ![room.x, room.y, room.width, room.height].every(Number.isInteger)
|
|
21
|
+
|| !(room.blacks === null || Number.isInteger(room.blacks) && room.blacks >= 0 && room.blacks <= room.width * room.height))
|
|
22
|
+
return false;
|
|
23
|
+
const cells = heyawakeCells(board, room);
|
|
24
|
+
if (!cells || cells.some(cell => occupied.has(cell)))
|
|
25
|
+
return false;
|
|
26
|
+
cells.forEach(cell => occupied.add(cell));
|
|
27
|
+
}
|
|
28
|
+
return occupied.size === board.width * board.height;
|
|
29
|
+
}
|
|
30
|
+
export function heyawakeNeighbours(board, cell) {
|
|
31
|
+
const x = cell % board.width, y = Math.floor(cell / board.width);
|
|
32
|
+
return [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]]
|
|
33
|
+
.filter(([nx, ny]) => nx >= 0 && nx < board.width && ny >= 0 && ny < board.height)
|
|
34
|
+
.map(([nx, ny]) => ny * board.width + nx);
|
|
35
|
+
}
|
|
36
|
+
export function roomForCell(board) {
|
|
37
|
+
const lookup = Array(board.width * board.height).fill(-1);
|
|
38
|
+
board.rooms.forEach((room, index) => heyawakeCells(board, room)?.forEach(cell => { lookup[cell] = index; }));
|
|
39
|
+
return lookup;
|
|
40
|
+
}
|
|
41
|
+
export function checkHeyawake(board, entries) {
|
|
42
|
+
if (!isHeyawakeBoard(board))
|
|
43
|
+
throw new RangeError("Invalid Heyawake board");
|
|
44
|
+
if (!Array.isArray(entries) || entries.length !== board.width * board.height
|
|
45
|
+
|| entries.some(value => value !== true && value !== false && value !== null))
|
|
46
|
+
throw new RangeError("Invalid Heyawake entries");
|
|
47
|
+
const errors = new Set(), complete = entries.every(value => value !== null), ids = roomForCell(board);
|
|
48
|
+
for (let cell = 0; cell < entries.length; cell += 1) {
|
|
49
|
+
if (entries[cell] !== true)
|
|
50
|
+
continue;
|
|
51
|
+
if (heyawakeNeighbours(board, cell).some(next => entries[next] === true)) {
|
|
52
|
+
errors.add(cell);
|
|
53
|
+
heyawakeNeighbours(board, cell).filter(next => entries[next] === true).forEach(next => errors.add(next));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (let index = 0; index < board.rooms.length; index += 1) {
|
|
57
|
+
const room = board.rooms[index];
|
|
58
|
+
const cells = heyawakeCells(board, room);
|
|
59
|
+
const marked = cells.filter(cell => entries[cell] === true).length;
|
|
60
|
+
if (room.blacks !== null && (marked > room.blacks || complete && marked !== room.blacks))
|
|
61
|
+
cells.forEach(cell => errors.add(cell));
|
|
62
|
+
}
|
|
63
|
+
if (complete) {
|
|
64
|
+
const white = entries.flatMap((value, cell) => value === false ? [cell] : []);
|
|
65
|
+
const reached = new Set();
|
|
66
|
+
if (white.length) {
|
|
67
|
+
const pending = [white[0]];
|
|
68
|
+
reached.add(white[0]);
|
|
69
|
+
while (pending.length)
|
|
70
|
+
for (const next of heyawakeNeighbours(board, pending.pop())) {
|
|
71
|
+
if (entries[next] === false && !reached.has(next)) {
|
|
72
|
+
reached.add(next);
|
|
73
|
+
pending.push(next);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (reached.size !== white.length)
|
|
78
|
+
white.filter(cell => !reached.has(cell)).forEach(cell => errors.add(cell));
|
|
79
|
+
checkRoomSpans(board, entries, ids, errors);
|
|
80
|
+
}
|
|
81
|
+
return { ok: errors.size === 0 && complete, complete, errors: [...errors].sort((a, b) => a - b) };
|
|
82
|
+
}
|
|
83
|
+
export function checkRoomSpans(board, entries, roomIds = roomForCell(board), errors = new Set()) {
|
|
84
|
+
for (let y = 0; y < board.height; y += 1) {
|
|
85
|
+
let rooms = new Set(), cells = [];
|
|
86
|
+
for (let x = 0; x <= board.width; x += 1) {
|
|
87
|
+
const cell = y * board.width + x;
|
|
88
|
+
if (x < board.width && entries[cell] === false) {
|
|
89
|
+
cells.push(cell);
|
|
90
|
+
rooms.add(roomIds[cell]);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
if (rooms.size > 2)
|
|
94
|
+
cells.forEach(value => errors.add(value));
|
|
95
|
+
rooms = new Set();
|
|
96
|
+
cells = [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (let x = 0; x < board.width; x += 1) {
|
|
101
|
+
let rooms = new Set(), cells = [];
|
|
102
|
+
for (let y = 0; y <= board.height; y += 1) {
|
|
103
|
+
const cell = y * board.width + x;
|
|
104
|
+
if (y < board.height && entries[cell] === false) {
|
|
105
|
+
cells.push(cell);
|
|
106
|
+
rooms.add(roomIds[cell]);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (rooms.size > 2)
|
|
110
|
+
cells.forEach(value => errors.add(value));
|
|
111
|
+
rooms = new Set();
|
|
112
|
+
cells = [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { heyawakeCells, isHeyawakeBoard } from "./heyawakeBoard.js";
|
|
2
|
+
import { heyawakeWords } from "./heyawakeStrings.js";
|
|
3
|
+
export function drawHeyawake(board, options = {}) {
|
|
4
|
+
if (!isHeyawakeBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Heyawake board");
|
|
6
|
+
const words = heyawakeWords(options.language), size = 48;
|
|
7
|
+
const palettes = { ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"], wood: ["#e5cda6", "#493e2f", "#bca582"], slate: ["#262a27", "#ece8dc", "#454a44"] };
|
|
8
|
+
const [paper, ink, grid] = palettes[options.material ?? "ivory"];
|
|
9
|
+
const roomAt = Array(board.width * board.height).fill(-1);
|
|
10
|
+
board.rooms.forEach((room, index) => heyawakeCells(board, room).forEach(cell => { roomAt[cell] = index; }));
|
|
11
|
+
const cells = Array.from({ length: board.width * board.height }, (_, cell) => {
|
|
12
|
+
const x = cell % board.width * size, y = Math.floor(cell / board.width) * size;
|
|
13
|
+
const entry = options.entries?.[cell], room = board.rooms[roomAt[cell]];
|
|
14
|
+
const selected = cell === options.selected, error = options.errors?.includes(cell);
|
|
15
|
+
const color = error ? "var(--kz-bad,#b5452c)" : ink;
|
|
16
|
+
const top = Math.floor(cell / board.width) === room.y ? 3 : 1;
|
|
17
|
+
const left = cell % board.width === room.x ? 3 : 1;
|
|
18
|
+
const right = cell % board.width === room.x + room.width - 1 ? 3 : 1;
|
|
19
|
+
const bottom = Math.floor(cell / board.width) === room.y + room.height - 1 ? 3 : 1;
|
|
20
|
+
const clue = cell === room.y * board.width + room.x && room.blacks !== null
|
|
21
|
+
? `<text x="${x + 5}" y="${y + 13}" fill="${ink}" font-size="11" font-weight="700">${room.blacks}</text>` : "";
|
|
22
|
+
const mark = entry === true ? `<rect x="${x + 1}" y="${y + 1}" width="46" height="46" fill="${color}"/>` : entry === false ? `<circle cx="${x + 24}" cy="${y + 24}" r="5" fill="${color}"/>` : "";
|
|
23
|
+
return `<g data-heyawake-cell="${cell}"><rect x="${x}" y="${y}" width="48" height="48" fill="${selected ? ink : paper}" fill-opacity="${selected ? ".12" : "1"}" stroke="${grid}" stroke-width="${Math.min(top, right, bottom, left)}"/>${clue}${mark}</g>`;
|
|
24
|
+
}).join("");
|
|
25
|
+
const roomLines = board.rooms.map(room => `<rect x="${room.x * size + 1.5}" y="${room.y * size + 1.5}" width="${room.width * size - 3}" height="${room.height * size - 3}" fill="none" stroke="${ink}" stroke-width="3" pointer-events="none"/>`).join("");
|
|
26
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${board.width * size} ${board.height * size}" role="img" aria-label="${words.title}" style="display:block;width:100%;height:auto;background:${paper}">${cells}${roomLines}</svg>`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HeyawakeBoard, HeyawakeGame } from "./heyawake.types.ts";
|
|
2
|
+
export declare function newHeyawake(board: HeyawakeBoard): HeyawakeGame;
|
|
3
|
+
export declare function setHeyawakeCell(game: HeyawakeGame, cell: number, value: boolean | null): HeyawakeGame;
|
|
4
|
+
export declare function undoHeyawake(game: HeyawakeGame): HeyawakeGame;
|
|
5
|
+
export declare function restartHeyawake(game: HeyawakeGame): HeyawakeGame;
|
|
6
|
+
export declare function hintHeyawake(game: HeyawakeGame): {
|
|
7
|
+
cell: number;
|
|
8
|
+
value: boolean;
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function heyawakeFinished(game: HeyawakeGame): boolean;
|
|
11
|
+
export declare function encodeHeyawake(game: HeyawakeGame): string;
|
|
12
|
+
export declare function decodeHeyawake(code: string): HeyawakeGame | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { checkHeyawake, isHeyawakeBoard } from "./heyawakeBoard.js";
|
|
2
|
+
import { solveHeyawake } from "./heyawakeSolve.js";
|
|
3
|
+
export function newHeyawake(board) {
|
|
4
|
+
if (!isHeyawakeBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Heyawake board");
|
|
6
|
+
return { board: publicBoard(board), entries: Array(board.width * board.height).fill(null), history: [], helped: false };
|
|
7
|
+
}
|
|
8
|
+
export function setHeyawakeCell(game, cell, value) {
|
|
9
|
+
if (value !== null && typeof value !== "boolean" || !Number.isInteger(cell) || cell < 0 || cell >= game.entries.length || game.entries[cell] === value)
|
|
10
|
+
return game;
|
|
11
|
+
const entries = [...game.entries];
|
|
12
|
+
entries[cell] = value;
|
|
13
|
+
return { ...game, entries, history: [...game.history, game.entries] };
|
|
14
|
+
}
|
|
15
|
+
export function undoHeyawake(game) {
|
|
16
|
+
const previous = game.history.at(-1);
|
|
17
|
+
return previous ? { ...game, entries: previous, history: game.history.slice(0, -1) } : game;
|
|
18
|
+
}
|
|
19
|
+
export function restartHeyawake(game) {
|
|
20
|
+
return { ...game, entries: Array(game.entries.length).fill(null), history: [], helped: false };
|
|
21
|
+
}
|
|
22
|
+
export function hintHeyawake(game) {
|
|
23
|
+
const result = solveHeyawake(game.board, game.entries);
|
|
24
|
+
if (!result.complete || result.count !== 1 || !result.solution)
|
|
25
|
+
return null;
|
|
26
|
+
const cell = game.entries.findIndex(value => value === null);
|
|
27
|
+
return cell < 0 ? null : { cell, value: result.solution[cell] };
|
|
28
|
+
}
|
|
29
|
+
export function heyawakeFinished(game) {
|
|
30
|
+
return checkHeyawake(game.board, game.entries).ok;
|
|
31
|
+
}
|
|
32
|
+
export function encodeHeyawake(game) {
|
|
33
|
+
return JSON.stringify({ version: 1, board: publicBoard(game.board), entries: game.entries.map(value => value === null ? "?" : value ? "#" : ".").join(""), helped: game.helped });
|
|
34
|
+
}
|
|
35
|
+
export function decodeHeyawake(code) {
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(code);
|
|
38
|
+
if (data.version !== 1 || !isHeyawakeBoard(data.board) || typeof data.helped !== "boolean" || typeof data.entries !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
const entries = [...data.entries].map((value) => value === "?" ? null : value === "#" ? true : value === "." ? false : undefined);
|
|
41
|
+
if (entries.length !== data.board.width * data.board.height || entries.some((value) => value === undefined))
|
|
42
|
+
return null;
|
|
43
|
+
return { board: publicBoard(data.board), entries: entries, history: [], helped: data.helped };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function publicBoard(board) {
|
|
50
|
+
return { width: board.width, height: board.height, rooms: board.rooms.map(room => ({ x: room.x, y: room.y, width: room.width, height: room.height, blacks: room.blacks })) };
|
|
51
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { HEYAWAKE_GENERATOR_MOST_CELLS, HEYAWAKE_LEVELS, HEYAWAKE_MOST_ATTEMPTS } from "./heyawake.constants.js";
|
|
2
|
+
import { checkHeyawake, heyawakeNeighbours, roomForCell } from "./heyawakeBoard.js";
|
|
3
|
+
import { solveHeyawake } from "./heyawakeSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom, shuffled } from "./random.js";
|
|
5
|
+
export function generateHeyawake(width = 4, height = width, level = "easy", seed = 1) {
|
|
6
|
+
if (![width, height].every(value => Number.isInteger(value) && value >= 4 && value <= 8) || width * height > HEYAWAKE_GENERATOR_MOST_CELLS
|
|
7
|
+
|| !HEYAWAKE_LEVELS.includes(level) || !isKazuSeed(seed))
|
|
8
|
+
throw new RangeError("Heyawake generation supports 4–8 cells per side and a valid level and seed");
|
|
9
|
+
const random = seededRandom(seed);
|
|
10
|
+
for (let attempt = 0; attempt < HEYAWAKE_MOST_ATTEMPTS; attempt += 1) {
|
|
11
|
+
const roomLimit = Math.min(10, 5 + Math.floor(attempt / 50));
|
|
12
|
+
const rooms = partitionRooms(width, height, random, roomLimit);
|
|
13
|
+
const open = { width, height, rooms: rooms.map(room => ({ ...room, blacks: null })) };
|
|
14
|
+
const solution = randomSolution(open, random);
|
|
15
|
+
if (!solution)
|
|
16
|
+
continue;
|
|
17
|
+
const order = shuffled(rooms.map((_, index) => index), random);
|
|
18
|
+
const clueCount = Math.max(2, Math.ceil(rooms.length * ({ easy: 0.95, medium: 0.75, hard: 0.55 }[level])));
|
|
19
|
+
const revealed = new Set(order.slice(0, clueCount));
|
|
20
|
+
const hidden = order.slice(clueCount);
|
|
21
|
+
for (let extra = 0; extra <= hidden.length; extra += 1) {
|
|
22
|
+
const clues = rooms.map((room, index) => ({ ...room, blacks: revealed.has(index) ? countBlacks(room, width, solution) : null }));
|
|
23
|
+
const board = { width, height, rooms: clues };
|
|
24
|
+
const proof = solveHeyawake(board);
|
|
25
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkHeyawake(board, proof.solution).ok) {
|
|
26
|
+
return { ...board, seed, level, solution: proof.solution };
|
|
27
|
+
}
|
|
28
|
+
if (extra < hidden.length)
|
|
29
|
+
revealed.add(hidden[extra]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw new Error("No unique Heyawake found within the generation budget; try another seed or smaller board");
|
|
33
|
+
}
|
|
34
|
+
function partitionRooms(width, height, random, maximumRooms) {
|
|
35
|
+
const pending = [{ x: 0, y: 0, width, height }];
|
|
36
|
+
const rooms = [];
|
|
37
|
+
while (pending.length) {
|
|
38
|
+
const area = pending.pop();
|
|
39
|
+
if (rooms.length + pending.length >= maximumRooms || area.width * area.height <= 3) {
|
|
40
|
+
rooms.push({ ...area, blacks: null });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const canVertical = area.width >= 4, canHorizontal = area.height >= 4;
|
|
44
|
+
if (!canVertical && !canHorizontal) {
|
|
45
|
+
rooms.push({ ...area, blacks: null });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const vertical = canVertical && (!canHorizontal || random() < 0.5);
|
|
49
|
+
if (vertical) {
|
|
50
|
+
const cut = 1 + Math.floor(random() * (area.width - 1));
|
|
51
|
+
pending.push({ x: area.x + cut, y: area.y, width: area.width - cut, height: area.height });
|
|
52
|
+
pending.push({ x: area.x, y: area.y, width: cut, height: area.height });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const cut = 1 + Math.floor(random() * (area.height - 1));
|
|
56
|
+
pending.push({ x: area.x, y: area.y + cut, width: area.width, height: area.height - cut });
|
|
57
|
+
pending.push({ x: area.x, y: area.y, width: area.width, height: cut });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return rooms;
|
|
61
|
+
}
|
|
62
|
+
function randomSolution(board, random) {
|
|
63
|
+
const entries = Array(board.width * board.height).fill(null);
|
|
64
|
+
const roomIds = roomForCell(board);
|
|
65
|
+
let nodes = 0;
|
|
66
|
+
const visit = (cell) => {
|
|
67
|
+
if (++nodes > 12000)
|
|
68
|
+
return false;
|
|
69
|
+
if (cell === entries.length) {
|
|
70
|
+
const blackCount = entries.filter(value => value === true).length;
|
|
71
|
+
return blackCount >= 2 && checkHeyawake(board, entries).ok;
|
|
72
|
+
}
|
|
73
|
+
const choices = random() < 0.31 ? [true, false] : [false, true];
|
|
74
|
+
for (const black of choices) {
|
|
75
|
+
if (black && heyawakeNeighbours(board, cell).some(next => entries[next] === true))
|
|
76
|
+
continue;
|
|
77
|
+
entries[cell] = black;
|
|
78
|
+
if (partialRoomSpanIsLegal(board, entries, roomIds) && visit(cell + 1))
|
|
79
|
+
return true;
|
|
80
|
+
entries[cell] = null;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
return visit(0) ? entries.map(value => value === true) : null;
|
|
85
|
+
}
|
|
86
|
+
function partialRoomSpanIsLegal(board, entries, roomIds) {
|
|
87
|
+
const exceeds = (cells) => {
|
|
88
|
+
let rooms = new Set();
|
|
89
|
+
for (const cell of cells) {
|
|
90
|
+
if (entries[cell] === false)
|
|
91
|
+
rooms.add(roomIds[cell]);
|
|
92
|
+
else
|
|
93
|
+
rooms = new Set();
|
|
94
|
+
if (rooms.size > 2)
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
for (let y = 0; y < board.height; y += 1)
|
|
100
|
+
if (exceeds(Array.from({ length: board.width }, (_, x) => y * board.width + x)))
|
|
101
|
+
return false;
|
|
102
|
+
for (let x = 0; x < board.width; x += 1)
|
|
103
|
+
if (exceeds(Array.from({ length: board.height }, (_, y) => y * board.width + x)))
|
|
104
|
+
return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function countBlacks(room, width, solution) {
|
|
108
|
+
let count = 0;
|
|
109
|
+
for (let y = room.y; y < room.y + room.height; y += 1)
|
|
110
|
+
for (let x = room.x; x < room.x + room.width; x += 1)
|
|
111
|
+
if (solution[y * width + x])
|
|
112
|
+
count += 1;
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { checkHeyawake, isHeyawakeBoard } from "./heyawakeBoard.js";
|
|
2
|
+
import { decodeHeyawake, encodeHeyawake, hintHeyawake, newHeyawake, restartHeyawake, setHeyawakeCell, undoHeyawake, heyawakeFinished } from "./heyawakeGame.js";
|
|
3
|
+
import { drawHeyawake } from "./heyawakeDraw.js";
|
|
4
|
+
import { KAZU_STYLE } from "./style.js";
|
|
5
|
+
import { HEYAWAKE_PLAY_STYLE } from "./heyawakeStyle.js";
|
|
6
|
+
import { heyawakeWords } from "./heyawakeStrings.js";
|
|
7
|
+
export function mountHeyawake(host, initial) {
|
|
8
|
+
if (!isHeyawakeBoard(initial.board))
|
|
9
|
+
throw new RangeError("Invalid Heyawake board");
|
|
10
|
+
const doc = host.ownerDocument;
|
|
11
|
+
let options = { ...initial }, game = initial.progress ? decodeHeyawake(initial.progress) : null;
|
|
12
|
+
if (!game || JSON.stringify(game.board) !== JSON.stringify(initial.board))
|
|
13
|
+
game = newHeyawake(initial.board);
|
|
14
|
+
let selected = 0, errors = [], message = "", dead = false, finished = heyawakeFinished(game);
|
|
15
|
+
const root = doc.createElement("div"), style = doc.createElement("style"), board = doc.createElement("div"), grid = doc.createElement("div"), status = doc.createElement("p"), tools = doc.createElement("div"), help = doc.createElement("p");
|
|
16
|
+
root.className = "kh-root";
|
|
17
|
+
style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".kh-palette") + HEYAWAKE_PLAY_STYLE;
|
|
18
|
+
board.className = "kh-board";
|
|
19
|
+
grid.className = "kh-grid";
|
|
20
|
+
status.className = "kh-status";
|
|
21
|
+
status.setAttribute("role", "status");
|
|
22
|
+
tools.className = "kh-tools";
|
|
23
|
+
help.className = "kh-help";
|
|
24
|
+
root.append(style, board, status, tools, help);
|
|
25
|
+
host.append(root);
|
|
26
|
+
const copy = () => ({ ...game, board: { width: game.board.width, height: game.board.height, rooms: game.board.rooms.map(room => ({ ...room })) }, entries: [...game.entries], history: game.history.map(turn => [...turn]) });
|
|
27
|
+
const words = () => heyawakeWords(options.language);
|
|
28
|
+
const button = (label, action, disabled = false) => { const item = doc.createElement("button"); item.type = "button"; item.textContent = label; item.disabled = disabled; item.onclick = action; tools.append(item); };
|
|
29
|
+
const changed = () => {
|
|
30
|
+
errors = [];
|
|
31
|
+
message = "";
|
|
32
|
+
const won = heyawakeFinished(game);
|
|
33
|
+
render();
|
|
34
|
+
options.onChange?.(copy());
|
|
35
|
+
root.dispatchEvent(new CustomEvent("heyawake-change", { detail: copy(), bubbles: true }));
|
|
36
|
+
if (won && !finished) {
|
|
37
|
+
options.onFinish?.(copy());
|
|
38
|
+
root.dispatchEvent(new CustomEvent("heyawake-finish", { detail: copy(), bubbles: true }));
|
|
39
|
+
}
|
|
40
|
+
finished = won;
|
|
41
|
+
};
|
|
42
|
+
const choose = (cell) => {
|
|
43
|
+
if (finished)
|
|
44
|
+
return;
|
|
45
|
+
selected = cell;
|
|
46
|
+
const value = game.entries[cell] === null ? true : game.entries[cell] === true ? false : null;
|
|
47
|
+
game = setHeyawakeCell(game, cell, value);
|
|
48
|
+
changed();
|
|
49
|
+
};
|
|
50
|
+
const restart = () => { game = restartHeyawake(game); finished = false; changed(); };
|
|
51
|
+
const render = () => {
|
|
52
|
+
if (dead)
|
|
53
|
+
return;
|
|
54
|
+
const w = words(), b = game.board, focused = grid.contains(doc.activeElement);
|
|
55
|
+
board.classList.toggle("kh-palette", (options.material ?? "ivory") === "ivory");
|
|
56
|
+
board.innerHTML = drawHeyawake(b, { ...options, entries: game.entries, selected, errors });
|
|
57
|
+
grid.replaceChildren();
|
|
58
|
+
grid.style.gridTemplateColumns = `repeat(${b.width},1fr)`;
|
|
59
|
+
game.entries.forEach((entry, cellIndex) => {
|
|
60
|
+
const cell = doc.createElement("button");
|
|
61
|
+
cell.type = "button";
|
|
62
|
+
cell.className = "kh-cell";
|
|
63
|
+
cell.dataset.cell = String(cellIndex);
|
|
64
|
+
cell.tabIndex = cellIndex === selected ? 0 : -1;
|
|
65
|
+
cell.setAttribute("aria-label", `${w.row} ${Math.floor(cellIndex / b.width) + 1}, ${w.column} ${cellIndex % b.width + 1}: ${entry === true ? w.black : entry === false ? w.white : w.blank}`);
|
|
66
|
+
cell.setAttribute("aria-pressed", String(entry !== null));
|
|
67
|
+
cell.onclick = () => choose(cellIndex);
|
|
68
|
+
cell.onkeydown = event => {
|
|
69
|
+
const moves = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -b.width, ArrowDown: b.width };
|
|
70
|
+
if (event.key in moves) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
selected = Math.max(0, Math.min(game.entries.length - 1, cellIndex + moves[event.key]));
|
|
73
|
+
render();
|
|
74
|
+
}
|
|
75
|
+
else if (event.key === " " || event.key === "Enter") {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
choose(cellIndex);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
grid.append(cell);
|
|
81
|
+
});
|
|
82
|
+
board.append(grid);
|
|
83
|
+
if (focused)
|
|
84
|
+
grid.querySelector(`[data-cell="${selected}"]`)?.focus();
|
|
85
|
+
const checked = checkHeyawake(b, game.entries);
|
|
86
|
+
status.textContent = heyawakeFinished(game) ? game.helped ? w.helped : w.won : message || (errors.length ? w.wrong : "");
|
|
87
|
+
status.classList.toggle("kh-errors", errors.length > 0);
|
|
88
|
+
help.textContent = w.help;
|
|
89
|
+
tools.replaceChildren();
|
|
90
|
+
button(w.undo, () => { game = undoHeyawake(game); finished = heyawakeFinished(game); changed(); }, !game.history.length);
|
|
91
|
+
button(w.check, () => { errors = checked.errors; message = checked.errors.length === 0 ? w.checked : w.wrong; render(); });
|
|
92
|
+
button(w.hint, () => {
|
|
93
|
+
const hint = hintHeyawake(game);
|
|
94
|
+
if (!hint) {
|
|
95
|
+
message = w.noHint;
|
|
96
|
+
render();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
game = { ...setHeyawakeCell(game, hint.cell, hint.value), helped: true };
|
|
100
|
+
changed();
|
|
101
|
+
}, finished);
|
|
102
|
+
button(w.restart, restart);
|
|
103
|
+
};
|
|
104
|
+
render();
|
|
105
|
+
return { game: copy, progress: () => encodeHeyawake(game), restart, set: (next) => { options = { ...options, ...next }; render(); }, destroy: () => { dead = true; root.remove(); } };
|
|
106
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HeyawakeGame, HeyawakeBoard } from "./heyawake.types.ts";
|
|
2
|
+
export type HeyawakeAppearance = {
|
|
3
|
+
language?: "en" | "ja";
|
|
4
|
+
material?: "ivory" | "wood" | "slate";
|
|
5
|
+
pieces?: "ink" | "tiles";
|
|
6
|
+
};
|
|
7
|
+
export type HeyawakeMountOptions = HeyawakeAppearance & {
|
|
8
|
+
board: HeyawakeBoard;
|
|
9
|
+
progress?: string;
|
|
10
|
+
onChange?: (game: HeyawakeGame) => void;
|
|
11
|
+
onFinish?: (game: HeyawakeGame) => void;
|
|
12
|
+
};
|
|
13
|
+
export type HeyawakeDrawOptions = HeyawakeAppearance & {
|
|
14
|
+
entries?: readonly (boolean | null)[];
|
|
15
|
+
selected?: number;
|
|
16
|
+
errors?: readonly number[];
|
|
17
|
+
};
|
|
18
|
+
export type HeyawakeMount = {
|
|
19
|
+
game: () => HeyawakeGame;
|
|
20
|
+
progress: () => string;
|
|
21
|
+
restart: () => void;
|
|
22
|
+
set: (options: HeyawakeAppearance) => void;
|
|
23
|
+
destroy: () => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { HEYAWAKE_MOST_NODES } from "./heyawake.constants.js";
|
|
2
|
+
import { checkHeyawake, heyawakeCells, heyawakeNeighbours, isHeyawakeBoard, roomForCell } from "./heyawakeBoard.js";
|
|
3
|
+
export function solveHeyawake(board, entries = Array(board.width * board.height).fill(null), options = {}) {
|
|
4
|
+
if (!isHeyawakeBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Heyawake board");
|
|
6
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 2)), maxNodes = Math.max(1, Math.floor(options.nodes ?? HEYAWAKE_MOST_NODES));
|
|
7
|
+
const work = [...entries], ids = roomForCell(board), roomCells = board.rooms.map(room => heyawakeCells(board, room));
|
|
8
|
+
if (work.length !== board.width * board.height || work.some(value => value !== true && value !== false && value !== null))
|
|
9
|
+
throw new RangeError("Invalid Heyawake entries");
|
|
10
|
+
let count = 0, nodes = 0, solution = null, stopped = false;
|
|
11
|
+
const visit = (index) => {
|
|
12
|
+
if (count >= limit || stopped)
|
|
13
|
+
return;
|
|
14
|
+
if (++nodes > maxNodes) {
|
|
15
|
+
stopped = true;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (index === work.length) {
|
|
19
|
+
const checked = checkHeyawake(board, work);
|
|
20
|
+
if (checked.ok) {
|
|
21
|
+
count += 1;
|
|
22
|
+
solution ?? (solution = work.map(value => value === true));
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (work[index] !== null) {
|
|
27
|
+
visit(index + 1);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const optionsAtCell = [false, true];
|
|
31
|
+
for (const black of optionsAtCell) {
|
|
32
|
+
if (black && heyawakeNeighbours(board, index).some(next => work[next] === true))
|
|
33
|
+
continue;
|
|
34
|
+
work[index] = black;
|
|
35
|
+
const room = board.rooms[ids[index]], cells = roomCells[ids[index]];
|
|
36
|
+
if (room.blacks !== null) {
|
|
37
|
+
const marked = cells.filter(cell => work[cell] === true).length;
|
|
38
|
+
const unknown = cells.filter(cell => work[cell] === null).length;
|
|
39
|
+
if (marked > room.blacks || marked + unknown < room.blacks) {
|
|
40
|
+
work[index] = null;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
visit(index + 1);
|
|
45
|
+
work[index] = null;
|
|
46
|
+
if (count >= limit || stopped)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
visit(0);
|
|
51
|
+
return { count, solution, nodes: Math.min(nodes, maxNodes), complete: !stopped && count < limit };
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function heyawakeWords(language?: "en" | "ja"): {
|
|
2
|
+
title: string;
|
|
3
|
+
ready: string;
|
|
4
|
+
wrong: string;
|
|
5
|
+
checked: string;
|
|
6
|
+
won: string;
|
|
7
|
+
helped: string;
|
|
8
|
+
undo: string;
|
|
9
|
+
check: string;
|
|
10
|
+
hint: string;
|
|
11
|
+
restart: string;
|
|
12
|
+
help: string;
|
|
13
|
+
black: string;
|
|
14
|
+
white: string;
|
|
15
|
+
blank: string;
|
|
16
|
+
row: string;
|
|
17
|
+
column: string;
|
|
18
|
+
noHint: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function heyawakeWords(language = "en") {
|
|
2
|
+
return language === "ja" ? {
|
|
3
|
+
title: "へやわけ", ready: "マスを選び、白・黒・空白を切り替えます", wrong: "ルールに合わないマスがあります", checked: "今のところ矛盾はありません", won: "完成しました", helped: "ヒントを使って完成しました", undo: "元に戻す", check: "確認", hint: "ヒント", restart: "やり直す", help: "黒マスは辺でつながらず、白マスは一続きです。白い直線は部屋を二つまで通ります。", black: "黒", white: "白", blank: "未入力", row: "行", column: "列", noHint: "一意に解ける状態ではありません"
|
|
4
|
+
} : {
|
|
5
|
+
title: "Heyawake", ready: "Select a cell to cycle white, black and blank", wrong: "Some cells break a rule", checked: "No contradiction found so far", won: "Puzzle complete", helped: "Solved with a hint", undo: "Undo", check: "Check", hint: "Hint", restart: "Restart", help: "Black cells do not touch; white cells stay connected. A straight white run spans at most two rooms.", black: "black", white: "white", blank: "blank", row: "row", column: "column", noHint: "A unique solution is not established"
|
|
6
|
+
};
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HEYAWAKE_PLAY_STYLE = "\n.kh-root{max-width:40rem;margin:auto;font:inherit;color:inherit}.kh-board{position:relative;max-width:min(92vw,36rem);margin:0 auto}.kh-board svg{display:block}.kh-grid{position:absolute;inset:0;display:grid;touch-action:manipulation}.kh-cell{position:relative;aspect-ratio:1;border:0;background:transparent;color:inherit;font:inherit;padding:0;cursor:pointer}.kh-cell:focus-visible{outline:3px solid var(--kz-select,#ffd45b);outline-offset:-3px;z-index:1}.kh-cell[aria-pressed=true]{font-weight:700}.kh-tools{display:flex;flex-wrap:wrap;justify-content:center;gap:.5rem;margin:.9rem 0}.kh-tools button{min-height:2.75rem;padding:.45rem .8rem;border:1px solid currentColor;border-radius:.5rem;background:transparent;color:inherit;font:inherit}.kh-status,.kh-help{text-align:center;min-height:1.5em}.kh-errors{color:var(--kz-bad,#b5452c)}";
|