@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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { hitoriNeighbors } from "./hitoriBoard.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hitori as variables: one per square, slot 0 for left white and slot 1 for shaded.
|
|
4
|
+
* `strength` is how much of the rules the pruning uses: 0 has the duplicates, the ban on touching shades, the
|
|
5
|
+
* pair and the sandwich; 1 adds that every white square must stay in reach of the others; 2 adds that a square
|
|
6
|
+
* whose shading would cut the whites in two must stay white. A finished answer must be one piece, which the
|
|
7
|
+
* search checks whatever the strength.
|
|
8
|
+
*/
|
|
9
|
+
export function hitoriModel(board, strength = 2) {
|
|
10
|
+
const size = board.size, total = size * size;
|
|
11
|
+
const neighbours = Array.from({ length: total }, (_, cell) => Int32Array.from(hitoriNeighbors(size, cell)));
|
|
12
|
+
const partners = Array.from({ length: total }, (_, cell) => {
|
|
13
|
+
const x = cell % size, y = Math.floor(cell / size), found = [];
|
|
14
|
+
for (let k = 0; k < size; k += 1) {
|
|
15
|
+
if (k !== x && board.numbers[y * size + k] === board.numbers[cell])
|
|
16
|
+
found.push(y * size + k);
|
|
17
|
+
if (k !== y && board.numbers[k * size + x] === board.numbers[cell])
|
|
18
|
+
found.push(k * size + x);
|
|
19
|
+
}
|
|
20
|
+
return Int32Array.from(found);
|
|
21
|
+
});
|
|
22
|
+
// A pair of equal numbers side by side shades every other equal number of that line; a number on both sides of a square keeps it white.
|
|
23
|
+
const pairs = [];
|
|
24
|
+
const sandwiches = [];
|
|
25
|
+
for (let y = 0; y < size; y += 1)
|
|
26
|
+
for (let x = 0; x < size; x += 1) {
|
|
27
|
+
const cell = y * size + x;
|
|
28
|
+
for (const [dx, dy] of [[1, 0], [0, 1]]) {
|
|
29
|
+
const nx = x + dx, ny = y + dy;
|
|
30
|
+
if (nx >= size || ny >= size)
|
|
31
|
+
continue;
|
|
32
|
+
const next = ny * size + nx;
|
|
33
|
+
if (board.numbers[cell] === board.numbers[next]) {
|
|
34
|
+
const others = partners[cell].filter(other => other !== next && (dx ? Math.floor(other / size) === y : other % size === x));
|
|
35
|
+
if (others.length)
|
|
36
|
+
pairs.push({ a: cell, b: next, others });
|
|
37
|
+
}
|
|
38
|
+
const fx = x + 2 * dx, fy = y + 2 * dy;
|
|
39
|
+
if (fx < size && fy < size && board.numbers[cell] === board.numbers[fy * size + fx])
|
|
40
|
+
sandwiches.push(next);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const starts = Int32Array.from({ length: total + 1 }, (_, i) => i * 2);
|
|
44
|
+
const disc = new Int32Array(total), low = new Int32Array(total), whitesBelow = new Int32Array(total);
|
|
45
|
+
const propagate = (alive) => {
|
|
46
|
+
let changed = true;
|
|
47
|
+
while (changed) {
|
|
48
|
+
changed = false;
|
|
49
|
+
for (let c = 0; c < total; c += 1) {
|
|
50
|
+
const white = alive[2 * c], shaded = alive[2 * c + 1];
|
|
51
|
+
if (!white && !shaded)
|
|
52
|
+
return false;
|
|
53
|
+
if (shaded && !white) {
|
|
54
|
+
const around = neighbours[c];
|
|
55
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
56
|
+
const n = around[k];
|
|
57
|
+
if (alive[2 * n + 1]) {
|
|
58
|
+
alive[2 * n + 1] = 0;
|
|
59
|
+
changed = true;
|
|
60
|
+
if (!alive[2 * n])
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (white && !shaded) {
|
|
66
|
+
const same = partners[c];
|
|
67
|
+
for (let k = 0; k < same.length; k += 1) {
|
|
68
|
+
const p = same[k];
|
|
69
|
+
if (alive[2 * p]) {
|
|
70
|
+
alive[2 * p] = 0;
|
|
71
|
+
changed = true;
|
|
72
|
+
if (!alive[2 * p + 1])
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (shaded) {
|
|
78
|
+
// A square is shaded only to settle a duplicate, so some equal number in its row or column must stay white.
|
|
79
|
+
const same = partners[c];
|
|
80
|
+
let free = 0, last = -1;
|
|
81
|
+
for (let k = 0; k < same.length; k += 1)
|
|
82
|
+
if (alive[2 * same[k]]) {
|
|
83
|
+
free += 1;
|
|
84
|
+
last = same[k];
|
|
85
|
+
}
|
|
86
|
+
if (free === 0) {
|
|
87
|
+
if (!white)
|
|
88
|
+
return false;
|
|
89
|
+
alive[2 * c + 1] = 0;
|
|
90
|
+
changed = true;
|
|
91
|
+
}
|
|
92
|
+
else if (free === 1 && !white && alive[2 * last + 1]) {
|
|
93
|
+
alive[2 * last + 1] = 0;
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const { a, b, others } of pairs) {
|
|
99
|
+
if (!alive[2 * a] && !alive[2 * b])
|
|
100
|
+
continue;
|
|
101
|
+
for (let k = 0; k < others.length; k += 1) {
|
|
102
|
+
const o = others[k];
|
|
103
|
+
if (alive[2 * o]) {
|
|
104
|
+
alive[2 * o] = 0;
|
|
105
|
+
changed = true;
|
|
106
|
+
if (!alive[2 * o + 1])
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (let k = 0; k < sandwiches.length; k += 1) {
|
|
112
|
+
const m = sandwiches[k];
|
|
113
|
+
if (alive[2 * m + 1]) {
|
|
114
|
+
alive[2 * m + 1] = 0;
|
|
115
|
+
changed = true;
|
|
116
|
+
if (!alive[2 * m])
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (strength > 0) {
|
|
121
|
+
const verdict = reach(alive);
|
|
122
|
+
if (verdict < 0)
|
|
123
|
+
return false;
|
|
124
|
+
if (verdict > 0)
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
};
|
|
130
|
+
const cutList = new Int32Array(total);
|
|
131
|
+
let clock = 0, cutCount = 0, whiteTotal = 0, current = new Uint8Array(0), power = 0;
|
|
132
|
+
const visit = (u, parent) => {
|
|
133
|
+
disc[u] = low[u] = ++clock;
|
|
134
|
+
let below = current[2 * u + 1] ? 0 : 1, cutWhites = 0, cutParts = 0;
|
|
135
|
+
const around = neighbours[u];
|
|
136
|
+
for (let k = 0; k < around.length; k += 1) {
|
|
137
|
+
const n = around[k];
|
|
138
|
+
if (!current[2 * n] || n === parent)
|
|
139
|
+
continue;
|
|
140
|
+
if (disc[n]) {
|
|
141
|
+
if (disc[n] < low[u])
|
|
142
|
+
low[u] = disc[n];
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
visit(n, u);
|
|
146
|
+
if (low[n] < low[u])
|
|
147
|
+
low[u] = low[n];
|
|
148
|
+
below += whitesBelow[n];
|
|
149
|
+
if (low[n] >= disc[u] && whitesBelow[n] > 0) {
|
|
150
|
+
cutParts += 1;
|
|
151
|
+
cutWhites += whitesBelow[n];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
whitesBelow[u] = below;
|
|
155
|
+
if (power > 1 && current[2 * u] && current[2 * u + 1] && cutParts + (whiteTotal - cutWhites > 0 ? 1 : 0) >= 2)
|
|
156
|
+
cutList[cutCount++] = u;
|
|
157
|
+
};
|
|
158
|
+
/** -1 when the white squares cannot be one piece, 1 when a square was forced white, 0 when nothing moved. */
|
|
159
|
+
const reach = (alive) => {
|
|
160
|
+
let root = -1;
|
|
161
|
+
whiteTotal = 0;
|
|
162
|
+
for (let c = 0; c < total; c += 1)
|
|
163
|
+
if (!alive[2 * c + 1]) {
|
|
164
|
+
whiteTotal += 1;
|
|
165
|
+
if (root < 0)
|
|
166
|
+
root = c;
|
|
167
|
+
}
|
|
168
|
+
if (root < 0)
|
|
169
|
+
return 0;
|
|
170
|
+
if (!alive[2 * root])
|
|
171
|
+
return -1;
|
|
172
|
+
disc.fill(0);
|
|
173
|
+
clock = 0;
|
|
174
|
+
cutCount = 0;
|
|
175
|
+
current = alive;
|
|
176
|
+
power = strength;
|
|
177
|
+
visit(root, -1);
|
|
178
|
+
let seenWhites = 0;
|
|
179
|
+
for (let c = 0; c < total; c += 1)
|
|
180
|
+
if (!alive[2 * c + 1] && disc[c])
|
|
181
|
+
seenWhites += 1;
|
|
182
|
+
if (seenWhites !== whiteTotal)
|
|
183
|
+
return -1;
|
|
184
|
+
for (let k = 0; k < cutCount; k += 1)
|
|
185
|
+
alive[2 * cutList[k] + 1] = 0;
|
|
186
|
+
return cutCount ? 1 : 0;
|
|
187
|
+
};
|
|
188
|
+
return { csp: { starts, propagate } };
|
|
189
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { checkHitori, isHitoriBoard } from "./hitoriBoard.js";
|
|
2
|
+
import { hitoriModel } from "./hitoriLogic.js";
|
|
3
|
+
import { logicCsp, openSlots, solveCsp } from "./csp.js";
|
|
4
|
+
/** Rates a board with one answer by solving it: with the plain rules, with the whites-stay-together rule, then by supposing. */
|
|
5
|
+
export function rateHitori(board) {
|
|
6
|
+
if (!isHitoriBoard(board))
|
|
7
|
+
throw new RangeError("Invalid Hitori board");
|
|
8
|
+
const strong = hitoriModel(board, 2).csp;
|
|
9
|
+
const proof = solveCsp(strong, openSlots(strong), 2, 300000);
|
|
10
|
+
if (proof.count !== 1 || proof.exhausted)
|
|
11
|
+
throw new RangeError("Hitori rating needs a board with one answer");
|
|
12
|
+
const shaded = Array.from({ length: board.size ** 2 }, (_, cell) => proof.solution[2 * cell + 1] === 1);
|
|
13
|
+
const solvedBy = (strength, depth) => {
|
|
14
|
+
const { csp } = hitoriModel(board, strength);
|
|
15
|
+
const run = logicCsp(csp, openSlots(csp), depth);
|
|
16
|
+
return { ...run, solved: run.solved && checkHitori(board, Array.from({ length: board.size ** 2 }, (_, cell) => run.alive[2 * cell + 1] === 1)).ok };
|
|
17
|
+
};
|
|
18
|
+
const plain = solvedBy(0, 0), reach = plain.solved ? plain : solvedBy(2, 0);
|
|
19
|
+
const probing = reach.solved ? reach : solvedBy(2, 1);
|
|
20
|
+
const repeats = board.numbers.filter((_, cell) => {
|
|
21
|
+
const x = cell % board.size, y = Math.floor(cell / board.size);
|
|
22
|
+
return board.numbers.some((other, at) => at !== cell && other === board.numbers[cell] && (at % board.size === x || Math.floor(at / board.size) === y));
|
|
23
|
+
}).length;
|
|
24
|
+
return {
|
|
25
|
+
depth: reach.solved ? 0 : probing.solved ? 1 : 2,
|
|
26
|
+
reach: !plain.solved && reach.solved,
|
|
27
|
+
probes: reach.solved ? 0 : probing.probes,
|
|
28
|
+
shaded: shaded.filter(Boolean).length,
|
|
29
|
+
shadedShare: shaded.filter(Boolean).length / shaded.length,
|
|
30
|
+
repeatShare: repeats / shaded.length,
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/hitoriSolve.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { HitoriBoard, HitoriSolve } from "./hitori.types.ts";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Counts distinct minimal shade patterns: an answer shades only squares that settle a duplicate, so extra shades
|
|
4
|
+
* are never a second answer. A non-complete result never asserts uniqueness.
|
|
5
|
+
*/
|
|
3
6
|
export declare function solveHitori(board: HitoriBoard, options?: {
|
|
4
7
|
limit?: number;
|
|
5
8
|
nodes?: number;
|