@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,93 @@
|
|
|
1
|
+
import { FILLOMINO_LEAST_GENERATED_SIDE, FILLOMINO_LEVELS, FILLOMINO_MOST_ATTEMPTS, FILLOMINO_MOST_SIDE } from "./fillomino.constants.js";
|
|
2
|
+
import { checkFillomino } from "./fillominoBoard.js";
|
|
3
|
+
import { partitionFillomino } from "./fillominoBuild.js";
|
|
4
|
+
import { fillominoModel, fillominoMostValue } from "./fillominoLogic.js";
|
|
5
|
+
import { solveFillomino } from "./fillominoSolve.js";
|
|
6
|
+
import { countCsp, decide, logicCsp, openSlots } from "./csp.js";
|
|
7
|
+
import { isKazuSeed, seededRandom, shuffled } from "./random.js";
|
|
8
|
+
/**
|
|
9
|
+
* Makes a seeded puzzle and proves its answer is the only one. The board is cut into connected regions, each holding
|
|
10
|
+
* its own size, with no two of one size touching; every square starts given, and givens are taken away for as long as
|
|
11
|
+
* the puzzle can still be solved the way the level asks: easy and medium by the rules alone (easy keeps most givens),
|
|
12
|
+
* hard by supposing one number at a time, extra-hard for as long as the answer stays single. If no board of the level
|
|
13
|
+
* is found within the attempts the next level down is tried, and a puzzle that gives every square is the last resort.
|
|
14
|
+
*/
|
|
15
|
+
export function generateFillomino(width = 5, height = width, level = "medium", seed = 1) {
|
|
16
|
+
if (![width, height].every(value => Number.isInteger(value) && value >= FILLOMINO_LEAST_GENERATED_SIDE && value <= FILLOMINO_MOST_SIDE)
|
|
17
|
+
|| !FILLOMINO_LEVELS.includes(level) || !isKazuSeed(seed)) {
|
|
18
|
+
throw new RangeError("Fillomino generation supports 4–12 cells per side and a valid level and seed");
|
|
19
|
+
}
|
|
20
|
+
const random = seededRandom(seed);
|
|
21
|
+
for (let aim = FILLOMINO_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
22
|
+
const aimed = FILLOMINO_LEVELS[aim];
|
|
23
|
+
for (let attempt = 0; attempt < FILLOMINO_MOST_ATTEMPTS; attempt += 1) {
|
|
24
|
+
const solution = partitionFillomino(width, height, aimed, random);
|
|
25
|
+
if (!solution)
|
|
26
|
+
continue;
|
|
27
|
+
const board = thin(width, height, solution, aimed, random);
|
|
28
|
+
if (!board)
|
|
29
|
+
continue;
|
|
30
|
+
const proof = solveFillomino(board);
|
|
31
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkFillomino(board, proof.solution).ok) {
|
|
32
|
+
return { ...board, seed, level, solution: proof.solution };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const board = { width, height, givens: [] };
|
|
37
|
+
const answer = partitionFillomino(width, height, "easy", random) ?? Array(width * height).fill(1);
|
|
38
|
+
return { ...board, givens: [...answer], seed, level, solution: answer };
|
|
39
|
+
}
|
|
40
|
+
/** Takes givens away from a full answer while the level's way of solving still works; null when the result is not the level. */
|
|
41
|
+
function thin(width, height, solution, level, random) {
|
|
42
|
+
const givens = [...solution];
|
|
43
|
+
let calls = 0;
|
|
44
|
+
const model = () => {
|
|
45
|
+
const { csp: inner, most } = fillominoModel({ width, height, givens }, givens);
|
|
46
|
+
const csp = { ...inner, propagate: (alive, decided) => { calls += 1; return inner.propagate(alive, decided); } };
|
|
47
|
+
const start = openSlots(csp);
|
|
48
|
+
givens.forEach((value, cell) => { if (value)
|
|
49
|
+
decide(csp, start, cell, cell * most + value - 1); });
|
|
50
|
+
return { csp, start };
|
|
51
|
+
};
|
|
52
|
+
// A region with no given can be as big as the squares with none joined together, which the solver must allow for; keeping
|
|
53
|
+
// those stretches short keeps every proof short, and keeps the board fair: no vast blank area.
|
|
54
|
+
const roomy = () => fillominoMostValue({ width, height, givens }, givens) > Math.max(12, Math.max(...solution));
|
|
55
|
+
const byRules = () => { const { csp, start } = model(); return logicCsp(csp, start, 0).solved; };
|
|
56
|
+
const unique = () => { const { csp, start } = model(); const count = countCsp(csp, start, 2, 500); return !count.exhausted && count.count === 1; };
|
|
57
|
+
const bySupposing = () => { const { csp, start } = model(); return logicCsp(csp, start, 1).solved; };
|
|
58
|
+
const keepAtLeast = level === "easy" ? Math.ceil(solution.length * .5) : 0;
|
|
59
|
+
let left = givens.length;
|
|
60
|
+
const order = shuffled(givens.map((_, i) => i), random);
|
|
61
|
+
// First as many as the rules alone allow.
|
|
62
|
+
for (const cell of order) {
|
|
63
|
+
if (left <= keepAtLeast)
|
|
64
|
+
break;
|
|
65
|
+
const was = givens[cell];
|
|
66
|
+
givens[cell] = 0;
|
|
67
|
+
if (!roomy() && byRules())
|
|
68
|
+
left -= 1;
|
|
69
|
+
else
|
|
70
|
+
givens[cell] = was;
|
|
71
|
+
}
|
|
72
|
+
if (level === "easy" || level === "medium")
|
|
73
|
+
return { width, height, givens };
|
|
74
|
+
// Then, beyond the rules: hard takes the first given that leaves a board solved by supposing, extra-hard keeps going while the answer stays single.
|
|
75
|
+
let taken = 0;
|
|
76
|
+
const from = calls, budget = width * height * 60;
|
|
77
|
+
for (const cell of order) {
|
|
78
|
+
if (!givens[cell] || calls - from > budget)
|
|
79
|
+
continue;
|
|
80
|
+
const was = givens[cell];
|
|
81
|
+
givens[cell] = 0;
|
|
82
|
+
if (!roomy() && unique() && (level === "extra-hard" || bySupposing())) {
|
|
83
|
+
taken += 1;
|
|
84
|
+
if (taken >= (level === "hard" ? 3 : 6))
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
givens[cell] = was;
|
|
89
|
+
}
|
|
90
|
+
if (!taken || byRules())
|
|
91
|
+
return null;
|
|
92
|
+
return { width, height, givens };
|
|
93
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { FillominoBoard } from "./fillomino.types.ts";
|
|
3
|
+
/** The most any square can be: a region holding a given or an entry is that number, and one with none lies among the squares with neither. */
|
|
4
|
+
export declare function fillominoMostValue(board: FillominoBoard, fixed: readonly number[]): number;
|
|
5
|
+
/**
|
|
6
|
+
* Fillomino as variables: one per square, a slot for each number it can be (1 up to `most`). A finished group of
|
|
7
|
+
* equal numbers keeps its neighbours off that number; a group still short must be able to grow, to squares that can
|
|
8
|
+
* still be that number, until it is the right size, and settles on them when there is exactly room; and a square
|
|
9
|
+
* can only be a number if the squares that can still be it, joined up, make a group big enough.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fillominoModel(board: FillominoBoard, fixed: readonly number[], most?: number): {
|
|
12
|
+
csp: Csp;
|
|
13
|
+
most: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { fillominoNeighbours } from "./fillominoBoard.js";
|
|
2
|
+
/** The most any square can be: a region holding a given or an entry is that number, and one with none lies among the squares with neither. */
|
|
3
|
+
export function fillominoMostValue(board, fixed) {
|
|
4
|
+
let most = 1;
|
|
5
|
+
for (const value of fixed)
|
|
6
|
+
if (value > most)
|
|
7
|
+
most = value;
|
|
8
|
+
const seen = new Uint8Array(fixed.length);
|
|
9
|
+
for (let start = 0; start < fixed.length; start += 1) {
|
|
10
|
+
if (fixed[start] || seen[start])
|
|
11
|
+
continue;
|
|
12
|
+
let size = 0;
|
|
13
|
+
const stack = [start];
|
|
14
|
+
seen[start] = 1;
|
|
15
|
+
while (stack.length) {
|
|
16
|
+
const cell = stack.pop();
|
|
17
|
+
size += 1;
|
|
18
|
+
for (const next of fillominoNeighbours(board, cell))
|
|
19
|
+
if (!fixed[next] && !seen[next]) {
|
|
20
|
+
seen[next] = 1;
|
|
21
|
+
stack.push(next);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (size > most)
|
|
25
|
+
most = size;
|
|
26
|
+
}
|
|
27
|
+
return most;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fillomino as variables: one per square, a slot for each number it can be (1 up to `most`). A finished group of
|
|
31
|
+
* equal numbers keeps its neighbours off that number; a group still short must be able to grow, to squares that can
|
|
32
|
+
* still be that number, until it is the right size, and settles on them when there is exactly room; and a square
|
|
33
|
+
* can only be a number if the squares that can still be it, joined up, make a group big enough.
|
|
34
|
+
*/
|
|
35
|
+
export function fillominoModel(board, fixed, most = fillominoMostValue(board, fixed)) {
|
|
36
|
+
const cells = board.width * board.height, values = most;
|
|
37
|
+
const nb = new Int32Array(cells * 4).fill(-1);
|
|
38
|
+
for (let cell = 0; cell < cells; cell += 1)
|
|
39
|
+
fillominoNeighbours(board, cell).forEach((next, k) => { nb[cell * 4 + k] = next; });
|
|
40
|
+
const starts = Int32Array.from({ length: cells + 1 }, (_, i) => i * values);
|
|
41
|
+
const only = new Int32Array(cells), label = new Int32Array(cells), size = new Int32Array(cells + 1), stack = new Int32Array(cells + 1), members = new Int32Array(cells), seen = new Int32Array(cells);
|
|
42
|
+
const dirty = new Uint8Array(values + 1);
|
|
43
|
+
let stamp = 0;
|
|
44
|
+
const propagate = (alive) => {
|
|
45
|
+
let changed = true;
|
|
46
|
+
dirty.fill(1);
|
|
47
|
+
while (changed) {
|
|
48
|
+
changed = false;
|
|
49
|
+
for (let cell = 0; cell < cells; cell += 1) {
|
|
50
|
+
let left = 0, last = 0;
|
|
51
|
+
const base = cell * values;
|
|
52
|
+
for (let v = 0; v < values; v += 1)
|
|
53
|
+
if (alive[base + v]) {
|
|
54
|
+
left += 1;
|
|
55
|
+
last = v + 1;
|
|
56
|
+
}
|
|
57
|
+
if (!left)
|
|
58
|
+
return false;
|
|
59
|
+
only[cell] = left === 1 ? last : 0;
|
|
60
|
+
}
|
|
61
|
+
// Groups of squares already settled on the same number.
|
|
62
|
+
label.fill(0);
|
|
63
|
+
for (let start = 0; start < cells; start += 1) {
|
|
64
|
+
const v = only[start];
|
|
65
|
+
if (!v || label[start])
|
|
66
|
+
continue;
|
|
67
|
+
let count = 0, top = 0;
|
|
68
|
+
stack[top++] = start;
|
|
69
|
+
label[start] = 1;
|
|
70
|
+
while (top) {
|
|
71
|
+
const cell = stack[--top];
|
|
72
|
+
members[count++] = cell;
|
|
73
|
+
for (let k = 0; k < 4; k += 1) {
|
|
74
|
+
const next = nb[cell * 4 + k];
|
|
75
|
+
if (next >= 0 && only[next] === v && !label[next]) {
|
|
76
|
+
label[next] = 1;
|
|
77
|
+
stack[top++] = next;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (count > v)
|
|
82
|
+
return false;
|
|
83
|
+
if (count === v) {
|
|
84
|
+
for (let m = 0; m < count; m += 1)
|
|
85
|
+
for (let k = 0; k < 4; k += 1) {
|
|
86
|
+
const next = nb[members[m] * 4 + k];
|
|
87
|
+
if (next >= 0 && only[next] !== v && alive[next * values + v - 1]) {
|
|
88
|
+
alive[next * values + v - 1] = 0;
|
|
89
|
+
dirty[v] = 1;
|
|
90
|
+
changed = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Short of its size: grow through squares that can still be v.
|
|
96
|
+
stamp += 1;
|
|
97
|
+
let reached = count, frontier = 0, lastFrontier = -1;
|
|
98
|
+
for (let m = 0; m < count; m += 1)
|
|
99
|
+
seen[members[m]] = stamp;
|
|
100
|
+
for (let at = 0; at < reached; at += 1) {
|
|
101
|
+
const cell = members[at];
|
|
102
|
+
for (let k = 0; k < 4; k += 1) {
|
|
103
|
+
const next = nb[cell * 4 + k];
|
|
104
|
+
if (next < 0 || seen[next] === stamp || !alive[next * values + v - 1])
|
|
105
|
+
continue;
|
|
106
|
+
seen[next] = stamp;
|
|
107
|
+
members[reached++] = next;
|
|
108
|
+
if (at < count) {
|
|
109
|
+
frontier += 1;
|
|
110
|
+
lastFrontier = next;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (reached < v)
|
|
115
|
+
return false;
|
|
116
|
+
if (reached === v) {
|
|
117
|
+
for (let m = 0; m < reached; m += 1) {
|
|
118
|
+
const base = members[m] * values;
|
|
119
|
+
for (let other = 0; other < values; other += 1)
|
|
120
|
+
if (other !== v - 1 && alive[base + other]) {
|
|
121
|
+
alive[base + other] = 0;
|
|
122
|
+
dirty[other + 1] = 1;
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (frontier === 1 && !only[lastFrontier]) {
|
|
128
|
+
const base = lastFrontier * values;
|
|
129
|
+
for (let other = 0; other < values; other += 1)
|
|
130
|
+
if (other !== v - 1 && alive[base + other]) {
|
|
131
|
+
alive[base + other] = 0;
|
|
132
|
+
dirty[other + 1] = 1;
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// A number needs room: the squares that can be it, joined up, must make a group at least that big.
|
|
138
|
+
for (let v = 2; v <= values; v += 1) {
|
|
139
|
+
if (!dirty[v])
|
|
140
|
+
continue;
|
|
141
|
+
dirty[v] = 0;
|
|
142
|
+
label.fill(0);
|
|
143
|
+
let components = 0;
|
|
144
|
+
const slot = v - 1;
|
|
145
|
+
for (let start = 0; start < cells; start += 1) {
|
|
146
|
+
if (label[start] || !alive[start * values + slot])
|
|
147
|
+
continue;
|
|
148
|
+
components += 1;
|
|
149
|
+
let count = 0, top = 0;
|
|
150
|
+
stack[top++] = start;
|
|
151
|
+
label[start] = components;
|
|
152
|
+
while (top) {
|
|
153
|
+
const cell = stack[--top];
|
|
154
|
+
count += 1;
|
|
155
|
+
for (let k = 0; k < 4; k += 1) {
|
|
156
|
+
const next = nb[cell * 4 + k];
|
|
157
|
+
if (next >= 0 && !label[next] && alive[next * values + slot]) {
|
|
158
|
+
label[next] = components;
|
|
159
|
+
stack[top++] = next;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
size[components] = count;
|
|
164
|
+
}
|
|
165
|
+
for (let cell = 0; cell < cells; cell += 1) {
|
|
166
|
+
if (label[cell] && size[label[cell]] < v) {
|
|
167
|
+
alive[cell * values + slot] = 0;
|
|
168
|
+
dirty[v] = 1;
|
|
169
|
+
changed = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
return { csp: { starts, propagate }, most: values };
|
|
177
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { checkFillomino } from "./fillominoBoard.js";
|
|
2
|
+
import { fillominoMostValue } from "./fillominoLogic.js";
|
|
3
|
+
import { drawFillomino } from "./fillominoDraw.js";
|
|
4
|
+
import { decodeFillomino, encodeFillomino, hintFillomino, newFillomino, restartFillomino, setFillominoCell, undoFillomino } from "./fillominoGame.js";
|
|
5
|
+
import { FILLOMINO_PLAY_STYLE } from "./fillominoStyle.js";
|
|
6
|
+
import { fillominoWords } from "./fillominoStrings.js";
|
|
7
|
+
export function mountFillomino(host, initial) {
|
|
8
|
+
const document = host.ownerDocument;
|
|
9
|
+
let options = { ...initial };
|
|
10
|
+
let game = restoreGame(initial);
|
|
11
|
+
let selected = null;
|
|
12
|
+
let errors = [];
|
|
13
|
+
let message = "";
|
|
14
|
+
let destroyed = false;
|
|
15
|
+
let lastFinishState = false;
|
|
16
|
+
const style = document.createElement("style");
|
|
17
|
+
style.textContent = FILLOMINO_PLAY_STYLE;
|
|
18
|
+
const root = document.createElement("div");
|
|
19
|
+
root.className = "fillomino-play";
|
|
20
|
+
const status = document.createElement("p");
|
|
21
|
+
status.className = "fillomino-status";
|
|
22
|
+
status.setAttribute("role", "status");
|
|
23
|
+
const board = document.createElement("div");
|
|
24
|
+
board.className = "fillomino-board";
|
|
25
|
+
const controls = document.createElement("div");
|
|
26
|
+
controls.className = "fillomino-controls";
|
|
27
|
+
root.append(status, board, controls);
|
|
28
|
+
host.append(style, root);
|
|
29
|
+
const language = () => options.language ?? "en";
|
|
30
|
+
const copyGame = (state = game) => ({
|
|
31
|
+
board: { ...state.board, givens: [...state.board.givens] },
|
|
32
|
+
entries: [...state.entries],
|
|
33
|
+
history: state.history.map(entries => [...entries]),
|
|
34
|
+
helped: state.helped,
|
|
35
|
+
});
|
|
36
|
+
const emit = () => {
|
|
37
|
+
const snapshot = copyGame();
|
|
38
|
+
options.onChange?.(snapshot);
|
|
39
|
+
host.dispatchEvent(new document.defaultView.CustomEvent("fillomino-change", { detail: snapshot }));
|
|
40
|
+
};
|
|
41
|
+
const button = (label, action, disabled = false) => {
|
|
42
|
+
const control = document.createElement("button");
|
|
43
|
+
control.type = "button";
|
|
44
|
+
control.textContent = label;
|
|
45
|
+
control.disabled = disabled;
|
|
46
|
+
control.addEventListener("click", action);
|
|
47
|
+
controls.append(control);
|
|
48
|
+
};
|
|
49
|
+
const update = () => {
|
|
50
|
+
if (destroyed)
|
|
51
|
+
return;
|
|
52
|
+
const words = fillominoWords(language());
|
|
53
|
+
const checked = checkFillomino(game.board, game.entries);
|
|
54
|
+
status.textContent = message || (checked.ok ? game.helped ? words.helped : words.solved : checked.errors.length ? words.wrong : words.remaining);
|
|
55
|
+
board.replaceChildren();
|
|
56
|
+
board.style.setProperty("--fillomino-width", String(game.board.width));
|
|
57
|
+
board.style.setProperty("--fillomino-height", String(game.board.height));
|
|
58
|
+
const drawing = document.createElement("div");
|
|
59
|
+
drawing.className = "fillomino-drawing";
|
|
60
|
+
drawing.innerHTML = drawFillomino(game.board, {
|
|
61
|
+
entries: game.entries,
|
|
62
|
+
selected,
|
|
63
|
+
errors,
|
|
64
|
+
material: options.material,
|
|
65
|
+
pieces: options.pieces,
|
|
66
|
+
language: language(),
|
|
67
|
+
});
|
|
68
|
+
const cells = document.createElement("div");
|
|
69
|
+
cells.className = "fillomino-cells";
|
|
70
|
+
cells.setAttribute("role", "grid");
|
|
71
|
+
cells.setAttribute("aria-label", words.title);
|
|
72
|
+
for (let cell = 0; cell < game.entries.length; cell += 1) {
|
|
73
|
+
const x = cell % game.board.width;
|
|
74
|
+
const y = Math.floor(cell / game.board.width);
|
|
75
|
+
const given = game.board.givens[cell];
|
|
76
|
+
const value = game.entries[cell];
|
|
77
|
+
const control = document.createElement("button");
|
|
78
|
+
control.type = "button";
|
|
79
|
+
control.className = "fillomino-cell";
|
|
80
|
+
control.dataset.cell = String(cell);
|
|
81
|
+
control.setAttribute("role", "gridcell");
|
|
82
|
+
control.setAttribute("aria-label", `${words.row} ${y + 1}, ${words.column} ${x + 1}, ${value || words.empty}${value ? `, ${words.filled}` : ""}${given ? `, ${words.clue}` : ""}`);
|
|
83
|
+
control.setAttribute("aria-pressed", String(selected === cell));
|
|
84
|
+
control.tabIndex = selected === cell || (selected === null && cell === 0) ? 0 : -1;
|
|
85
|
+
control.addEventListener("click", () => {
|
|
86
|
+
selected = cell;
|
|
87
|
+
message = "";
|
|
88
|
+
update();
|
|
89
|
+
});
|
|
90
|
+
control.addEventListener("keydown", event => moveByKeyboard(event, cell));
|
|
91
|
+
cells.append(control);
|
|
92
|
+
}
|
|
93
|
+
drawing.append(cells);
|
|
94
|
+
board.append(drawing);
|
|
95
|
+
controls.replaceChildren();
|
|
96
|
+
const label = document.createElement("label");
|
|
97
|
+
label.className = "fillomino-number-label";
|
|
98
|
+
label.textContent = words.selectNumber;
|
|
99
|
+
const number = document.createElement("select");
|
|
100
|
+
number.setAttribute("aria-label", words.selectNumber);
|
|
101
|
+
number.dataset.number = "true";
|
|
102
|
+
// Only numbers an answer can hold: more than the biggest given only fits among squares that have no given.
|
|
103
|
+
const most = Math.max(fillominoMostValue(game.board, game.board.givens), ...game.entries);
|
|
104
|
+
for (let value = 1; value <= most; value += 1) {
|
|
105
|
+
const option = document.createElement("option");
|
|
106
|
+
option.value = String(value);
|
|
107
|
+
option.textContent = String(value);
|
|
108
|
+
number.append(option);
|
|
109
|
+
}
|
|
110
|
+
label.append(number);
|
|
111
|
+
controls.append(label);
|
|
112
|
+
button(words.fill, () => applySelected(Number(number.value)), selected === null || Boolean(game.board.givens[selected]));
|
|
113
|
+
button(words.clear, () => applySelected(0), selected === null || Boolean(game.board.givens[selected]));
|
|
114
|
+
button(words.undo, () => {
|
|
115
|
+
game = undoFillomino(game);
|
|
116
|
+
errors = [];
|
|
117
|
+
message = "";
|
|
118
|
+
emit();
|
|
119
|
+
update();
|
|
120
|
+
}, game.history.length === 0);
|
|
121
|
+
button(words.check, checkBoard);
|
|
122
|
+
button(words.hint, giveHint);
|
|
123
|
+
button(words.restart, restart);
|
|
124
|
+
const nowFinished = checked.ok;
|
|
125
|
+
if (nowFinished && !lastFinishState)
|
|
126
|
+
options.onFinish?.(copyGame());
|
|
127
|
+
lastFinishState = nowFinished;
|
|
128
|
+
};
|
|
129
|
+
const applySelected = (value) => {
|
|
130
|
+
if (selected === null)
|
|
131
|
+
return;
|
|
132
|
+
const next = setFillominoCell(game, selected, value);
|
|
133
|
+
if (next === game)
|
|
134
|
+
return;
|
|
135
|
+
game = next;
|
|
136
|
+
errors = [];
|
|
137
|
+
message = "";
|
|
138
|
+
emit();
|
|
139
|
+
update();
|
|
140
|
+
};
|
|
141
|
+
const moveByKeyboard = (event, cell) => {
|
|
142
|
+
const x = cell % game.board.width;
|
|
143
|
+
const y = Math.floor(cell / game.board.width);
|
|
144
|
+
const moves = {
|
|
145
|
+
ArrowUp: y > 0 ? cell - game.board.width : cell,
|
|
146
|
+
ArrowDown: y + 1 < game.board.height ? cell + game.board.width : cell,
|
|
147
|
+
ArrowLeft: x > 0 ? cell - 1 : cell,
|
|
148
|
+
ArrowRight: x + 1 < game.board.width ? cell + 1 : cell,
|
|
149
|
+
};
|
|
150
|
+
if (event.key in moves) {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
selected = moves[event.key];
|
|
153
|
+
update();
|
|
154
|
+
board.querySelector(`button[data-cell="${selected}"]`)?.focus();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (/^[1-9]$/.test(event.key)) {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
selected = cell;
|
|
160
|
+
applySelected(Number(event.key));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
selected = cell;
|
|
166
|
+
applySelected(0);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const checkBoard = () => {
|
|
170
|
+
const checked = checkFillomino(game.board, game.entries);
|
|
171
|
+
errors = checked.errors;
|
|
172
|
+
message = checked.ok ? game.helped ? fillominoWords(language()).helped : fillominoWords(language()).solved
|
|
173
|
+
: checked.errors.length ? fillominoWords(language()).wrong : fillominoWords(language()).checked;
|
|
174
|
+
update();
|
|
175
|
+
};
|
|
176
|
+
const giveHint = () => {
|
|
177
|
+
const hint = hintFillomino(game);
|
|
178
|
+
if (!hint) {
|
|
179
|
+
message = fillominoWords(language()).noHint;
|
|
180
|
+
update();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
game = setFillominoCell(game, hint.cell, hint.value);
|
|
184
|
+
game = { ...game, helped: true };
|
|
185
|
+
selected = hint.cell;
|
|
186
|
+
errors = [];
|
|
187
|
+
message = fillominoWords(language()).hintWhy;
|
|
188
|
+
emit();
|
|
189
|
+
update();
|
|
190
|
+
};
|
|
191
|
+
const restart = () => {
|
|
192
|
+
game = restartFillomino(game);
|
|
193
|
+
selected = null;
|
|
194
|
+
errors = [];
|
|
195
|
+
message = "";
|
|
196
|
+
emit();
|
|
197
|
+
update();
|
|
198
|
+
};
|
|
199
|
+
update();
|
|
200
|
+
return {
|
|
201
|
+
game: () => copyGame(),
|
|
202
|
+
progress: () => encodeFillomino(game),
|
|
203
|
+
set: (next) => { options = { ...options, ...next }; update(); },
|
|
204
|
+
restart,
|
|
205
|
+
destroy: () => { destroyed = true; root.remove(); style.remove(); },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function restoreGame(source) {
|
|
209
|
+
const saved = source.progress ? decodeFillomino(source.progress) : null;
|
|
210
|
+
return saved && saved.board.width === source.board.width && saved.board.height === source.board.height
|
|
211
|
+
&& saved.board.givens.every((given, cell) => given === source.board.givens[cell])
|
|
212
|
+
? saved
|
|
213
|
+
: newFillomino(source.board);
|
|
214
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FillominoBoard, FillominoGame } from "./fillomino.types.ts";
|
|
2
|
+
export type FillominoLanguage = "en" | "ja";
|
|
3
|
+
export type FillominoMaterial = "ivory" | "wood" | "slate";
|
|
4
|
+
export type FillominoPieces = "ink" | "tiles";
|
|
5
|
+
export type FillominoDrawOptions = {
|
|
6
|
+
entries?: readonly number[];
|
|
7
|
+
errors?: readonly number[];
|
|
8
|
+
selected?: number | null;
|
|
9
|
+
material?: FillominoMaterial;
|
|
10
|
+
pieces?: FillominoPieces;
|
|
11
|
+
language?: FillominoLanguage;
|
|
12
|
+
};
|
|
13
|
+
export type FillominoMountOptions = FillominoDrawOptions & {
|
|
14
|
+
board: FillominoBoard;
|
|
15
|
+
progress?: string;
|
|
16
|
+
onChange?: (game: FillominoGame) => void;
|
|
17
|
+
onFinish?: (game: FillominoGame) => void;
|
|
18
|
+
};
|
|
19
|
+
export type FillominoMount = {
|
|
20
|
+
game: () => FillominoGame;
|
|
21
|
+
progress: () => string;
|
|
22
|
+
set: (options: FillominoDrawOptions) => void;
|
|
23
|
+
restart: () => void;
|
|
24
|
+
destroy: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FillominoBoard, FillominoRating } from "./fillomino.types.ts";
|
|
2
|
+
/** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one number at a time. */
|
|
3
|
+
export declare function rateFillomino(board: FillominoBoard): FillominoRating;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isFillominoBoard } from "./fillominoBoard.js";
|
|
2
|
+
import { fillominoModel } from "./fillominoLogic.js";
|
|
3
|
+
import { decide, logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
/** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one number at a time. */
|
|
5
|
+
export function rateFillomino(board) {
|
|
6
|
+
if (!isFillominoBoard(board))
|
|
7
|
+
throw new RangeError("Invalid Fillomino board");
|
|
8
|
+
const { csp, most } = fillominoModel(board, board.givens);
|
|
9
|
+
const start = openSlots(csp);
|
|
10
|
+
board.givens.forEach((value, cell) => { if (value)
|
|
11
|
+
decide(csp, start, cell, cell * most + value - 1); });
|
|
12
|
+
const proof = solveCsp(csp, start, 2, 200000);
|
|
13
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
14
|
+
throw new RangeError("Fillomino rating needs a board with one answer");
|
|
15
|
+
const plain = logicCsp(csp, start, 0);
|
|
16
|
+
const probing = plain.solved ? plain : logicCsp(csp, start, 1);
|
|
17
|
+
const answer = Array.from({ length: board.givens.length }, (_, cell) => { for (let v = 0; v < most; v += 1)
|
|
18
|
+
if (proof.solution[cell * most + v])
|
|
19
|
+
return v + 1; return 0; });
|
|
20
|
+
const regions = new Map();
|
|
21
|
+
const label = Array(answer.length).fill(-1);
|
|
22
|
+
let count = 0, largest = 0, unnamed = 0;
|
|
23
|
+
for (let first = 0; first < answer.length; first += 1) {
|
|
24
|
+
if (label[first] >= 0)
|
|
25
|
+
continue;
|
|
26
|
+
const members = [first];
|
|
27
|
+
label[first] = count;
|
|
28
|
+
for (let at = 0; at < members.length; at += 1) {
|
|
29
|
+
const cell = members[at], x = cell % board.width, y = Math.floor(cell / board.width);
|
|
30
|
+
for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
|
|
31
|
+
if (nx < 0 || ny < 0 || nx >= board.width || ny >= board.height)
|
|
32
|
+
continue;
|
|
33
|
+
const next = ny * board.width + nx;
|
|
34
|
+
if (label[next] < 0 && answer[next] === answer[first]) {
|
|
35
|
+
label[next] = count;
|
|
36
|
+
members.push(next);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
largest = Math.max(largest, members.length);
|
|
41
|
+
if (!members.some(cell => board.givens[cell]))
|
|
42
|
+
unnamed += 1;
|
|
43
|
+
regions.set(String(count), members.length);
|
|
44
|
+
count += 1;
|
|
45
|
+
}
|
|
46
|
+
const givens = board.givens.filter(Boolean).length;
|
|
47
|
+
return {
|
|
48
|
+
depth: plain.solved ? 0 : probing.solved ? 1 : 2,
|
|
49
|
+
probes: plain.solved ? 0 : probing.probes,
|
|
50
|
+
givens,
|
|
51
|
+
givenShare: givens / board.givens.length,
|
|
52
|
+
regions: count,
|
|
53
|
+
unnamed,
|
|
54
|
+
largest,
|
|
55
|
+
meanRegion: board.givens.length / count,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FillominoBoard, FillominoSolve } from "./fillomino.types.ts";
|
|
2
|
+
/** Counts completed partitions. `complete` is false when a bound, including the answer limit, stops search. */
|
|
3
|
+
export declare function solveFillomino(board: FillominoBoard, entries?: readonly number[], options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): FillominoSolve;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { FILLOMINO_MOST_NODES } from "./fillomino.constants.js";
|
|
2
|
+
import { isFillominoBoard } from "./fillominoBoard.js";
|
|
3
|
+
import { fillominoModel } from "./fillominoLogic.js";
|
|
4
|
+
import { decide, openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Counts completed partitions. `complete` is false when a bound, including the answer limit, stops search. */
|
|
6
|
+
export function solveFillomino(board, entries = board.givens, options = {}) {
|
|
7
|
+
if (!isFillominoBoard(board))
|
|
8
|
+
throw new RangeError("Invalid Fillomino board");
|
|
9
|
+
const limit = options.limit ?? 2;
|
|
10
|
+
const budget = options.nodes ?? FILLOMINO_MOST_NODES;
|
|
11
|
+
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1) {
|
|
12
|
+
throw new RangeError("Invalid search bounds");
|
|
13
|
+
}
|
|
14
|
+
if (!Array.isArray(entries) || entries.length !== board.givens.length) {
|
|
15
|
+
throw new RangeError("Invalid Fillomino entries");
|
|
16
|
+
}
|
|
17
|
+
const fixed = entries.map((value, cell) => {
|
|
18
|
+
if (!Number.isInteger(value) || value < 0 || value > entries.length)
|
|
19
|
+
return -1;
|
|
20
|
+
if (board.givens[cell] && value !== board.givens[cell])
|
|
21
|
+
return -1;
|
|
22
|
+
return value || board.givens[cell];
|
|
23
|
+
});
|
|
24
|
+
if (fixed.some(value => value < 0))
|
|
25
|
+
return { count: 0, solution: null, complete: true, nodes: 0 };
|
|
26
|
+
const { csp, most } = fillominoModel(board, fixed);
|
|
27
|
+
const start = openSlots(csp);
|
|
28
|
+
fixed.forEach((value, cell) => { if (value)
|
|
29
|
+
decide(csp, start, cell, cell * most + value - 1); });
|
|
30
|
+
const found = solveCsp(csp, start, limit, budget);
|
|
31
|
+
const solution = found.solution ? Array.from({ length: fixed.length }, (_, cell) => {
|
|
32
|
+
for (let v = 0; v < most; v += 1)
|
|
33
|
+
if (found.solution[cell * most + v])
|
|
34
|
+
return v + 1;
|
|
35
|
+
return 0;
|
|
36
|
+
}) : null;
|
|
37
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
38
|
+
}
|