@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,2 @@
|
|
|
1
|
+
export const HEYAWAKE_PLAY_STYLE = `
|
|
2
|
+
.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)}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { generateHeyawake } from "./heyawakeGenerate.js";
|
|
2
|
+
self.onmessage = (event) => {
|
|
3
|
+
try {
|
|
4
|
+
const { width, height, level, seed } = event.data;
|
|
5
|
+
self.postMessage({ puzzle: generateHeyawake(width, height, level, seed) });
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
self.postMessage({ error: error instanceof Error ? error.message : String(error) });
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawHitori } from "./hitoriDraw.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type HitoriBoard = {
|
|
2
|
+
size: 5 | 7;
|
|
3
|
+
numbers: readonly number[];
|
|
4
|
+
};
|
|
5
|
+
export type HitoriPuzzle = HitoriBoard & {
|
|
6
|
+
seed: number;
|
|
7
|
+
solution: readonly boolean[];
|
|
8
|
+
};
|
|
9
|
+
export type HitoriSolve = {
|
|
10
|
+
count: number;
|
|
11
|
+
solution: readonly boolean[] | null;
|
|
12
|
+
complete: boolean;
|
|
13
|
+
nodes: number;
|
|
14
|
+
};
|
|
15
|
+
export type HitoriGame = {
|
|
16
|
+
board: HitoriBoard;
|
|
17
|
+
shaded: readonly boolean[];
|
|
18
|
+
history: readonly (readonly boolean[])[];
|
|
19
|
+
helped: boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HitoriBoard } from "./hitori.types.ts";
|
|
2
|
+
export declare function isHitoriBoard(board: HitoriBoard): boolean;
|
|
3
|
+
export declare function hitoriNeighbors(size: number, cell: number): number[];
|
|
4
|
+
/** Checks unshaded row and column duplicates, adjacent shades, and white connectivity. */
|
|
5
|
+
export declare function checkHitori(board: HitoriBoard, shaded: readonly boolean[]): {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
errors: readonly number[];
|
|
8
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function isHitoriBoard(board) {
|
|
2
|
+
return !!board && (board.size === 5 || board.size === 7)
|
|
3
|
+
&& Array.isArray(board.numbers)
|
|
4
|
+
&& board.numbers.length === board.size * board.size
|
|
5
|
+
&& board.numbers.every(value => Number.isInteger(value) && value >= 1 && value <= board.size);
|
|
6
|
+
}
|
|
7
|
+
export function hitoriNeighbors(size, cell) {
|
|
8
|
+
const x = cell % size, y = Math.floor(cell / size), neighbors = [];
|
|
9
|
+
if (x > 0)
|
|
10
|
+
neighbors.push(cell - 1);
|
|
11
|
+
if (x < size - 1)
|
|
12
|
+
neighbors.push(cell + 1);
|
|
13
|
+
if (y > 0)
|
|
14
|
+
neighbors.push(cell - size);
|
|
15
|
+
if (y < size - 1)
|
|
16
|
+
neighbors.push(cell + size);
|
|
17
|
+
return neighbors;
|
|
18
|
+
}
|
|
19
|
+
/** Checks unshaded row and column duplicates, adjacent shades, and white connectivity. */
|
|
20
|
+
export function checkHitori(board, shaded) {
|
|
21
|
+
if (!isHitoriBoard(board))
|
|
22
|
+
return { ok: false, errors: [] };
|
|
23
|
+
const size = board.size, total = size * size;
|
|
24
|
+
if (shaded.length !== total || !shaded.every(value => typeof value === "boolean"))
|
|
25
|
+
return { ok: false, errors: [] };
|
|
26
|
+
const errors = new Set();
|
|
27
|
+
for (let y = 0; y < size; y += 1)
|
|
28
|
+
for (let x = 0; x < size; x += 1) {
|
|
29
|
+
const cell = y * size + x;
|
|
30
|
+
if (shaded[cell])
|
|
31
|
+
continue;
|
|
32
|
+
for (let otherX = x + 1; otherX < size; otherX += 1) {
|
|
33
|
+
const other = y * size + otherX;
|
|
34
|
+
if (!shaded[other] && board.numbers[cell] === board.numbers[other]) {
|
|
35
|
+
errors.add(cell);
|
|
36
|
+
errors.add(other);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (let otherY = y + 1; otherY < size; otherY += 1) {
|
|
40
|
+
const other = otherY * size + x;
|
|
41
|
+
if (!shaded[other] && board.numbers[cell] === board.numbers[other]) {
|
|
42
|
+
errors.add(cell);
|
|
43
|
+
errors.add(other);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (let cell = 0; cell < total; cell += 1)
|
|
48
|
+
if (shaded[cell]) {
|
|
49
|
+
for (const neighbor of hitoriNeighbors(size, cell))
|
|
50
|
+
if (shaded[neighbor]) {
|
|
51
|
+
errors.add(cell);
|
|
52
|
+
errors.add(neighbor);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const white = shaded.map((isShaded, cell) => isShaded ? -1 : cell).filter(cell => cell >= 0), reached = new Set();
|
|
56
|
+
if (white.length) {
|
|
57
|
+
const stack = [white[0]];
|
|
58
|
+
reached.add(white[0]);
|
|
59
|
+
while (stack.length)
|
|
60
|
+
for (const neighbor of hitoriNeighbors(size, stack.pop())) {
|
|
61
|
+
if (!shaded[neighbor] && !reached.has(neighbor)) {
|
|
62
|
+
reached.add(neighbor);
|
|
63
|
+
stack.push(neighbor);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (reached.size !== white.length)
|
|
68
|
+
for (const cell of white)
|
|
69
|
+
if (!reached.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 { HitoriBoard } from "./hitori.types.ts";
|
|
2
|
+
import type { HitoriDrawOptions } from "./hitoriPlay.types.ts";
|
|
3
|
+
/** SVG text contains only the public numbers and the player's marks. */
|
|
4
|
+
export declare function drawHitori(board: HitoriBoard, options?: HitoriDrawOptions): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isHitoriBoard } from "./hitoriBoard.js";
|
|
2
|
+
/** SVG text contains only the public numbers and the player's marks. */
|
|
3
|
+
export function drawHitori(board, options = {}) {
|
|
4
|
+
if (!isHitoriBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Hitori board");
|
|
6
|
+
const size = board.size, cellSize = 52;
|
|
7
|
+
const palettes = {
|
|
8
|
+
ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"],
|
|
9
|
+
wood: ["#e5cda6", "#493e2f", "#bca582"],
|
|
10
|
+
slate: ["#333a42", "#f1eee7", "#707984"],
|
|
11
|
+
};
|
|
12
|
+
const [paper, ink, grid] = palettes[options.material ?? "ivory"], shaded = options.shaded ?? [];
|
|
13
|
+
const cells = board.numbers.map((number, cell) => {
|
|
14
|
+
const x = cell % size * cellSize, y = Math.floor(cell / size) * cellSize;
|
|
15
|
+
const black = shaded[cell], selected = options.selected === cell, error = options.errors?.includes(cell);
|
|
16
|
+
const tile = options.pieces === "tiles" && !black
|
|
17
|
+
? `<rect x="${x + 10}" y="${y + 10}" width="32" height="32" rx="6" fill="${paper}" stroke="${grid}"/>`
|
|
18
|
+
: "";
|
|
19
|
+
const background = black ? ink : selected ? `${ink}22` : paper;
|
|
20
|
+
const foreground = black ? paper : error ? "#b5452c" : ink;
|
|
21
|
+
return `<g><rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" fill="${background}" stroke="${grid}" stroke-width="1"/>${tile}<text x="${x + cellSize / 2}" y="${y + cellSize / 2}" text-anchor="middle" dominant-baseline="middle" fill="${foreground}" font-size="22" font-weight="${selected ? 700 : 500}" font-family="system-ui,sans-serif">${number}</text></g>`;
|
|
22
|
+
}).join("");
|
|
23
|
+
const width = size * cellSize;
|
|
24
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 ${width + 4} ${width + 4}" role="img" aria-label="Hitori" style="display:block;width:100%;height:auto;background:${paper}">${cells}<rect width="${width}" height="${width}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="2"/></svg>`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HitoriBoard, HitoriGame } from "./hitori.types.ts";
|
|
2
|
+
export declare function newHitori(board: HitoriBoard): HitoriGame;
|
|
3
|
+
export declare function shadeHitori(game: HitoriGame, cell: number): HitoriGame;
|
|
4
|
+
export declare function undoHitori(game: HitoriGame): HitoriGame;
|
|
5
|
+
export declare function restartHitori(game: HitoriGame): HitoriGame;
|
|
6
|
+
/** Gives one next shade in the unique minimal solution, if uniqueness was proved. */
|
|
7
|
+
export declare function hintHitori(game: HitoriGame): number | null;
|
|
8
|
+
/** Versioned public clue and mark data; the generated answer is not saved. */
|
|
9
|
+
export declare function encodeHitori(game: HitoriGame): string;
|
|
10
|
+
export declare function decodeHitori(code: string): HitoriGame | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isHitoriBoard } from "./hitoriBoard.js";
|
|
2
|
+
import { solveHitori } from "./hitoriSolve.js";
|
|
3
|
+
export function newHitori(board) {
|
|
4
|
+
if (!isHitoriBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Hitori board");
|
|
6
|
+
return { board: { size: board.size, numbers: [...board.numbers] }, shaded: Array(board.size ** 2).fill(false), history: [], helped: false };
|
|
7
|
+
}
|
|
8
|
+
export function shadeHitori(game, cell) {
|
|
9
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= game.shaded.length)
|
|
10
|
+
return game;
|
|
11
|
+
const shaded = [...game.shaded];
|
|
12
|
+
shaded[cell] = !shaded[cell];
|
|
13
|
+
return { ...game, shaded, history: [...game.history, [...game.shaded]] };
|
|
14
|
+
}
|
|
15
|
+
export function undoHitori(game) {
|
|
16
|
+
const prior = game.history.at(-1);
|
|
17
|
+
return prior ? { ...game, shaded: [...prior], history: game.history.slice(0, -1) } : game;
|
|
18
|
+
}
|
|
19
|
+
export function restartHitori(game) {
|
|
20
|
+
return { ...game, shaded: Array(game.shaded.length).fill(false), history: [], helped: false };
|
|
21
|
+
}
|
|
22
|
+
/** Gives one next shade in the unique minimal solution, if uniqueness was proved. */
|
|
23
|
+
export function hintHitori(game) {
|
|
24
|
+
const result = solveHitori(game.board);
|
|
25
|
+
if (!result.complete || result.count !== 1 || !result.solution)
|
|
26
|
+
return null;
|
|
27
|
+
const cell = result.solution.findIndex((isShaded, index) => isShaded !== game.shaded[index]);
|
|
28
|
+
return cell < 0 ? null : cell;
|
|
29
|
+
}
|
|
30
|
+
/** Versioned public clue and mark data; the generated answer is not saved. */
|
|
31
|
+
export function encodeHitori(game) {
|
|
32
|
+
return JSON.stringify({ version: 1, board: game.board, shaded: game.shaded, helped: game.helped });
|
|
33
|
+
}
|
|
34
|
+
export function decodeHitori(code) {
|
|
35
|
+
try {
|
|
36
|
+
if (code.length > 10000)
|
|
37
|
+
return null;
|
|
38
|
+
const value = JSON.parse(code);
|
|
39
|
+
if (value.version !== 1 || !isHitoriBoard(value.board) || !Array.isArray(value.shaded)
|
|
40
|
+
|| value.shaded.length !== value.board.size ** 2 || !value.shaded.every((cell) => typeof cell === "boolean")
|
|
41
|
+
|| typeof value.helped !== "boolean")
|
|
42
|
+
return null;
|
|
43
|
+
return { ...newHitori(value.board), shaded: [...value.shaded], helped: value.helped };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { HITORI_MAX_NODES, HITORI_SIZES } from "./hitori.constants.js";
|
|
2
|
+
import { checkHitori } from "./hitoriBoard.js";
|
|
3
|
+
import { solveHitori } from "./hitoriSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom, shuffled } from "./random.js";
|
|
5
|
+
const layouts = {
|
|
6
|
+
5: [
|
|
7
|
+
{
|
|
8
|
+
numbers: [
|
|
9
|
+
1, 3, 3, 1, 5, 2, 3,
|
|
10
|
+
4, 5, 1, 3, 4, 5, 1,
|
|
11
|
+
2, 4, 5, 1, 2, 3, 5,
|
|
12
|
+
1, 2, 3, 4,
|
|
13
|
+
],
|
|
14
|
+
shade: [
|
|
15
|
+
false, true, false, true, false, ...Array(20).fill(false),
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
numbers: [
|
|
20
|
+
1, 2, 3, 4, 5, 2, 3,
|
|
21
|
+
4, 1, 1, 3, 4, 5, 1,
|
|
22
|
+
2, 3, 5, 1, 4, 3, 5,
|
|
23
|
+
1, 2, 3, 4,
|
|
24
|
+
],
|
|
25
|
+
shade: [
|
|
26
|
+
false, false, false, false, false, false, false,
|
|
27
|
+
false, true, false, false, false, false, false,
|
|
28
|
+
false, true, false, false, true, false, false,
|
|
29
|
+
false, false, false, false,
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
numbers: [
|
|
34
|
+
1, 4, 3, 4, 1, 2, 3,
|
|
35
|
+
4, 5, 1, 3, 4, 5, 1,
|
|
36
|
+
2, 4, 5, 1, 2, 3, 5,
|
|
37
|
+
1, 2, 3, 4,
|
|
38
|
+
],
|
|
39
|
+
shade: [
|
|
40
|
+
false, true, false, false, true, false, false,
|
|
41
|
+
false, false, false, false, false, false, false,
|
|
42
|
+
false, false, false, false, false, false, false,
|
|
43
|
+
false, false, false, false,
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
numbers: [
|
|
48
|
+
1, 4, 3, 4, 5, 2, 3,
|
|
49
|
+
2, 5, 1, 2, 4, 5, 1,
|
|
50
|
+
2, 4, 5, 1, 2, 3, 5,
|
|
51
|
+
5, 2, 3, 4,
|
|
52
|
+
],
|
|
53
|
+
shade: [
|
|
54
|
+
false, true, false, false, false, false, false,
|
|
55
|
+
true, false, false, true, false, false, false,
|
|
56
|
+
false, false, false, false, false, false, false,
|
|
57
|
+
true, false, false, false,
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
7: [
|
|
62
|
+
{
|
|
63
|
+
numbers: [
|
|
64
|
+
1, 2, 3, 4, 2, 6, 7,
|
|
65
|
+
2, 6, 4, 5, 6, 7, 1,
|
|
66
|
+
3, 4, 5, 6, 1, 1, 1,
|
|
67
|
+
4, 5, 6, 7, 1, 2, 3,
|
|
68
|
+
5, 6, 7, 1, 2, 3, 4,
|
|
69
|
+
6, 7, 1, 2, 3, 4, 5,
|
|
70
|
+
7, 1, 2, 3, 4, 5, 6,
|
|
71
|
+
],
|
|
72
|
+
shade: [
|
|
73
|
+
false, false, false, false, true, false, false,
|
|
74
|
+
false, true, false, false, false, false, false,
|
|
75
|
+
false, false, false, false, true, false, true,
|
|
76
|
+
...Array(28).fill(false),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
numbers: [
|
|
81
|
+
1, 2, 4, 4, 5, 7, 7,
|
|
82
|
+
2, 3, 4, 5, 5, 7, 1,
|
|
83
|
+
3, 4, 5, 6, 7, 1, 2,
|
|
84
|
+
4, 5, 6, 7, 1, 2, 7,
|
|
85
|
+
5, 6, 7, 1, 2, 3, 4,
|
|
86
|
+
6, 2, 1, 2, 3, 4, 5,
|
|
87
|
+
7, 1, 2, 3, 7, 5, 6,
|
|
88
|
+
],
|
|
89
|
+
shade: [
|
|
90
|
+
false, false, true, false, false, true, false,
|
|
91
|
+
false, false, false, false, true, false, false,
|
|
92
|
+
false, false, false, false, false, false, false,
|
|
93
|
+
false, false, false, false, false, false, true,
|
|
94
|
+
false, false, false, false, false, false, false,
|
|
95
|
+
false, true, false, false, false, false, false,
|
|
96
|
+
false, false, false, false, true, false, false,
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
numbers: [
|
|
101
|
+
1, 2, 3, 4, 5, 6, 7,
|
|
102
|
+
2, 3, 4, 5, 6, 7, 1,
|
|
103
|
+
3, 4, 5, 6, 1, 1, 6,
|
|
104
|
+
4, 6, 6, 6, 1, 2, 3,
|
|
105
|
+
5, 6, 7, 1, 2, 3, 4,
|
|
106
|
+
6, 7, 1, 2, 3, 4, 5,
|
|
107
|
+
7, 1, 2, 3, 4, 5, 6,
|
|
108
|
+
],
|
|
109
|
+
shade: [
|
|
110
|
+
false, false, false, false, false, false, false,
|
|
111
|
+
false, false, false, false, false, false, false,
|
|
112
|
+
false, false, false, false, true, false, true,
|
|
113
|
+
false, true, false, true, false, false, false,
|
|
114
|
+
false, false, false, false, false, false, false,
|
|
115
|
+
false, ...Array(13).fill(false),
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
numbers: [
|
|
120
|
+
1, 2, 3, 4, 6, 6, 7,
|
|
121
|
+
2, 2, 4, 5, 6, 2, 1,
|
|
122
|
+
3, 4, 5, 6, 7, 1, 2,
|
|
123
|
+
4, 5, 6, 7, 1, 2, 3,
|
|
124
|
+
5, 6, 7, 1, 2, 3, 4,
|
|
125
|
+
6, 7, 1, 2, 3, 4, 5,
|
|
126
|
+
7, 1, 2, 3, 6, 5, 6,
|
|
127
|
+
],
|
|
128
|
+
shade: [
|
|
129
|
+
false, false, false, false, true, false, false,
|
|
130
|
+
false, true, false, false, false, true, false,
|
|
131
|
+
false, false, false, false, false, false, false,
|
|
132
|
+
false, false, false, false, false, false, false,
|
|
133
|
+
false, false, false, false, false, false, false,
|
|
134
|
+
false, false, false, false, false, false, false,
|
|
135
|
+
false, false, false, false, true, false, false,
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
/** Seeded number relabelings and board symmetries of original layouts, accepted only after a uniqueness proof. */
|
|
141
|
+
export function generateHitori(size = 5, seed = 1) {
|
|
142
|
+
if (!HITORI_SIZES.includes(size) || !isKazuSeed(seed))
|
|
143
|
+
throw new RangeError("Invalid Hitori settings");
|
|
144
|
+
const random = seededRandom(seed), templates = layouts[size];
|
|
145
|
+
const template = templates[Math.floor(random() * templates.length)];
|
|
146
|
+
const labels = shuffled(Array.from({ length: size }, (_, index) => index + 1), random);
|
|
147
|
+
const turns = Math.floor(random() * 4), reflected = random() < 0.5;
|
|
148
|
+
const numbers = Array(size * size), solution = Array(size * size);
|
|
149
|
+
for (let y = 0; y < size; y += 1)
|
|
150
|
+
for (let x = 0; x < size; x += 1) {
|
|
151
|
+
let tx = reflected ? size - 1 - x : x, ty = y;
|
|
152
|
+
for (let turn = 0; turn < turns; turn += 1)
|
|
153
|
+
[tx, ty] = [size - 1 - ty, tx];
|
|
154
|
+
const source = y * size + x, destination = ty * size + tx;
|
|
155
|
+
numbers[destination] = labels[template.numbers[source] - 1];
|
|
156
|
+
solution[destination] = template.shade[source];
|
|
157
|
+
}
|
|
158
|
+
const board = { size, numbers }, proof = solveHitori(board, { nodes: HITORI_MAX_NODES });
|
|
159
|
+
if (!checkHitori(board, solution).ok || !proof.complete || proof.count !== 1 || !proof.solution) {
|
|
160
|
+
throw new Error("No uniquely solvable Hitori found within the generation budget; try another seed");
|
|
161
|
+
}
|
|
162
|
+
return { ...board, seed, solution: proof.solution };
|
|
163
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { checkHitori } from "./hitoriBoard.js";
|
|
2
|
+
import { decodeHitori, encodeHitori, hintHitori, newHitori, restartHitori, shadeHitori, undoHitori } from "./hitoriGame.js";
|
|
3
|
+
import { drawHitori } from "./hitoriDraw.js";
|
|
4
|
+
import { KAZU_STYLE } from "./style.js";
|
|
5
|
+
import { HITORI_PLAY_STYLE } from "./hitoriStyle.js";
|
|
6
|
+
import { hitoriWords } from "./hitoriStrings.js";
|
|
7
|
+
/** A keyboard- and touch-friendly Hitori player, mounted in any host element. */
|
|
8
|
+
export function mountHitori(host, initial) {
|
|
9
|
+
const doc = host.ownerDocument;
|
|
10
|
+
let options = { ...initial }, game = initial.progress ? decodeHitori(initial.progress) : null;
|
|
11
|
+
game ?? (game = newHitori(initial.board));
|
|
12
|
+
let selected = 0, errors = [], message = "", finished = checkHitori(game.board, game.shaded).ok, dead = false;
|
|
13
|
+
const root = doc.createElement("div"), style = doc.createElement("style"), board = doc.createElement("div");
|
|
14
|
+
const status = doc.createElement("p"), tools = doc.createElement("div"), help = doc.createElement("p");
|
|
15
|
+
root.className = "kh-root";
|
|
16
|
+
style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".ks-palette") + HITORI_PLAY_STYLE;
|
|
17
|
+
board.className = "kh-board";
|
|
18
|
+
status.className = "kh-status";
|
|
19
|
+
status.setAttribute("role", "status");
|
|
20
|
+
tools.className = "kh-tools";
|
|
21
|
+
help.className = "kh-help";
|
|
22
|
+
root.append(style, board, status, tools, help);
|
|
23
|
+
host.append(root);
|
|
24
|
+
const copy = () => ({ ...game, board: { ...game.board, numbers: [...game.board.numbers] }, shaded: [...game.shaded], history: game.history.map(turn => [...turn]) });
|
|
25
|
+
const button = (label, action, disabled = false) => {
|
|
26
|
+
const control = doc.createElement("button");
|
|
27
|
+
control.type = "button";
|
|
28
|
+
control.textContent = label;
|
|
29
|
+
control.disabled = disabled;
|
|
30
|
+
control.onclick = action;
|
|
31
|
+
tools.append(control);
|
|
32
|
+
};
|
|
33
|
+
const changed = () => {
|
|
34
|
+
errors = [];
|
|
35
|
+
message = "";
|
|
36
|
+
const wasFinished = finished;
|
|
37
|
+
finished = checkHitori(game.board, game.shaded).ok;
|
|
38
|
+
render();
|
|
39
|
+
options.onChange?.(copy());
|
|
40
|
+
if (finished && !wasFinished)
|
|
41
|
+
options.onFinish?.(copy());
|
|
42
|
+
};
|
|
43
|
+
const shade = (cell) => {
|
|
44
|
+
if (finished)
|
|
45
|
+
return;
|
|
46
|
+
selected = cell;
|
|
47
|
+
game = shadeHitori(game, cell);
|
|
48
|
+
changed();
|
|
49
|
+
};
|
|
50
|
+
const render = () => {
|
|
51
|
+
if (dead)
|
|
52
|
+
return;
|
|
53
|
+
const words = hitoriWords(options.language), focused = board.contains(doc.activeElement), size = game.board.size;
|
|
54
|
+
board.innerHTML = drawHitori(game.board, { ...options, shaded: game.shaded, selected, errors });
|
|
55
|
+
const cells = doc.createElement("div");
|
|
56
|
+
cells.className = "kh-cells";
|
|
57
|
+
cells.style.gridTemplateColumns = `repeat(${size},1fr)`;
|
|
58
|
+
for (let cell = 0; cell < game.shaded.length; cell += 1) {
|
|
59
|
+
const control = doc.createElement("button");
|
|
60
|
+
control.type = "button";
|
|
61
|
+
control.className = "kh-cell";
|
|
62
|
+
control.dataset.cell = String(cell);
|
|
63
|
+
control.tabIndex = cell === selected ? 0 : -1;
|
|
64
|
+
const state = game.shaded[cell] ? words.shaded : words.white;
|
|
65
|
+
control.setAttribute("aria-label", `${words.cell} ${Math.floor(cell / size) + 1}, ${words.column} ${cell % size + 1}: ${game.board.numbers[cell]}, ${state}`);
|
|
66
|
+
control.setAttribute("aria-pressed", String(game.shaded[cell]));
|
|
67
|
+
control.onclick = () => shade(cell);
|
|
68
|
+
control.onkeydown = event => {
|
|
69
|
+
const moves = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -size, ArrowDown: size };
|
|
70
|
+
if (event.key in moves) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
selected = Math.max(0, Math.min(game.shaded.length - 1, cell + moves[event.key]));
|
|
73
|
+
render();
|
|
74
|
+
}
|
|
75
|
+
else if (event.key === " " || event.key === "Enter") {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
shade(cell);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
cells.append(control);
|
|
81
|
+
}
|
|
82
|
+
board.append(cells);
|
|
83
|
+
if (focused)
|
|
84
|
+
board.querySelector(`[data-cell="${selected}"]`)?.focus();
|
|
85
|
+
status.textContent = finished ? (game.helped ? words.helped : words.won) : message;
|
|
86
|
+
help.textContent = words.help;
|
|
87
|
+
tools.replaceChildren();
|
|
88
|
+
button(words.undo, () => { game = undoHitori(game); changed(); }, !game.history.length);
|
|
89
|
+
button(words.check, () => { errors = checkHitori(game.board, game.shaded).errors; message = errors.length ? words.bad : words.good; render(); });
|
|
90
|
+
button(words.hint, () => {
|
|
91
|
+
const cell = hintHitori(game);
|
|
92
|
+
if (cell === null) {
|
|
93
|
+
message = words.noHint;
|
|
94
|
+
render();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
selected = cell;
|
|
98
|
+
game = { ...shadeHitori(game, cell), helped: true };
|
|
99
|
+
changed();
|
|
100
|
+
}, finished);
|
|
101
|
+
button(words.restart, () => { game = restartHitori(game); finished = false; changed(); });
|
|
102
|
+
};
|
|
103
|
+
render();
|
|
104
|
+
return {
|
|
105
|
+
game: copy,
|
|
106
|
+
progress: () => encodeHitori(game),
|
|
107
|
+
restart: () => { game = restartHitori(game); finished = false; changed(); },
|
|
108
|
+
set: (next) => { options = { ...options, ...next }; render(); },
|
|
109
|
+
destroy: () => { dead = true; root.remove(); },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HitoriBoard, HitoriGame } from "./hitori.types.ts";
|
|
2
|
+
export type HitoriLanguage = "en" | "ja";
|
|
3
|
+
export type HitoriMaterial = "ivory" | "wood" | "slate";
|
|
4
|
+
export type HitoriPieces = "ink" | "tiles";
|
|
5
|
+
export type HitoriDrawOptions = {
|
|
6
|
+
shaded?: readonly boolean[];
|
|
7
|
+
selected?: number | null;
|
|
8
|
+
errors?: readonly number[];
|
|
9
|
+
material?: HitoriMaterial;
|
|
10
|
+
pieces?: HitoriPieces;
|
|
11
|
+
language?: HitoriLanguage;
|
|
12
|
+
};
|
|
13
|
+
export type HitoriMountOptions = HitoriDrawOptions & {
|
|
14
|
+
board: HitoriBoard;
|
|
15
|
+
progress?: string;
|
|
16
|
+
onChange?: (game: HitoriGame) => void;
|
|
17
|
+
onFinish?: (game: HitoriGame) => void;
|
|
18
|
+
};
|
|
19
|
+
export type HitoriMount = {
|
|
20
|
+
game: () => HitoriGame;
|
|
21
|
+
progress: () => string;
|
|
22
|
+
restart: () => void;
|
|
23
|
+
set: (options: HitoriDrawOptions) => void;
|
|
24
|
+
destroy: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HitoriBoard, HitoriSolve } from "./hitori.types.ts";
|
|
2
|
+
/** Counts distinct minimal shade patterns. A non-complete result never asserts uniqueness. */
|
|
3
|
+
export declare function solveHitori(board: HitoriBoard, options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): HitoriSolve;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { HITORI_MAX_NODES } from "./hitori.constants.js";
|
|
2
|
+
import { checkHitori, hitoriNeighbors, isHitoriBoard } from "./hitoriBoard.js";
|
|
3
|
+
function firstUnshadedDuplicate(board, shaded) {
|
|
4
|
+
const size = board.size;
|
|
5
|
+
for (let y = 0; y < size; y += 1)
|
|
6
|
+
for (let x = 0; x < size; x += 1) {
|
|
7
|
+
const cell = y * size + x;
|
|
8
|
+
if (shaded[cell])
|
|
9
|
+
continue;
|
|
10
|
+
for (let otherX = x + 1; otherX < size; otherX += 1) {
|
|
11
|
+
const other = y * size + otherX;
|
|
12
|
+
if (!shaded[other] && board.numbers[cell] === board.numbers[other])
|
|
13
|
+
return [cell, other];
|
|
14
|
+
}
|
|
15
|
+
for (let otherY = y + 1; otherY < size; otherY += 1) {
|
|
16
|
+
const other = otherY * size + x;
|
|
17
|
+
if (!shaded[other] && board.numbers[cell] === board.numbers[other])
|
|
18
|
+
return [cell, other];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/** A valid answer cannot contain an unnecessary shade; this avoids treating extra black cells as separate puzzle solutions. */
|
|
24
|
+
function isMinimalSolution(board, shaded) {
|
|
25
|
+
if (!checkHitori(board, shaded).ok)
|
|
26
|
+
return false;
|
|
27
|
+
for (let cell = 0; cell < shaded.length; cell += 1)
|
|
28
|
+
if (shaded[cell]) {
|
|
29
|
+
const smaller = [...shaded];
|
|
30
|
+
smaller[cell] = false;
|
|
31
|
+
if (checkHitori(board, smaller).ok)
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/** Counts distinct minimal shade patterns. A non-complete result never asserts uniqueness. */
|
|
37
|
+
export function solveHitori(board, options = {}) {
|
|
38
|
+
if (!isHitoriBoard(board))
|
|
39
|
+
throw new RangeError("Invalid Hitori board");
|
|
40
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 2));
|
|
41
|
+
const budget = Math.max(1, Math.floor(options.nodes ?? HITORI_MAX_NODES));
|
|
42
|
+
let nodes = 0, count = 0, solution = null, exhausted = false;
|
|
43
|
+
const search = (shaded) => {
|
|
44
|
+
if (count >= limit || exhausted)
|
|
45
|
+
return;
|
|
46
|
+
nodes += 1;
|
|
47
|
+
if (nodes > budget) {
|
|
48
|
+
exhausted = true;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const duplicate = firstUnshadedDuplicate(board, shaded);
|
|
52
|
+
if (!duplicate) {
|
|
53
|
+
if (isMinimalSolution(board, shaded)) {
|
|
54
|
+
count += 1;
|
|
55
|
+
solution ?? (solution = [...shaded]);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const cell of duplicate) {
|
|
60
|
+
if (hitoriNeighbors(board.size, cell).some(neighbor => shaded[neighbor]))
|
|
61
|
+
continue;
|
|
62
|
+
shaded[cell] = true;
|
|
63
|
+
search(shaded);
|
|
64
|
+
shaded[cell] = false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
search(Array(board.size * board.size).fill(false));
|
|
68
|
+
return { count, solution, complete: !exhausted && count < limit, nodes: Math.min(nodes, budget) };
|
|
69
|
+
}
|