@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,182 @@
|
|
|
1
|
+
import { SLITHERLINK_LEVELS, SLITHERLINK_MOST_ATTEMPTS, SLITHERLINK_MOST_SIDE } from "./slitherlink.constants.js";
|
|
2
|
+
import { checkSlitherlink, slitherlinkCellEdges } from "./slitherlinkBoard.js";
|
|
3
|
+
import { slitherlinkModel } from "./slitherlinkLogic.js";
|
|
4
|
+
import { solveSlitherlink } from "./slitherlinkSolve.js";
|
|
5
|
+
import { templateSlitherlink } from "./slitherlinkTemplate.js";
|
|
6
|
+
import { countCsp, logicCsp, openSlots } from "./csp.js";
|
|
7
|
+
import { isKazuSeed, seededRandom, shuffled } from "./random.js";
|
|
8
|
+
/**
|
|
9
|
+
* Makes a seeded puzzle and proves it has one answer. A random winding loop is grown cell by cell, every square is
|
|
10
|
+
* numbered with how many of its edges the loop uses, and then numbers are taken away for as long as the puzzle can
|
|
11
|
+
* still be solved the way the level asks: easy and medium by the rules alone (easy keeps most numbers), hard by
|
|
12
|
+
* supposing one edge at a time, extra-hard the most-supposing of several such boards. Numbers that say 0 are
|
|
13
|
+
* taken away first, because they say least. If no board of the level is found within the attempts the next level
|
|
14
|
+
* down is tried, and the first generator, which cannot fail, is the last.
|
|
15
|
+
*/
|
|
16
|
+
export function generateSlitherlink(width = 5, height = width, seed = 1, level = "medium") {
|
|
17
|
+
if (![width, height].every(side => Number.isInteger(side) && side >= 2 && side <= SLITHERLINK_MOST_SIDE)
|
|
18
|
+
|| !isKazuSeed(seed) || !SLITHERLINK_LEVELS.includes(level))
|
|
19
|
+
throw new RangeError("Invalid Slitherlink settings");
|
|
20
|
+
const random = seededRandom(seed);
|
|
21
|
+
// The work an attempt does is counted in rule runs, never in time, so a seed makes the same puzzle on every machine. When a level has used its share the next one down is tried.
|
|
22
|
+
const work = { calls: 0, limit: width * height * 900 };
|
|
23
|
+
for (let aim = SLITHERLINK_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
24
|
+
work.calls = 0;
|
|
25
|
+
const aimed = SLITHERLINK_LEVELS[aim];
|
|
26
|
+
const wanted = 1;
|
|
27
|
+
let best = null, found = 0;
|
|
28
|
+
for (let attempt = 0; attempt < SLITHERLINK_MOST_ATTEMPTS && found < wanted && work.calls < work.limit; attempt += 1) {
|
|
29
|
+
const made = attemptSlitherlink(width, height, aimed, random, work);
|
|
30
|
+
if (!made)
|
|
31
|
+
continue;
|
|
32
|
+
found += 1;
|
|
33
|
+
if (!best || made.probes > best.probes)
|
|
34
|
+
best = made;
|
|
35
|
+
}
|
|
36
|
+
if (best) {
|
|
37
|
+
const proof = solveSlitherlink(best.board);
|
|
38
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkSlitherlink(best.board, proof.solution).ok) {
|
|
39
|
+
return { ...best.board, seed, level, solution: proof.solution };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { ...templateSlitherlink(width, height, seed), level };
|
|
44
|
+
}
|
|
45
|
+
/** The loop of a random region of squares: connected, without a hole, and never touching itself at a corner. */
|
|
46
|
+
function growRegion(width, height, share, thin, random) {
|
|
47
|
+
const stride = width + 2, inside = new Uint8Array(stride * (height + 2));
|
|
48
|
+
const at = (x, y) => (y + 1) * stride + x + 1;
|
|
49
|
+
const target = Math.max(2, Math.round(width * height * share));
|
|
50
|
+
const start = at(Math.floor(random() * width), Math.floor(random() * height));
|
|
51
|
+
inside[start] = 1;
|
|
52
|
+
let size = 1;
|
|
53
|
+
const outsideReaches = () => {
|
|
54
|
+
// Every square not in the region must still be reachable from beyond the board.
|
|
55
|
+
const seen = new Uint8Array(inside.length), stack = [0];
|
|
56
|
+
seen[0] = 1;
|
|
57
|
+
let reached = 1;
|
|
58
|
+
while (stack.length) {
|
|
59
|
+
const cell = stack.pop();
|
|
60
|
+
for (const next of [cell - 1, cell + 1, cell - stride, cell + stride]) {
|
|
61
|
+
if (next < 0 || next >= inside.length || seen[next] || inside[next])
|
|
62
|
+
continue;
|
|
63
|
+
seen[next] = 1;
|
|
64
|
+
reached += 1;
|
|
65
|
+
stack.push(next);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return reached === inside.length - size;
|
|
69
|
+
};
|
|
70
|
+
while (size < target) {
|
|
71
|
+
const options = [];
|
|
72
|
+
for (let y = 0; y < height; y += 1)
|
|
73
|
+
for (let x = 0; x < width; x += 1) {
|
|
74
|
+
const cell = at(x, y);
|
|
75
|
+
if (inside[cell])
|
|
76
|
+
continue;
|
|
77
|
+
const touching = (inside[cell - 1] + inside[cell + 1] + inside[cell - stride] + inside[cell + stride]);
|
|
78
|
+
if (!touching)
|
|
79
|
+
continue;
|
|
80
|
+
// Two squares touching only at a corner would pinch the loop where it passes itself.
|
|
81
|
+
if ([[cell - 1, cell - stride, cell - stride - 1], [cell + 1, cell - stride, cell - stride + 1], [cell - 1, cell + stride, cell + stride - 1], [cell + 1, cell + stride, cell + stride + 1]]
|
|
82
|
+
.some(([a, b, diagonal]) => inside[diagonal] && !inside[a] && !inside[b]))
|
|
83
|
+
continue;
|
|
84
|
+
inside[cell] = 1;
|
|
85
|
+
size += 1;
|
|
86
|
+
const fine = outsideReaches();
|
|
87
|
+
inside[cell] = 0;
|
|
88
|
+
size -= 1;
|
|
89
|
+
if (!fine)
|
|
90
|
+
continue;
|
|
91
|
+
options.push({ cell, touching });
|
|
92
|
+
}
|
|
93
|
+
if (!options.length)
|
|
94
|
+
break;
|
|
95
|
+
const thinnest = options.filter(option => option.touching === 1);
|
|
96
|
+
const pool = thinnest.length && random() < thin ? thinnest : options;
|
|
97
|
+
inside[pool[Math.floor(random() * pool.length)].cell] = 1;
|
|
98
|
+
size += 1;
|
|
99
|
+
}
|
|
100
|
+
const region = new Uint8Array(width * height);
|
|
101
|
+
for (let y = 0; y < height; y += 1)
|
|
102
|
+
for (let x = 0; x < width; x += 1)
|
|
103
|
+
region[y * width + x] = inside[at(x, y)];
|
|
104
|
+
return region;
|
|
105
|
+
}
|
|
106
|
+
function loopOf(width, height, region) {
|
|
107
|
+
const blank = { width, height, clues: Array(width * height).fill(null) };
|
|
108
|
+
const edges = new Set();
|
|
109
|
+
for (let cell = 0; cell < region.length; cell += 1) {
|
|
110
|
+
if (!region[cell])
|
|
111
|
+
continue;
|
|
112
|
+
const [top, right, bottom, left] = slitherlinkCellEdges(blank, cell);
|
|
113
|
+
const x = cell % width;
|
|
114
|
+
if (cell < width || !region[cell - width])
|
|
115
|
+
edges.add(top);
|
|
116
|
+
if (x === width - 1 || !region[cell + 1])
|
|
117
|
+
edges.add(right);
|
|
118
|
+
if (cell >= width * (height - 1) || !region[cell + width])
|
|
119
|
+
edges.add(bottom);
|
|
120
|
+
if (x === 0 || !region[cell - 1])
|
|
121
|
+
edges.add(left);
|
|
122
|
+
}
|
|
123
|
+
return edges;
|
|
124
|
+
}
|
|
125
|
+
function attemptSlitherlink(width, height, level, random, work) {
|
|
126
|
+
const share = { easy: .45, medium: .5, hard: .55, "extra-hard": .6 }[level] * (.9 + random() * .2);
|
|
127
|
+
const region = growRegion(width, height, share, .75, random);
|
|
128
|
+
const edges = loopOf(width, height, region);
|
|
129
|
+
const blank = { width, height, clues: Array(width * height).fill(null) };
|
|
130
|
+
const full = Array.from({ length: width * height }, (_, cell) => slitherlinkCellEdges(blank, cell).filter(edge => edges.has(edge)).length);
|
|
131
|
+
const clues = [...full];
|
|
132
|
+
let calls = 0;
|
|
133
|
+
const csp = () => {
|
|
134
|
+
const { csp: model } = slitherlinkModel({ width, height, clues });
|
|
135
|
+
return { ...model, propagate: (alive, decided) => { calls += 1; work.calls += 1; return model.propagate(alive, decided); } };
|
|
136
|
+
};
|
|
137
|
+
const byRules = () => { const c = csp(); return logicCsp(c, openSlots(c), 0).solved; };
|
|
138
|
+
const bySupposing = () => {
|
|
139
|
+
const c = csp();
|
|
140
|
+
const count = countCsp(c, openSlots(c), 2, 800);
|
|
141
|
+
if (!count.exhausted && count.count !== 1)
|
|
142
|
+
return false;
|
|
143
|
+
return logicCsp(c, openSlots(c), 1).solved;
|
|
144
|
+
};
|
|
145
|
+
if (!byRules())
|
|
146
|
+
return null;
|
|
147
|
+
const order = shuffled(clues.map((_, cell) => cell), random).sort((a, b) => (full[a] === 0 ? 0 : 1) - (full[b] === 0 ? 0 : 1));
|
|
148
|
+
const keepAtLeast = level === "easy" ? Math.ceil(width * height * .7) : 0;
|
|
149
|
+
let left = clues.length;
|
|
150
|
+
/** Takes numbers away while `test` still passes, until `spent` says the work is used up or `done` says the board is hard enough. */
|
|
151
|
+
const thin = (test, spent, done = () => false) => {
|
|
152
|
+
for (const cell of order) {
|
|
153
|
+
if (clues[cell] === null)
|
|
154
|
+
continue;
|
|
155
|
+
if (left <= keepAtLeast || spent())
|
|
156
|
+
return;
|
|
157
|
+
const was = clues[cell];
|
|
158
|
+
clues[cell] = null;
|
|
159
|
+
if (!test()) {
|
|
160
|
+
clues[cell] = was;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
left -= 1;
|
|
164
|
+
if (done())
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
thin(byRules, () => false);
|
|
169
|
+
// Hard stops thinning when its share of work is used; extra-hard is given four times as much and so ends sparser.
|
|
170
|
+
const budget = width * height * (level === "extra-hard" ? 420 : 120);
|
|
171
|
+
const from = calls;
|
|
172
|
+
if (level === "hard" || level === "extra-hard")
|
|
173
|
+
thin(bySupposing, () => calls - from > budget);
|
|
174
|
+
const board = { width, height, clues };
|
|
175
|
+
const c = csp();
|
|
176
|
+
if (logicCsp(c, openSlots(c), 0).solved)
|
|
177
|
+
return level === "easy" || level === "medium" ? { board, probes: 0 } : null;
|
|
178
|
+
if (level === "easy" || level === "medium")
|
|
179
|
+
return null;
|
|
180
|
+
const probing = logicCsp(c, openSlots(c), 1);
|
|
181
|
+
return probing.solved ? { board, probes: probing.probes } : level === "extra-hard" ? { board, probes: 1000 } : null;
|
|
182
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { SlitherlinkBoard } from "./slitherlink.types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Slitherlink as variables: one per edge, slot 0 for off and slot 1 for on. The pruning rules are the ones
|
|
5
|
+
* a person uses first: a number counts the edges round its square, every corner has none or two edges, and
|
|
6
|
+
* a loop may not close while any other part of it is still to be drawn or while a number is not yet met.
|
|
7
|
+
*/
|
|
8
|
+
export declare function slitherlinkModel(board: SlitherlinkBoard): {
|
|
9
|
+
csp: Csp;
|
|
10
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { edgeVertices, slitherlinkCellEdges, slitherlinkEdgeCount, slitherlinkVertexEdges } from "./slitherlinkBoard.js";
|
|
2
|
+
/**
|
|
3
|
+
* Slitherlink as variables: one per edge, slot 0 for off and slot 1 for on. The pruning rules are the ones
|
|
4
|
+
* a person uses first: a number counts the edges round its square, every corner has none or two edges, and
|
|
5
|
+
* a loop may not close while any other part of it is still to be drawn or while a number is not yet met.
|
|
6
|
+
*/
|
|
7
|
+
export function slitherlinkModel(board) {
|
|
8
|
+
const edges = slitherlinkEdgeCount(board), vertexCount = (board.width + 1) * (board.height + 1);
|
|
9
|
+
const at = Array.from({ length: vertexCount }, (_, vertex) => Int32Array.from(slitherlinkVertexEdges(board, vertex)));
|
|
10
|
+
const endA = new Int32Array(edges), endB = new Int32Array(edges);
|
|
11
|
+
for (let edge = 0; edge < edges; edge += 1) {
|
|
12
|
+
const [a, b] = edgeVertices(board, edge);
|
|
13
|
+
endA[edge] = a;
|
|
14
|
+
endB[edge] = b;
|
|
15
|
+
}
|
|
16
|
+
const clues = board.clues.flatMap((clue, cell) => clue === null ? [] : [{ need: clue, edges: Int32Array.from(slitherlinkCellEdges(board, cell)) }]);
|
|
17
|
+
const cluesOf = Array.from({ length: edges }, () => []);
|
|
18
|
+
clues.forEach((clue, index) => clue.edges.forEach(edge => cluesOf[edge].push(index)));
|
|
19
|
+
const starts = Int32Array.from({ length: edges + 1 }, (_, i) => i * 2);
|
|
20
|
+
const endList = new Int32Array(vertexCount), parent = new Int32Array(vertexCount), degree = new Uint8Array(vertexCount), componentEdges = new Int32Array(vertexCount), openEnds = new Int32Array(vertexCount);
|
|
21
|
+
const vertexQueue = new Int32Array(vertexCount * 8), clueQueue = new Int32Array(clues.length * 8 + 8);
|
|
22
|
+
const vertexQueued = new Uint8Array(vertexCount), clueQueued = new Uint8Array(clues.length);
|
|
23
|
+
const reachable = new Int32Array(vertexCount);
|
|
24
|
+
const findIn = (table, v) => { while (table[v] !== v) {
|
|
25
|
+
table[v] = table[table[v]];
|
|
26
|
+
v = table[v];
|
|
27
|
+
} return v; };
|
|
28
|
+
const find = (v) => { while (parent[v] !== v) {
|
|
29
|
+
parent[v] = parent[parent[v]];
|
|
30
|
+
v = parent[v];
|
|
31
|
+
} return v; };
|
|
32
|
+
const propagate = (alive, decided) => {
|
|
33
|
+
let vertexHead = 0, vertexTail = 0, clueHead = 0, clueTail = 0;
|
|
34
|
+
const touch = (e) => {
|
|
35
|
+
const a = endA[e], b = endB[e];
|
|
36
|
+
if (!vertexQueued[a]) {
|
|
37
|
+
vertexQueued[a] = 1;
|
|
38
|
+
vertexQueue[vertexTail++ % vertexQueue.length] = a;
|
|
39
|
+
}
|
|
40
|
+
if (!vertexQueued[b]) {
|
|
41
|
+
vertexQueued[b] = 1;
|
|
42
|
+
vertexQueue[vertexTail++ % vertexQueue.length] = b;
|
|
43
|
+
}
|
|
44
|
+
for (const index of cluesOf[e])
|
|
45
|
+
if (!clueQueued[index]) {
|
|
46
|
+
clueQueued[index] = 1;
|
|
47
|
+
clueQueue[clueTail++ % clueQueue.length] = index;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const clear = () => { vertexQueued.fill(0); clueQueued.fill(0); return false; };
|
|
51
|
+
if (decided === undefined)
|
|
52
|
+
for (let e = 0; e < edges; e += 1)
|
|
53
|
+
touch(e);
|
|
54
|
+
else
|
|
55
|
+
touch(decided);
|
|
56
|
+
for (;;) {
|
|
57
|
+
while (vertexHead < vertexTail || clueHead < clueTail) {
|
|
58
|
+
while (vertexHead < vertexTail) {
|
|
59
|
+
const v = vertexQueue[vertexHead++ % vertexQueue.length];
|
|
60
|
+
vertexQueued[v] = 0;
|
|
61
|
+
const around = at[v];
|
|
62
|
+
let on = 0, open = 0, lastOpen = -1;
|
|
63
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
64
|
+
const e = around[k], off = alive[2 * e], onSlot = alive[2 * e + 1];
|
|
65
|
+
if (!off && !onSlot)
|
|
66
|
+
return clear();
|
|
67
|
+
if (onSlot && !off)
|
|
68
|
+
on += 1;
|
|
69
|
+
else if (off && onSlot) {
|
|
70
|
+
open += 1;
|
|
71
|
+
lastOpen = e;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (on > 2 || on === 1 && !open)
|
|
75
|
+
return clear();
|
|
76
|
+
if (on === 2 && open) {
|
|
77
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
78
|
+
const e = around[k];
|
|
79
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
80
|
+
alive[2 * e + 1] = 0;
|
|
81
|
+
touch(e);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (open === 1 && on < 2) {
|
|
86
|
+
if (on === 1)
|
|
87
|
+
alive[2 * lastOpen] = 0;
|
|
88
|
+
else
|
|
89
|
+
alive[2 * lastOpen + 1] = 0;
|
|
90
|
+
touch(lastOpen);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
while (clueHead < clueTail) {
|
|
94
|
+
const index = clueQueue[clueHead++ % clueQueue.length], clue = clues[index];
|
|
95
|
+
clueQueued[index] = 0;
|
|
96
|
+
let on = 0, open = 0;
|
|
97
|
+
for (let k = 0; k < 4; k += 1) {
|
|
98
|
+
const e = clue.edges[k];
|
|
99
|
+
if (!alive[2 * e] && !alive[2 * e + 1])
|
|
100
|
+
return clear();
|
|
101
|
+
if (alive[2 * e + 1] && !alive[2 * e])
|
|
102
|
+
on += 1;
|
|
103
|
+
else if (alive[2 * e] && alive[2 * e + 1])
|
|
104
|
+
open += 1;
|
|
105
|
+
}
|
|
106
|
+
if (on > clue.need || on + open < clue.need)
|
|
107
|
+
return clear();
|
|
108
|
+
if (open && on === clue.need) {
|
|
109
|
+
for (let k = 0; k < 4; k += 1) {
|
|
110
|
+
const e = clue.edges[k];
|
|
111
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
112
|
+
alive[2 * e + 1] = 0;
|
|
113
|
+
touch(e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (open && on + open === clue.need) {
|
|
118
|
+
for (let k = 0; k < 4; k += 1) {
|
|
119
|
+
const e = clue.edges[k];
|
|
120
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
121
|
+
alive[2 * e] = 0;
|
|
122
|
+
touch(e);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Loops: join the drawn edges into pieces, then forbid an edge that would close a piece too early.
|
|
129
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
130
|
+
parent[v] = v;
|
|
131
|
+
degree[v] = 0;
|
|
132
|
+
componentEdges[v] = 0;
|
|
133
|
+
openEnds[v] = 0;
|
|
134
|
+
}
|
|
135
|
+
let drawn = 0;
|
|
136
|
+
for (let e = 0; e < edges; e += 1) {
|
|
137
|
+
if (!(alive[2 * e + 1] && !alive[2 * e]))
|
|
138
|
+
continue;
|
|
139
|
+
drawn += 1;
|
|
140
|
+
const a = endA[e], b = endB[e];
|
|
141
|
+
degree[a] += 1;
|
|
142
|
+
degree[b] += 1;
|
|
143
|
+
parent[find(a)] = find(b);
|
|
144
|
+
}
|
|
145
|
+
if (!drawn) {
|
|
146
|
+
// No loop yet: a fully decided empty drawing is not an answer.
|
|
147
|
+
for (let e = 0; e < edges; e += 1)
|
|
148
|
+
if (alive[2 * e] && alive[2 * e + 1])
|
|
149
|
+
return true;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
for (let e = 0; e < edges; e += 1)
|
|
153
|
+
if (alive[2 * e + 1] && !alive[2 * e])
|
|
154
|
+
componentEdges[find(endA[e])] += 1;
|
|
155
|
+
// Everything drawn must still be able to join up: the edges not ruled out have to connect all of it.
|
|
156
|
+
for (let v = 0; v < vertexCount; v += 1)
|
|
157
|
+
reachable[v] = v;
|
|
158
|
+
for (let e = 0; e < edges; e += 1)
|
|
159
|
+
if (alive[2 * e + 1])
|
|
160
|
+
reachable[findIn(reachable, endA[e])] = findIn(reachable, endB[e]);
|
|
161
|
+
let anchor = -1;
|
|
162
|
+
for (let e = 0; e < edges; e += 1) {
|
|
163
|
+
if (!(alive[2 * e + 1] && !alive[2 * e]))
|
|
164
|
+
continue;
|
|
165
|
+
const root = findIn(reachable, endA[e]);
|
|
166
|
+
if (anchor < 0)
|
|
167
|
+
anchor = root;
|
|
168
|
+
else if (root !== anchor)
|
|
169
|
+
return clear();
|
|
170
|
+
}
|
|
171
|
+
let ends = 0;
|
|
172
|
+
for (let v = 0; v < vertexCount; v += 1)
|
|
173
|
+
if (degree[v] === 1) {
|
|
174
|
+
openEnds[find(v)] += 1;
|
|
175
|
+
endList[ends++] = v;
|
|
176
|
+
}
|
|
177
|
+
let moved = false;
|
|
178
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
179
|
+
if (degree[v] === 0 || find(v) !== v || openEnds[v])
|
|
180
|
+
continue;
|
|
181
|
+
if (componentEdges[v] !== drawn)
|
|
182
|
+
return clear();
|
|
183
|
+
// A finished loop is the whole answer: nothing else may be drawn.
|
|
184
|
+
for (let e = 0; e < edges; e += 1)
|
|
185
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
186
|
+
alive[2 * e + 1] = 0;
|
|
187
|
+
touch(e);
|
|
188
|
+
moved = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// An edge joining the two ends of one piece would close it.
|
|
192
|
+
for (let k = 0; k < ends; k += 1) {
|
|
193
|
+
const a = endList[k], around = at[a];
|
|
194
|
+
for (let j = 0; j < around.length; j += 1) {
|
|
195
|
+
const e = around[j];
|
|
196
|
+
if (!(alive[2 * e] && alive[2 * e + 1]))
|
|
197
|
+
continue;
|
|
198
|
+
const b = endA[e] === a ? endB[e] : endA[e];
|
|
199
|
+
if (degree[b] !== 1 || find(a) !== find(b))
|
|
200
|
+
continue;
|
|
201
|
+
// Closing this edge finishes the loop: allowed only when it is the whole drawing and every number is met.
|
|
202
|
+
let whole = componentEdges[find(a)] === drawn;
|
|
203
|
+
if (whole) {
|
|
204
|
+
for (const clue of clues) {
|
|
205
|
+
let on = 0;
|
|
206
|
+
for (let k2 = 0; k2 < 4; k2 += 1) {
|
|
207
|
+
const x = clue.edges[k2];
|
|
208
|
+
if (x === e || alive[2 * x + 1] && !alive[2 * x])
|
|
209
|
+
on += 1;
|
|
210
|
+
}
|
|
211
|
+
if (on !== clue.need) {
|
|
212
|
+
whole = false;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!whole) {
|
|
218
|
+
alive[2 * e + 1] = 0;
|
|
219
|
+
touch(e);
|
|
220
|
+
moved = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!moved)
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const choose = (alive) => {
|
|
229
|
+
// Branch where the board is most pinned down: next to a loose end of the drawing, else on the numbered square with the fewest edges left.
|
|
230
|
+
let best = -1, bestScore = 99;
|
|
231
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
232
|
+
const around = at[v];
|
|
233
|
+
let on = 0, first = -1;
|
|
234
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
235
|
+
const e = around[k];
|
|
236
|
+
if (alive[2 * e + 1] && !alive[2 * e])
|
|
237
|
+
on += 1;
|
|
238
|
+
else if (alive[2 * e] && alive[2 * e + 1] && first < 0)
|
|
239
|
+
first = e;
|
|
240
|
+
}
|
|
241
|
+
if (first >= 0 && on === 1)
|
|
242
|
+
return first;
|
|
243
|
+
}
|
|
244
|
+
for (const clue of clues) {
|
|
245
|
+
let open = 0, first = -1;
|
|
246
|
+
for (let k = 0; k < 4; k += 1) {
|
|
247
|
+
const e = clue.edges[k];
|
|
248
|
+
if (alive[2 * e] && alive[2 * e + 1]) {
|
|
249
|
+
open += 1;
|
|
250
|
+
if (first < 0)
|
|
251
|
+
first = e;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (open && open < bestScore) {
|
|
255
|
+
bestScore = open;
|
|
256
|
+
best = first;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (best >= 0)
|
|
260
|
+
return best;
|
|
261
|
+
for (let e = 0; e < edges; e += 1)
|
|
262
|
+
if (alive[2 * e] && alive[2 * e + 1])
|
|
263
|
+
return e;
|
|
264
|
+
return -1;
|
|
265
|
+
};
|
|
266
|
+
return { csp: { starts, propagate, choose } };
|
|
267
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SlitherlinkMount, SlitherlinkMountOptions } from "./slitherlinkPlay.types.ts";
|
|
2
|
+
/** Mounts an accessible dot-and-edge loop player in the given host element. */
|
|
3
|
+
export declare function mountSlitherlink(host: HTMLElement, initial: SlitherlinkMountOptions): SlitherlinkMount;
|