@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
package/dist/csp.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE SMALL ENGINE for the puzzles that are solved by narrowing down what each square, edge or rectangle can
|
|
3
|
+
* still be. A puzzle supplies its variables and one function that prunes; this file does the two things
|
|
4
|
+
* every one of them needs: counting answers (a search that stops at a limit) and rating how hard a puzzle
|
|
5
|
+
* is (how deep the reasoning must go before the grid is decided).
|
|
6
|
+
*
|
|
7
|
+
* A variable has a few slots (its possible values). `alive[slot]` is 1 while that value is still possible.
|
|
8
|
+
* A variable is decided when exactly one of its slots is alive and impossible when none is.
|
|
9
|
+
*/
|
|
10
|
+
/** How many values a variable still has. */
|
|
11
|
+
export function slotsLeft(csp, alive, variable) {
|
|
12
|
+
let left = 0;
|
|
13
|
+
for (let slot = csp.starts[variable]; slot < csp.starts[variable + 1]; slot += 1)
|
|
14
|
+
left += alive[slot];
|
|
15
|
+
return left;
|
|
16
|
+
}
|
|
17
|
+
/** Every value of every variable still possible. */
|
|
18
|
+
export function openSlots(csp) {
|
|
19
|
+
return new Uint8Array(csp.starts[csp.starts.length - 1]).fill(1);
|
|
20
|
+
}
|
|
21
|
+
/** Leaves `variable` with only `slot` (a slot number, not an offset). */
|
|
22
|
+
export function decide(csp, alive, variable, slot) {
|
|
23
|
+
for (let at = csp.starts[variable]; at < csp.starts[variable + 1]; at += 1)
|
|
24
|
+
alive[at] = at === slot ? 1 : 0;
|
|
25
|
+
}
|
|
26
|
+
/** The slot a decided variable has left, or -1 when it has more than one or none. */
|
|
27
|
+
export function decidedSlot(csp, alive, variable) {
|
|
28
|
+
let found = -1;
|
|
29
|
+
for (let slot = csp.starts[variable]; slot < csp.starts[variable + 1]; slot += 1) {
|
|
30
|
+
if (!alive[slot])
|
|
31
|
+
continue;
|
|
32
|
+
if (found >= 0)
|
|
33
|
+
return -1;
|
|
34
|
+
found = slot;
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Counts the ways to decide every variable, up to `limit`. `exhausted` says the node budget ran out;
|
|
40
|
+
* `stopped` says the limit was reached. Either way the count must not be read as proof of anything
|
|
41
|
+
* beyond what was seen.
|
|
42
|
+
*/
|
|
43
|
+
export function countCsp(csp, start, limit, budget) {
|
|
44
|
+
const variables = csp.starts.length - 1;
|
|
45
|
+
let count = 0, nodes = 0, exhausted = false, stopped = false;
|
|
46
|
+
let solution = null;
|
|
47
|
+
const solutions = [];
|
|
48
|
+
const visit = (alive, hint) => {
|
|
49
|
+
if (exhausted || stopped)
|
|
50
|
+
return;
|
|
51
|
+
if (++nodes > budget) {
|
|
52
|
+
exhausted = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!csp.propagate(alive, hint))
|
|
56
|
+
return;
|
|
57
|
+
let pick = csp.choose ? csp.choose(alive) : -1, best = 99999;
|
|
58
|
+
if (!csp.choose) {
|
|
59
|
+
for (let v = 0; v < variables; v += 1) {
|
|
60
|
+
const left = slotsLeft(csp, alive, v);
|
|
61
|
+
if (left > 1 && left < best) {
|
|
62
|
+
best = left;
|
|
63
|
+
pick = v;
|
|
64
|
+
if (left === 2)
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (pick < 0) {
|
|
70
|
+
count += 1;
|
|
71
|
+
solution ?? (solution = alive.slice());
|
|
72
|
+
solutions.push(alive.slice());
|
|
73
|
+
if (count >= limit)
|
|
74
|
+
stopped = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (let slot = csp.starts[pick]; slot < csp.starts[pick + 1]; slot += 1) {
|
|
78
|
+
if (!alive[slot])
|
|
79
|
+
continue;
|
|
80
|
+
const next = alive.slice();
|
|
81
|
+
decide(csp, next, pick, slot);
|
|
82
|
+
visit(next, pick);
|
|
83
|
+
if (exhausted || stopped)
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
visit(start.slice());
|
|
88
|
+
return { count, solution, solutions, stopped, exhausted, nodes: Math.min(nodes, budget) };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Solves by reasoning alone, never by guessing a value and keeping it. `depth` 0 is the rules' own
|
|
92
|
+
* pruning. Depth 1 adds probing: suppose one value, prune, and when that breaks a rule the value is
|
|
93
|
+
* ruled out. Depth 2 lets a probe probe. `probes` counts the values ruled out by supposing, which is how
|
|
94
|
+
* much supposing the puzzle asks of a person.
|
|
95
|
+
*/
|
|
96
|
+
export function logicCsp(csp, start, depth) {
|
|
97
|
+
const variables = csp.starts.length - 1;
|
|
98
|
+
let probes = 0;
|
|
99
|
+
const decidedAll = (alive) => {
|
|
100
|
+
for (let v = 0; v < variables; v += 1)
|
|
101
|
+
if (slotsLeft(csp, alive, v) > 1)
|
|
102
|
+
return false;
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
const run = (alive, level, hint) => {
|
|
106
|
+
let first = hint;
|
|
107
|
+
for (;;) {
|
|
108
|
+
if (!csp.propagate(alive, first))
|
|
109
|
+
return false;
|
|
110
|
+
first = undefined;
|
|
111
|
+
let open = false;
|
|
112
|
+
for (let v = 0; v < variables && !open; v += 1)
|
|
113
|
+
if (slotsLeft(csp, alive, v) > 1)
|
|
114
|
+
open = true;
|
|
115
|
+
if (!open)
|
|
116
|
+
return "solved";
|
|
117
|
+
if (level === 0)
|
|
118
|
+
return "stuck";
|
|
119
|
+
let moved = false;
|
|
120
|
+
for (let v = 0; v < variables; v += 1) {
|
|
121
|
+
if (slotsLeft(csp, alive, v) < 2)
|
|
122
|
+
continue;
|
|
123
|
+
for (let slot = csp.starts[v]; slot < csp.starts[v + 1]; slot += 1) {
|
|
124
|
+
if (!alive[slot] || slotsLeft(csp, alive, v) < 2)
|
|
125
|
+
continue;
|
|
126
|
+
const trial = alive.slice();
|
|
127
|
+
decide(csp, trial, v, slot);
|
|
128
|
+
const result = run(trial, level - 1, v);
|
|
129
|
+
if (result === false) {
|
|
130
|
+
alive[slot] = 0;
|
|
131
|
+
probes += 1;
|
|
132
|
+
moved = true;
|
|
133
|
+
if (!csp.propagate(alive))
|
|
134
|
+
return false;
|
|
135
|
+
if (decidedAll(alive))
|
|
136
|
+
return "solved";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!moved)
|
|
141
|
+
return "stuck";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const alive = start.slice();
|
|
145
|
+
const result = run(alive, depth);
|
|
146
|
+
return { solved: result === "solved", contradiction: result === false, probes, alive };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Counts answers like `countCsp`, but first tries to settle the puzzle by reasoning (rules, then one level of
|
|
150
|
+
* supposing): reasoning that leaves every variable decided proves exactly one answer, and reasoning that breaks a
|
|
151
|
+
* rule proves none, in far fewer steps than a search. Only a puzzle that reasoning leaves open is searched, from the
|
|
152
|
+
* state reasoning reached.
|
|
153
|
+
*/
|
|
154
|
+
export function solveCsp(csp, start, limit, budget, depth = 1) {
|
|
155
|
+
const plain = logicCsp(csp, start, 0);
|
|
156
|
+
const reasoned = plain.solved || plain.contradiction || depth < 1 ? plain : logicCsp(csp, start, 1);
|
|
157
|
+
if (reasoned.contradiction)
|
|
158
|
+
return { count: 0, solution: null, solutions: [], stopped: false, exhausted: false, nodes: 0 };
|
|
159
|
+
if (reasoned.solved)
|
|
160
|
+
return { count: 1, solution: reasoned.alive, solutions: [reasoned.alive], stopped: limit <= 1, exhausted: false, nodes: 0 };
|
|
161
|
+
return countCsp(csp, reasoned.alive, limit, budget);
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawFillomino } from "./fillominoDraw.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./fillomino.types.ts";
|
|
2
|
+
export * from "./fillomino.constants.ts";
|
|
3
|
+
export * from "./fillominoBoard.ts";
|
|
4
|
+
export * from "./fillominoSolve.ts";
|
|
5
|
+
export * from "./fillominoGenerate.ts";
|
|
6
|
+
export * from "./fillominoRate.ts";
|
|
7
|
+
export * from "./fillominoGame.ts";
|
|
8
|
+
export { drawFillomino } from "./fillominoDraw.ts";
|
|
9
|
+
export type { FillominoDrawOptions } from "./fillominoPlay.types.ts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./fillomino.types.js";
|
|
2
|
+
export * from "./fillomino.constants.js";
|
|
3
|
+
export * from "./fillominoBoard.js";
|
|
4
|
+
export * from "./fillominoSolve.js";
|
|
5
|
+
export * from "./fillominoGenerate.js";
|
|
6
|
+
export * from "./fillominoRate.js";
|
|
7
|
+
export * from "./fillominoGame.js";
|
|
8
|
+
export { drawFillomino } from "./fillominoDraw.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const FILLOMINO_MOST_SIDE = 12;
|
|
2
|
+
export declare const FILLOMINO_GENERATOR_MOST_CELLS = 144;
|
|
3
|
+
export declare const FILLOMINO_MOST_NODES = 80000;
|
|
4
|
+
export declare const FILLOMINO_MOST_ATTEMPTS = 120;
|
|
5
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateFillomino`. */
|
|
6
|
+
export declare const FILLOMINO_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
|
|
7
|
+
/** The sides on offer for a square board; any side from 4 to `FILLOMINO_MOST_SIDE` can be made. */
|
|
8
|
+
export declare const FILLOMINO_SIZES: readonly [6, 8, 10, 12];
|
|
9
|
+
export declare const FILLOMINO_LEAST_GENERATED_SIDE = 4;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const FILLOMINO_MOST_SIDE = 12;
|
|
2
|
+
export const FILLOMINO_GENERATOR_MOST_CELLS = 144;
|
|
3
|
+
export const FILLOMINO_MOST_NODES = 80000;
|
|
4
|
+
export const FILLOMINO_MOST_ATTEMPTS = 120;
|
|
5
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateFillomino`. */
|
|
6
|
+
export const FILLOMINO_LEVELS = ["easy", "medium", "hard", "extra-hard"];
|
|
7
|
+
/** The sides on offer for a square board; any side from 4 to `FILLOMINO_MOST_SIDE` can be made. */
|
|
8
|
+
export const FILLOMINO_SIZES = [6, 8, 10, 12];
|
|
9
|
+
export const FILLOMINO_LEAST_GENERATED_SIDE = 4;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type FillominoBoard = {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
givens: readonly number[];
|
|
5
|
+
};
|
|
6
|
+
export type FillominoPuzzle = FillominoBoard & {
|
|
7
|
+
seed: number;
|
|
8
|
+
level: FillominoLevel;
|
|
9
|
+
solution: readonly number[];
|
|
10
|
+
};
|
|
11
|
+
export type FillominoLevel = "easy" | "medium" | "hard" | "extra-hard";
|
|
12
|
+
export type FillominoCheck = {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
complete: boolean;
|
|
15
|
+
filled: number;
|
|
16
|
+
regions: number;
|
|
17
|
+
errors: readonly number[];
|
|
18
|
+
};
|
|
19
|
+
export type FillominoSolve = {
|
|
20
|
+
count: number;
|
|
21
|
+
solution: readonly number[] | null;
|
|
22
|
+
complete: boolean;
|
|
23
|
+
nodes: number;
|
|
24
|
+
};
|
|
25
|
+
export type FillominoGame = {
|
|
26
|
+
board: FillominoBoard;
|
|
27
|
+
entries: readonly number[];
|
|
28
|
+
history: readonly (readonly number[])[];
|
|
29
|
+
helped: boolean;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* How hard a board is, measured by solving it. `depth` 0 means the rules solve it, 1 that somebody has to suppose a
|
|
33
|
+
* number in a square and watch it break, 2 that more than that is needed; `probes` is how many suppositions depth 1
|
|
34
|
+
* needed. The rest describes the board.
|
|
35
|
+
*/
|
|
36
|
+
export type FillominoRating = {
|
|
37
|
+
depth: 0 | 1 | 2;
|
|
38
|
+
probes: number;
|
|
39
|
+
givens: number;
|
|
40
|
+
/** Squares that are given, as a share of the board. */
|
|
41
|
+
givenShare: number;
|
|
42
|
+
regions: number;
|
|
43
|
+
/** Regions with no given in them: they are found only by what is round them. */
|
|
44
|
+
unnamed: number;
|
|
45
|
+
largest: number;
|
|
46
|
+
meanRegion: number;
|
|
47
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FillominoBoard, FillominoCheck } from "./fillomino.types.ts";
|
|
2
|
+
export declare function isFillominoBoard(value: unknown): value is FillominoBoard;
|
|
3
|
+
export declare function fillominoNeighbours(board: FillominoBoard, cell: number): number[];
|
|
4
|
+
export declare function fillominoRegions(board: FillominoBoard, entries: readonly number[]): number[][];
|
|
5
|
+
/** Checks region connectivity and area without consulting a generated answer. */
|
|
6
|
+
export declare function checkFillomino(board: FillominoBoard, entries: readonly number[]): FillominoCheck;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { FILLOMINO_MOST_SIDE } from "./fillomino.constants.js";
|
|
2
|
+
export function isFillominoBoard(value) {
|
|
3
|
+
if (!value || typeof value !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
const board = value;
|
|
6
|
+
const cells = board.width * board.height;
|
|
7
|
+
return Number.isInteger(board.width) && board.width >= 2 && board.width <= FILLOMINO_MOST_SIDE
|
|
8
|
+
&& Number.isInteger(board.height) && board.height >= 2 && board.height <= FILLOMINO_MOST_SIDE
|
|
9
|
+
&& Array.isArray(board.givens) && board.givens.length === cells
|
|
10
|
+
&& board.givens.every(value => Number.isInteger(value) && value >= 0 && value <= cells);
|
|
11
|
+
}
|
|
12
|
+
export function fillominoNeighbours(board, cell) {
|
|
13
|
+
const x = cell % board.width;
|
|
14
|
+
const y = Math.floor(cell / board.width);
|
|
15
|
+
const neighbours = [];
|
|
16
|
+
if (x > 0)
|
|
17
|
+
neighbours.push(cell - 1);
|
|
18
|
+
if (x + 1 < board.width)
|
|
19
|
+
neighbours.push(cell + 1);
|
|
20
|
+
if (y > 0)
|
|
21
|
+
neighbours.push(cell - board.width);
|
|
22
|
+
if (y + 1 < board.height)
|
|
23
|
+
neighbours.push(cell + board.width);
|
|
24
|
+
return neighbours;
|
|
25
|
+
}
|
|
26
|
+
export function fillominoRegions(board, entries) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const regions = [];
|
|
29
|
+
for (let start = 0; start < entries.length; start += 1) {
|
|
30
|
+
if (!entries[start] || seen.has(start))
|
|
31
|
+
continue;
|
|
32
|
+
const value = entries[start];
|
|
33
|
+
const region = [];
|
|
34
|
+
const pending = [start];
|
|
35
|
+
seen.add(start);
|
|
36
|
+
while (pending.length) {
|
|
37
|
+
const cell = pending.pop();
|
|
38
|
+
region.push(cell);
|
|
39
|
+
for (const next of fillominoNeighbours(board, cell)) {
|
|
40
|
+
if (!seen.has(next) && entries[next] === value) {
|
|
41
|
+
seen.add(next);
|
|
42
|
+
pending.push(next);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
regions.push(region);
|
|
47
|
+
}
|
|
48
|
+
return regions;
|
|
49
|
+
}
|
|
50
|
+
/** Checks region connectivity and area without consulting a generated answer. */
|
|
51
|
+
export function checkFillomino(board, entries) {
|
|
52
|
+
if (!isFillominoBoard(board))
|
|
53
|
+
throw new RangeError("Invalid Fillomino board");
|
|
54
|
+
if (!Array.isArray(entries) || entries.length !== board.givens.length)
|
|
55
|
+
throw new RangeError("Invalid Fillomino entries");
|
|
56
|
+
const errors = new Set();
|
|
57
|
+
let filled = 0;
|
|
58
|
+
entries.forEach((value, cell) => {
|
|
59
|
+
if (!Number.isInteger(value) || value < 0 || value > entries.length) {
|
|
60
|
+
errors.add(cell);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (value > 0)
|
|
64
|
+
filled += 1;
|
|
65
|
+
if (board.givens[cell] && value !== board.givens[cell])
|
|
66
|
+
errors.add(cell);
|
|
67
|
+
});
|
|
68
|
+
const regions = fillominoRegions(board, entries);
|
|
69
|
+
for (const region of regions) {
|
|
70
|
+
const number = entries[region[0]];
|
|
71
|
+
if (region.length > number)
|
|
72
|
+
region.forEach(cell => errors.add(cell));
|
|
73
|
+
}
|
|
74
|
+
const componentErrors = errors.size > 0;
|
|
75
|
+
const complete = filled === entries.length;
|
|
76
|
+
const validAreas = regions.every(region => region.length === entries[region[0]]);
|
|
77
|
+
return {
|
|
78
|
+
ok: complete && !componentErrors && validAreas,
|
|
79
|
+
complete,
|
|
80
|
+
filled,
|
|
81
|
+
regions: regions.length,
|
|
82
|
+
errors: [...errors].sort((a, b) => a - b),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FillominoLevel } from "./fillomino.types.ts";
|
|
2
|
+
import type { Random } from "./random.ts";
|
|
3
|
+
/** How likely each region size is, by level: a small board of small regions is easy, long ones with few givens are not. */
|
|
4
|
+
export declare const FILLOMINO_SIZES_BY_LEVEL: Record<FillominoLevel, readonly number[]>;
|
|
5
|
+
/**
|
|
6
|
+
* A full Fillomino answer: the board cut into connected regions, every region holding its own size, and no two
|
|
7
|
+
* regions of the same size touching. Regions are grown one at a time from the square with the fewest free neighbours;
|
|
8
|
+
* null when a corner of the board cannot be finished, so the caller tries again.
|
|
9
|
+
*/
|
|
10
|
+
export declare function partitionFillomino(width: number, height: number, level: FillominoLevel, random: Random): number[] | null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fillominoNeighbours } from "./fillominoBoard.js";
|
|
2
|
+
import { shuffled } from "./random.js";
|
|
3
|
+
/** How likely each region size is, by level: a small board of small regions is easy, long ones with few givens are not. */
|
|
4
|
+
export const FILLOMINO_SIZES_BY_LEVEL = {
|
|
5
|
+
easy: [1, 2, 2, 2, 3, 3, 3, 4, 4],
|
|
6
|
+
medium: [1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 6],
|
|
7
|
+
hard: [2, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 7, 8],
|
|
8
|
+
"extra-hard": [2, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9],
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A full Fillomino answer: the board cut into connected regions, every region holding its own size, and no two
|
|
12
|
+
* regions of the same size touching. Regions are grown one at a time from the square with the fewest free neighbours;
|
|
13
|
+
* null when a corner of the board cannot be finished, so the caller tries again.
|
|
14
|
+
*/
|
|
15
|
+
export function partitionFillomino(width, height, level, random) {
|
|
16
|
+
const board = { width, height, givens: Array(width * height).fill(0) };
|
|
17
|
+
const entries = Array(width * height).fill(0);
|
|
18
|
+
const sizes = FILLOMINO_SIZES_BY_LEVEL[level];
|
|
19
|
+
for (let guard = 0; guard < width * height; guard += 1) {
|
|
20
|
+
let anchor = -1, fewest = 9, ties = 0;
|
|
21
|
+
for (let cell = 0; cell < entries.length; cell += 1) {
|
|
22
|
+
if (entries[cell])
|
|
23
|
+
continue;
|
|
24
|
+
const free = fillominoNeighbours(board, cell).filter(next => !entries[next]).length;
|
|
25
|
+
if (free < fewest) {
|
|
26
|
+
fewest = free;
|
|
27
|
+
anchor = cell;
|
|
28
|
+
ties = 1;
|
|
29
|
+
}
|
|
30
|
+
else if (free === fewest && random() * ++ties < 1)
|
|
31
|
+
anchor = cell;
|
|
32
|
+
}
|
|
33
|
+
if (anchor < 0)
|
|
34
|
+
return entries;
|
|
35
|
+
const left = entries.filter(value => !value).length;
|
|
36
|
+
let done = false;
|
|
37
|
+
const tried = new Set();
|
|
38
|
+
// Sizes come in the order of a shuffle of the weighted list, so common sizes are tried first more often.
|
|
39
|
+
for (const area of shuffled(sizes, random)) {
|
|
40
|
+
if (tried.has(area) || area > left)
|
|
41
|
+
continue;
|
|
42
|
+
tried.add(area);
|
|
43
|
+
if (fillominoNeighbours(board, anchor).some(next => entries[next] === area))
|
|
44
|
+
continue;
|
|
45
|
+
for (let proposal = 0; proposal < 6 && !done; proposal += 1) {
|
|
46
|
+
const cells = [anchor], members = new Set(cells);
|
|
47
|
+
while (cells.length < area) {
|
|
48
|
+
const frontier = new Set();
|
|
49
|
+
for (const cell of cells)
|
|
50
|
+
for (const next of fillominoNeighbours(board, cell)) {
|
|
51
|
+
if (entries[next] || members.has(next))
|
|
52
|
+
continue;
|
|
53
|
+
if (fillominoNeighbours(board, next).some(other => entries[other] === area))
|
|
54
|
+
continue;
|
|
55
|
+
frontier.add(next);
|
|
56
|
+
}
|
|
57
|
+
if (!frontier.size)
|
|
58
|
+
break;
|
|
59
|
+
// Prefer squares that are hard to reach later: those with few free neighbours.
|
|
60
|
+
const options = [...frontier].sort((a, b) => fillominoNeighbours(board, a).filter(n => !entries[n] && !members.has(n)).length - fillominoNeighbours(board, b).filter(n => !entries[n] && !members.has(n)).length);
|
|
61
|
+
const pick = random() < .55 ? options[0] : options[Math.floor(random() * options.length)];
|
|
62
|
+
cells.push(pick);
|
|
63
|
+
members.add(pick);
|
|
64
|
+
}
|
|
65
|
+
if (cells.length !== area)
|
|
66
|
+
continue;
|
|
67
|
+
for (const cell of cells)
|
|
68
|
+
entries[cell] = area;
|
|
69
|
+
done = true;
|
|
70
|
+
}
|
|
71
|
+
if (done)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (!done)
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isFillominoBoard } from "./fillominoBoard.js";
|
|
2
|
+
import { fillominoWords } from "./fillominoStrings.js";
|
|
3
|
+
export function drawFillomino(board, options = {}) {
|
|
4
|
+
if (!isFillominoBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Fillomino board");
|
|
6
|
+
const words = fillominoWords(options.language);
|
|
7
|
+
const size = 48;
|
|
8
|
+
const [paper, ink, grid] = {
|
|
9
|
+
ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"],
|
|
10
|
+
wood: ["#e5cda6", "#493e2f", "#bca582"],
|
|
11
|
+
slate: ["#262a27", "#ece8dc", "#454a44"],
|
|
12
|
+
}[options.material ?? "ivory"];
|
|
13
|
+
const entries = options.entries ?? board.givens;
|
|
14
|
+
const markedErrors = new Set(options.errors ?? []);
|
|
15
|
+
const cells = board.givens.map((given, cell) => {
|
|
16
|
+
const x = cell % board.width * size;
|
|
17
|
+
const y = Math.floor(cell / board.width) * size;
|
|
18
|
+
const value = entries[cell] ?? 0;
|
|
19
|
+
const active = options.selected === cell;
|
|
20
|
+
const error = markedErrors.has(cell);
|
|
21
|
+
const fill = error ? "var(--kz-bad,#b5452c)" : active ? "var(--kz-focus,#d8bd68)" : "transparent";
|
|
22
|
+
const tile = value && options.pieces === "tiles"
|
|
23
|
+
? `<rect x="${x + 10}" y="${y + 10}" width="28" height="28" rx="6" fill="${paper}" stroke="${grid}"/>`
|
|
24
|
+
: "";
|
|
25
|
+
const text = value
|
|
26
|
+
? `<text x="${x + 24}" y="${y + 25}" dominant-baseline="middle" text-anchor="middle" fill="${ink}" font-size="21" font-weight="${given ? 750 : 500}" font-family="system-ui,sans-serif">${value}</text>`
|
|
27
|
+
: "";
|
|
28
|
+
return `<g data-fillomino-cell="${cell}"><rect x="${x}" y="${y}" width="${size}" height="${size}" fill="${fill}" fill-opacity="${active || error ? ".28" : "1"}" stroke="${grid}"/>${tile}${text}</g>`;
|
|
29
|
+
}).join("");
|
|
30
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 ${board.width * size + 4} ${board.height * size + 4}" role="img" aria-label="${words.title}" style="display:block;width:100%;height:auto;background:${paper}">${cells}<rect width="${board.width * size}" height="${board.height * size}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="2"/></svg>`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FillominoBoard, FillominoGame } from "./fillomino.types.ts";
|
|
2
|
+
export declare function newFillomino(board: FillominoBoard): FillominoGame;
|
|
3
|
+
export declare function setFillominoCell(game: FillominoGame, cell: number, value: number): FillominoGame;
|
|
4
|
+
export declare function undoFillomino(game: FillominoGame): FillominoGame;
|
|
5
|
+
export declare function hintFillomino(game: FillominoGame): {
|
|
6
|
+
cell: number;
|
|
7
|
+
value: number;
|
|
8
|
+
why: string;
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function fillominoFinished(game: FillominoGame): boolean;
|
|
11
|
+
export declare function restartFillomino(game: FillominoGame): FillominoGame;
|
|
12
|
+
export declare function encodeFillomino(game: FillominoGame): string;
|
|
13
|
+
export declare function decodeFillomino(value: string): FillominoGame | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { checkFillomino, isFillominoBoard } from "./fillominoBoard.js";
|
|
2
|
+
import { solveFillomino } from "./fillominoSolve.js";
|
|
3
|
+
export function newFillomino(board) {
|
|
4
|
+
if (!isFillominoBoard(board))
|
|
5
|
+
throw new RangeError("Invalid Fillomino board");
|
|
6
|
+
return {
|
|
7
|
+
board: { width: board.width, height: board.height, givens: [...board.givens] },
|
|
8
|
+
entries: [...board.givens],
|
|
9
|
+
history: [],
|
|
10
|
+
helped: false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function setFillominoCell(game, cell, value) {
|
|
14
|
+
if (!Number.isInteger(cell) || cell < 0 || cell >= game.entries.length
|
|
15
|
+
|| !Number.isInteger(value) || value < 0 || value > game.entries.length
|
|
16
|
+
|| game.board.givens[cell])
|
|
17
|
+
return game;
|
|
18
|
+
if (game.entries[cell] === value)
|
|
19
|
+
return game;
|
|
20
|
+
const entries = [...game.entries];
|
|
21
|
+
entries[cell] = value;
|
|
22
|
+
return { ...game, entries, history: [...game.history, [...game.entries]] };
|
|
23
|
+
}
|
|
24
|
+
export function undoFillomino(game) {
|
|
25
|
+
const previous = game.history.at(-1);
|
|
26
|
+
return previous
|
|
27
|
+
? { ...game, entries: [...previous], history: game.history.slice(0, -1) }
|
|
28
|
+
: game;
|
|
29
|
+
}
|
|
30
|
+
export function hintFillomino(game) {
|
|
31
|
+
const result = solveFillomino(game.board, game.entries);
|
|
32
|
+
if (!result.complete || result.count !== 1 || !result.solution)
|
|
33
|
+
return null;
|
|
34
|
+
const cell = result.solution.findIndex((value, index) => !game.entries[index] && value > 0);
|
|
35
|
+
return cell < 0 ? null : { cell, value: result.solution[cell], why: "unique-completion" };
|
|
36
|
+
}
|
|
37
|
+
export function fillominoFinished(game) {
|
|
38
|
+
return checkFillomino(game.board, game.entries).ok;
|
|
39
|
+
}
|
|
40
|
+
export function restartFillomino(game) {
|
|
41
|
+
return { ...newFillomino(game.board), helped: game.helped };
|
|
42
|
+
}
|
|
43
|
+
export function encodeFillomino(game) {
|
|
44
|
+
return JSON.stringify({ version: 1, board: game.board, entries: game.entries, helped: game.helped });
|
|
45
|
+
}
|
|
46
|
+
export function decodeFillomino(value) {
|
|
47
|
+
try {
|
|
48
|
+
if (value.length > 40000)
|
|
49
|
+
return null;
|
|
50
|
+
const parsed = JSON.parse(value);
|
|
51
|
+
if (parsed.version !== 1 || !isFillominoBoard(parsed.board) || !Array.isArray(parsed.entries)
|
|
52
|
+
|| typeof parsed.helped !== "boolean" || parsed.entries.length !== parsed.board.givens.length) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const entries = parsed.entries;
|
|
56
|
+
if (entries.some(entry => !Number.isInteger(entry) || Number(entry) < 0 || Number(entry) > entries.length))
|
|
57
|
+
return null;
|
|
58
|
+
if (parsed.board.givens.some((given, cell) => given > 0 && entries[cell] !== given))
|
|
59
|
+
return null;
|
|
60
|
+
return { ...newFillomino(parsed.board), entries: [...entries], helped: parsed.helped };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FillominoLevel, FillominoPuzzle } from "./fillomino.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Makes a seeded puzzle and proves its answer is the only one. The board is cut into connected regions, each holding
|
|
4
|
+
* its own size, with no two of one size touching; every square starts given, and givens are taken away for as long as
|
|
5
|
+
* the puzzle can still be solved the way the level asks: easy and medium by the rules alone (easy keeps most givens),
|
|
6
|
+
* hard by supposing one number at a time, extra-hard for as long as the answer stays single. If no board of the level
|
|
7
|
+
* is found within the attempts the next level down is tried, and a puzzle that gives every square is the last resort.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateFillomino(width?: number, height?: number, level?: FillominoLevel, seed?: number): FillominoPuzzle;
|