@johnmorrisdotca/kazu 1.2.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 +23 -0
- package/README.md +58 -16
- package/dist/akari-entry.d.ts +1 -0
- package/dist/akari-entry.js +1 -0
- package/dist/akari.constants.d.ts +5 -0
- package/dist/akari.constants.js +5 -0
- package/dist/akari.types.d.ts +19 -0
- package/dist/akariGenerate.d.ts +8 -5
- package/dist/akariGenerate.js +88 -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/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/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/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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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/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;
|
package/dist/csp.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE SMALL ENGINE for the puzzles that are solved by narrowing down what each square, edge or rectangle can
|
|
3
|
+
* still be. A puzzle supplies its variables and one function that prunes; this file does the two things
|
|
4
|
+
* every one of them needs: counting answers (a search that stops at a limit) and rating how hard a puzzle
|
|
5
|
+
* is (how deep the reasoning must go before the grid is decided).
|
|
6
|
+
*
|
|
7
|
+
* A variable has a few slots (its possible values). `alive[slot]` is 1 while that value is still possible.
|
|
8
|
+
* A variable is decided when exactly one of its slots is alive and impossible when none is.
|
|
9
|
+
*/
|
|
10
|
+
/** How many values a variable still has. */
|
|
11
|
+
export function slotsLeft(csp, alive, variable) {
|
|
12
|
+
let left = 0;
|
|
13
|
+
for (let slot = csp.starts[variable]; slot < csp.starts[variable + 1]; slot += 1)
|
|
14
|
+
left += alive[slot];
|
|
15
|
+
return left;
|
|
16
|
+
}
|
|
17
|
+
/** Every value of every variable still possible. */
|
|
18
|
+
export function openSlots(csp) {
|
|
19
|
+
return new Uint8Array(csp.starts[csp.starts.length - 1]).fill(1);
|
|
20
|
+
}
|
|
21
|
+
/** Leaves `variable` with only `slot` (a slot number, not an offset). */
|
|
22
|
+
export function decide(csp, alive, variable, slot) {
|
|
23
|
+
for (let at = csp.starts[variable]; at < csp.starts[variable + 1]; at += 1)
|
|
24
|
+
alive[at] = at === slot ? 1 : 0;
|
|
25
|
+
}
|
|
26
|
+
/** The slot a decided variable has left, or -1 when it has more than one or none. */
|
|
27
|
+
export function decidedSlot(csp, alive, variable) {
|
|
28
|
+
let found = -1;
|
|
29
|
+
for (let slot = csp.starts[variable]; slot < csp.starts[variable + 1]; slot += 1) {
|
|
30
|
+
if (!alive[slot])
|
|
31
|
+
continue;
|
|
32
|
+
if (found >= 0)
|
|
33
|
+
return -1;
|
|
34
|
+
found = slot;
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Counts the ways to decide every variable, up to `limit`. `exhausted` says the node budget ran out;
|
|
40
|
+
* `stopped` says the limit was reached. Either way the count must not be read as proof of anything
|
|
41
|
+
* beyond what was seen.
|
|
42
|
+
*/
|
|
43
|
+
export function countCsp(csp, start, limit, budget) {
|
|
44
|
+
const variables = csp.starts.length - 1;
|
|
45
|
+
let count = 0, nodes = 0, exhausted = false, stopped = false;
|
|
46
|
+
let solution = null;
|
|
47
|
+
const solutions = [];
|
|
48
|
+
const visit = (alive, hint) => {
|
|
49
|
+
if (exhausted || stopped)
|
|
50
|
+
return;
|
|
51
|
+
if (++nodes > budget) {
|
|
52
|
+
exhausted = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!csp.propagate(alive, hint))
|
|
56
|
+
return;
|
|
57
|
+
let pick = csp.choose ? csp.choose(alive) : -1, best = 99999;
|
|
58
|
+
if (!csp.choose) {
|
|
59
|
+
for (let v = 0; v < variables; v += 1) {
|
|
60
|
+
const left = slotsLeft(csp, alive, v);
|
|
61
|
+
if (left > 1 && left < best) {
|
|
62
|
+
best = left;
|
|
63
|
+
pick = v;
|
|
64
|
+
if (left === 2)
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (pick < 0) {
|
|
70
|
+
count += 1;
|
|
71
|
+
solution ?? (solution = alive.slice());
|
|
72
|
+
solutions.push(alive.slice());
|
|
73
|
+
if (count >= limit)
|
|
74
|
+
stopped = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (let slot = csp.starts[pick]; slot < csp.starts[pick + 1]; slot += 1) {
|
|
78
|
+
if (!alive[slot])
|
|
79
|
+
continue;
|
|
80
|
+
const next = alive.slice();
|
|
81
|
+
decide(csp, next, pick, slot);
|
|
82
|
+
visit(next, pick);
|
|
83
|
+
if (exhausted || stopped)
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
visit(start.slice());
|
|
88
|
+
return { count, solution, solutions, stopped, exhausted, nodes: Math.min(nodes, budget) };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Solves by reasoning alone, never by guessing a value and keeping it. `depth` 0 is the rules' own
|
|
92
|
+
* pruning. Depth 1 adds probing: suppose one value, prune, and when that breaks a rule the value is
|
|
93
|
+
* ruled out. Depth 2 lets a probe probe. `probes` counts the values ruled out by supposing, which is how
|
|
94
|
+
* much supposing the puzzle asks of a person.
|
|
95
|
+
*/
|
|
96
|
+
export function logicCsp(csp, start, depth) {
|
|
97
|
+
const variables = csp.starts.length - 1;
|
|
98
|
+
let probes = 0;
|
|
99
|
+
const decidedAll = (alive) => {
|
|
100
|
+
for (let v = 0; v < variables; v += 1)
|
|
101
|
+
if (slotsLeft(csp, alive, v) > 1)
|
|
102
|
+
return false;
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
const run = (alive, level, hint) => {
|
|
106
|
+
let first = hint;
|
|
107
|
+
for (;;) {
|
|
108
|
+
if (!csp.propagate(alive, first))
|
|
109
|
+
return false;
|
|
110
|
+
first = undefined;
|
|
111
|
+
let open = false;
|
|
112
|
+
for (let v = 0; v < variables && !open; v += 1)
|
|
113
|
+
if (slotsLeft(csp, alive, v) > 1)
|
|
114
|
+
open = true;
|
|
115
|
+
if (!open)
|
|
116
|
+
return "solved";
|
|
117
|
+
if (level === 0)
|
|
118
|
+
return "stuck";
|
|
119
|
+
let moved = false;
|
|
120
|
+
for (let v = 0; v < variables; v += 1) {
|
|
121
|
+
if (slotsLeft(csp, alive, v) < 2)
|
|
122
|
+
continue;
|
|
123
|
+
for (let slot = csp.starts[v]; slot < csp.starts[v + 1]; slot += 1) {
|
|
124
|
+
if (!alive[slot] || slotsLeft(csp, alive, v) < 2)
|
|
125
|
+
continue;
|
|
126
|
+
const trial = alive.slice();
|
|
127
|
+
decide(csp, trial, v, slot);
|
|
128
|
+
const result = run(trial, level - 1, v);
|
|
129
|
+
if (result === false) {
|
|
130
|
+
alive[slot] = 0;
|
|
131
|
+
probes += 1;
|
|
132
|
+
moved = true;
|
|
133
|
+
if (!csp.propagate(alive))
|
|
134
|
+
return false;
|
|
135
|
+
if (decidedAll(alive))
|
|
136
|
+
return "solved";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!moved)
|
|
141
|
+
return "stuck";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const alive = start.slice();
|
|
145
|
+
const result = run(alive, depth);
|
|
146
|
+
return { solved: result === "solved", contradiction: result === false, probes, alive };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Counts answers like `countCsp`, but first tries to settle the puzzle by reasoning (rules, then one level of
|
|
150
|
+
* supposing): reasoning that leaves every variable decided proves exactly one answer, and reasoning that breaks a
|
|
151
|
+
* rule proves none, in far fewer steps than a search. Only a puzzle that reasoning leaves open is searched, from the
|
|
152
|
+
* state reasoning reached.
|
|
153
|
+
*/
|
|
154
|
+
export function solveCsp(csp, start, limit, budget, depth = 1) {
|
|
155
|
+
const plain = logicCsp(csp, start, 0);
|
|
156
|
+
const reasoned = plain.solved || plain.contradiction || depth < 1 ? plain : logicCsp(csp, start, 1);
|
|
157
|
+
if (reasoned.contradiction)
|
|
158
|
+
return { count: 0, solution: null, solutions: [], stopped: false, exhausted: false, nodes: 0 };
|
|
159
|
+
if (reasoned.solved)
|
|
160
|
+
return { count: 1, solution: reasoned.alive, solutions: [reasoned.alive], stopped: limit <= 1, exhausted: false, nodes: 0 };
|
|
161
|
+
return countCsp(csp, reasoned.alive, limit, budget);
|
|
162
|
+
}
|
|
@@ -3,6 +3,7 @@ export * from "./fillomino.constants.ts";
|
|
|
3
3
|
export * from "./fillominoBoard.ts";
|
|
4
4
|
export * from "./fillominoSolve.ts";
|
|
5
5
|
export * from "./fillominoGenerate.ts";
|
|
6
|
+
export * from "./fillominoRate.ts";
|
|
6
7
|
export * from "./fillominoGame.ts";
|
|
7
8
|
export { drawFillomino } from "./fillominoDraw.ts";
|
|
8
9
|
export type { FillominoDrawOptions } from "./fillominoPlay.types.ts";
|
package/dist/fillomino-entry.js
CHANGED
|
@@ -3,5 +3,6 @@ export * from "./fillomino.constants.js";
|
|
|
3
3
|
export * from "./fillominoBoard.js";
|
|
4
4
|
export * from "./fillominoSolve.js";
|
|
5
5
|
export * from "./fillominoGenerate.js";
|
|
6
|
+
export * from "./fillominoRate.js";
|
|
6
7
|
export * from "./fillominoGame.js";
|
|
7
8
|
export { drawFillomino } from "./fillominoDraw.js";
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export declare const FILLOMINO_MOST_SIDE = 12;
|
|
2
|
-
export declare const FILLOMINO_GENERATOR_MOST_CELLS =
|
|
2
|
+
export declare const FILLOMINO_GENERATOR_MOST_CELLS = 144;
|
|
3
3
|
export declare const FILLOMINO_MOST_NODES = 80000;
|
|
4
4
|
export declare const FILLOMINO_MOST_ATTEMPTS = 120;
|
|
5
|
-
|
|
5
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateFillomino`. */
|
|
6
|
+
export declare const FILLOMINO_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
|
|
7
|
+
/** The sides on offer for a square board; any side from 4 to `FILLOMINO_MOST_SIDE` can be made. */
|
|
8
|
+
export declare const FILLOMINO_SIZES: readonly [6, 8, 10, 12];
|
|
9
|
+
export declare const FILLOMINO_LEAST_GENERATED_SIDE = 4;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export const FILLOMINO_MOST_SIDE = 12;
|
|
2
|
-
export const FILLOMINO_GENERATOR_MOST_CELLS =
|
|
2
|
+
export const FILLOMINO_GENERATOR_MOST_CELLS = 144;
|
|
3
3
|
export const FILLOMINO_MOST_NODES = 80000;
|
|
4
4
|
export const FILLOMINO_MOST_ATTEMPTS = 120;
|
|
5
|
-
|
|
5
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateFillomino`. */
|
|
6
|
+
export const FILLOMINO_LEVELS = ["easy", "medium", "hard", "extra-hard"];
|
|
7
|
+
/** The sides on offer for a square board; any side from 4 to `FILLOMINO_MOST_SIDE` can be made. */
|
|
8
|
+
export const FILLOMINO_SIZES = [6, 8, 10, 12];
|
|
9
|
+
export const FILLOMINO_LEAST_GENERATED_SIDE = 4;
|
|
@@ -8,7 +8,7 @@ export type FillominoPuzzle = FillominoBoard & {
|
|
|
8
8
|
level: FillominoLevel;
|
|
9
9
|
solution: readonly number[];
|
|
10
10
|
};
|
|
11
|
-
export type FillominoLevel = "easy" | "medium" | "hard";
|
|
11
|
+
export type FillominoLevel = "easy" | "medium" | "hard" | "extra-hard";
|
|
12
12
|
export type FillominoCheck = {
|
|
13
13
|
ok: boolean;
|
|
14
14
|
complete: boolean;
|
|
@@ -28,3 +28,20 @@ export type FillominoGame = {
|
|
|
28
28
|
history: readonly (readonly number[])[];
|
|
29
29
|
helped: boolean;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* How hard a board is, measured by solving it. `depth` 0 means the rules solve it, 1 that somebody has to suppose a
|
|
33
|
+
* number in a square and watch it break, 2 that more than that is needed; `probes` is how many suppositions depth 1
|
|
34
|
+
* needed. The rest describes the board.
|
|
35
|
+
*/
|
|
36
|
+
export type FillominoRating = {
|
|
37
|
+
depth: 0 | 1 | 2;
|
|
38
|
+
probes: number;
|
|
39
|
+
givens: number;
|
|
40
|
+
/** Squares that are given, as a share of the board. */
|
|
41
|
+
givenShare: number;
|
|
42
|
+
regions: number;
|
|
43
|
+
/** Regions with no given in them: they are found only by what is round them. */
|
|
44
|
+
unnamed: number;
|
|
45
|
+
largest: number;
|
|
46
|
+
meanRegion: number;
|
|
47
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FillominoLevel } from "./fillomino.types.ts";
|
|
2
|
+
import type { Random } from "./random.ts";
|
|
3
|
+
/** How likely each region size is, by level: a small board of small regions is easy, long ones with few givens are not. */
|
|
4
|
+
export declare const FILLOMINO_SIZES_BY_LEVEL: Record<FillominoLevel, readonly number[]>;
|
|
5
|
+
/**
|
|
6
|
+
* A full Fillomino answer: the board cut into connected regions, every region holding its own size, and no two
|
|
7
|
+
* regions of the same size touching. Regions are grown one at a time from the square with the fewest free neighbours;
|
|
8
|
+
* null when a corner of the board cannot be finished, so the caller tries again.
|
|
9
|
+
*/
|
|
10
|
+
export declare function partitionFillomino(width: number, height: number, level: FillominoLevel, random: Random): number[] | null;
|