@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
package/dist/slitherlinkSolve.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { SLITHERLINK_MOST_NODES } from "./slitherlink.constants.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { isSlitherlinkBoard, slitherlinkEdgeCount } from "./slitherlinkBoard.js";
|
|
3
|
+
import { slitherlinkModel } from "./slitherlinkLogic.js";
|
|
4
|
+
import { openSlots, solveCsp } from "./csp.js";
|
|
5
|
+
/** Counts loop solutions with the pruning in `slitherlinkLogic.ts` under explicit work bounds. */
|
|
4
6
|
export function solveSlitherlink(board, options = {}) {
|
|
5
7
|
if (!isSlitherlinkBoard(board))
|
|
6
8
|
throw new RangeError("Invalid Slitherlink board");
|
|
@@ -9,119 +11,9 @@ export function solveSlitherlink(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
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let complete = true;
|
|
18
|
-
let solution = null;
|
|
19
|
-
const search = (start) => {
|
|
20
|
-
if (!complete)
|
|
21
|
-
return;
|
|
22
|
-
nodes += 1;
|
|
23
|
-
if (nodes > budget) {
|
|
24
|
-
complete = false;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const state = start.slice();
|
|
28
|
-
let changed = true;
|
|
29
|
-
while (changed) {
|
|
30
|
-
changed = false;
|
|
31
|
-
for (const group of vertices) {
|
|
32
|
-
const on = group.filter(edge => state[edge] === 1).length;
|
|
33
|
-
const unknown = group.filter(edge => state[edge] < 0);
|
|
34
|
-
if (on > 2 || on === 1 && !unknown.length)
|
|
35
|
-
return;
|
|
36
|
-
if (on === 2) {
|
|
37
|
-
for (const edge of unknown) {
|
|
38
|
-
state[edge] = 0;
|
|
39
|
-
changed = true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else if (on === 1 && unknown.length === 1) {
|
|
43
|
-
state[unknown[0]] = 1;
|
|
44
|
-
changed = true;
|
|
45
|
-
}
|
|
46
|
-
else if (on === 0 && unknown.length === 1) {
|
|
47
|
-
state[unknown[0]] = 0;
|
|
48
|
-
changed = true;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
let clueIndex = 0;
|
|
52
|
-
for (const clue of board.clues) {
|
|
53
|
-
if (clue === null)
|
|
54
|
-
continue;
|
|
55
|
-
const group = cells[clueIndex++];
|
|
56
|
-
const on = group.filter(edge => state[edge] === 1).length;
|
|
57
|
-
const unknown = group.filter(edge => state[edge] < 0);
|
|
58
|
-
if (on > clue || on + unknown.length < clue)
|
|
59
|
-
return;
|
|
60
|
-
if (on === clue) {
|
|
61
|
-
for (const edge of unknown) {
|
|
62
|
-
state[edge] = 0;
|
|
63
|
-
changed = true;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (on + unknown.length === clue) {
|
|
67
|
-
for (const edge of unknown) {
|
|
68
|
-
state[edge] = 1;
|
|
69
|
-
changed = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const selected = Array.from(state).flatMap((value, edge) => value === 1 ? [edge] : []);
|
|
75
|
-
const closed = closedComponents(board, selected);
|
|
76
|
-
if (closed.length > 1)
|
|
77
|
-
return;
|
|
78
|
-
if (closed.length === 1 && closed[0].length !== selected.length)
|
|
79
|
-
return;
|
|
80
|
-
const branch = state.findIndex(value => value < 0);
|
|
81
|
-
if (branch < 0) {
|
|
82
|
-
if (!checkSlitherlink(board, selected).ok)
|
|
83
|
-
return;
|
|
84
|
-
count += 1;
|
|
85
|
-
solution ?? (solution = selected);
|
|
86
|
-
if (count >= limit)
|
|
87
|
-
complete = false;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
for (const value of [1, 0]) {
|
|
91
|
-
const next = state.slice();
|
|
92
|
-
next[branch] = value;
|
|
93
|
-
search(next);
|
|
94
|
-
if (!complete)
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
search(new Int8Array(edgeCount).fill(-1));
|
|
99
|
-
return { count, solution, complete, nodes };
|
|
100
|
-
}
|
|
101
|
-
function closedComponents(board, selected) {
|
|
102
|
-
const unseen = new Set(selected);
|
|
103
|
-
const components = [];
|
|
104
|
-
while (unseen.size) {
|
|
105
|
-
const first = unseen.values().next().value;
|
|
106
|
-
const component = [first];
|
|
107
|
-
const queue = [first];
|
|
108
|
-
unseen.delete(first);
|
|
109
|
-
while (queue.length) {
|
|
110
|
-
const edge = queue.pop();
|
|
111
|
-
for (const vertex of edgeVertices(board, edge)) {
|
|
112
|
-
for (const adjacent of slitherlinkVertexEdges(board, vertex)) {
|
|
113
|
-
if (!unseen.has(adjacent))
|
|
114
|
-
continue;
|
|
115
|
-
unseen.delete(adjacent);
|
|
116
|
-
component.push(adjacent);
|
|
117
|
-
queue.push(adjacent);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const vertices = new Set(component.flatMap(edge => edgeVertices(board, edge)));
|
|
122
|
-
if ([...vertices].every(vertex => slitherlinkVertexEdges(board, vertex)
|
|
123
|
-
.filter(edge => component.includes(edge)).length === 2))
|
|
124
|
-
components.push(component);
|
|
125
|
-
}
|
|
126
|
-
return components;
|
|
14
|
+
const { csp } = slitherlinkModel(board);
|
|
15
|
+
const found = solveCsp(csp, openSlots(csp), limit, budget);
|
|
16
|
+
const edges = slitherlinkEdgeCount(board);
|
|
17
|
+
const solution = found.solution ? Array.from({ length: edges }, (_, edge) => edge).filter(edge => found.solution[2 * edge + 1] === 1) : null;
|
|
18
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
127
19
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SlitherlinkPuzzle } from "./slitherlink.types.ts";
|
|
2
|
+
/** The first generator, kept as the fallback that cannot fail: a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
|
|
3
|
+
export declare function templateSlitherlink(width?: number, height?: number, seed?: number): Omit<SlitherlinkPuzzle, "level">;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { SLITHERLINK_MOST_SIDE } from "./slitherlink.constants.js";
|
|
2
|
+
import { checkSlitherlink, isSlitherlinkBoard, slitherlinkCellEdges, } from "./slitherlinkBoard.js";
|
|
3
|
+
import { solveSlitherlink } from "./slitherlinkSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/** The first generator, kept as the fallback that cannot fail: a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
|
|
6
|
+
export function templateSlitherlink(width = 5, height = width, seed = 1) {
|
|
7
|
+
if (![width, height].every(side => Number.isInteger(side) && side >= 2 && side <= SLITHERLINK_MOST_SIDE)
|
|
8
|
+
|| !isKazuSeed(seed))
|
|
9
|
+
throw new RangeError("Invalid Slitherlink settings");
|
|
10
|
+
const random = seededRandom(seed);
|
|
11
|
+
const useL = width >= 3 && height >= 3 && random() < .5;
|
|
12
|
+
const region = [];
|
|
13
|
+
let x0;
|
|
14
|
+
let y0;
|
|
15
|
+
let blockWidth;
|
|
16
|
+
let blockHeight;
|
|
17
|
+
if (useL) {
|
|
18
|
+
blockWidth = 2 + Math.floor(random() * (width - 1));
|
|
19
|
+
blockHeight = 2 + Math.floor(random() * (height - 1));
|
|
20
|
+
x0 = Math.floor(random() * (width - blockWidth + 1));
|
|
21
|
+
y0 = Math.floor(random() * (height - blockHeight + 1));
|
|
22
|
+
for (let y = 0; y < blockHeight; y += 1) {
|
|
23
|
+
for (let x = 0; x < blockWidth; x += 1) {
|
|
24
|
+
if (x === 0 || y === 0)
|
|
25
|
+
region.push((y0 + y) * width + x0 + x);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
blockWidth = 1 + Math.floor(random() * width);
|
|
31
|
+
blockHeight = 1 + Math.floor(random() * height);
|
|
32
|
+
if (blockWidth === 1 && blockHeight === 1)
|
|
33
|
+
blockWidth = 2;
|
|
34
|
+
x0 = Math.floor(random() * (width - blockWidth + 1));
|
|
35
|
+
y0 = Math.floor(random() * (height - blockHeight + 1));
|
|
36
|
+
for (let y = 0; y < blockHeight; y += 1) {
|
|
37
|
+
for (let x = 0; x < blockWidth; x += 1)
|
|
38
|
+
region.push((y0 + y) * width + x0 + x);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const blank = { width, height, clues: Array(width * height).fill(null) };
|
|
42
|
+
const regionSet = new Set(region);
|
|
43
|
+
const edges = new Set();
|
|
44
|
+
for (const cell of region) {
|
|
45
|
+
const [top, right, bottom, left] = slitherlinkCellEdges(blank, cell);
|
|
46
|
+
const x = cell % width;
|
|
47
|
+
if (!regionSet.has(cell - width))
|
|
48
|
+
edges.add(top);
|
|
49
|
+
if (x === width - 1 || !regionSet.has(cell + 1))
|
|
50
|
+
edges.add(right);
|
|
51
|
+
if (!regionSet.has(cell + width))
|
|
52
|
+
edges.add(bottom);
|
|
53
|
+
if (x === 0 || !regionSet.has(cell - 1))
|
|
54
|
+
edges.add(left);
|
|
55
|
+
}
|
|
56
|
+
let board = {
|
|
57
|
+
width,
|
|
58
|
+
height,
|
|
59
|
+
clues: Array.from({ length: width * height }, (_, cell) => slitherlinkCellEdges(blank, cell).filter(edge => edges.has(edge)).length),
|
|
60
|
+
};
|
|
61
|
+
let answer = [...edges];
|
|
62
|
+
// Seeded axis reflections give each selected shape more than one orientation.
|
|
63
|
+
const reflectX = random() < .5;
|
|
64
|
+
const reflectY = random() < .5;
|
|
65
|
+
if (reflectX || reflectY) {
|
|
66
|
+
const horizontalCount = width * (height + 1);
|
|
67
|
+
const mapEdge = (edge) => {
|
|
68
|
+
if (edge < horizontalCount) {
|
|
69
|
+
const x = edge % width;
|
|
70
|
+
const y = Math.floor(edge / width);
|
|
71
|
+
const nextX = reflectX ? width - 1 - x : x;
|
|
72
|
+
const nextY = reflectY ? height - y : y;
|
|
73
|
+
return nextY * width + nextX;
|
|
74
|
+
}
|
|
75
|
+
const local = edge - horizontalCount;
|
|
76
|
+
const x = local % (width + 1);
|
|
77
|
+
const y = Math.floor(local / (width + 1));
|
|
78
|
+
const nextX = reflectX ? width - x : x;
|
|
79
|
+
const nextY = reflectY ? height - 1 - y : y;
|
|
80
|
+
return horizontalCount + nextY * (width + 1) + nextX;
|
|
81
|
+
};
|
|
82
|
+
const reflected = Array(width * height).fill(null);
|
|
83
|
+
board.clues.forEach((clue, cell) => {
|
|
84
|
+
const x = cell % width;
|
|
85
|
+
const y = Math.floor(cell / width);
|
|
86
|
+
const nextX = reflectX ? width - 1 - x : x;
|
|
87
|
+
const nextY = reflectY ? height - 1 - y : y;
|
|
88
|
+
reflected[nextY * width + nextX] = clue;
|
|
89
|
+
});
|
|
90
|
+
board = { width, height, clues: reflected };
|
|
91
|
+
answer = answer.map(mapEdge);
|
|
92
|
+
}
|
|
93
|
+
if (!isSlitherlinkBoard(board) || !checkSlitherlink(board, answer).ok) {
|
|
94
|
+
throw new Error("Unable to construct Slitherlink loop");
|
|
95
|
+
}
|
|
96
|
+
const proof = solveSlitherlink(board, { limit: 2 });
|
|
97
|
+
if (!proof.complete || proof.count !== 1 || !proof.solution) {
|
|
98
|
+
throw new Error("No uniquely solvable Slitherlink found within the generation budget");
|
|
99
|
+
}
|
|
100
|
+
return { ...board, seed, solution: proof.solution };
|
|
101
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export declare const VERSION = "1.
|
|
2
|
+
export declare const VERSION = "1.3.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export const VERSION = "1.
|
|
2
|
+
export const VERSION = "1.3.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kazu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Grid number and logic puzzles for JavaScript and TypeScript: Sudoku and its variants, Futoshiki, Skyscrapers, Shikaku, Hitori, Nurikabe, Akari, Juosan, Slitherlink, Masyu, Yajilin, Ripple Effect, Kakuro, Fillomino and Heyawake. Dedicated typed engines, independently checked puzzles, SVG boards and bilingual touch and keyboard players. Zero runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"puzzle",
|