@johnmorrisdotca/kazu 1.2.0 → 1.4.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 +34 -0
- package/README.md +69 -27
- package/dist/akari-entry.d.ts +1 -0
- package/dist/akari-entry.js +1 -0
- package/dist/akari.constants.d.ts +12 -0
- package/dist/akari.constants.js +12 -0
- package/dist/akari.types.d.ts +19 -0
- package/dist/akariGenerate.d.ts +10 -5
- package/dist/akariGenerate.js +101 -65
- package/dist/akariLogic.d.ts +10 -0
- package/dist/akariLogic.js +84 -0
- package/dist/akariRate.d.ts +6 -0
- package/dist/akariRate.js +34 -0
- package/dist/akariSolve.d.ts +1 -1
- package/dist/akariSolve.js +8 -96
- package/dist/akariTemplate.d.ts +7 -0
- package/dist/akariTemplate.js +78 -0
- package/dist/cells.d.ts +4 -4
- package/dist/cells.js +6 -6
- package/dist/csp.d.ts +63 -0
- package/dist/csp.js +162 -0
- package/dist/fillomino-entry.d.ts +1 -0
- package/dist/fillomino-entry.js +1 -0
- package/dist/fillomino.constants.d.ts +6 -2
- package/dist/fillomino.constants.js +6 -2
- package/dist/fillomino.types.d.ts +18 -1
- package/dist/fillominoBuild.d.ts +10 -0
- package/dist/fillominoBuild.js +78 -0
- package/dist/fillominoGenerate.d.ts +7 -1
- package/dist/fillominoGenerate.js +75 -107
- package/dist/fillominoLogic.d.ts +14 -0
- package/dist/fillominoLogic.js +177 -0
- package/dist/fillominoMount.js +4 -1
- package/dist/fillominoRate.d.ts +3 -0
- package/dist/fillominoRate.js +57 -0
- package/dist/fillominoSolve.js +15 -88
- package/dist/groupSolve.d.ts +9 -0
- package/dist/groupSolve.js +66 -0
- package/dist/hitori-entry.d.ts +1 -0
- package/dist/hitori-entry.js +1 -0
- package/dist/hitori.constants.d.ts +7 -1
- package/dist/hitori.constants.js +7 -1
- package/dist/hitori.types.d.ts +18 -1
- package/dist/hitoriBoard.js +2 -1
- package/dist/hitoriBuild.d.ts +12 -0
- package/dist/hitoriBuild.js +117 -0
- package/dist/hitoriGenerate.d.ts +10 -3
- package/dist/hitoriGenerate.js +59 -157
- package/dist/hitoriLogic.d.ts +12 -0
- package/dist/hitoriLogic.js +189 -0
- package/dist/hitoriRate.d.ts +3 -0
- package/dist/hitoriRate.js +32 -0
- package/dist/hitoriSolve.d.ts +4 -1
- package/dist/hitoriSolve.js +11 -62
- package/dist/kakuro-entry.d.ts +1 -0
- package/dist/kakuro-entry.js +1 -0
- package/dist/kakuro.constants.d.ts +6 -1
- package/dist/kakuro.constants.js +6 -1
- package/dist/kakuro.types.d.ts +19 -0
- package/dist/kakuroBuild.d.ts +30 -0
- package/dist/kakuroBuild.js +337 -0
- package/dist/kakuroGenerate.d.ts +10 -3
- package/dist/kakuroGenerate.js +54 -122
- package/dist/kakuroLogic.d.ts +13 -0
- package/dist/kakuroLogic.js +153 -0
- package/dist/kakuroRate.d.ts +3 -0
- package/dist/kakuroRate.js +40 -0
- package/dist/kakuroSolve.js +17 -69
- package/dist/kakuroTemplate.d.ts +3 -0
- package/dist/kakuroTemplate.js +130 -0
- package/dist/kinds.js +1 -1
- package/dist/layout.js +1 -0
- package/dist/mount.d.ts +0 -18
- package/dist/mount.js +24 -3
- package/dist/names.js +3 -3
- package/dist/numberPlace.d.ts +1 -1
- package/dist/numberPlace.js +21 -8
- package/dist/shikaku-entry.d.ts +1 -0
- package/dist/shikaku-entry.js +1 -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/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-entry.d.ts +1 -0
- package/dist/slitherlink-entry.js +1 -0
- package/dist/slitherlink.constants.d.ts +5 -0
- package/dist/slitherlink.constants.js +5 -0
- package/dist/slitherlink.types.d.ts +18 -0
- package/dist/slitherlinkGenerate.d.ts +10 -3
- package/dist/slitherlinkGenerate.js +161 -80
- package/dist/slitherlinkLogic.d.ts +10 -0
- package/dist/slitherlinkLogic.js +267 -0
- package/dist/slitherlinkRate.d.ts +3 -0
- package/dist/slitherlinkRate.js +26 -0
- package/dist/slitherlinkSolve.d.ts +1 -1
- package/dist/slitherlinkSolve.js +9 -117
- package/dist/slitherlinkTemplate.d.ts +3 -0
- package/dist/slitherlinkTemplate.js +101 -0
- package/dist/strings.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/akariGenerate.js
CHANGED
|
@@ -1,78 +1,114 @@
|
|
|
1
|
-
import { AKARI_MOST_SIDE } from "./akari.constants.js";
|
|
2
|
-
import { checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
1
|
+
import { AKARI_DENSER_ATTEMPTS, AKARI_DENSER_CROWDS, AKARI_LEVELS, AKARI_MOST_ATTEMPTS, AKARI_MOST_SIDE } from "./akari.constants.js";
|
|
2
|
+
import { akariNeighbours, akariVisible, checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
3
|
+
import { akariModel } from "./akariLogic.js";
|
|
3
4
|
import { solveAkari } from "./akariSolve.js";
|
|
4
|
-
import {
|
|
5
|
+
import { templateAkari } from "./akariTemplate.js";
|
|
6
|
+
import { logicCsp, openSlots } from "./csp.js";
|
|
7
|
+
import { isKazuSeed, seededRandom, shuffled } from "./random.js";
|
|
5
8
|
/**
|
|
6
|
-
* Makes a seeded board
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* Makes a seeded board and proves it has one answer. A random wall of black squares is lit by randomly placed
|
|
10
|
+
* bulbs, every black square that touches a white one is numbered, and then numbers 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
|
|
12
|
+
* of its numbers), hard by supposing one square at a time, extra-hard for as long as the answer stays single,
|
|
13
|
+
* which leaves a board that needs more supposing than that. If no board of the level is found within the
|
|
14
|
+
* attempts, the next one down is tried; if none of the levels finds one, the same is tried again with more
|
|
15
|
+
* black squares, which a large board needs to be solvable at all (see `AKARI_DENSER_CROWDS`). The fixed lattice
|
|
16
|
+
* of the first generator is only what is left if even that fails, which no seed does at any size offered.
|
|
9
17
|
*/
|
|
10
|
-
export function generateAkari(width = 7, height = width, seed = 1) {
|
|
18
|
+
export function generateAkari(width = 7, height = width, seed = 1, level = "medium") {
|
|
11
19
|
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= AKARI_MOST_SIDE)
|
|
12
|
-
|| !isKazuSeed(seed))
|
|
20
|
+
|| !isKazuSeed(seed) || !AKARI_LEVELS.includes(level))
|
|
13
21
|
throw new RangeError("Invalid Akari settings");
|
|
14
22
|
const random = seededRandom(seed);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
cells[room] = null;
|
|
27
|
-
cells[room + 1] = null;
|
|
28
|
-
cells[clueY * width + bulbX] = 1;
|
|
29
|
-
bulbs.push(y * width + bulbX);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
cells[room] = null;
|
|
33
|
-
cells[clueY * width + x] = 1;
|
|
34
|
-
bulbs.push(room);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
23
|
+
// The plain attempts come first, as they always have, so every seed that made a board before makes the same one.
|
|
24
|
+
for (let aim = AKARI_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
25
|
+
const found = bestAkari(width, height, AKARI_LEVELS[aim], random, 1, AKARI_MOST_ATTEMPTS);
|
|
26
|
+
if (found)
|
|
27
|
+
return { ...found.board, seed, level, solution: found.solution };
|
|
28
|
+
}
|
|
29
|
+
for (const crowd of AKARI_DENSER_CROWDS)
|
|
30
|
+
for (let aim = AKARI_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
31
|
+
const found = bestAkari(width, height, AKARI_LEVELS[aim], random, crowd, AKARI_DENSER_ATTEMPTS);
|
|
32
|
+
if (found)
|
|
33
|
+
return { ...found.board, seed, level, solution: found.solution };
|
|
37
34
|
}
|
|
35
|
+
const fallback = templateAkari(width, height, seed);
|
|
36
|
+
return { ...fallback, level };
|
|
37
|
+
}
|
|
38
|
+
/** The best board of the level found within `attempts` tries at this share of black squares, with its answer, or null. */
|
|
39
|
+
function bestAkari(width, height, level, random, crowd, attempts) {
|
|
40
|
+
// Extra-hard is the hardest of several boards that need supposing: the most suppositions wins.
|
|
41
|
+
const wanted = level === "extra-hard" ? 8 : 1;
|
|
42
|
+
let best = null, found = 0;
|
|
43
|
+
for (let attempt = 0; attempt < attempts && found < wanted; attempt += 1) {
|
|
44
|
+
const made = attemptAkari(width, height, level, random, crowd);
|
|
45
|
+
if (!made)
|
|
46
|
+
continue;
|
|
47
|
+
const proof = solveAkari(made.board, { limit: 2 });
|
|
48
|
+
if (!proof.complete || proof.count !== 1 || !proof.solution || !checkAkari(made.board, proof.solution).ok)
|
|
49
|
+
continue;
|
|
50
|
+
found += 1;
|
|
51
|
+
if (!best || made.probes > best.probes)
|
|
52
|
+
best = { board: made.board, probes: made.probes, solution: proof.solution };
|
|
38
53
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
cells[room] = null;
|
|
54
|
-
cells[y * width + clueX] = 1;
|
|
55
|
-
bulbs.push(room);
|
|
56
|
-
}
|
|
54
|
+
return best;
|
|
55
|
+
}
|
|
56
|
+
function attemptAkari(width, height, level, random, crowd) {
|
|
57
|
+
const size = width * height;
|
|
58
|
+
const density = { easy: .2, medium: .22, hard: .24, "extra-hard": .26 }[level] * (.85 + random() * .3) * crowd;
|
|
59
|
+
const cells = Array.from({ length: size }, () => random() < density ? false : null);
|
|
60
|
+
if (random() < .5)
|
|
61
|
+
for (let y = 0; y < height; y += 1)
|
|
62
|
+
for (let x = 0; x < width; x += 1) {
|
|
63
|
+
const mirror = (height - 1 - y) * width + width - 1 - x;
|
|
64
|
+
if (y * width + x < mirror)
|
|
65
|
+
cells[mirror] = cells[y * width + x];
|
|
57
66
|
}
|
|
58
|
-
|
|
67
|
+
if (!cells.includes(null))
|
|
68
|
+
return null;
|
|
69
|
+
const base = { width, height, cells };
|
|
70
|
+
// Light every white square with randomly chosen bulbs that never see one another.
|
|
71
|
+
const lit = new Set(), bulbs = new Set();
|
|
72
|
+
for (const cell of shuffled(cells.flatMap((value, at) => value === null ? [at] : []), random)) {
|
|
73
|
+
if (lit.has(cell))
|
|
74
|
+
continue;
|
|
75
|
+
bulbs.add(cell);
|
|
76
|
+
for (const seen of akariVisible(base, cell))
|
|
77
|
+
lit.add(seen);
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
const numbered = cells.flatMap((value, at) => value === false && akariNeighbours(base, at).some(n => cells[n] === null) ? [at] : []);
|
|
80
|
+
for (const at of numbered)
|
|
81
|
+
cells[at] = akariNeighbours(base, at).filter(n => bulbs.has(n)).length;
|
|
82
|
+
if (!numbered.length)
|
|
83
|
+
return null;
|
|
84
|
+
const accepts = () => {
|
|
85
|
+
const { csp } = akariModel({ width, height, cells });
|
|
86
|
+
return logicCsp(csp, openSlots(csp), level === "easy" || level === "medium" ? 0 : 1).solved;
|
|
66
87
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
if (!accepts())
|
|
89
|
+
return null;
|
|
90
|
+
const keepAtLeast = level === "easy" ? Math.ceil(numbered.length * .7) : 0;
|
|
91
|
+
let left = numbered.length;
|
|
92
|
+
for (const at of shuffled(numbered, random)) {
|
|
93
|
+
if (left <= keepAtLeast)
|
|
94
|
+
break;
|
|
95
|
+
const was = cells[at];
|
|
96
|
+
cells[at] = false;
|
|
97
|
+
if (accepts())
|
|
98
|
+
left -= 1;
|
|
99
|
+
else
|
|
100
|
+
cells[at] = was;
|
|
76
101
|
}
|
|
77
|
-
|
|
102
|
+
const board = { width, height, cells };
|
|
103
|
+
if (!isAkariBoard(board))
|
|
104
|
+
return null;
|
|
105
|
+
const { csp } = akariModel(board);
|
|
106
|
+
if (level === "easy" || level === "medium")
|
|
107
|
+
return { board, probes: 0 };
|
|
108
|
+
if (logicCsp(csp, openSlots(csp), 0).solved)
|
|
109
|
+
return null;
|
|
110
|
+
const probing = logicCsp(csp, openSlots(csp), 1);
|
|
111
|
+
if (!probing.solved)
|
|
112
|
+
return level === "extra-hard" ? { board, probes: 1000 } : null;
|
|
113
|
+
return probing.probes >= Math.max(2, Math.round(size * .02)) ? { board, probes: probing.probes } : null;
|
|
78
114
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { AkariBoard } from "./akari.types.ts";
|
|
3
|
+
/** An Akari board as variables: one per white square, slot 0 for no bulb and slot 1 for a bulb. */
|
|
4
|
+
export type AkariModel = {
|
|
5
|
+
csp: Csp;
|
|
6
|
+
whites: readonly number[];
|
|
7
|
+
index: Int32Array;
|
|
8
|
+
};
|
|
9
|
+
/** Builds the pruning rules of Akari: a bulb shades its lines, a number counts its neighbours, every white square needs a lamp. */
|
|
10
|
+
export declare function akariModel(board: AkariBoard): AkariModel;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { akariNeighbours, akariVisible } from "./akariBoard.js";
|
|
2
|
+
/** Builds the pruning rules of Akari: a bulb shades its lines, a number counts its neighbours, every white square needs a lamp. */
|
|
3
|
+
export function akariModel(board) {
|
|
4
|
+
const whites = board.cells.flatMap((value, cell) => value === null ? [cell] : []);
|
|
5
|
+
const index = new Int32Array(board.cells.length).fill(-1);
|
|
6
|
+
whites.forEach((cell, i) => { index[cell] = i; });
|
|
7
|
+
const sees = whites.map(cell => Int32Array.from(akariVisible(board, cell).filter(other => other !== cell).map(other => index[other])));
|
|
8
|
+
const clues = board.cells.flatMap((value, cell) => typeof value === "number"
|
|
9
|
+
? [{ need: value, near: Int32Array.from(akariNeighbours(board, cell).filter(other => index[other] >= 0).map(other => index[other])) }]
|
|
10
|
+
: []);
|
|
11
|
+
const starts = Int32Array.from({ length: whites.length + 1 }, (_, i) => i * 2);
|
|
12
|
+
const propagate = (alive) => {
|
|
13
|
+
let changed = true;
|
|
14
|
+
while (changed) {
|
|
15
|
+
changed = false;
|
|
16
|
+
for (let i = 0; i < whites.length; i += 1) {
|
|
17
|
+
if (!alive[2 * i] && !alive[2 * i + 1])
|
|
18
|
+
return false;
|
|
19
|
+
if (alive[2 * i] || !alive[2 * i + 1])
|
|
20
|
+
continue;
|
|
21
|
+
const ray = sees[i];
|
|
22
|
+
for (let k = 0; k < ray.length; k += 1) {
|
|
23
|
+
const j = ray[k];
|
|
24
|
+
if (alive[2 * j + 1]) {
|
|
25
|
+
alive[2 * j + 1] = 0;
|
|
26
|
+
changed = true;
|
|
27
|
+
if (!alive[2 * j])
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const clue of clues) {
|
|
33
|
+
let on = 0, maybe = 0;
|
|
34
|
+
for (let k = 0; k < clue.near.length; k += 1) {
|
|
35
|
+
const j = clue.near[k];
|
|
36
|
+
if (alive[2 * j + 1]) {
|
|
37
|
+
maybe += 1;
|
|
38
|
+
if (!alive[2 * j])
|
|
39
|
+
on += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (on > clue.need || maybe < clue.need)
|
|
43
|
+
return false;
|
|
44
|
+
if (on === clue.need && maybe > on) {
|
|
45
|
+
for (let k = 0; k < clue.near.length; k += 1) {
|
|
46
|
+
const j = clue.near[k];
|
|
47
|
+
if (alive[2 * j + 1] && alive[2 * j]) {
|
|
48
|
+
alive[2 * j + 1] = 0;
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (maybe === clue.need && maybe > on) {
|
|
54
|
+
for (let k = 0; k < clue.near.length; k += 1) {
|
|
55
|
+
const j = clue.near[k];
|
|
56
|
+
if (alive[2 * j + 1] && alive[2 * j]) {
|
|
57
|
+
alive[2 * j] = 0;
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; i < whites.length; i += 1) {
|
|
64
|
+
let lamps = alive[2 * i + 1] ? 1 : 0, only = alive[2 * i + 1] ? i : -1;
|
|
65
|
+
const ray = sees[i];
|
|
66
|
+
for (let k = 0; k < ray.length && lamps < 2; k += 1) {
|
|
67
|
+
const j = ray[k];
|
|
68
|
+
if (alive[2 * j + 1]) {
|
|
69
|
+
lamps += 1;
|
|
70
|
+
only = j;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (lamps === 0)
|
|
74
|
+
return false;
|
|
75
|
+
if (lamps === 1 && alive[2 * only]) {
|
|
76
|
+
alive[2 * only] = 0;
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
return { csp: { starts, propagate }, whites, index };
|
|
84
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AkariBoard, AkariRating } from "./akari.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Rates a board by solving it as a person would: with the rules alone (depth 0), then with one supposition at
|
|
4
|
+
* a time (depth 1), and anything beyond that is depth 2. A board with no single answer cannot be rated.
|
|
5
|
+
*/
|
|
6
|
+
export declare function rateAkari(board: AkariBoard): AkariRating;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isAkariBoard } from "./akariBoard.js";
|
|
2
|
+
import { akariModel } from "./akariLogic.js";
|
|
3
|
+
import { logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
/**
|
|
5
|
+
* Rates a board by solving it as a person would: with the rules alone (depth 0), then with one supposition at
|
|
6
|
+
* a time (depth 1), and anything beyond that is depth 2. A board with no single answer cannot be rated.
|
|
7
|
+
*/
|
|
8
|
+
export function rateAkari(board) {
|
|
9
|
+
if (!isAkariBoard(board))
|
|
10
|
+
throw new RangeError("Invalid Akari board");
|
|
11
|
+
const { csp, whites } = akariModel(board);
|
|
12
|
+
const proof = solveCsp(csp, openSlots(csp), 2, 250000);
|
|
13
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
14
|
+
throw new RangeError("Akari rating needs a board with one answer");
|
|
15
|
+
const plain = logicCsp(csp, openSlots(csp), 0);
|
|
16
|
+
const probing = plain.solved ? plain : logicCsp(csp, openSlots(csp), 1);
|
|
17
|
+
const blacks = board.cells.length - whites.length;
|
|
18
|
+
const clues = board.cells.filter(cell => typeof cell === "number").length;
|
|
19
|
+
const lit = board.cells.map((cell, at) => cell === false || typeof cell === "number" ? at : -1).filter(at => at >= 0);
|
|
20
|
+
const touching = lit.filter(at => {
|
|
21
|
+
const x = at % board.width, y = Math.floor(at / board.width);
|
|
22
|
+
return [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]].some(([nx, ny]) => nx >= 0 && ny >= 0 && nx < board.width && ny < board.height && board.cells[ny * board.width + nx] === null);
|
|
23
|
+
}).length;
|
|
24
|
+
return {
|
|
25
|
+
depth: plain.solved ? 0 : probing.solved ? 1 : 2,
|
|
26
|
+
probes: plain.solved ? 0 : probing.probes,
|
|
27
|
+
whites: whites.length,
|
|
28
|
+
blacks,
|
|
29
|
+
clues,
|
|
30
|
+
bulbs: proof.solution ? whites.filter((_, i) => proof.solution[2 * i + 1] === 1).length : 0,
|
|
31
|
+
clueShare: touching ? clues / touching : 0,
|
|
32
|
+
openShare: whites.length / board.cells.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/akariSolve.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AkariBoard, AkariSolve } from "./akari.types.ts";
|
|
2
|
-
/** Bounded
|
|
2
|
+
/** Bounded constraint search over the rules in `akariLogic.ts`; a stopped proof is always marked incomplete. */
|
|
3
3
|
export declare function solveAkari(board: AkariBoard, options?: {
|
|
4
4
|
limit?: number;
|
|
5
5
|
nodes?: number;
|
package/dist/akariSolve.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AKARI_MOST_NODES } from "./akari.constants.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { isAkariBoard } from "./akariBoard.js";
|
|
3
|
+
import { akariModel } from "./akariLogic.js";
|
|
4
|
+
import { openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Bounded constraint search over the rules in `akariLogic.ts`; a stopped proof is always marked incomplete. */
|
|
4
6
|
export function solveAkari(board, options = {}) {
|
|
5
7
|
if (!isAkariBoard(board))
|
|
6
8
|
throw new RangeError("Invalid Akari board");
|
|
@@ -9,98 +11,8 @@ export function solveAkari(board, options = {}) {
|
|
|
9
11
|
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1) {
|
|
10
12
|
throw new RangeError("Invalid search bounds");
|
|
11
13
|
}
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let complete = true;
|
|
17
|
-
let solution = null;
|
|
18
|
-
const search = (start) => {
|
|
19
|
-
if (!complete)
|
|
20
|
-
return;
|
|
21
|
-
nodes += 1;
|
|
22
|
-
if (nodes > budget) {
|
|
23
|
-
complete = false;
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const assigned = start.slice();
|
|
27
|
-
let changed = true;
|
|
28
|
-
while (changed) {
|
|
29
|
-
changed = false;
|
|
30
|
-
// A placed bulb rules out every other white square in its four rays.
|
|
31
|
-
for (let i = 0; i < whites.length; i += 1) {
|
|
32
|
-
if (assigned[i] !== 1)
|
|
33
|
-
continue;
|
|
34
|
-
for (const cell of visible[i]) {
|
|
35
|
-
if (cell === whites[i])
|
|
36
|
-
continue;
|
|
37
|
-
const other = whites.indexOf(cell);
|
|
38
|
-
if (assigned[other] === 1)
|
|
39
|
-
return;
|
|
40
|
-
if (assigned[other] < 0) {
|
|
41
|
-
assigned[other] = 0;
|
|
42
|
-
changed = true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// Numbered black squares provide lower and upper bounds on adjacent bulbs.
|
|
47
|
-
for (let cell = 0; cell < board.cells.length; cell += 1) {
|
|
48
|
-
const clue = board.cells[cell];
|
|
49
|
-
if (typeof clue !== "number")
|
|
50
|
-
continue;
|
|
51
|
-
const nearby = akariNeighbours(board, cell)
|
|
52
|
-
.map(neighbour => whites.indexOf(neighbour))
|
|
53
|
-
.filter(index => index >= 0);
|
|
54
|
-
const on = nearby.filter(index => assigned[index] === 1).length;
|
|
55
|
-
const unknown = nearby.filter(index => assigned[index] < 0);
|
|
56
|
-
if (on > clue || on + unknown.length < clue)
|
|
57
|
-
return;
|
|
58
|
-
if (on === clue) {
|
|
59
|
-
for (const index of unknown) {
|
|
60
|
-
assigned[index] = 0;
|
|
61
|
-
changed = true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (on + unknown.length === clue) {
|
|
65
|
-
for (const index of unknown) {
|
|
66
|
-
assigned[index] = 1;
|
|
67
|
-
changed = true;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Every white cell still needs a possible source of light.
|
|
72
|
-
for (let i = 0; i < whites.length; i += 1) {
|
|
73
|
-
const ray = visible[i].map(cell => whites.indexOf(cell));
|
|
74
|
-
if (ray.some(index => assigned[index] === 1))
|
|
75
|
-
continue;
|
|
76
|
-
const unknown = ray.filter(index => assigned[index] < 0);
|
|
77
|
-
if (!unknown.length)
|
|
78
|
-
return;
|
|
79
|
-
if (unknown.length === 1) {
|
|
80
|
-
assigned[unknown[0]] = 1;
|
|
81
|
-
changed = true;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const branch = assigned.findIndex(value => value < 0);
|
|
86
|
-
if (branch < 0) {
|
|
87
|
-
const bulbs = whites.filter((_, index) => assigned[index] === 1);
|
|
88
|
-
if (!checkAkari(board, bulbs).ok)
|
|
89
|
-
return;
|
|
90
|
-
count += 1;
|
|
91
|
-
solution ?? (solution = bulbs);
|
|
92
|
-
if (count >= limit)
|
|
93
|
-
complete = false;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
for (const value of [1, 0]) {
|
|
97
|
-
const next = assigned.slice();
|
|
98
|
-
next[branch] = value;
|
|
99
|
-
search(next);
|
|
100
|
-
if (!complete)
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
search(new Int8Array(whites.length).fill(-1));
|
|
105
|
-
return { count, solution, complete, nodes };
|
|
14
|
+
const { csp, whites } = akariModel(board);
|
|
15
|
+
const found = solveCsp(csp, openSlots(csp), limit, budget);
|
|
16
|
+
const solution = found.solution ? whites.filter((_, i) => found.solution[2 * i + 1] === 1) : null;
|
|
17
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
106
18
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AkariPuzzle } from "./akari.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The first generator, kept as the fallback that cannot fail: makes a seeded board from two original room-grid families: horizontal or vertical paired
|
|
4
|
+
* corridors, with isolated singleton rooms at the edges. Seeded bulb direction and reflections
|
|
5
|
+
* vary each layout. Generation returns only after the independent counter proves uniqueness.
|
|
6
|
+
*/
|
|
7
|
+
export declare function templateAkari(width?: number, height?: number, seed?: number): Omit<AkariPuzzle, "level">;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AKARI_MOST_SIDE } from "./akari.constants.js";
|
|
2
|
+
import { checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
3
|
+
import { solveAkari } from "./akariSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/**
|
|
6
|
+
* The first generator, kept as the fallback that cannot fail: makes a seeded board from two original room-grid families: horizontal or vertical paired
|
|
7
|
+
* corridors, with isolated singleton rooms at the edges. Seeded bulb direction and reflections
|
|
8
|
+
* vary each layout. Generation returns only after the independent counter proves uniqueness.
|
|
9
|
+
*/
|
|
10
|
+
export function templateAkari(width = 7, height = width, seed = 1) {
|
|
11
|
+
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= AKARI_MOST_SIDE)
|
|
12
|
+
|| !isKazuSeed(seed))
|
|
13
|
+
throw new RangeError("Invalid Akari settings");
|
|
14
|
+
const random = seededRandom(seed);
|
|
15
|
+
const cells = Array(width * height).fill(false);
|
|
16
|
+
const bulbs = [];
|
|
17
|
+
const horizontal = random() < .5;
|
|
18
|
+
if (horizontal) {
|
|
19
|
+
const firstRow = height > 2 ? 1 : 0;
|
|
20
|
+
for (let y = firstRow; y < height; y += 3) {
|
|
21
|
+
const clueY = y > 0 ? y - 1 : y + 1;
|
|
22
|
+
for (let x = 0; x < width; x += 3) {
|
|
23
|
+
const room = y * width + x;
|
|
24
|
+
if (x + 1 < width) {
|
|
25
|
+
const bulbX = random() < .5 ? x : x + 1;
|
|
26
|
+
cells[room] = null;
|
|
27
|
+
cells[room + 1] = null;
|
|
28
|
+
cells[clueY * width + bulbX] = 1;
|
|
29
|
+
bulbs.push(y * width + bulbX);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
cells[room] = null;
|
|
33
|
+
cells[clueY * width + x] = 1;
|
|
34
|
+
bulbs.push(room);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const firstColumn = width > 2 ? 1 : 0;
|
|
41
|
+
for (let x = firstColumn; x < width; x += 3) {
|
|
42
|
+
const clueX = x > 0 ? x - 1 : x + 1;
|
|
43
|
+
for (let y = 0; y < height; y += 3) {
|
|
44
|
+
const room = y * width + x;
|
|
45
|
+
if (y + 1 < height) {
|
|
46
|
+
const bulbY = random() < .5 ? y : y + 1;
|
|
47
|
+
cells[room] = null;
|
|
48
|
+
cells[room + width] = null;
|
|
49
|
+
cells[bulbY * width + clueX] = 1;
|
|
50
|
+
bulbs.push(bulbY * width + x);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
cells[room] = null;
|
|
54
|
+
cells[y * width + clueX] = 1;
|
|
55
|
+
bulbs.push(room);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Reflections are applied to the board and the construction answer together.
|
|
61
|
+
const reflectX = random() < .5, reflectY = random() < .5;
|
|
62
|
+
const original = [...cells];
|
|
63
|
+
const map = (cell) => {
|
|
64
|
+
const x = cell % width, y = Math.floor(cell / width);
|
|
65
|
+
return (reflectY ? height - 1 - y : y) * width + (reflectX ? width - 1 - x : x);
|
|
66
|
+
};
|
|
67
|
+
for (let cell = 0; cell < original.length; cell += 1)
|
|
68
|
+
cells[map(cell)] = original[cell];
|
|
69
|
+
const answer = bulbs.map(map);
|
|
70
|
+
const board = { width, height, cells };
|
|
71
|
+
if (!isAkariBoard(board) || !checkAkari(board, answer).ok)
|
|
72
|
+
throw new Error("Unable to construct Akari puzzle");
|
|
73
|
+
const proof = solveAkari(board, { limit: 2 });
|
|
74
|
+
if (!proof.complete || proof.count !== 1 || !proof.solution) {
|
|
75
|
+
throw new Error("No uniquely solvable Akari found within the generation budget");
|
|
76
|
+
}
|
|
77
|
+
return { ...board, seed, solution: proof.solution };
|
|
78
|
+
}
|
package/dist/cells.d.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* A GRID OF NUMBERS AS A STRING: for an address, a POST body, and a kept run.
|
|
3
3
|
*
|
|
4
4
|
* Row-major, one character per cell: a digit for a value, `.` for an empty cell. Past nine the
|
|
5
|
-
* values are letters, A for 10 up to G for 16
|
|
6
|
-
* character is still one cell. Upper case only in a code, so one grid has one spelling. These are
|
|
5
|
+
* values are letters, A for 10 up to G for 16 as a 16×16 Sudoku is printed, and on to P for 25 on a
|
|
6
|
+
* 25×25 (`symbolOf`), so one character is still one cell. Upper case only in a code, so one grid has one spelling. These are
|
|
7
7
|
* the spellings itsutsu.com has always stored, and they decode here unchanged.
|
|
8
8
|
*/
|
|
9
9
|
export declare const EMPTY_CELL = ".";
|
|
10
|
-
/** How a value is written, in a code and on a cell: 1–9, then A–
|
|
10
|
+
/** How a value is written, in a code and on a cell: 1–9, then A–P. */
|
|
11
11
|
export declare function symbolOf(value: number): string;
|
|
12
|
-
/** The value a symbol names, or 0 for one that is not 1–9 or A–
|
|
12
|
+
/** The value a symbol names, or 0 for one that is not 1–9 or A–P (either case). */
|
|
13
13
|
export declare function valueOfSymbol(symbol: string): number;
|
|
14
14
|
/** `[0, 3, 0, 1]` → `".3.1"`. */
|
|
15
15
|
export declare function encodeCells(cells: readonly number[]): string;
|
package/dist/cells.js
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
* A GRID OF NUMBERS AS A STRING: for an address, a POST body, and a kept run.
|
|
3
3
|
*
|
|
4
4
|
* Row-major, one character per cell: a digit for a value, `.` for an empty cell. Past nine the
|
|
5
|
-
* values are letters, A for 10 up to G for 16
|
|
6
|
-
* character is still one cell. Upper case only in a code, so one grid has one spelling. These are
|
|
5
|
+
* values are letters, A for 10 up to G for 16 as a 16×16 Sudoku is printed, and on to P for 25 on a
|
|
6
|
+
* 25×25 (`symbolOf`), so one character is still one cell. Upper case only in a code, so one grid has one spelling. These are
|
|
7
7
|
* the spellings itsutsu.com has always stored, and they decode here unchanged.
|
|
8
8
|
*/
|
|
9
9
|
export const EMPTY_CELL = ".";
|
|
10
|
-
/** The letters after 9, in order: A is 10, G is 16. */
|
|
11
|
-
const PAST_NINE = "
|
|
12
|
-
/** How a value is written, in a code and on a cell: 1–9, then A–
|
|
10
|
+
/** The letters after 9, in order: A is 10, G is 16, P is 25. */
|
|
11
|
+
const PAST_NINE = "ABCDEFGHIJKLMNOP";
|
|
12
|
+
/** How a value is written, in a code and on a cell: 1–9, then A–P. */
|
|
13
13
|
export function symbolOf(value) {
|
|
14
14
|
return value <= 9 ? String(value) : PAST_NINE[value - 10];
|
|
15
15
|
}
|
|
16
|
-
/** The value a symbol names, or 0 for one that is not 1–9 or A–
|
|
16
|
+
/** The value a symbol names, or 0 for one that is not 1–9 or A–P (either case). */
|
|
17
17
|
export function valueOfSymbol(symbol) {
|
|
18
18
|
if (symbol.length !== 1)
|
|
19
19
|
return 0;
|
package/dist/csp.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
export type Csp = {
|
|
11
|
+
/** The slots of variable `v` are `starts[v]` up to, not including, `starts[v + 1]`. */
|
|
12
|
+
starts: Int32Array;
|
|
13
|
+
/**
|
|
14
|
+
* Prunes `alive` as far as the rules force and says `false` when they cannot all be kept. Never guesses.
|
|
15
|
+
* `decided` names a variable that was just narrowed from a state that was already pruned, so a rule may
|
|
16
|
+
* look only at what that can change; leaving it out means look at everything.
|
|
17
|
+
*/
|
|
18
|
+
propagate: (alive: Uint8Array, decided?: number) => boolean;
|
|
19
|
+
/** Which variable to branch on in a search; the default is the one with the fewest values left. */
|
|
20
|
+
choose?: (alive: Uint8Array) => number;
|
|
21
|
+
};
|
|
22
|
+
/** How many values a variable still has. */
|
|
23
|
+
export declare function slotsLeft(csp: Csp, alive: Uint8Array, variable: number): number;
|
|
24
|
+
/** Every value of every variable still possible. */
|
|
25
|
+
export declare function openSlots(csp: Csp): Uint8Array;
|
|
26
|
+
/** Leaves `variable` with only `slot` (a slot number, not an offset). */
|
|
27
|
+
export declare function decide(csp: Csp, alive: Uint8Array, variable: number, slot: number): void;
|
|
28
|
+
/** The slot a decided variable has left, or -1 when it has more than one or none. */
|
|
29
|
+
export declare function decidedSlot(csp: Csp, alive: Uint8Array, variable: number): number;
|
|
30
|
+
export type CspCount = {
|
|
31
|
+
count: number;
|
|
32
|
+
solution: Uint8Array | null;
|
|
33
|
+
solutions: Uint8Array[];
|
|
34
|
+
stopped: boolean;
|
|
35
|
+
exhausted: boolean;
|
|
36
|
+
nodes: number;
|
|
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 declare function countCsp(csp: Csp, start: Uint8Array, limit: number, budget: number): CspCount;
|
|
44
|
+
export type CspLogic = {
|
|
45
|
+
solved: boolean;
|
|
46
|
+
contradiction: boolean;
|
|
47
|
+
probes: number;
|
|
48
|
+
alive: Uint8Array;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Solves by reasoning alone, never by guessing a value and keeping it. `depth` 0 is the rules' own
|
|
52
|
+
* pruning. Depth 1 adds probing: suppose one value, prune, and when that breaks a rule the value is
|
|
53
|
+
* ruled out. Depth 2 lets a probe probe. `probes` counts the values ruled out by supposing, which is how
|
|
54
|
+
* much supposing the puzzle asks of a person.
|
|
55
|
+
*/
|
|
56
|
+
export declare function logicCsp(csp: Csp, start: Uint8Array, depth: number): CspLogic;
|
|
57
|
+
/**
|
|
58
|
+
* Counts answers like `countCsp`, but first tries to settle the puzzle by reasoning (rules, then one level of
|
|
59
|
+
* supposing): reasoning that leaves every variable decided proves exactly one answer, and reasoning that breaks a
|
|
60
|
+
* rule proves none, in far fewer steps than a search. Only a puzzle that reasoning leaves open is searched, from the
|
|
61
|
+
* state reasoning reached.
|
|
62
|
+
*/
|
|
63
|
+
export declare function solveCsp(csp: Csp, start: Uint8Array, limit: number, budget: number, depth?: number): CspCount;
|