@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,153 @@
|
|
|
1
|
+
import { kakuroRuns } from "./kakuroBoard.js";
|
|
2
|
+
/** Every way to choose `length` different digits that add to `sum`, as 9-bit masks (bit d - 1 for digit d), by length then sum. */
|
|
3
|
+
const COMBINATIONS = (() => {
|
|
4
|
+
const table = Array.from({ length: 10 }, () => Array.from({ length: 46 }, () => []));
|
|
5
|
+
for (let mask = 1; mask < 512; mask += 1) {
|
|
6
|
+
let length = 0, sum = 0;
|
|
7
|
+
for (let d = 1; d <= 9; d += 1)
|
|
8
|
+
if (mask & 1 << d - 1) {
|
|
9
|
+
length += 1;
|
|
10
|
+
sum += d;
|
|
11
|
+
}
|
|
12
|
+
table[length][sum].push(mask);
|
|
13
|
+
}
|
|
14
|
+
return table;
|
|
15
|
+
})();
|
|
16
|
+
/**
|
|
17
|
+
* Kakuro as variables: one per white square, nine slots for the digits 1 to 9. A run's pruning keeps only the digit
|
|
18
|
+
* sets that add up to its total and fit what its squares can still be, so a square keeps only digits some such
|
|
19
|
+
* set uses; a digit that every such set uses must appear somewhere (`strength` 1), and a digit that only one square
|
|
20
|
+
* can take is that square's.
|
|
21
|
+
*/
|
|
22
|
+
export function kakuroModel(board, strength = 1, random) {
|
|
23
|
+
const whites = board.cells.flatMap((tile, cell) => tile.kind === "white" ? [cell] : []);
|
|
24
|
+
const index = new Int32Array(board.cells.length).fill(-1);
|
|
25
|
+
whites.forEach((cell, i) => { index[cell] = i; });
|
|
26
|
+
const runs = kakuroRuns(board).map(run => ({ cells: Int32Array.from(run.cells.map(cell => index[cell])), sum: run.sum, combos: COMBINATIONS[run.cells.length][run.sum] }));
|
|
27
|
+
const runsOf = whites.map(() => []);
|
|
28
|
+
runs.forEach((run, r) => run.cells.forEach(cell => runsOf[cell].push(r)));
|
|
29
|
+
const starts = Int32Array.from({ length: whites.length + 1 }, (_, i) => i * 9);
|
|
30
|
+
const domain = new Int16Array(whites.length), loaded = new Int32Array(whites.length), touched = new Int32Array(whites.length), changedCells = new Int32Array(whites.length);
|
|
31
|
+
const queue = new Int32Array(runs.length * 4 + 4), queued = new Uint8Array(runs.length);
|
|
32
|
+
let stamp = 0;
|
|
33
|
+
const propagate = (alive, decided) => {
|
|
34
|
+
stamp += 1;
|
|
35
|
+
let changes = 0;
|
|
36
|
+
// A square's digits are read from `alive` the first time a run asks, so a call after one decision touches only what that reaches.
|
|
37
|
+
const read = (i) => {
|
|
38
|
+
if (loaded[i] !== stamp) {
|
|
39
|
+
let mask = 0;
|
|
40
|
+
for (let d = 0; d < 9; d += 1)
|
|
41
|
+
if (alive[9 * i + d])
|
|
42
|
+
mask |= 1 << d;
|
|
43
|
+
domain[i] = mask;
|
|
44
|
+
loaded[i] = stamp;
|
|
45
|
+
}
|
|
46
|
+
return domain[i];
|
|
47
|
+
};
|
|
48
|
+
let head = 0, tail = 0;
|
|
49
|
+
const push = (r) => { if (!queued[r]) {
|
|
50
|
+
queued[r] = 1;
|
|
51
|
+
queue[tail++ % queue.length] = r;
|
|
52
|
+
} };
|
|
53
|
+
if (decided === undefined)
|
|
54
|
+
for (let r = 0; r < runs.length; r += 1)
|
|
55
|
+
push(r);
|
|
56
|
+
else
|
|
57
|
+
for (const r of runsOf[decided])
|
|
58
|
+
push(r);
|
|
59
|
+
while (head < tail) {
|
|
60
|
+
const r = queue[head++ % queue.length], run = runs[r], cells = run.cells;
|
|
61
|
+
queued[r] = 0;
|
|
62
|
+
let known = 0, union = 0;
|
|
63
|
+
for (let k = 0; k < cells.length; k += 1) {
|
|
64
|
+
const mask = read(cells[k]);
|
|
65
|
+
if (!mask)
|
|
66
|
+
return fail();
|
|
67
|
+
union |= mask;
|
|
68
|
+
if (!(mask & mask - 1)) {
|
|
69
|
+
if (known & mask)
|
|
70
|
+
return fail();
|
|
71
|
+
known |= mask;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
let usable = 0, always = 511, viable = 0;
|
|
75
|
+
for (let c = 0; c < run.combos.length; c += 1) {
|
|
76
|
+
const combo = run.combos[c];
|
|
77
|
+
if (combo & ~union || known & ~combo)
|
|
78
|
+
continue;
|
|
79
|
+
let fits = true;
|
|
80
|
+
for (let k = 0; k < cells.length && fits; k += 1)
|
|
81
|
+
if (!(domain[cells[k]] & combo))
|
|
82
|
+
fits = false;
|
|
83
|
+
if (!fits)
|
|
84
|
+
continue;
|
|
85
|
+
viable += 1;
|
|
86
|
+
usable |= combo;
|
|
87
|
+
always &= combo;
|
|
88
|
+
}
|
|
89
|
+
if (!viable)
|
|
90
|
+
return fail();
|
|
91
|
+
for (let k = 0; k < cells.length; k += 1) {
|
|
92
|
+
const cell = cells[k], was = domain[cell];
|
|
93
|
+
let mask = was & usable;
|
|
94
|
+
// Squares not yet decided cannot take the digits the decided ones of the run hold.
|
|
95
|
+
if (was & was - 1)
|
|
96
|
+
mask &= ~known;
|
|
97
|
+
if (strength > 0 && always && mask & mask - 1) {
|
|
98
|
+
// A digit every viable set uses must go somewhere: if only this square can hold it, it does.
|
|
99
|
+
for (let d = 0; d < 9; d += 1) {
|
|
100
|
+
if (!(always & 1 << d) || !(mask & 1 << d))
|
|
101
|
+
continue;
|
|
102
|
+
let holders = 0;
|
|
103
|
+
for (let j = 0; j < cells.length; j += 1)
|
|
104
|
+
if (domain[cells[j]] & 1 << d)
|
|
105
|
+
holders += 1;
|
|
106
|
+
if (holders === 1) {
|
|
107
|
+
mask = 1 << d;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!mask)
|
|
113
|
+
return fail();
|
|
114
|
+
if (mask !== was) {
|
|
115
|
+
if (touched[cell] !== stamp) {
|
|
116
|
+
touched[cell] = stamp;
|
|
117
|
+
changedCells[changes++] = cell;
|
|
118
|
+
}
|
|
119
|
+
domain[cell] = mask;
|
|
120
|
+
for (const other of runsOf[cell])
|
|
121
|
+
push(other);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (let c = 0; c < changes; c += 1) {
|
|
126
|
+
const i = changedCells[c];
|
|
127
|
+
for (let d = 0; d < 9; d += 1)
|
|
128
|
+
alive[9 * i + d] = domain[i] & 1 << d ? 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
};
|
|
132
|
+
const fail = () => { queued.fill(0); return false; };
|
|
133
|
+
// A search that picks at random among the squares with fewest digits left can be restarted when it gets lost.
|
|
134
|
+
const choose = random ? (alive) => {
|
|
135
|
+
let best = 10, count = 0, pick = -1;
|
|
136
|
+
for (let i = 0; i < whites.length; i += 1) {
|
|
137
|
+
let left = 0;
|
|
138
|
+
for (let d = 0; d < 9; d += 1)
|
|
139
|
+
left += alive[9 * i + d];
|
|
140
|
+
if (left < 2 || left > best)
|
|
141
|
+
continue;
|
|
142
|
+
if (left < best) {
|
|
143
|
+
best = left;
|
|
144
|
+
count = 0;
|
|
145
|
+
}
|
|
146
|
+
count += 1;
|
|
147
|
+
if (random() * count < 1)
|
|
148
|
+
pick = i;
|
|
149
|
+
}
|
|
150
|
+
return pick;
|
|
151
|
+
} : undefined;
|
|
152
|
+
return { csp: { starts, propagate, choose }, whites, index };
|
|
153
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { progressKakuro } from "./kakuroBoard.js";
|
|
2
|
+
import { drawKakuro } from "./kakuroDraw.js";
|
|
3
|
+
import { decodeKakuro, encodeKakuro, enterKakuro, finishedKakuro, hintKakuro, newKakuro, undoKakuro } from "./kakuroGame.js";
|
|
4
|
+
import { KAKURO_PLAY_STYLE } from "./kakuroStyle.js";
|
|
5
|
+
import { kakuroWords } from "./kakuroStrings.js";
|
|
6
|
+
export function mountKakuro(host, initial) {
|
|
7
|
+
const doc = host.ownerDocument;
|
|
8
|
+
let options = { ...initial };
|
|
9
|
+
const words = () => kakuroWords(options.language);
|
|
10
|
+
let game = initial.progress ? decodeKakuro(initial.progress) ?? newKakuro(initial.board) : newKakuro(initial.board);
|
|
11
|
+
let selected = game.board.cells.findIndex(cell => cell.kind === "white"), pencil = false, message = "", dead = false;
|
|
12
|
+
const root = doc.createElement("div"), style = doc.createElement("style"), board = doc.createElement("div"), tools = doc.createElement("div"), pad = doc.createElement("div"), status = doc.createElement("p"), help = doc.createElement("p"), grid = doc.createElement("div");
|
|
13
|
+
root.className = "kk-root";
|
|
14
|
+
style.textContent = KAKURO_PLAY_STYLE;
|
|
15
|
+
board.className = "kk-board";
|
|
16
|
+
tools.className = "kk-tools";
|
|
17
|
+
pad.className = "kk-pad";
|
|
18
|
+
pad.setAttribute("role", "group");
|
|
19
|
+
status.className = "kk-status";
|
|
20
|
+
status.setAttribute("role", "status");
|
|
21
|
+
help.className = "kk-help";
|
|
22
|
+
grid.className = "kk-cells";
|
|
23
|
+
board.append(doc.createElement("div"), grid);
|
|
24
|
+
root.append(style, board, pad, status, tools, help);
|
|
25
|
+
host.append(root);
|
|
26
|
+
const copy = () => ({ ...game, board: { ...game.board, cells: game.board.cells.map(cell => ({ ...cell })) }, values: [...game.values], notes: game.notes.map(mark => [...mark]), history: [] });
|
|
27
|
+
const update = (next) => { game = next; render(); options.onChange?.(copy()); if (finishedKakuro(game))
|
|
28
|
+
options.onFinish?.(copy()); };
|
|
29
|
+
const button = (label, action) => { const item = doc.createElement("button"); item.type = "button"; item.textContent = label; item.onclick = action; tools.append(item); };
|
|
30
|
+
const render = () => {
|
|
31
|
+
if (dead)
|
|
32
|
+
return;
|
|
33
|
+
const w = words(), b = game.board, focus = grid.contains(doc.activeElement);
|
|
34
|
+
board.firstElementChild.innerHTML = drawKakuro(b, { values: game.values, notes: game.notes, selected, material: options.material, pieces: options.pieces });
|
|
35
|
+
grid.style.gridTemplateColumns = `repeat(${b.width},1fr)`;
|
|
36
|
+
grid.replaceChildren();
|
|
37
|
+
b.cells.forEach((cell, index) => {
|
|
38
|
+
const button = doc.createElement("button");
|
|
39
|
+
button.type = "button";
|
|
40
|
+
button.className = "kk-cell";
|
|
41
|
+
button.disabled = cell.kind === "black";
|
|
42
|
+
button.tabIndex = selected === index ? 0 : -1;
|
|
43
|
+
const row = Math.floor(index / b.width) + 1, column = index % b.width + 1;
|
|
44
|
+
const tile = cell.kind === "black" ? `${w.across} ${cell.across ?? "—"}, ${w.down} ${cell.down ?? "—"}` : game.values[index] || game.notes[index].length ? `value ${game.values[index] || game.notes[index].join(",")}` : w.blank;
|
|
45
|
+
button.setAttribute("aria-label", `${w.row} ${row}, ${w.column} ${column}: ${tile}`);
|
|
46
|
+
button.onclick = () => { selected = index; render(); };
|
|
47
|
+
button.onkeydown = event => {
|
|
48
|
+
const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -b.width, ArrowDown: b.width };
|
|
49
|
+
if (event.key in step) {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
let next = index;
|
|
52
|
+
do {
|
|
53
|
+
next += step[event.key];
|
|
54
|
+
} while (next >= 0 && next < b.cells.length && b.cells[next].kind === "black");
|
|
55
|
+
if (next >= 0 && next < b.cells.length)
|
|
56
|
+
selected = next;
|
|
57
|
+
render();
|
|
58
|
+
}
|
|
59
|
+
else if (/^[1-9]$/.test(event.key)) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
update(enterKakuro(game, index, Number(event.key), pencil));
|
|
62
|
+
}
|
|
63
|
+
else if (event.key === "0" || event.key === "Backspace" || event.key === "Delete") {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
update(enterKakuro(game, index, 0));
|
|
66
|
+
}
|
|
67
|
+
else if (event.key.toLowerCase() === "p") {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
pencil = !pencil;
|
|
70
|
+
render();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
grid.append(button);
|
|
74
|
+
});
|
|
75
|
+
if (focus)
|
|
76
|
+
grid.querySelector(`button[aria-label^="${w.row} ${Math.floor(selected / b.width) + 1}, ${w.column} ${selected % b.width + 1}:"]`)?.focus();
|
|
77
|
+
tools.replaceChildren();
|
|
78
|
+
button(w.undo, () => update(undoKakuro(game)));
|
|
79
|
+
button(w.pencil, () => { pencil = !pencil; render(); });
|
|
80
|
+
button(w.hint, () => { const hint = hintKakuro(game); if (!hint) {
|
|
81
|
+
message = w.wrong;
|
|
82
|
+
render();
|
|
83
|
+
return;
|
|
84
|
+
} update({ ...enterKakuro(game, hint.cell, hint.digit), helped: true }); message = `${w.row} ${Math.floor(hint.cell / b.width) + 1}, ${w.column} ${hint.cell % b.width + 1}: ${hint.digit}`; render(); });
|
|
85
|
+
button(w.check, () => { const result = progressKakuro(game.board, game.values); message = result.errors.length ? w.wrong : finishedKakuro(game) ? game.helped ? w.helped : w.won : w.checked; render(); });
|
|
86
|
+
button(w.restart, () => { game = newKakuro(game.board); message = w.ready; render(); });
|
|
87
|
+
pad.replaceChildren();
|
|
88
|
+
for (let digit = 1; digit <= 9; digit += 1) {
|
|
89
|
+
const key = doc.createElement("button");
|
|
90
|
+
key.type = "button";
|
|
91
|
+
key.textContent = String(digit);
|
|
92
|
+
key.setAttribute("aria-label", `${digit}`);
|
|
93
|
+
key.onclick = () => { if (selected >= 0)
|
|
94
|
+
update(enterKakuro(game, selected, digit, pencil)); };
|
|
95
|
+
pad.append(key);
|
|
96
|
+
}
|
|
97
|
+
const clear = doc.createElement("button");
|
|
98
|
+
clear.type = "button";
|
|
99
|
+
clear.textContent = w.clear;
|
|
100
|
+
clear.setAttribute("aria-label", w.clear);
|
|
101
|
+
clear.onclick = () => { if (selected >= 0)
|
|
102
|
+
update(enterKakuro(game, selected, 0)); };
|
|
103
|
+
pad.append(clear);
|
|
104
|
+
status.textContent = finishedKakuro(game) ? game.helped ? w.helped : w.won : message || w.ready;
|
|
105
|
+
help.textContent = w.help;
|
|
106
|
+
};
|
|
107
|
+
render();
|
|
108
|
+
return { game: copy, progress: () => encodeKakuro(game), set: next => { options = { ...options, ...next }; render(); }, restart: () => { game = newKakuro(game.board); render(); }, destroy: () => { dead = true; root.remove(); } };
|
|
109
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { KakuroBoard, KakuroGame, KakuroLanguage, KakuroMaterial, KakuroPieces } from "./kakuro.types.ts";
|
|
2
|
+
import type { KakuroDrawOptions } from "./kakuroDraw.ts";
|
|
3
|
+
export type KakuroMountOptions = KakuroDrawOptions & {
|
|
4
|
+
board: KakuroBoard;
|
|
5
|
+
language?: KakuroLanguage;
|
|
6
|
+
progress?: string;
|
|
7
|
+
onChange?: (game: KakuroGame) => void;
|
|
8
|
+
onFinish?: (game: KakuroGame) => void;
|
|
9
|
+
material?: KakuroMaterial;
|
|
10
|
+
pieces?: KakuroPieces;
|
|
11
|
+
};
|
|
12
|
+
export type KakuroMount = {
|
|
13
|
+
game: () => KakuroGame;
|
|
14
|
+
progress: () => string;
|
|
15
|
+
set: (options: KakuroDrawOptions & {
|
|
16
|
+
language?: KakuroLanguage;
|
|
17
|
+
}) => void;
|
|
18
|
+
restart: () => void;
|
|
19
|
+
destroy: () => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { KakuroBoard, KakuroRating } from "./kakuro.types.ts";
|
|
2
|
+
/** Rates a board with one answer by solving it: with the single-run rules, with the digit-that-must-appear rule, then by supposing. */
|
|
3
|
+
export declare function rateKakuro(board: KakuroBoard): KakuroRating;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isKakuroBoard, kakuroRuns } from "./kakuroBoard.js";
|
|
2
|
+
import { kakuroModel } from "./kakuroLogic.js";
|
|
3
|
+
import { logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
const COMBOS = (length, sum) => {
|
|
5
|
+
let count = 0;
|
|
6
|
+
for (let mask = 1; mask < 512; mask += 1) {
|
|
7
|
+
let n = 0, total = 0;
|
|
8
|
+
for (let d = 1; d <= 9; d += 1)
|
|
9
|
+
if (mask & 1 << d - 1) {
|
|
10
|
+
n += 1;
|
|
11
|
+
total += d;
|
|
12
|
+
}
|
|
13
|
+
if (n === length && total === sum)
|
|
14
|
+
count += 1;
|
|
15
|
+
}
|
|
16
|
+
return count;
|
|
17
|
+
};
|
|
18
|
+
/** Rates a board with one answer by solving it: with the single-run rules, with the digit-that-must-appear rule, then by supposing. */
|
|
19
|
+
export function rateKakuro(board) {
|
|
20
|
+
if (!isKakuroBoard(board))
|
|
21
|
+
throw new RangeError("Invalid Kakuro board");
|
|
22
|
+
const weak = kakuroModel(board, 0), strong = kakuroModel(board, 1);
|
|
23
|
+
const proof = solveCsp(strong.csp, openSlots(strong.csp), 2, 250000);
|
|
24
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
25
|
+
throw new RangeError("Kakuro rating needs a board with one answer");
|
|
26
|
+
const plain = logicCsp(weak.csp, openSlots(weak.csp), 0);
|
|
27
|
+
const rules = plain.solved ? plain : logicCsp(strong.csp, openSlots(strong.csp), 0);
|
|
28
|
+
const probing = rules.solved ? rules : logicCsp(strong.csp, openSlots(strong.csp), 1);
|
|
29
|
+
const runs = kakuroRuns(board);
|
|
30
|
+
return {
|
|
31
|
+
depth: rules.solved ? 0 : probing.solved ? 1 : 2,
|
|
32
|
+
plain: plain.solved,
|
|
33
|
+
probes: rules.solved ? 0 : probing.probes,
|
|
34
|
+
whites: strong.whites.length,
|
|
35
|
+
runs: runs.length,
|
|
36
|
+
longest: Math.max(...runs.map(run => run.cells.length)),
|
|
37
|
+
meanRun: runs.reduce((total, run) => total + run.cells.length, 0) / runs.length,
|
|
38
|
+
fixedShare: runs.filter(run => COMBOS(run.cells.length, run.sum) === 1).length / runs.length,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { KakuroBoard, KakuroSolve } from "./kakuro.types.ts";
|
|
2
|
+
/** Bounded exact counter over across and down runs; an interrupted search never proves uniqueness. */
|
|
3
|
+
export declare function solveKakuro(board: KakuroBoard, entries?: readonly number[], options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): KakuroSolve;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { KAKURO_MAX_NODES } from "./kakuro.constants.js";
|
|
2
|
+
import { isKakuroBoard } from "./kakuroBoard.js";
|
|
3
|
+
import { kakuroModel } from "./kakuroLogic.js";
|
|
4
|
+
import { decide, openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Bounded exact counter over across and down runs; an interrupted search never proves uniqueness. */
|
|
6
|
+
export function solveKakuro(board, entries = [], options = {}) {
|
|
7
|
+
if (!isKakuroBoard(board))
|
|
8
|
+
throw new RangeError("Invalid Kakuro board");
|
|
9
|
+
const limit = options.limit ?? 2, budget = options.nodes ?? KAKURO_MAX_NODES;
|
|
10
|
+
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1)
|
|
11
|
+
throw new RangeError("Invalid search bounds");
|
|
12
|
+
if (entries.length && entries.length !== board.cells.length)
|
|
13
|
+
throw new RangeError("Invalid Kakuro entries");
|
|
14
|
+
const { csp, whites } = kakuroModel(board);
|
|
15
|
+
const start = openSlots(csp);
|
|
16
|
+
for (let cell = 0; cell < entries.length; cell += 1) {
|
|
17
|
+
const value = entries[cell];
|
|
18
|
+
if (board.cells[cell].kind === "black" ? value !== 0 : !Number.isInteger(value) || value < 0 || value > 9) {
|
|
19
|
+
return { count: 0, solution: null, complete: true, nodes: 0 };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
whites.forEach((cell, i) => { const value = entries[cell]; if (value)
|
|
23
|
+
decide(csp, start, i, 9 * i + value - 1); });
|
|
24
|
+
const found = solveCsp(csp, start, limit, budget);
|
|
25
|
+
let solution = null;
|
|
26
|
+
if (found.solution) {
|
|
27
|
+
solution = Array(board.cells.length).fill(0);
|
|
28
|
+
whites.forEach((cell, i) => { for (let d = 0; d < 9; d += 1)
|
|
29
|
+
if (found.solution[9 * i + d])
|
|
30
|
+
solution[cell] = d + 1; });
|
|
31
|
+
}
|
|
32
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
33
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type KakuroLanguage = "en" | "ja";
|
|
2
|
+
export declare const KAKURO_STRINGS: {
|
|
3
|
+
readonly en: {
|
|
4
|
+
readonly title: "Kakuro";
|
|
5
|
+
readonly help: "Fill each white cell with 1–9. Digits in each across or down run add to its clue and cannot repeat. Arrows move; type a digit or use the number pad; P toggles pencil marks.";
|
|
6
|
+
readonly undo: "Undo";
|
|
7
|
+
readonly hint: "Hint";
|
|
8
|
+
readonly check: "Check";
|
|
9
|
+
readonly restart: "Restart";
|
|
10
|
+
readonly ready: "Choose a white cell.";
|
|
11
|
+
readonly won: "Every run is correct. Solved!";
|
|
12
|
+
readonly helped: "Solved with help.";
|
|
13
|
+
readonly checked: "No run errors so far. Keep filling.";
|
|
14
|
+
readonly wrong: "Some runs have a repeated digit or impossible total.";
|
|
15
|
+
readonly across: "Across";
|
|
16
|
+
readonly down: "Down";
|
|
17
|
+
readonly pencil: "Pencil marks";
|
|
18
|
+
readonly row: "Row";
|
|
19
|
+
readonly column: "column";
|
|
20
|
+
readonly blank: "blank";
|
|
21
|
+
readonly clear: "Clear";
|
|
22
|
+
};
|
|
23
|
+
readonly ja: {
|
|
24
|
+
readonly title: "カックロ";
|
|
25
|
+
readonly help: "白マスに1〜9を入れます。横または縦の連続した数字はヒントの合計になり、同じ数字は使えません。矢印または数字パッドで入力、Pでメモを切り替えます。";
|
|
26
|
+
readonly undo: "戻す";
|
|
27
|
+
readonly hint: "ヒント";
|
|
28
|
+
readonly check: "確認";
|
|
29
|
+
readonly restart: "やり直す";
|
|
30
|
+
readonly ready: "白マスを選んでください。";
|
|
31
|
+
readonly won: "すべての合計が合いました。完成!";
|
|
32
|
+
readonly helped: "ヒントを使って完成!";
|
|
33
|
+
readonly checked: "ここまでの合計に誤りはありません。続けてください。";
|
|
34
|
+
readonly wrong: "同じ数字または合計の誤りがあります。";
|
|
35
|
+
readonly across: "横";
|
|
36
|
+
readonly down: "縦";
|
|
37
|
+
readonly pencil: "メモ";
|
|
38
|
+
readonly row: "行";
|
|
39
|
+
readonly column: "列";
|
|
40
|
+
readonly blank: "空白";
|
|
41
|
+
readonly clear: "消す";
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export declare function kakuroWords(language?: KakuroLanguage): {
|
|
45
|
+
readonly title: "Kakuro";
|
|
46
|
+
readonly help: "Fill each white cell with 1–9. Digits in each across or down run add to its clue and cannot repeat. Arrows move; type a digit or use the number pad; P toggles pencil marks.";
|
|
47
|
+
readonly undo: "Undo";
|
|
48
|
+
readonly hint: "Hint";
|
|
49
|
+
readonly check: "Check";
|
|
50
|
+
readonly restart: "Restart";
|
|
51
|
+
readonly ready: "Choose a white cell.";
|
|
52
|
+
readonly won: "Every run is correct. Solved!";
|
|
53
|
+
readonly helped: "Solved with help.";
|
|
54
|
+
readonly checked: "No run errors so far. Keep filling.";
|
|
55
|
+
readonly wrong: "Some runs have a repeated digit or impossible total.";
|
|
56
|
+
readonly across: "Across";
|
|
57
|
+
readonly down: "Down";
|
|
58
|
+
readonly pencil: "Pencil marks";
|
|
59
|
+
readonly row: "Row";
|
|
60
|
+
readonly column: "column";
|
|
61
|
+
readonly blank: "blank";
|
|
62
|
+
readonly clear: "Clear";
|
|
63
|
+
} | {
|
|
64
|
+
readonly title: "カックロ";
|
|
65
|
+
readonly help: "白マスに1〜9を入れます。横または縦の連続した数字はヒントの合計になり、同じ数字は使えません。矢印または数字パッドで入力、Pでメモを切り替えます。";
|
|
66
|
+
readonly undo: "戻す";
|
|
67
|
+
readonly hint: "ヒント";
|
|
68
|
+
readonly check: "確認";
|
|
69
|
+
readonly restart: "やり直す";
|
|
70
|
+
readonly ready: "白マスを選んでください。";
|
|
71
|
+
readonly won: "すべての合計が合いました。完成!";
|
|
72
|
+
readonly helped: "ヒントを使って完成!";
|
|
73
|
+
readonly checked: "ここまでの合計に誤りはありません。続けてください。";
|
|
74
|
+
readonly wrong: "同じ数字または合計の誤りがあります。";
|
|
75
|
+
readonly across: "横";
|
|
76
|
+
readonly down: "縦";
|
|
77
|
+
readonly pencil: "メモ";
|
|
78
|
+
readonly row: "行";
|
|
79
|
+
readonly column: "列";
|
|
80
|
+
readonly blank: "空白";
|
|
81
|
+
readonly clear: "消す";
|
|
82
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const KAKURO_STRINGS = {
|
|
2
|
+
en: { title: "Kakuro", help: "Fill each white cell with 1–9. Digits in each across or down run add to its clue and cannot repeat. Arrows move; type a digit or use the number pad; P toggles pencil marks.", undo: "Undo", hint: "Hint", check: "Check", restart: "Restart", ready: "Choose a white cell.", won: "Every run is correct. Solved!", helped: "Solved with help.", checked: "No run errors so far. Keep filling.", wrong: "Some runs have a repeated digit or impossible total.", across: "Across", down: "Down", pencil: "Pencil marks", row: "Row", column: "column", blank: "blank", clear: "Clear" },
|
|
3
|
+
ja: { title: "カックロ", help: "白マスに1〜9を入れます。横または縦の連続した数字はヒントの合計になり、同じ数字は使えません。矢印または数字パッドで入力、Pでメモを切り替えます。", undo: "戻す", hint: "ヒント", check: "確認", restart: "やり直す", ready: "白マスを選んでください。", won: "すべての合計が合いました。完成!", helped: "ヒントを使って完成!", checked: "ここまでの合計に誤りはありません。続けてください。", wrong: "同じ数字または合計の誤りがあります。", across: "横", down: "縦", pencil: "メモ", row: "行", column: "列", blank: "空白", clear: "消す" }
|
|
4
|
+
};
|
|
5
|
+
export function kakuroWords(language = "en") { return KAKURO_STRINGS[language]; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const KAKURO_PLAY_STYLE = ".kk-root{max-width:760px;margin:auto;color:var(--ink,#1f2320);font:16px/1.5 system-ui,sans-serif}.kk-board{position:relative;width:100%;touch-action:manipulation}.kk-cells{position:absolute;inset:2px;display:grid}.kk-cell{border:0;background:transparent;min-width:0;min-height:0;padding:0;touch-action:manipulation}.kk-cell:focus-visible,.kk-pad button:focus-visible{outline:3px solid #b5452c;outline-offset:-3px}.kk-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}.kk-tools button,.kk-pad button{font:inherit;min-height:44px;padding:0 14px;border-radius:999px;border:1px solid #ddd6c6;background:#fbf8f1;color:inherit}.kk-pad{display:grid;grid-template-columns:repeat(5,minmax(44px,1fr));gap:8px;margin:12px 0}.kk-pad button{padding:0;touch-action:manipulation}.kk-status{min-height:1.5em}.kk-help{opacity:.8;font-size:14px}";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const KAKURO_PLAY_STYLE = `.kk-root{max-width:760px;margin:auto;color:var(--ink,#1f2320);font:16px/1.5 system-ui,sans-serif}.kk-board{position:relative;width:100%;touch-action:manipulation}.kk-cells{position:absolute;inset:2px;display:grid}.kk-cell{border:0;background:transparent;min-width:0;min-height:0;padding:0;touch-action:manipulation}.kk-cell:focus-visible,.kk-pad button:focus-visible{outline:3px solid #b5452c;outline-offset:-3px}.kk-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}.kk-tools button,.kk-pad button{font:inherit;min-height:44px;padding:0 14px;border-radius:999px;border:1px solid #ddd6c6;background:#fbf8f1;color:inherit}.kk-pad{display:grid;grid-template-columns:repeat(5,minmax(44px,1fr));gap:8px;margin:12px 0}.kk-pad button{padding:0;touch-action:manipulation}.kk-status{min-height:1.5em}.kk-help{opacity:.8;font-size:14px}`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { KakuroPuzzle } from "./kakuro.types.ts";
|
|
2
|
+
/** The first generator, kept as the fallback that cannot fail on a 10×10: a seeded family of crossing runs, returned only when uniquely proved. */
|
|
3
|
+
export declare function templateKakuro(seed?: number): Omit<KakuroPuzzle, "level">;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { KAKURO_GENERATION_ATTEMPTS, KAKURO_GENERATION_NODES, KAKURO_MAX_ATTEMPTS, KAKURO_MAX_NODES } from "./kakuro.constants.js";
|
|
2
|
+
import { isKakuroBoard } from "./kakuroBoard.js";
|
|
3
|
+
import { solveKakuro } from "./kakuroSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/** The first generator, kept as the fallback that cannot fail on a 10×10: a seeded family of crossing runs, returned only when uniquely proved. */
|
|
6
|
+
export function templateKakuro(seed = 1) {
|
|
7
|
+
if (!isKazuSeed(seed))
|
|
8
|
+
throw new RangeError("Invalid Kakuro seed.");
|
|
9
|
+
const random = seededRandom(seed);
|
|
10
|
+
const budget = { attempts: KAKURO_GENERATION_ATTEMPTS, nodes: KAKURO_GENERATION_NODES };
|
|
11
|
+
for (let attempt = 0; attempt < KAKURO_MAX_ATTEMPTS && budget.attempts > 0 && budget.nodes > 0; attempt += 1) {
|
|
12
|
+
budget.attempts -= 1;
|
|
13
|
+
const rowGroups = random() < .5 ? [2, 2, 2] : random() < .5 ? [2, 3] : [3, 2];
|
|
14
|
+
const columnGroups = rowGroups.length === 3 ? [2, 2, 2] : [2, 3];
|
|
15
|
+
if (random() < .5)
|
|
16
|
+
columnGroups.reverse();
|
|
17
|
+
const rowStarts = groupStarts(rowGroups, random);
|
|
18
|
+
const columnStarts = groupStarts(columnGroups, random);
|
|
19
|
+
const cells = Array.from({ length: 100 }, () => ({ kind: "black", across: null, down: null }));
|
|
20
|
+
const solution = Array(100).fill(0);
|
|
21
|
+
const board = { width: 10, height: 10, cells };
|
|
22
|
+
let failed = false;
|
|
23
|
+
for (const [rowIndex, rowStart] of rowStarts.entries()) {
|
|
24
|
+
for (const [columnIndex, columnStart] of columnStarts.entries()) {
|
|
25
|
+
const shape = { height: rowGroups[rowIndex], width: columnGroups[columnIndex] };
|
|
26
|
+
const local = chooseUniqueTile(shape.width, shape.height, random, budget);
|
|
27
|
+
if (!local) {
|
|
28
|
+
failed = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
placeTile(board, solution, rowStart, columnStart, local);
|
|
32
|
+
}
|
|
33
|
+
if (failed)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (failed)
|
|
37
|
+
continue;
|
|
38
|
+
if (!isKakuroBoard(board))
|
|
39
|
+
throw new Error(JSON.stringify(board));
|
|
40
|
+
const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(KAKURO_MAX_NODES, budget.nodes) });
|
|
41
|
+
budget.nodes -= Math.min(budget.nodes, proof.nodes);
|
|
42
|
+
if (proof.complete && proof.count === 1 && proof.solution) {
|
|
43
|
+
return { ...board, seed, solution: proof.solution };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error("No uniquely solvable Kakuro board was proved within the generation budget.");
|
|
47
|
+
}
|
|
48
|
+
function groupStarts(groups, random) {
|
|
49
|
+
const used = groups.reduce((sum, size) => sum + size, 0) + groups.length - 1;
|
|
50
|
+
let at = 1 + (8 - used > 0 && random() < .5 ? 8 - used : 0);
|
|
51
|
+
return groups.map(size => { const start = at; at += size + 1; return start; });
|
|
52
|
+
}
|
|
53
|
+
function chooseUniqueTile(width, height, random, budget) {
|
|
54
|
+
for (let attempt = 0; attempt < KAKURO_MAX_ATTEMPTS && budget.attempts > 0 && budget.nodes > 0; attempt += 1) {
|
|
55
|
+
budget.attempts -= 1;
|
|
56
|
+
const candidate = randomRectangle(width, height, random);
|
|
57
|
+
if (!candidate)
|
|
58
|
+
continue;
|
|
59
|
+
const board = tileBoard(candidate);
|
|
60
|
+
const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(25000, budget.nodes) });
|
|
61
|
+
budget.nodes -= Math.min(budget.nodes, proof.nodes);
|
|
62
|
+
if (!budget.nodes)
|
|
63
|
+
return null;
|
|
64
|
+
if (proof.complete && proof.count === 1)
|
|
65
|
+
return proof.solution ? splitSolution(proof.solution, width, height) : candidate;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function randomRectangle(width, height, random) {
|
|
70
|
+
const rows = [];
|
|
71
|
+
for (let y = 0; y < height; y += 1) {
|
|
72
|
+
let row = null;
|
|
73
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
74
|
+
const available = shuffled([1, 2, 3, 4, 5, 6, 7, 8, 9], random);
|
|
75
|
+
const candidate = available.slice(0, width);
|
|
76
|
+
if (candidate.every((digit, x) => rows.every(previous => previous[x] !== digit))) {
|
|
77
|
+
row = candidate;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!row)
|
|
82
|
+
return null;
|
|
83
|
+
rows.push(row);
|
|
84
|
+
}
|
|
85
|
+
return rows;
|
|
86
|
+
}
|
|
87
|
+
function tileBoard(solution) {
|
|
88
|
+
const height = solution.length + 1, width = solution[0].length + 1;
|
|
89
|
+
const cells = Array.from({ length: width * height }, () => ({ kind: "black", across: null, down: null }));
|
|
90
|
+
for (let y = 0; y < solution.length; y += 1)
|
|
91
|
+
for (let x = 0; x < solution[y].length; x += 1)
|
|
92
|
+
cells[(y + 1) * width + x + 1] = { kind: "white" };
|
|
93
|
+
for (let y = 0; y < solution.length; y += 1)
|
|
94
|
+
cells[(y + 1) * width].across = solution[y].reduce((sum, value) => sum + value, 0);
|
|
95
|
+
for (let x = 0; x < solution[0].length; x += 1)
|
|
96
|
+
cells[x + 1].down = solution.reduce((sum, row) => sum + row[x], 0);
|
|
97
|
+
return { width, height, cells };
|
|
98
|
+
}
|
|
99
|
+
function placeTile(board, full, top, left, solution) {
|
|
100
|
+
const width = board.width;
|
|
101
|
+
const cells = board.cells;
|
|
102
|
+
for (let y = 0; y < solution.length; y += 1)
|
|
103
|
+
for (let x = 0; x < solution[y].length; x += 1)
|
|
104
|
+
full[(top + y) * width + left + x] = solution[y][x];
|
|
105
|
+
for (let y = 0; y < solution.length; y += 1) {
|
|
106
|
+
const clue = (top + y) * width + left - 1;
|
|
107
|
+
cells[clue].across = solution[y].reduce((sum, value) => sum + value, 0);
|
|
108
|
+
}
|
|
109
|
+
for (let x = 0; x < solution[0].length; x += 1) {
|
|
110
|
+
const clue = (top - 1) * width + left + x;
|
|
111
|
+
cells[clue].down = solution.reduce((sum, row) => sum + row[x], 0);
|
|
112
|
+
}
|
|
113
|
+
for (let y = 0; y < solution.length; y += 1)
|
|
114
|
+
for (let x = 0; x < solution[y].length; x += 1)
|
|
115
|
+
cells[(top + y) * width + left + x] = { kind: "white" };
|
|
116
|
+
}
|
|
117
|
+
function splitSolution(values, width, height) {
|
|
118
|
+
const result = [];
|
|
119
|
+
for (let y = 1; y <= height; y += 1)
|
|
120
|
+
result.push(values.slice(y * (width + 1) + 1, y * (width + 1) + width + 1));
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function shuffled(items, random) {
|
|
124
|
+
const result = [...items];
|
|
125
|
+
for (let i = result.length - 1; i > 0; i -= 1) {
|
|
126
|
+
const j = Math.floor(random() * (i + 1));
|
|
127
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawMasyu } from "./masyuDraw.js";
|