@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,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,10 @@
|
|
|
1
|
+
import type { HitoriLevel, HitoriPuzzle } from "./hitori.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Makes a seeded puzzle and proves its answer is the only one. The shaded squares are chosen first (never touching,
|
|
4
|
+
* the rest in one piece), the white squares get numbers that repeat nowhere in their row or column, and every shaded
|
|
5
|
+
* square takes a number that a white square in its row or column already has. Boards with more than one answer are
|
|
6
|
+
* repaired by renumbering a shaded square so that the other answer repeats a number, and the result is rated:
|
|
7
|
+
* easy by the plain rules, medium once the whites must stay in one piece, hard by supposing, and extra-hard the
|
|
8
|
+
* most-supposing of several boards.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateHitori(size?: number, seed?: number, level?: HitoriLevel): HitoriPuzzle;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { HITORI_LEAST_SIDE, HITORI_LEVELS, HITORI_MOST_ATTEMPTS, HITORI_MOST_SIDE } from "./hitori.constants.js";
|
|
2
|
+
import { checkHitori } from "./hitoriBoard.js";
|
|
3
|
+
import { candidateHitori, latinSquare } from "./hitoriBuild.js";
|
|
4
|
+
import { hitoriModel } from "./hitoriLogic.js";
|
|
5
|
+
import { solveHitori } from "./hitoriSolve.js";
|
|
6
|
+
import { logicCsp, openSlots } from "./csp.js";
|
|
7
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
8
|
+
/**
|
|
9
|
+
* Makes a seeded puzzle and proves its answer is the only one. The shaded squares are chosen first (never touching,
|
|
10
|
+
* the rest in one piece), the white squares get numbers that repeat nowhere in their row or column, and every shaded
|
|
11
|
+
* square takes a number that a white square in its row or column already has. Boards with more than one answer are
|
|
12
|
+
* repaired by renumbering a shaded square so that the other answer repeats a number, and the result is rated:
|
|
13
|
+
* easy by the plain rules, medium once the whites must stay in one piece, hard by supposing, and extra-hard the
|
|
14
|
+
* most-supposing of several boards.
|
|
15
|
+
*/
|
|
16
|
+
export function generateHitori(size = 5, seed = 1, level = "medium") {
|
|
17
|
+
if (!Number.isInteger(size) || size < HITORI_LEAST_SIDE || size > HITORI_MOST_SIDE || !isKazuSeed(seed) || !HITORI_LEVELS.includes(level)) {
|
|
18
|
+
throw new RangeError("Invalid Hitori settings");
|
|
19
|
+
}
|
|
20
|
+
const random = seededRandom(seed);
|
|
21
|
+
const base = latinSquare(size, random);
|
|
22
|
+
if (!base)
|
|
23
|
+
throw new Error("No Hitori found within the generation budget; try another seed");
|
|
24
|
+
for (let aim = HITORI_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
25
|
+
const aimed = HITORI_LEVELS[aim];
|
|
26
|
+
const wanted = aimed === "extra-hard" ? 4 : 1;
|
|
27
|
+
let best = null, found = 0;
|
|
28
|
+
for (let attempt = 0; attempt < HITORI_MOST_ATTEMPTS && found < wanted; attempt += 1) {
|
|
29
|
+
const built = candidateHitori(size, aimed, random, base);
|
|
30
|
+
const made = built && rated(built.board, built.shaded, aimed);
|
|
31
|
+
if (!made)
|
|
32
|
+
continue;
|
|
33
|
+
found += 1;
|
|
34
|
+
if (!best || made.score > best.score)
|
|
35
|
+
best = made;
|
|
36
|
+
}
|
|
37
|
+
if (best) {
|
|
38
|
+
const proof = solveHitori(best.board);
|
|
39
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkHitori(best.board, proof.solution).ok) {
|
|
40
|
+
return { ...best.board, seed, level, solution: proof.solution };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw new Error("No uniquely solvable Hitori found within the generation budget; try another seed");
|
|
45
|
+
}
|
|
46
|
+
function rated(board, shaded, level) {
|
|
47
|
+
const total = board.size * board.size;
|
|
48
|
+
const solved = (strength, depth) => {
|
|
49
|
+
const { csp } = hitoriModel(board, strength);
|
|
50
|
+
const run = logicCsp(csp, openSlots(csp), depth);
|
|
51
|
+
return run.solved && checkHitori(board, Array.from({ length: total }, (_, cell) => run.alive[2 * cell + 1] === 1)).ok ? run : null;
|
|
52
|
+
};
|
|
53
|
+
if (level === "easy")
|
|
54
|
+
return solved(0, 0) ? { board, shaded, score: 0 } : null;
|
|
55
|
+
if (solved(0, 0))
|
|
56
|
+
return null;
|
|
57
|
+
if (level === "medium")
|
|
58
|
+
return solved(2, 0) ? { board, shaded, score: 0 } : null;
|
|
59
|
+
if (solved(2, 0))
|
|
60
|
+
return null;
|
|
61
|
+
const probing = solved(2, 1);
|
|
62
|
+
if (!probing)
|
|
63
|
+
return level === "extra-hard" ? { board, shaded, score: 1000 } : null;
|
|
64
|
+
return { board, shaded, score: probing.probes };
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { HitoriBoard } from "./hitori.types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Hitori as variables: one per square, slot 0 for left white and slot 1 for shaded.
|
|
5
|
+
* `strength` is how much of the rules the pruning uses: 0 has the duplicates, the ban on touching shades, the
|
|
6
|
+
* pair and the sandwich; 1 adds that every white square must stay in reach of the others; 2 adds that a square
|
|
7
|
+
* whose shading would cut the whites in two must stay white. A finished answer must be one piece, which the
|
|
8
|
+
* search checks whatever the strength.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hitoriModel(board: HitoriBoard, strength?: 0 | 1 | 2): {
|
|
11
|
+
csp: Csp;
|
|
12
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { hitoriNeighbors } from "./hitoriBoard.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hitori as variables: one per square, slot 0 for left white and slot 1 for shaded.
|
|
4
|
+
* `strength` is how much of the rules the pruning uses: 0 has the duplicates, the ban on touching shades, the
|
|
5
|
+
* pair and the sandwich; 1 adds that every white square must stay in reach of the others; 2 adds that a square
|
|
6
|
+
* whose shading would cut the whites in two must stay white. A finished answer must be one piece, which the
|
|
7
|
+
* search checks whatever the strength.
|
|
8
|
+
*/
|
|
9
|
+
export function hitoriModel(board, strength = 2) {
|
|
10
|
+
const size = board.size, total = size * size;
|
|
11
|
+
const neighbours = Array.from({ length: total }, (_, cell) => Int32Array.from(hitoriNeighbors(size, cell)));
|
|
12
|
+
const partners = Array.from({ length: total }, (_, cell) => {
|
|
13
|
+
const x = cell % size, y = Math.floor(cell / size), found = [];
|
|
14
|
+
for (let k = 0; k < size; k += 1) {
|
|
15
|
+
if (k !== x && board.numbers[y * size + k] === board.numbers[cell])
|
|
16
|
+
found.push(y * size + k);
|
|
17
|
+
if (k !== y && board.numbers[k * size + x] === board.numbers[cell])
|
|
18
|
+
found.push(k * size + x);
|
|
19
|
+
}
|
|
20
|
+
return Int32Array.from(found);
|
|
21
|
+
});
|
|
22
|
+
// A pair of equal numbers side by side shades every other equal number of that line; a number on both sides of a square keeps it white.
|
|
23
|
+
const pairs = [];
|
|
24
|
+
const sandwiches = [];
|
|
25
|
+
for (let y = 0; y < size; y += 1)
|
|
26
|
+
for (let x = 0; x < size; x += 1) {
|
|
27
|
+
const cell = y * size + x;
|
|
28
|
+
for (const [dx, dy] of [[1, 0], [0, 1]]) {
|
|
29
|
+
const nx = x + dx, ny = y + dy;
|
|
30
|
+
if (nx >= size || ny >= size)
|
|
31
|
+
continue;
|
|
32
|
+
const next = ny * size + nx;
|
|
33
|
+
if (board.numbers[cell] === board.numbers[next]) {
|
|
34
|
+
const others = partners[cell].filter(other => other !== next && (dx ? Math.floor(other / size) === y : other % size === x));
|
|
35
|
+
if (others.length)
|
|
36
|
+
pairs.push({ a: cell, b: next, others });
|
|
37
|
+
}
|
|
38
|
+
const fx = x + 2 * dx, fy = y + 2 * dy;
|
|
39
|
+
if (fx < size && fy < size && board.numbers[cell] === board.numbers[fy * size + fx])
|
|
40
|
+
sandwiches.push(next);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const starts = Int32Array.from({ length: total + 1 }, (_, i) => i * 2);
|
|
44
|
+
const disc = new Int32Array(total), low = new Int32Array(total), whitesBelow = new Int32Array(total);
|
|
45
|
+
const propagate = (alive) => {
|
|
46
|
+
let changed = true;
|
|
47
|
+
while (changed) {
|
|
48
|
+
changed = false;
|
|
49
|
+
for (let c = 0; c < total; c += 1) {
|
|
50
|
+
const white = alive[2 * c], shaded = alive[2 * c + 1];
|
|
51
|
+
if (!white && !shaded)
|
|
52
|
+
return false;
|
|
53
|
+
if (shaded && !white) {
|
|
54
|
+
const around = neighbours[c];
|
|
55
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
56
|
+
const n = around[k];
|
|
57
|
+
if (alive[2 * n + 1]) {
|
|
58
|
+
alive[2 * n + 1] = 0;
|
|
59
|
+
changed = true;
|
|
60
|
+
if (!alive[2 * n])
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (white && !shaded) {
|
|
66
|
+
const same = partners[c];
|
|
67
|
+
for (let k = 0; k < same.length; k += 1) {
|
|
68
|
+
const p = same[k];
|
|
69
|
+
if (alive[2 * p]) {
|
|
70
|
+
alive[2 * p] = 0;
|
|
71
|
+
changed = true;
|
|
72
|
+
if (!alive[2 * p + 1])
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (shaded) {
|
|
78
|
+
// A square is shaded only to settle a duplicate, so some equal number in its row or column must stay white.
|
|
79
|
+
const same = partners[c];
|
|
80
|
+
let free = 0, last = -1;
|
|
81
|
+
for (let k = 0; k < same.length; k += 1)
|
|
82
|
+
if (alive[2 * same[k]]) {
|
|
83
|
+
free += 1;
|
|
84
|
+
last = same[k];
|
|
85
|
+
}
|
|
86
|
+
if (free === 0) {
|
|
87
|
+
if (!white)
|
|
88
|
+
return false;
|
|
89
|
+
alive[2 * c + 1] = 0;
|
|
90
|
+
changed = true;
|
|
91
|
+
}
|
|
92
|
+
else if (free === 1 && !white && alive[2 * last + 1]) {
|
|
93
|
+
alive[2 * last + 1] = 0;
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const { a, b, others } of pairs) {
|
|
99
|
+
if (!alive[2 * a] && !alive[2 * b])
|
|
100
|
+
continue;
|
|
101
|
+
for (let k = 0; k < others.length; k += 1) {
|
|
102
|
+
const o = others[k];
|
|
103
|
+
if (alive[2 * o]) {
|
|
104
|
+
alive[2 * o] = 0;
|
|
105
|
+
changed = true;
|
|
106
|
+
if (!alive[2 * o + 1])
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (let k = 0; k < sandwiches.length; k += 1) {
|
|
112
|
+
const m = sandwiches[k];
|
|
113
|
+
if (alive[2 * m + 1]) {
|
|
114
|
+
alive[2 * m + 1] = 0;
|
|
115
|
+
changed = true;
|
|
116
|
+
if (!alive[2 * m])
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (strength > 0) {
|
|
121
|
+
const verdict = reach(alive);
|
|
122
|
+
if (verdict < 0)
|
|
123
|
+
return false;
|
|
124
|
+
if (verdict > 0)
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
};
|
|
130
|
+
const cutList = new Int32Array(total);
|
|
131
|
+
let clock = 0, cutCount = 0, whiteTotal = 0, current = new Uint8Array(0), power = 0;
|
|
132
|
+
const visit = (u, parent) => {
|
|
133
|
+
disc[u] = low[u] = ++clock;
|
|
134
|
+
let below = current[2 * u + 1] ? 0 : 1, cutWhites = 0, cutParts = 0;
|
|
135
|
+
const around = neighbours[u];
|
|
136
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
137
|
+
const n = around[k];
|
|
138
|
+
if (!current[2 * n] || n === parent)
|
|
139
|
+
continue;
|
|
140
|
+
if (disc[n]) {
|
|
141
|
+
if (disc[n] < low[u])
|
|
142
|
+
low[u] = disc[n];
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
visit(n, u);
|
|
146
|
+
if (low[n] < low[u])
|
|
147
|
+
low[u] = low[n];
|
|
148
|
+
below += whitesBelow[n];
|
|
149
|
+
if (low[n] >= disc[u] && whitesBelow[n] > 0) {
|
|
150
|
+
cutParts += 1;
|
|
151
|
+
cutWhites += whitesBelow[n];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
whitesBelow[u] = below;
|
|
155
|
+
if (power > 1 && current[2 * u] && current[2 * u + 1] && cutParts + (whiteTotal - cutWhites > 0 ? 1 : 0) >= 2)
|
|
156
|
+
cutList[cutCount++] = u;
|
|
157
|
+
};
|
|
158
|
+
/** -1 when the white squares cannot be one piece, 1 when a square was forced white, 0 when nothing moved. */
|
|
159
|
+
const reach = (alive) => {
|
|
160
|
+
let root = -1;
|
|
161
|
+
whiteTotal = 0;
|
|
162
|
+
for (let c = 0; c < total; c += 1)
|
|
163
|
+
if (!alive[2 * c + 1]) {
|
|
164
|
+
whiteTotal += 1;
|
|
165
|
+
if (root < 0)
|
|
166
|
+
root = c;
|
|
167
|
+
}
|
|
168
|
+
if (root < 0)
|
|
169
|
+
return 0;
|
|
170
|
+
if (!alive[2 * root])
|
|
171
|
+
return -1;
|
|
172
|
+
disc.fill(0);
|
|
173
|
+
clock = 0;
|
|
174
|
+
cutCount = 0;
|
|
175
|
+
current = alive;
|
|
176
|
+
power = strength;
|
|
177
|
+
visit(root, -1);
|
|
178
|
+
let seenWhites = 0;
|
|
179
|
+
for (let c = 0; c < total; c += 1)
|
|
180
|
+
if (!alive[2 * c + 1] && disc[c])
|
|
181
|
+
seenWhites += 1;
|
|
182
|
+
if (seenWhites !== whiteTotal)
|
|
183
|
+
return -1;
|
|
184
|
+
for (let k = 0; k < cutCount; k += 1)
|
|
185
|
+
alive[2 * cutList[k] + 1] = 0;
|
|
186
|
+
return cutCount ? 1 : 0;
|
|
187
|
+
};
|
|
188
|
+
return { csp: { starts, propagate } };
|
|
189
|
+
}
|
|
@@ -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,32 @@
|
|
|
1
|
+
import { checkHitori, isHitoriBoard } from "./hitoriBoard.js";
|
|
2
|
+
import { hitoriModel } from "./hitoriLogic.js";
|
|
3
|
+
import { logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
/** Rates a board with one answer by solving it: with the plain rules, with the whites-stay-together rule, then by supposing. */
|
|
5
|
+
export function rateHitori(board) {
|
|
6
|
+
if (!isHitoriBoard(board))
|
|
7
|
+
throw new RangeError("Invalid Hitori board");
|
|
8
|
+
const strong = hitoriModel(board, 2).csp;
|
|
9
|
+
const proof = solveCsp(strong, openSlots(strong), 2, 300000);
|
|
10
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
11
|
+
throw new RangeError("Hitori rating needs a board with one answer");
|
|
12
|
+
const shaded = Array.from({ length: board.size ** 2 }, (_, cell) => proof.solution[2 * cell + 1] === 1);
|
|
13
|
+
const solvedBy = (strength, depth) => {
|
|
14
|
+
const { csp } = hitoriModel(board, strength);
|
|
15
|
+
const run = logicCsp(csp, openSlots(csp), depth);
|
|
16
|
+
return { ...run, solved: run.solved && checkHitori(board, Array.from({ length: board.size ** 2 }, (_, cell) => run.alive[2 * cell + 1] === 1)).ok };
|
|
17
|
+
};
|
|
18
|
+
const plain = solvedBy(0, 0), reach = plain.solved ? plain : solvedBy(2, 0);
|
|
19
|
+
const probing = reach.solved ? reach : solvedBy(2, 1);
|
|
20
|
+
const repeats = board.numbers.filter((_, cell) => {
|
|
21
|
+
const x = cell % board.size, y = Math.floor(cell / board.size);
|
|
22
|
+
return board.numbers.some((other, at) => at !== cell && other === board.numbers[cell] && (at % board.size === x || Math.floor(at / board.size) === y));
|
|
23
|
+
}).length;
|
|
24
|
+
return {
|
|
25
|
+
depth: reach.solved ? 0 : probing.solved ? 1 : 2,
|
|
26
|
+
reach: !plain.solved && reach.solved,
|
|
27
|
+
probes: reach.solved ? 0 : probing.probes,
|
|
28
|
+
shaded: shaded.filter(Boolean).length,
|
|
29
|
+
shadedShare: shaded.filter(Boolean).length / shaded.length,
|
|
30
|
+
repeatShare: repeats / shaded.length,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HitoriBoard, HitoriSolve } from "./hitori.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Counts distinct minimal shade patterns: an answer shades only squares that settle a duplicate, so extra shades
|
|
4
|
+
* are never a second answer. A non-complete result never asserts uniqueness.
|
|
5
|
+
*/
|
|
6
|
+
export declare function solveHitori(board: HitoriBoard, options?: {
|
|
7
|
+
limit?: number;
|
|
8
|
+
nodes?: number;
|
|
9
|
+
}): HitoriSolve;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { HITORI_MAX_NODES } from "./hitori.constants.js";
|
|
2
|
+
import { isHitoriBoard } from "./hitoriBoard.js";
|
|
3
|
+
import { hitoriModel } from "./hitoriLogic.js";
|
|
4
|
+
import { openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/**
|
|
6
|
+
* Counts distinct minimal shade patterns: an answer shades only squares that settle a duplicate, so extra shades
|
|
7
|
+
* are never a second answer. A non-complete result never asserts uniqueness.
|
|
8
|
+
*/
|
|
9
|
+
export function solveHitori(board, options = {}) {
|
|
10
|
+
if (!isHitoriBoard(board))
|
|
11
|
+
throw new RangeError("Invalid Hitori board");
|
|
12
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 2));
|
|
13
|
+
const budget = Math.max(1, Math.floor(options.nodes ?? HITORI_MAX_NODES));
|
|
14
|
+
const { csp } = hitoriModel(board);
|
|
15
|
+
const found = solveCsp(csp, openSlots(csp), limit, budget);
|
|
16
|
+
const solution = found.solution ? Array.from({ length: board.size ** 2 }, (_, cell) => found.solution[2 * cell + 1] === 1) : null;
|
|
17
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
18
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { HitoriLanguage } from "./hitoriPlay.types.ts";
|
|
2
|
+
export declare const HITORI_STRINGS: {
|
|
3
|
+
readonly en: {
|
|
4
|
+
readonly title: "Hitori";
|
|
5
|
+
readonly help: "Tap a cell or press Space to shade it. Shaded cells cannot touch; the remaining numbers must be unique in every row and column, and the unshaded cells must stay connected. Arrow keys 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 bad: "Some rules are broken.";
|
|
13
|
+
readonly good: "No rule errors yet. Keep going.";
|
|
14
|
+
readonly noHint: "No proved hint is available.";
|
|
15
|
+
readonly cell: "Row";
|
|
16
|
+
readonly column: "column";
|
|
17
|
+
readonly shaded: "shaded";
|
|
18
|
+
readonly white: "unshaded";
|
|
19
|
+
};
|
|
20
|
+
readonly ja: {
|
|
21
|
+
readonly title: "ひとりにしてくれ";
|
|
22
|
+
readonly help: "マスを押すかスペースキーで黒くします。黒マスは辺で隣り合えません。白マスの行と列に同じ数字がなく、白マス全体がつながるようにします。矢印キーで移動します。";
|
|
23
|
+
readonly undo: "戻す";
|
|
24
|
+
readonly hint: "ヒント";
|
|
25
|
+
readonly check: "確認";
|
|
26
|
+
readonly restart: "やり直す";
|
|
27
|
+
readonly won: "完成!";
|
|
28
|
+
readonly helped: "ヒントを使って完成。";
|
|
29
|
+
readonly bad: "ルールに合わないところがあります。";
|
|
30
|
+
readonly good: "今のところルール違反はありません。";
|
|
31
|
+
readonly noHint: "確かなヒントはありません。";
|
|
32
|
+
readonly cell: "行";
|
|
33
|
+
readonly column: "列";
|
|
34
|
+
readonly shaded: "黒";
|
|
35
|
+
readonly white: "白";
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare function hitoriWords(language?: HitoriLanguage): {
|
|
39
|
+
readonly title: "Hitori";
|
|
40
|
+
readonly help: "Tap a cell or press Space to shade it. Shaded cells cannot touch; the remaining numbers must be unique in every row and column, and the unshaded cells must stay connected. Arrow keys 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 bad: "Some rules are broken.";
|
|
48
|
+
readonly good: "No rule errors yet. Keep going.";
|
|
49
|
+
readonly noHint: "No proved hint is available.";
|
|
50
|
+
readonly cell: "Row";
|
|
51
|
+
readonly column: "column";
|
|
52
|
+
readonly shaded: "shaded";
|
|
53
|
+
readonly white: "unshaded";
|
|
54
|
+
} | {
|
|
55
|
+
readonly title: "ひとりにしてくれ";
|
|
56
|
+
readonly help: "マスを押すかスペースキーで黒くします。黒マスは辺で隣り合えません。白マスの行と列に同じ数字がなく、白マス全体がつながるようにします。矢印キーで移動します。";
|
|
57
|
+
readonly undo: "戻す";
|
|
58
|
+
readonly hint: "ヒント";
|
|
59
|
+
readonly check: "確認";
|
|
60
|
+
readonly restart: "やり直す";
|
|
61
|
+
readonly won: "完成!";
|
|
62
|
+
readonly helped: "ヒントを使って完成。";
|
|
63
|
+
readonly bad: "ルールに合わないところがあります。";
|
|
64
|
+
readonly good: "今のところルール違反はありません。";
|
|
65
|
+
readonly noHint: "確かなヒントはありません。";
|
|
66
|
+
readonly cell: "行";
|
|
67
|
+
readonly column: "列";
|
|
68
|
+
readonly shaded: "黒";
|
|
69
|
+
readonly white: "白";
|
|
70
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const HITORI_STRINGS = {
|
|
2
|
+
en: {
|
|
3
|
+
title: "Hitori",
|
|
4
|
+
help: "Tap a cell or press Space to shade it. Shaded cells cannot touch; the remaining numbers must be unique in every row and column, and the unshaded cells must stay connected. Arrow keys move.",
|
|
5
|
+
undo: "Undo", hint: "Hint", check: "Check", restart: "Restart", won: "Solved!", helped: "Solved with help.",
|
|
6
|
+
bad: "Some rules are broken.", good: "No rule errors yet. Keep going.", noHint: "No proved hint is available.",
|
|
7
|
+
cell: "Row", column: "column", shaded: "shaded", white: "unshaded",
|
|
8
|
+
},
|
|
9
|
+
ja: {
|
|
10
|
+
title: "ひとりにしてくれ",
|
|
11
|
+
help: "マスを押すかスペースキーで黒くします。黒マスは辺で隣り合えません。白マスの行と列に同じ数字がなく、白マス全体がつながるようにします。矢印キーで移動します。",
|
|
12
|
+
undo: "戻す", hint: "ヒント", check: "確認", restart: "やり直す", won: "完成!", helped: "ヒントを使って完成。",
|
|
13
|
+
bad: "ルールに合わないところがあります。", good: "今のところルール違反はありません。", noHint: "確かなヒントはありません。",
|
|
14
|
+
cell: "行", column: "列", shaded: "黒", white: "白",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export function hitoriWords(language = "en") { return HITORI_STRINGS[language]; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HITORI_PLAY_STYLE = "\n.kh-root{color:var(--ks-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto}\n.kh-board{position:relative;width:100%;min-width:240px;touch-action:manipulation}\n.kh-cells{position:absolute;inset:0;display:grid;padding:2px}\n.kh-cell{background:transparent;border:0;min-width:0;min-height:0;padding:0;cursor:pointer;touch-action:manipulation}\n.kh-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px;border-radius:4px}\n.kh-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}\n.kh-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:var(--ks-ink,#1f2320);cursor:pointer}\n.kh-tools button:disabled{opacity:.45;cursor:default}.kh-status{min-height:1.5em}.kh-help{font-size:14px;opacity:.8}";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const HITORI_PLAY_STYLE = `
|
|
2
|
+
.kh-root{color:var(--ks-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto}
|
|
3
|
+
.kh-board{position:relative;width:100%;min-width:240px;touch-action:manipulation}
|
|
4
|
+
.kh-cells{position:absolute;inset:0;display:grid;padding:2px}
|
|
5
|
+
.kh-cell{background:transparent;border:0;min-width:0;min-height:0;padding:0;cursor:pointer;touch-action:manipulation}
|
|
6
|
+
.kh-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px;border-radius:4px}
|
|
7
|
+
.kh-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}
|
|
8
|
+
.kh-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:var(--ks-ink,#1f2320);cursor:pointer}
|
|
9
|
+
.kh-tools button:disabled{opacity:.45;cursor:default}.kh-status{min-height:1.5em}.kh-help{font-size:14px;opacity:.8}`;
|