@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/fillominoSolve.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { FILLOMINO_MOST_NODES } from "./fillomino.constants.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isFillominoBoard } from "./fillominoBoard.js";
|
|
3
|
+
import { fillominoModel } from "./fillominoLogic.js";
|
|
4
|
+
import { decide, openSlots, solveCsp } from "./csp.js";
|
|
3
5
|
/** Counts completed partitions. `complete` is false when a bound, including the answer limit, stops search. */
|
|
4
6
|
export function solveFillomino(board, entries = board.givens, options = {}) {
|
|
5
7
|
if (!isFillominoBoard(board))
|
|
@@ -21,91 +23,16 @@ export function solveFillomino(board, entries = board.givens, options = {}) {
|
|
|
21
23
|
});
|
|
22
24
|
if (fixed.some(value => value < 0))
|
|
23
25
|
return { count: 0, solution: null, complete: true, nodes: 0 };
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
anchor = cell;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (anchor < 0) {
|
|
41
|
-
count += 1;
|
|
42
|
-
solution ?? (solution = [...resolved]);
|
|
43
|
-
if (count >= limit)
|
|
44
|
-
state.stopped = true;
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const choices = fixed[anchor] ? [fixed[anchor]] : Array.from({ length: entries.length }, (_, index) => index + 1);
|
|
48
|
-
for (const value of choices) {
|
|
49
|
-
const shapes = regionShapes(board, anchor, value, resolved, fixed, state, budget);
|
|
50
|
-
if (state.stopped)
|
|
51
|
-
return;
|
|
52
|
-
for (const shape of shapes) {
|
|
53
|
-
for (const cell of shape)
|
|
54
|
-
resolved[cell] = value;
|
|
55
|
-
visit();
|
|
56
|
-
for (const cell of shape)
|
|
57
|
-
resolved[cell] = 0;
|
|
58
|
-
if (state.stopped)
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
visit();
|
|
64
|
-
const complete = !state.stopped;
|
|
65
|
-
return { count, solution, complete, nodes: Math.min(state.nodes, budget) };
|
|
66
|
-
}
|
|
67
|
-
function regionShapes(board, anchor, area, resolved, fixed, state, budget) {
|
|
68
|
-
if (fixed[anchor] !== 0 && fixed[anchor] !== area)
|
|
69
|
-
return [];
|
|
70
|
-
const shapes = [];
|
|
71
|
-
const seen = new Set();
|
|
72
|
-
const grow = (cells) => {
|
|
73
|
-
if (state.stopped)
|
|
74
|
-
return;
|
|
75
|
-
if (++state.nodes > budget) {
|
|
76
|
-
state.stopped = true;
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const key = [...cells].sort((a, b) => a - b).join(",");
|
|
80
|
-
if (seen.has(key))
|
|
81
|
-
return;
|
|
82
|
-
seen.add(key);
|
|
83
|
-
if (cells.length === area) {
|
|
84
|
-
const members = new Set(cells);
|
|
85
|
-
for (const cell of cells) {
|
|
86
|
-
for (const next of fillominoNeighbours(board, cell)) {
|
|
87
|
-
if (members.has(next))
|
|
88
|
-
continue;
|
|
89
|
-
if (resolved[next] === area || fixed[next] === area)
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
shapes.push([...cells]);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const frontier = new Set();
|
|
97
|
-
for (const cell of cells) {
|
|
98
|
-
for (const next of fillominoNeighbours(board, cell)) {
|
|
99
|
-
if (cells.includes(next) || resolved[next] !== 0)
|
|
100
|
-
continue;
|
|
101
|
-
if (fixed[next] !== 0 && fixed[next] !== area)
|
|
102
|
-
continue;
|
|
103
|
-
frontier.add(next);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
for (const next of frontier)
|
|
107
|
-
grow([...cells, next]);
|
|
108
|
-
};
|
|
109
|
-
grow([anchor]);
|
|
110
|
-
return shapes;
|
|
26
|
+
const { csp, most } = fillominoModel(board, fixed);
|
|
27
|
+
const start = openSlots(csp);
|
|
28
|
+
fixed.forEach((value, cell) => { if (value)
|
|
29
|
+
decide(csp, start, cell, cell * most + value - 1); });
|
|
30
|
+
const found = solveCsp(csp, start, limit, budget);
|
|
31
|
+
const solution = found.solution ? Array.from({ length: fixed.length }, (_, cell) => {
|
|
32
|
+
for (let v = 0; v < most; v += 1)
|
|
33
|
+
if (found.solution[cell * most + v])
|
|
34
|
+
return v + 1;
|
|
35
|
+
return 0;
|
|
36
|
+
}) : null;
|
|
37
|
+
return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
|
|
111
38
|
}
|
package/dist/hitori-entry.d.ts
CHANGED
package/dist/hitori-entry.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export declare const HITORI_MAX_NODES = 300000;
|
|
2
|
-
|
|
2
|
+
/** The sides on offer; any side from 4 to `HITORI_MOST_SIDE` is a board `isHitoriBoard` takes. */
|
|
3
|
+
export declare const HITORI_SIZES: readonly [5, 6, 7, 8, 9, 10, 12];
|
|
4
|
+
export declare const HITORI_MOST_SIDE = 12;
|
|
5
|
+
export declare const HITORI_LEAST_SIDE = 4;
|
|
6
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateHitori`. */
|
|
7
|
+
export declare const HITORI_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
|
|
8
|
+
export declare const HITORI_MOST_ATTEMPTS = 600;
|
package/dist/hitori.constants.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export const HITORI_MAX_NODES = 300000;
|
|
2
|
-
|
|
2
|
+
/** The sides on offer; any side from 4 to `HITORI_MOST_SIDE` is a board `isHitoriBoard` takes. */
|
|
3
|
+
export const HITORI_SIZES = [5, 6, 7, 8, 9, 10, 12];
|
|
4
|
+
export const HITORI_MOST_SIDE = 12;
|
|
5
|
+
export const HITORI_LEAST_SIDE = 4;
|
|
6
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateHitori`. */
|
|
7
|
+
export const HITORI_LEVELS = ["easy", "medium", "hard", "extra-hard"];
|
|
8
|
+
export const HITORI_MOST_ATTEMPTS = 600;
|
package/dist/hitori.types.d.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
export type HitoriBoard = {
|
|
2
|
-
size:
|
|
2
|
+
size: number;
|
|
3
3
|
numbers: readonly number[];
|
|
4
4
|
};
|
|
5
|
+
export type HitoriLevel = "easy" | "medium" | "hard" | "extra-hard";
|
|
5
6
|
export type HitoriPuzzle = HitoriBoard & {
|
|
6
7
|
seed: number;
|
|
8
|
+
level: HitoriLevel;
|
|
7
9
|
solution: readonly boolean[];
|
|
8
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* How hard a board is, measured by solving it. `depth` 0 means the rules solve it, 1 that somebody has to suppose a
|
|
13
|
+
* square shaded or white and watch it break, 2 that more than that is needed. `reach` says the rule that the white
|
|
14
|
+
* squares stay in one piece was needed at depth 0. `probes` is the suppositions depth 1 needed.
|
|
15
|
+
*/
|
|
16
|
+
export type HitoriRating = {
|
|
17
|
+
depth: 0 | 1 | 2;
|
|
18
|
+
reach: boolean;
|
|
19
|
+
probes: number;
|
|
20
|
+
shaded: number;
|
|
21
|
+
/** Shaded squares as a share of the board. */
|
|
22
|
+
shadedShare: number;
|
|
23
|
+
/** Squares whose number repeats in their row or column, as a share of the board. */
|
|
24
|
+
repeatShare: number;
|
|
25
|
+
};
|
|
9
26
|
export type HitoriSolve = {
|
|
10
27
|
count: number;
|
|
11
28
|
solution: readonly boolean[] | null;
|
package/dist/hitoriBoard.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { HITORI_LEAST_SIDE, HITORI_MOST_SIDE } from "./hitori.constants.js";
|
|
1
2
|
export function isHitoriBoard(board) {
|
|
2
|
-
return !!board && (board.size
|
|
3
|
+
return !!board && Number.isInteger(board.size) && board.size >= HITORI_LEAST_SIDE && board.size <= HITORI_MOST_SIDE
|
|
3
4
|
&& Array.isArray(board.numbers)
|
|
4
5
|
&& board.numbers.length === board.size * board.size
|
|
5
6
|
&& board.numbers.every(value => Number.isInteger(value) && value >= 1 && value <= board.size);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HitoriBoard, HitoriLevel } from "./hitori.types.ts";
|
|
2
|
+
import type { Random } from "./random.ts";
|
|
3
|
+
export declare const SHARE: Record<HitoriLevel, number>;
|
|
4
|
+
/** A board with one answer, or null when the random choices did not lead to one. How the numbers are chosen leans on the level. */
|
|
5
|
+
export declare function candidateHitori(size: number, level: HitoriLevel, random: Random, base?: readonly number[]): {
|
|
6
|
+
board: HitoriBoard;
|
|
7
|
+
shaded: readonly boolean[];
|
|
8
|
+
} | null;
|
|
9
|
+
/** A random Latin square: every number once in each row and column, built row by row with backtracking. */
|
|
10
|
+
export declare function latinSquare(size: number, random: Random): number[] | null;
|
|
11
|
+
/** The same square with its rows, columns and numbers renamed at random: still one of each in every line. */
|
|
12
|
+
export declare function isotope(square: readonly number[], size: number, random: Random): number[];
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { hitoriNeighbors } from "./hitoriBoard.js";
|
|
2
|
+
import { hitoriModel } from "./hitoriLogic.js";
|
|
3
|
+
import { countCsp, openSlots } from "./csp.js";
|
|
4
|
+
import { shuffled } from "./random.js";
|
|
5
|
+
export const SHARE = { easy: .26, medium: .27, hard: .28, "extra-hard": .3 };
|
|
6
|
+
/** A board with one answer, or null when the random choices did not lead to one. How the numbers are chosen leans on the level. */
|
|
7
|
+
export function candidateHitori(size, level, random, base = latinSquare(size, random) ?? []) {
|
|
8
|
+
const total = size * size;
|
|
9
|
+
if (base.length !== total)
|
|
10
|
+
return null;
|
|
11
|
+
const target = Math.round(total * SHARE[level] * (.9 + random() * .2));
|
|
12
|
+
const shaded = Array(total).fill(false);
|
|
13
|
+
let count = 0;
|
|
14
|
+
const around = Array.from({ length: total }, (_, cell) => hitoriNeighbors(size, cell));
|
|
15
|
+
const mark = new Int32Array(total), stack = new Int32Array(total);
|
|
16
|
+
let stamp = 0;
|
|
17
|
+
const connected = () => {
|
|
18
|
+
const first = shaded.indexOf(false);
|
|
19
|
+
stamp += 1;
|
|
20
|
+
let top = 0, reached = 1;
|
|
21
|
+
stack[top++] = first;
|
|
22
|
+
mark[first] = stamp;
|
|
23
|
+
while (top)
|
|
24
|
+
for (const next of around[stack[--top]])
|
|
25
|
+
if (!shaded[next] && mark[next] !== stamp) {
|
|
26
|
+
mark[next] = stamp;
|
|
27
|
+
reached += 1;
|
|
28
|
+
stack[top++] = next;
|
|
29
|
+
}
|
|
30
|
+
return reached === total - count;
|
|
31
|
+
};
|
|
32
|
+
for (const cell of shuffled(Array.from({ length: total }, (_, i) => i), random)) {
|
|
33
|
+
if (count >= target)
|
|
34
|
+
break;
|
|
35
|
+
if (around[cell].some(next => shaded[next]))
|
|
36
|
+
continue;
|
|
37
|
+
shaded[cell] = true;
|
|
38
|
+
count += 1;
|
|
39
|
+
if (!connected()) {
|
|
40
|
+
shaded[cell] = false;
|
|
41
|
+
count -= 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (count < Math.max(2, target - 2))
|
|
45
|
+
return null;
|
|
46
|
+
const numbers = isotope(base, size, random).map((n, cell) => shaded[cell] ? 0 : n);
|
|
47
|
+
const partnersOf = (cell) => {
|
|
48
|
+
const x = cell % size, y = Math.floor(cell / size), found = [];
|
|
49
|
+
for (let k = 0; k < size; k += 1) {
|
|
50
|
+
if (k !== x && !shaded[y * size + k])
|
|
51
|
+
found.push(y * size + k);
|
|
52
|
+
if (k !== y && !shaded[k * size + x])
|
|
53
|
+
found.push(k * size + x);
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
};
|
|
57
|
+
const blacks = shaded.flatMap((on, cell) => on ? [cell] : []);
|
|
58
|
+
for (const cell of blacks) {
|
|
59
|
+
const options = partnersOf(cell);
|
|
60
|
+
if (!options.length)
|
|
61
|
+
return null;
|
|
62
|
+
numbers[cell] = numbers[options[Math.floor(random() * options.length)]];
|
|
63
|
+
}
|
|
64
|
+
const board = { size, numbers };
|
|
65
|
+
const { csp } = hitoriModel(board, 2);
|
|
66
|
+
let proof = countCsp(csp, openSlots(csp), 2, 20000);
|
|
67
|
+
for (let repair = 0; repair < 60 && proof.count > 1 && !proof.exhausted; repair += 1) {
|
|
68
|
+
const other = proof.solutions[1].slice(), mine = (alive, cell) => alive[2 * cell + 1] === 1;
|
|
69
|
+
const candidates = shuffled(blacks.filter(cell => !mine(other, cell)), random);
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const cell of candidates) {
|
|
72
|
+
const keepers = partnersOf(cell).filter(p => !mine(other, p));
|
|
73
|
+
if (!keepers.length)
|
|
74
|
+
continue;
|
|
75
|
+
numbers[cell] = numbers[keepers[Math.floor(random() * keepers.length)]];
|
|
76
|
+
changed = true;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (!changed)
|
|
80
|
+
return null;
|
|
81
|
+
proof = countCsp(csp, openSlots(csp), 2, 20000);
|
|
82
|
+
}
|
|
83
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
84
|
+
return null;
|
|
85
|
+
return { board, shaded };
|
|
86
|
+
}
|
|
87
|
+
/** A random Latin square: every number once in each row and column, built row by row with backtracking. */
|
|
88
|
+
export function latinSquare(size, random) {
|
|
89
|
+
const cells = Array(size * size).fill(0);
|
|
90
|
+
let steps = 0;
|
|
91
|
+
const fill = (at) => {
|
|
92
|
+
if (at === cells.length)
|
|
93
|
+
return true;
|
|
94
|
+
if (++steps > 20000)
|
|
95
|
+
return false;
|
|
96
|
+
const x = at % size, y = Math.floor(at / size);
|
|
97
|
+
const used = new Set();
|
|
98
|
+
for (let k = 0; k < x; k += 1)
|
|
99
|
+
used.add(cells[y * size + k]);
|
|
100
|
+
for (let k = 0; k < y; k += 1)
|
|
101
|
+
used.add(cells[k * size + x]);
|
|
102
|
+
for (const n of shuffled(Array.from({ length: size }, (_, i) => i + 1).filter(i => !used.has(i)), random)) {
|
|
103
|
+
cells[at] = n;
|
|
104
|
+
if (fill(at + 1))
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
cells[at] = 0;
|
|
108
|
+
return false;
|
|
109
|
+
};
|
|
110
|
+
return fill(0) ? cells : null;
|
|
111
|
+
}
|
|
112
|
+
/** The same square with its rows, columns and numbers renamed at random: still one of each in every line. */
|
|
113
|
+
export function isotope(square, size, random) {
|
|
114
|
+
const rows = shuffled(Array.from({ length: size }, (_, i) => i), random), cols = shuffled(Array.from({ length: size }, (_, i) => i), random);
|
|
115
|
+
const names = shuffled(Array.from({ length: size }, (_, i) => i + 1), random);
|
|
116
|
+
return Array.from({ length: size * size }, (_, cell) => names[square[rows[Math.floor(cell / size)] * size + cols[cell % size]] - 1]);
|
|
117
|
+
}
|
package/dist/hitoriGenerate.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import type { HitoriPuzzle } from "./hitori.types.ts";
|
|
2
|
-
/**
|
|
3
|
-
|
|
1
|
+
import type { HitoriLevel, HitoriPuzzle } from "./hitori.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Makes a seeded puzzle and proves its answer is the only one. The shaded squares are chosen first (never touching,
|
|
4
|
+
* the rest in one piece), the white squares get numbers that repeat nowhere in their row or column, and every shaded
|
|
5
|
+
* square takes a number that a white square in its row or column already has. Boards with more than one answer are
|
|
6
|
+
* repaired by renumbering a shaded square so that the other answer repeats a number, and the result is rated:
|
|
7
|
+
* easy by the plain rules, medium once the whites must stay in one piece, hard by supposing, and extra-hard the
|
|
8
|
+
* most-supposing of several boards.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateHitori(size?: number, seed?: number, level?: HitoriLevel): HitoriPuzzle;
|
package/dist/hitoriGenerate.js
CHANGED
|
@@ -1,163 +1,65 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HITORI_LEAST_SIDE, HITORI_LEVELS, HITORI_MOST_ATTEMPTS, HITORI_MOST_SIDE } from "./hitori.constants.js";
|
|
2
2
|
import { checkHitori } from "./hitoriBoard.js";
|
|
3
|
+
import { candidateHitori, latinSquare } from "./hitoriBuild.js";
|
|
4
|
+
import { hitoriModel } from "./hitoriLogic.js";
|
|
3
5
|
import { solveHitori } from "./hitoriSolve.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
numbers: [
|
|
20
|
-
1, 2, 3, 4, 5, 2, 3,
|
|
21
|
-
4, 1, 1, 3, 4, 5, 1,
|
|
22
|
-
2, 3, 5, 1, 4, 3, 5,
|
|
23
|
-
1, 2, 3, 4,
|
|
24
|
-
],
|
|
25
|
-
shade: [
|
|
26
|
-
false, false, false, false, false, false, false,
|
|
27
|
-
false, true, false, false, false, false, false,
|
|
28
|
-
false, true, false, false, true, false, false,
|
|
29
|
-
false, false, false, false,
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
numbers: [
|
|
34
|
-
1, 4, 3, 4, 1, 2, 3,
|
|
35
|
-
4, 5, 1, 3, 4, 5, 1,
|
|
36
|
-
2, 4, 5, 1, 2, 3, 5,
|
|
37
|
-
1, 2, 3, 4,
|
|
38
|
-
],
|
|
39
|
-
shade: [
|
|
40
|
-
false, true, false, false, true, false, false,
|
|
41
|
-
false, false, false, false, false, false, false,
|
|
42
|
-
false, false, false, false, false, false, false,
|
|
43
|
-
false, false, false, false,
|
|
44
|
-
],
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
numbers: [
|
|
48
|
-
1, 4, 3, 4, 5, 2, 3,
|
|
49
|
-
2, 5, 1, 2, 4, 5, 1,
|
|
50
|
-
2, 4, 5, 1, 2, 3, 5,
|
|
51
|
-
5, 2, 3, 4,
|
|
52
|
-
],
|
|
53
|
-
shade: [
|
|
54
|
-
false, true, false, false, false, false, false,
|
|
55
|
-
true, false, false, true, false, false, false,
|
|
56
|
-
false, false, false, false, false, false, false,
|
|
57
|
-
true, false, false, false,
|
|
58
|
-
],
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
7: [
|
|
62
|
-
{
|
|
63
|
-
numbers: [
|
|
64
|
-
1, 2, 3, 4, 2, 6, 7,
|
|
65
|
-
2, 6, 4, 5, 6, 7, 1,
|
|
66
|
-
3, 4, 5, 6, 1, 1, 1,
|
|
67
|
-
4, 5, 6, 7, 1, 2, 3,
|
|
68
|
-
5, 6, 7, 1, 2, 3, 4,
|
|
69
|
-
6, 7, 1, 2, 3, 4, 5,
|
|
70
|
-
7, 1, 2, 3, 4, 5, 6,
|
|
71
|
-
],
|
|
72
|
-
shade: [
|
|
73
|
-
false, false, false, false, true, false, false,
|
|
74
|
-
false, true, false, false, false, false, false,
|
|
75
|
-
false, false, false, false, true, false, true,
|
|
76
|
-
...Array(28).fill(false),
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
numbers: [
|
|
81
|
-
1, 2, 4, 4, 5, 7, 7,
|
|
82
|
-
2, 3, 4, 5, 5, 7, 1,
|
|
83
|
-
3, 4, 5, 6, 7, 1, 2,
|
|
84
|
-
4, 5, 6, 7, 1, 2, 7,
|
|
85
|
-
5, 6, 7, 1, 2, 3, 4,
|
|
86
|
-
6, 2, 1, 2, 3, 4, 5,
|
|
87
|
-
7, 1, 2, 3, 7, 5, 6,
|
|
88
|
-
],
|
|
89
|
-
shade: [
|
|
90
|
-
false, false, true, false, false, true, false,
|
|
91
|
-
false, false, false, false, true, false, false,
|
|
92
|
-
false, false, false, false, false, false, false,
|
|
93
|
-
false, false, false, false, false, false, true,
|
|
94
|
-
false, false, false, false, false, false, false,
|
|
95
|
-
false, true, false, false, false, false, false,
|
|
96
|
-
false, false, false, false, true, false, false,
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
numbers: [
|
|
101
|
-
1, 2, 3, 4, 5, 6, 7,
|
|
102
|
-
2, 3, 4, 5, 6, 7, 1,
|
|
103
|
-
3, 4, 5, 6, 1, 1, 6,
|
|
104
|
-
4, 6, 6, 6, 1, 2, 3,
|
|
105
|
-
5, 6, 7, 1, 2, 3, 4,
|
|
106
|
-
6, 7, 1, 2, 3, 4, 5,
|
|
107
|
-
7, 1, 2, 3, 4, 5, 6,
|
|
108
|
-
],
|
|
109
|
-
shade: [
|
|
110
|
-
false, false, false, false, false, false, false,
|
|
111
|
-
false, false, false, false, false, false, false,
|
|
112
|
-
false, false, false, false, true, false, true,
|
|
113
|
-
false, true, false, true, false, false, false,
|
|
114
|
-
false, false, false, false, false, false, false,
|
|
115
|
-
false, ...Array(13).fill(false),
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
numbers: [
|
|
120
|
-
1, 2, 3, 4, 6, 6, 7,
|
|
121
|
-
2, 2, 4, 5, 6, 2, 1,
|
|
122
|
-
3, 4, 5, 6, 7, 1, 2,
|
|
123
|
-
4, 5, 6, 7, 1, 2, 3,
|
|
124
|
-
5, 6, 7, 1, 2, 3, 4,
|
|
125
|
-
6, 7, 1, 2, 3, 4, 5,
|
|
126
|
-
7, 1, 2, 3, 6, 5, 6,
|
|
127
|
-
],
|
|
128
|
-
shade: [
|
|
129
|
-
false, false, false, false, true, false, false,
|
|
130
|
-
false, true, false, false, false, true, false,
|
|
131
|
-
false, false, false, false, false, false, false,
|
|
132
|
-
false, false, false, false, false, false, false,
|
|
133
|
-
false, false, false, false, false, false, false,
|
|
134
|
-
false, false, false, false, false, false, false,
|
|
135
|
-
false, false, false, false, true, false, false,
|
|
136
|
-
],
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
};
|
|
140
|
-
/** Seeded number relabelings and board symmetries of original layouts, accepted only after a uniqueness proof. */
|
|
141
|
-
export function generateHitori(size = 5, seed = 1) {
|
|
142
|
-
if (!HITORI_SIZES.includes(size) || !isKazuSeed(seed))
|
|
6
|
+
import { logicCsp, openSlots } from "./csp.js";
|
|
7
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
8
|
+
/**
|
|
9
|
+
* Makes a seeded puzzle and proves its answer is the only one. The shaded squares are chosen first (never touching,
|
|
10
|
+
* the rest in one piece), the white squares get numbers that repeat nowhere in their row or column, and every shaded
|
|
11
|
+
* square takes a number that a white square in its row or column already has. Boards with more than one answer are
|
|
12
|
+
* repaired by renumbering a shaded square so that the other answer repeats a number, and the result is rated:
|
|
13
|
+
* easy by the plain rules, medium once the whites must stay in one piece, hard by supposing, and extra-hard the
|
|
14
|
+
* most-supposing of several boards.
|
|
15
|
+
*/
|
|
16
|
+
export function generateHitori(size = 5, seed = 1, level = "medium") {
|
|
17
|
+
if (!Number.isInteger(size) || size < HITORI_LEAST_SIDE || size > HITORI_MOST_SIDE || !isKazuSeed(seed) || !HITORI_LEVELS.includes(level)) {
|
|
143
18
|
throw new RangeError("Invalid Hitori settings");
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
for (let
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
19
|
+
}
|
|
20
|
+
const random = seededRandom(seed);
|
|
21
|
+
const base = latinSquare(size, random);
|
|
22
|
+
if (!base)
|
|
23
|
+
throw new Error("No Hitori found within the generation budget; try another seed");
|
|
24
|
+
for (let aim = HITORI_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
25
|
+
const aimed = HITORI_LEVELS[aim];
|
|
26
|
+
const wanted = aimed === "extra-hard" ? 4 : 1;
|
|
27
|
+
let best = null, found = 0;
|
|
28
|
+
for (let attempt = 0; attempt < HITORI_MOST_ATTEMPTS && found < wanted; attempt += 1) {
|
|
29
|
+
const built = candidateHitori(size, aimed, random, base);
|
|
30
|
+
const made = built && rated(built.board, built.shaded, aimed);
|
|
31
|
+
if (!made)
|
|
32
|
+
continue;
|
|
33
|
+
found += 1;
|
|
34
|
+
if (!best || made.score > best.score)
|
|
35
|
+
best = made;
|
|
36
|
+
}
|
|
37
|
+
if (best) {
|
|
38
|
+
const proof = solveHitori(best.board);
|
|
39
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkHitori(best.board, proof.solution).ok) {
|
|
40
|
+
return { ...best.board, seed, level, solution: proof.solution };
|
|
41
|
+
}
|
|
157
42
|
}
|
|
158
|
-
const board = { size, numbers }, proof = solveHitori(board, { nodes: HITORI_MAX_NODES });
|
|
159
|
-
if (!checkHitori(board, solution).ok || !proof.complete || proof.count !== 1 || !proof.solution) {
|
|
160
|
-
throw new Error("No uniquely solvable Hitori found within the generation budget; try another seed");
|
|
161
43
|
}
|
|
162
|
-
|
|
44
|
+
throw new Error("No uniquely solvable Hitori found within the generation budget; try another seed");
|
|
45
|
+
}
|
|
46
|
+
function rated(board, shaded, level) {
|
|
47
|
+
const total = board.size * board.size;
|
|
48
|
+
const solved = (strength, depth) => {
|
|
49
|
+
const { csp } = hitoriModel(board, strength);
|
|
50
|
+
const run = logicCsp(csp, openSlots(csp), depth);
|
|
51
|
+
return run.solved && checkHitori(board, Array.from({ length: total }, (_, cell) => run.alive[2 * cell + 1] === 1)).ok ? run : null;
|
|
52
|
+
};
|
|
53
|
+
if (level === "easy")
|
|
54
|
+
return solved(0, 0) ? { board, shaded, score: 0 } : null;
|
|
55
|
+
if (solved(0, 0))
|
|
56
|
+
return null;
|
|
57
|
+
if (level === "medium")
|
|
58
|
+
return solved(2, 0) ? { board, shaded, score: 0 } : null;
|
|
59
|
+
if (solved(2, 0))
|
|
60
|
+
return null;
|
|
61
|
+
const probing = solved(2, 1);
|
|
62
|
+
if (!probing)
|
|
63
|
+
return level === "extra-hard" ? { board, shaded, score: 1000 } : null;
|
|
64
|
+
return { board, shaded, score: probing.probes };
|
|
163
65
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { HitoriBoard } from "./hitori.types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Hitori as variables: one per square, slot 0 for left white and slot 1 for shaded.
|
|
5
|
+
* `strength` is how much of the rules the pruning uses: 0 has the duplicates, the ban on touching shades, the
|
|
6
|
+
* pair and the sandwich; 1 adds that every white square must stay in reach of the others; 2 adds that a square
|
|
7
|
+
* whose shading would cut the whites in two must stay white. A finished answer must be one piece, which the
|
|
8
|
+
* search checks whatever the strength.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hitoriModel(board: HitoriBoard, strength?: 0 | 1 | 2): {
|
|
11
|
+
csp: Csp;
|
|
12
|
+
};
|