@johnmorrisdotca/kazu 1.0.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 +29 -0
- package/LICENSE +21 -0
- package/README.md +568 -0
- package/dist/cells.d.ts +28 -0
- package/dist/cells.js +65 -0
- package/dist/check.d.ts +15 -0
- package/dist/check.js +189 -0
- package/dist/clock.d.ts +2 -0
- package/dist/clock.js +9 -0
- package/dist/conflicts.d.ts +14 -0
- package/dist/conflicts.js +79 -0
- package/dist/draw-entry.d.ts +14 -0
- package/dist/draw-entry.js +10 -0
- package/dist/draw.d.ts +47 -0
- package/dist/draw.js +219 -0
- package/dist/element-define.d.ts +1 -0
- package/dist/element-define.js +13 -0
- package/dist/element.d.ts +48 -0
- package/dist/element.js +150 -0
- package/dist/game.d.ts +65 -0
- package/dist/game.js +120 -0
- package/dist/generate.d.ts +11 -0
- package/dist/generate.js +35 -0
- package/dist/geometry.d.ts +33 -0
- package/dist/geometry.js +27 -0
- package/dist/givens.d.ts +30 -0
- package/dist/givens.js +43 -0
- package/dist/groupSolve.d.ts +68 -0
- package/dist/groupSolve.js +284 -0
- package/dist/hint.d.ts +39 -0
- package/dist/hint.js +158 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +31 -0
- package/dist/jigsaw.d.ts +21 -0
- package/dist/jigsaw.js +144 -0
- package/dist/kinds.d.ts +51 -0
- package/dist/kinds.js +40 -0
- package/dist/layout.d.ts +63 -0
- package/dist/layout.js +128 -0
- package/dist/moreOrLess.d.ts +7 -0
- package/dist/moreOrLess.js +90 -0
- package/dist/moreOrLessCode.d.ts +22 -0
- package/dist/moreOrLessCode.js +52 -0
- package/dist/moreOrLessSolve.d.ts +45 -0
- package/dist/moreOrLessSolve.js +180 -0
- package/dist/mount.d.ts +117 -0
- package/dist/mount.js +558 -0
- package/dist/names.d.ts +41 -0
- package/dist/names.js +163 -0
- package/dist/numberPlace.d.ts +14 -0
- package/dist/numberPlace.js +123 -0
- package/dist/play-entry.d.ts +9 -0
- package/dist/play-entry.js +8 -0
- package/dist/playStyle.d.ts +11 -0
- package/dist/playStyle.js +47 -0
- package/dist/progress.d.ts +29 -0
- package/dist/progress.js +96 -0
- package/dist/random.d.ts +25 -0
- package/dist/random.js +42 -0
- package/dist/solve.d.ts +22 -0
- package/dist/solve.js +60 -0
- package/dist/strings.d.ts +17 -0
- package/dist/strings.js +143 -0
- package/dist/style.d.ts +13 -0
- package/dist/style.js +61 -0
- package/dist/sumCages.d.ts +60 -0
- package/dist/sumCages.js +190 -0
- package/dist/towers.d.ts +3 -0
- package/dist/towers.js +48 -0
- package/dist/towersCode.d.ts +31 -0
- package/dist/towersCode.js +79 -0
- package/dist/towersSolve.d.ts +65 -0
- package/dist/towersSolve.js +276 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +104 -0
package/dist/layout.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH CELLS MUST HOLD EVERY NUMBER ONCE: the one thing that differs between Sudoku and the
|
|
3
|
+
* puzzles built on it.
|
|
4
|
+
*
|
|
5
|
+
* Classic Sudoku asks it of every row, column and box. Diagonal adds the two long diagonals.
|
|
6
|
+
* Jigsaw keeps the rows and columns and trades the boxes for irregular regions. Sum Cages adds
|
|
7
|
+
* cages, each also a group. The solver, the generator and the check read a layout rather than
|
|
8
|
+
* knowing which puzzle they are in, so a variant is a new list of groups and never a new solver.
|
|
9
|
+
*
|
|
10
|
+
* `region` is what the grid draws heavier rules between: the boxes, or the jigsaw's regions.
|
|
11
|
+
* `diagonal` says the diagonals are groups too, so the grid can shade them.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* A 9×9 has 3×3 boxes and a 4×4 has 2×2; a 6×6 has boxes two rows tall and three columns wide,
|
|
15
|
+
* which is the one people get wrong and the reason this is a table rather than a square root.
|
|
16
|
+
*/
|
|
17
|
+
export const KAZU_BOXES = {
|
|
18
|
+
4: { rows: 2, cols: 2 },
|
|
19
|
+
6: { rows: 2, cols: 3 },
|
|
20
|
+
9: { rows: 3, cols: 3 },
|
|
21
|
+
16: { rows: 4, cols: 4 },
|
|
22
|
+
};
|
|
23
|
+
/** The box a cell is in, numbered row-major from 0. */
|
|
24
|
+
export function boxOf(size, index) {
|
|
25
|
+
const boxes = KAZU_BOXES[size];
|
|
26
|
+
const row = Math.floor(index / size);
|
|
27
|
+
const col = index % size;
|
|
28
|
+
return Math.floor(row / boxes.rows) * (size / boxes.cols) + Math.floor(col / boxes.cols);
|
|
29
|
+
}
|
|
30
|
+
function build(size, region, regionWord, diagonal) {
|
|
31
|
+
const groups = [];
|
|
32
|
+
const add = (cells) => groups.push(cells);
|
|
33
|
+
for (let r = 0; r < size; r += 1)
|
|
34
|
+
add(Array.from({ length: size }, (_, c) => r * size + c));
|
|
35
|
+
for (let c = 0; c < size; c += 1)
|
|
36
|
+
add(Array.from({ length: size }, (_, r) => r * size + c));
|
|
37
|
+
for (let g = 0; g < size; g += 1)
|
|
38
|
+
add(region.flatMap((value, index) => (value === g ? [index] : [])));
|
|
39
|
+
if (diagonal) {
|
|
40
|
+
add(Array.from({ length: size }, (_, i) => i * size + i));
|
|
41
|
+
add(Array.from({ length: size }, (_, i) => i * size + (size - 1 - i)));
|
|
42
|
+
}
|
|
43
|
+
const groupsOf = Array.from({ length: size * size }, () => []);
|
|
44
|
+
groups.forEach((cells, g) => cells.forEach((index) => groupsOf[index].push(g)));
|
|
45
|
+
return { size, groups, groupsOf, region, regionWord, diagonal };
|
|
46
|
+
}
|
|
47
|
+
const CLASSIC = new Map();
|
|
48
|
+
/** Rows, columns and boxes; with `diagonal`, the two long diagonals as well. */
|
|
49
|
+
export function boxedLayout(size, diagonal = false) {
|
|
50
|
+
const key = `${size}:${diagonal}`;
|
|
51
|
+
const known = CLASSIC.get(key);
|
|
52
|
+
if (known !== undefined)
|
|
53
|
+
return known;
|
|
54
|
+
const layout = build(size, Array.from({ length: size * size }, (_, index) => boxOf(size, index)), "box", diagonal);
|
|
55
|
+
CLASSIC.set(key, layout);
|
|
56
|
+
return layout;
|
|
57
|
+
}
|
|
58
|
+
/** Rows, columns and the given regions, numbered 0..size-1, each `size` cells. */
|
|
59
|
+
export function regionLayout(size, region) {
|
|
60
|
+
return build(size, [...region], "region", false);
|
|
61
|
+
}
|
|
62
|
+
/** Rows, columns and boxes, and the cages over them, each cage a group of its own as well. */
|
|
63
|
+
export function cagedLayout(size, cages) {
|
|
64
|
+
const boxed = boxedLayout(size);
|
|
65
|
+
const groups = [...boxed.groups, ...cages.map((cage) => [...cage.cells])];
|
|
66
|
+
const groupsOf = Array.from({ length: size * size }, () => []);
|
|
67
|
+
groups.forEach((cells, g) => cells.forEach((index) => groupsOf[index].push(g)));
|
|
68
|
+
const cageOf = new Array(size * size).fill(-1);
|
|
69
|
+
cages.forEach((cage, c) => cage.cells.forEach((index) => (cageOf[index] = c)));
|
|
70
|
+
return {
|
|
71
|
+
...boxed,
|
|
72
|
+
groups,
|
|
73
|
+
groupsOf,
|
|
74
|
+
cages: cages.map((cage) => ({ cells: [...cage.cells], sum: cage.sum })),
|
|
75
|
+
cageOf,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** The cells sharing an edge with `index`. */
|
|
79
|
+
export function neighbours(size, index) {
|
|
80
|
+
const row = Math.floor(index / size);
|
|
81
|
+
const col = index % size;
|
|
82
|
+
const out = [];
|
|
83
|
+
if (row > 0)
|
|
84
|
+
out.push(index - size);
|
|
85
|
+
if (row < size - 1)
|
|
86
|
+
out.push(index + size);
|
|
87
|
+
if (col > 0)
|
|
88
|
+
out.push(index - 1);
|
|
89
|
+
if (col < size - 1)
|
|
90
|
+
out.push(index + 1);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether `region` divides a size×size grid into `size` regions of `size` cells each, every one of
|
|
95
|
+
* them joined edge to edge. O(cells): a check of a Jigsaw asks it of whatever regions it was sent.
|
|
96
|
+
*/
|
|
97
|
+
export function regionsAreSound(size, region) {
|
|
98
|
+
if (region.length !== size * size)
|
|
99
|
+
return false;
|
|
100
|
+
const counts = new Array(size).fill(0);
|
|
101
|
+
for (const value of region) {
|
|
102
|
+
if (!Number.isInteger(value) || value < 0 || value >= size)
|
|
103
|
+
return false;
|
|
104
|
+
counts[value] += 1;
|
|
105
|
+
}
|
|
106
|
+
if (counts.some((count) => count !== size))
|
|
107
|
+
return false;
|
|
108
|
+
const seen = new Array(size * size).fill(false);
|
|
109
|
+
for (let g = 0; g < size; g += 1) {
|
|
110
|
+
const start = region.indexOf(g);
|
|
111
|
+
const stack = [start];
|
|
112
|
+
seen[start] = true;
|
|
113
|
+
let reached = 0;
|
|
114
|
+
while (stack.length > 0) {
|
|
115
|
+
const index = stack.pop();
|
|
116
|
+
reached += 1;
|
|
117
|
+
for (const next of neighbours(size, index)) {
|
|
118
|
+
if (!seen[next] && region[next] === g) {
|
|
119
|
+
seen[next] = true;
|
|
120
|
+
stack.push(next);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (reached !== size)
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KazuLevel, KazuPuzzle } from "./kinds.ts";
|
|
2
|
+
import { type Grid } from "./moreOrLessSolve.ts";
|
|
3
|
+
import { type Random } from "./random.ts";
|
|
4
|
+
/** A seeded Latin square: every row and column holds 1..size once. Towers starts from one too. */
|
|
5
|
+
export declare function latinSquare(size: number, random: Random): Grid;
|
|
6
|
+
/** A Futoshiki (More or Less) of this side, level and seed: 4, 5, 6 or 7. */
|
|
7
|
+
export declare function generateMoreOrLess(size: number, level: KazuLevel, seed: number): KazuPuzzle;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { encodeCells } from "./cells.js";
|
|
2
|
+
import { encodeMoreOrLess } from "./moreOrLessCode.js";
|
|
3
|
+
import { countSolutions, guessDepth } from "./moreOrLessSolve.js";
|
|
4
|
+
import { seededRandom, shuffled } from "./random.js";
|
|
5
|
+
/**
|
|
6
|
+
* Making a Futoshiki (More or Less) puzzle from a seed.
|
|
7
|
+
*
|
|
8
|
+
* A seeded Latin square first; then marks on a third of the edges, chosen at random; then givens
|
|
9
|
+
* added one at a time until the solver counts one answer within the level's depth; then every
|
|
10
|
+
* given and every mark tried for removal, in a seeded order, keeping the removal only while the
|
|
11
|
+
* puzzle stays unique and within its level. What is left is the puzzle: nothing in it is there
|
|
12
|
+
* for decoration.
|
|
13
|
+
*
|
|
14
|
+
* Deterministic in the seed.
|
|
15
|
+
*/
|
|
16
|
+
const LEVELS = { easy: 0, medium: 1, hard: Infinity };
|
|
17
|
+
const MARK_SHARE = 0.35;
|
|
18
|
+
/** A seeded Latin square: every row and column holds 1..size once. Towers starts from one too. */
|
|
19
|
+
export function latinSquare(size, random) {
|
|
20
|
+
const grid = new Array(size * size).fill(0);
|
|
21
|
+
const values = Array.from({ length: size }, (_, i) => i + 1);
|
|
22
|
+
const rows = new Array(size).fill(0);
|
|
23
|
+
const cols = new Array(size).fill(0);
|
|
24
|
+
const fill = (index) => {
|
|
25
|
+
if (index === grid.length)
|
|
26
|
+
return true;
|
|
27
|
+
const row = Math.floor(index / size);
|
|
28
|
+
const col = index % size;
|
|
29
|
+
for (const value of shuffled(values, random)) {
|
|
30
|
+
const bit = 1 << value;
|
|
31
|
+
if ((rows[row] | cols[col]) & bit)
|
|
32
|
+
continue;
|
|
33
|
+
grid[index] = value;
|
|
34
|
+
rows[row] |= bit;
|
|
35
|
+
cols[col] |= bit;
|
|
36
|
+
if (fill(index + 1))
|
|
37
|
+
return true;
|
|
38
|
+
rows[row] &= ~bit;
|
|
39
|
+
cols[col] &= ~bit;
|
|
40
|
+
grid[index] = 0;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
fill(0);
|
|
45
|
+
return grid;
|
|
46
|
+
}
|
|
47
|
+
/** Every edge between two cells, as the mark the answer makes true on it. */
|
|
48
|
+
function everyMark(solution, size) {
|
|
49
|
+
const marks = [];
|
|
50
|
+
for (let index = 0; index < solution.length; index += 1) {
|
|
51
|
+
const col = index % size;
|
|
52
|
+
for (const other of [col < size - 1 ? index + 1 : -1, index + size < solution.length ? index + size : -1]) {
|
|
53
|
+
if (other === -1)
|
|
54
|
+
continue;
|
|
55
|
+
marks.push(solution[index] < solution[other] ? { less: index, more: other } : { less: other, more: index });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return marks;
|
|
59
|
+
}
|
|
60
|
+
/** A Futoshiki (More or Less) of this side, level and seed: 4, 5, 6 or 7. */
|
|
61
|
+
export function generateMoreOrLess(size, level, seed) {
|
|
62
|
+
const random = seededRandom(seed);
|
|
63
|
+
const solution = latinSquare(size, random);
|
|
64
|
+
const allowed = LEVELS[level];
|
|
65
|
+
// Unique, and within the level: a hard puzzle may be as deep as it likes, so its depth is never measured.
|
|
66
|
+
const fits = (grid, marks) => countSolutions(grid, size, marks, 2) === 1 && (allowed === Infinity || guessDepth(grid, size, marks, allowed) <= allowed);
|
|
67
|
+
let marks = shuffled(everyMark(solution, size), random).slice(0, Math.round(everyMark(solution, size).length * MARK_SHARE));
|
|
68
|
+
const givens = new Array(size * size).fill(0);
|
|
69
|
+
// Givens until it is a puzzle of the level asked for.
|
|
70
|
+
for (const index of shuffled(givens.map((_, i) => i), random)) {
|
|
71
|
+
if (fits(givens, marks))
|
|
72
|
+
break;
|
|
73
|
+
givens[index] = solution[index];
|
|
74
|
+
}
|
|
75
|
+
// Then nothing that is not needed: each given and each mark, in a seeded order.
|
|
76
|
+
for (const index of shuffled(givens.map((_, i) => i), random)) {
|
|
77
|
+
if (givens[index] === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const value = givens[index];
|
|
80
|
+
givens[index] = 0;
|
|
81
|
+
if (!fits(givens, marks))
|
|
82
|
+
givens[index] = value;
|
|
83
|
+
}
|
|
84
|
+
for (const mark of shuffled(marks, random)) {
|
|
85
|
+
const without = marks.filter((each) => each !== mark);
|
|
86
|
+
if (fits(givens, without))
|
|
87
|
+
marks = without;
|
|
88
|
+
}
|
|
89
|
+
return { kind: "more-or-less", size, level, seed, givens: encodeMoreOrLess(givens, marks, size), solution: encodeCells(solution) };
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* More or Less (Futoshiki) as a string: the cells, then the marks between them.
|
|
3
|
+
*
|
|
4
|
+
* The cells come first, row-major, as every number grid is written (`.` for
|
|
5
|
+
* empty). Then one character per edge between two cells: the horizontal
|
|
6
|
+
* edges row by row (`size − 1` per row), then the vertical edges row by row
|
|
7
|
+
* (`size` per row, `size − 1` rows). `<` and `>` say which side is bigger,
|
|
8
|
+
* the way the mark is drawn between the cells; `^` and `v` do the same for
|
|
9
|
+
* an edge between a cell and the one below it (`v` points down at the
|
|
10
|
+
* smaller number). `.` is no mark. A 7×7 is 49 + 42 + 42 characters.
|
|
11
|
+
*/
|
|
12
|
+
/** A mark: the cell at `less` holds a smaller number than the cell at `more`. Both are indexes, always adjacent. */
|
|
13
|
+
export type Mark = {
|
|
14
|
+
less: number;
|
|
15
|
+
more: number;
|
|
16
|
+
};
|
|
17
|
+
export declare const NO_MARK = ".";
|
|
18
|
+
export declare function encodeMoreOrLess(cells: readonly number[], marks: readonly Mark[], size: number): string;
|
|
19
|
+
export declare function decodeMoreOrLess(code: string, size: number): {
|
|
20
|
+
cells: number[];
|
|
21
|
+
marks: Mark[];
|
|
22
|
+
} | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { decodeCells, encodeCells } from "./cells.js";
|
|
2
|
+
export const NO_MARK = ".";
|
|
3
|
+
export function encodeMoreOrLess(cells, marks, size) {
|
|
4
|
+
const horizontal = new Array(size * (size - 1)).fill(NO_MARK);
|
|
5
|
+
const vertical = new Array((size - 1) * size).fill(NO_MARK);
|
|
6
|
+
for (const mark of marks) {
|
|
7
|
+
const low = Math.min(mark.less, mark.more);
|
|
8
|
+
const high = Math.max(mark.less, mark.more);
|
|
9
|
+
if (high === low + 1) {
|
|
10
|
+
const row = Math.floor(low / size);
|
|
11
|
+
const col = low % size;
|
|
12
|
+
horizontal[row * (size - 1) + col] = mark.less === low ? "<" : ">";
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
vertical[low] = mark.less === low ? "^" : "v";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return `${encodeCells(cells)}${horizontal.join("")}${vertical.join("")}`;
|
|
19
|
+
}
|
|
20
|
+
export function decodeMoreOrLess(code, size) {
|
|
21
|
+
const cellsLength = size * size;
|
|
22
|
+
const horizontalLength = size * (size - 1);
|
|
23
|
+
const verticalLength = (size - 1) * size;
|
|
24
|
+
if (typeof code !== "string" || code.length !== cellsLength + horizontalLength + verticalLength)
|
|
25
|
+
return null;
|
|
26
|
+
const cells = decodeCells(code.slice(0, cellsLength), size);
|
|
27
|
+
if (cells === null)
|
|
28
|
+
return null;
|
|
29
|
+
const marks = [];
|
|
30
|
+
const horizontal = code.slice(cellsLength, cellsLength + horizontalLength);
|
|
31
|
+
for (let i = 0; i < horizontal.length; i += 1) {
|
|
32
|
+
const row = Math.floor(i / (size - 1));
|
|
33
|
+
const col = i % (size - 1);
|
|
34
|
+
const left = row * size + col;
|
|
35
|
+
if (horizontal[i] === "<")
|
|
36
|
+
marks.push({ less: left, more: left + 1 });
|
|
37
|
+
else if (horizontal[i] === ">")
|
|
38
|
+
marks.push({ less: left + 1, more: left });
|
|
39
|
+
else if (horizontal[i] !== NO_MARK)
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const vertical = code.slice(cellsLength + horizontalLength);
|
|
43
|
+
for (let i = 0; i < vertical.length; i += 1) {
|
|
44
|
+
if (vertical[i] === "^")
|
|
45
|
+
marks.push({ less: i, more: i + size });
|
|
46
|
+
else if (vertical[i] === "v")
|
|
47
|
+
marks.push({ less: i + size, more: i });
|
|
48
|
+
else if (vertical[i] !== NO_MARK)
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return { cells, marks };
|
|
52
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Mark } from "./moreOrLessCode.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The Futoshiki solver (More or Less): counting, the reasoning a person does,
|
|
4
|
+
* and how deep a guess goes.
|
|
5
|
+
*
|
|
6
|
+
* A Latin square with marks: every row and column holds each number once,
|
|
7
|
+
* and each mark says which of two neighbouring cells is bigger. Candidates
|
|
8
|
+
* are bitmasks, bit `v` for value `v`, and a mark prunes both ends — the
|
|
9
|
+
* smaller side can hold nothing at or above the bigger side's largest
|
|
10
|
+
* candidate, and the other way round.
|
|
11
|
+
*/
|
|
12
|
+
export type Grid = number[];
|
|
13
|
+
/**
|
|
14
|
+
* How many answers the grid has, up to `limit`.
|
|
15
|
+
*
|
|
16
|
+
* Reasoning at every node (`applySingles`), then a branch on the cell with
|
|
17
|
+
* the fewest candidates. Plain backtracking with the marks checked after
|
|
18
|
+
* each assignment took twelve seconds to prove a hard 7×7 unique — a Latin
|
|
19
|
+
* square with few givens has a great many near-answers, and only the marks'
|
|
20
|
+
* pruning, applied as candidates are narrowed rather than after a value is
|
|
21
|
+
* placed, cuts them off early.
|
|
22
|
+
*/
|
|
23
|
+
export declare function countSolutions(grid: Grid, size: number, marks: readonly Mark[], limit?: number, first?: (answer: Grid) => void): number;
|
|
24
|
+
/** The one answer the marks allow, or null when they allow none or more than one: see `groupSolve.ts`'s `solutionOf`. */
|
|
25
|
+
export declare function solutionOf(grid: Grid, size: number, marks: readonly Mark[]): Grid | null;
|
|
26
|
+
export type SinglesResult = {
|
|
27
|
+
grid: Grid;
|
|
28
|
+
solved: boolean;
|
|
29
|
+
contradiction: boolean;
|
|
30
|
+
candidates: number[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* What a person can see without trying anything: the marks narrowing both
|
|
34
|
+
* ends, a cell with one candidate, a value with one place in its row or
|
|
35
|
+
* column. Runs to a fixpoint. Returns a new grid; the input is left alone.
|
|
36
|
+
*/
|
|
37
|
+
export declare function applySingles(grid: Grid, size: number, marks: readonly Mark[]): SinglesResult;
|
|
38
|
+
/**
|
|
39
|
+
* How many guesses, each followed by every single it lets loose, a solver
|
|
40
|
+
* needs: 0 when reasoning finishes it, `Infinity` when there is no answer —
|
|
41
|
+
* or when it needs more than `limit`, which is all a caller asking "is this
|
|
42
|
+
* within the level" needs to know, and what keeps a hard 7×7 from being
|
|
43
|
+
* searched to the bottom for an answer nobody asked for.
|
|
44
|
+
*/
|
|
45
|
+
export declare function guessDepth(grid: Grid, size: number, marks: readonly Mark[], limit?: number): number;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const ALL = (size) => (1 << (size + 1)) - 2;
|
|
2
|
+
function bitCount(mask) {
|
|
3
|
+
let count = 0;
|
|
4
|
+
for (let m = mask; m !== 0; m &= m - 1)
|
|
5
|
+
count += 1;
|
|
6
|
+
return count;
|
|
7
|
+
}
|
|
8
|
+
function lowestBit(mask) {
|
|
9
|
+
return 31 - Math.clz32(mask & -mask);
|
|
10
|
+
}
|
|
11
|
+
function highestBit(mask) {
|
|
12
|
+
return 31 - Math.clz32(mask);
|
|
13
|
+
}
|
|
14
|
+
/** Bits strictly below `value`, and strictly above it. */
|
|
15
|
+
const below = (value) => (1 << value) - 1;
|
|
16
|
+
const above = (value, size) => ALL(size) & ~((1 << (value + 1)) - 1);
|
|
17
|
+
/**
|
|
18
|
+
* How many answers the grid has, up to `limit`.
|
|
19
|
+
*
|
|
20
|
+
* Reasoning at every node (`applySingles`), then a branch on the cell with
|
|
21
|
+
* the fewest candidates. Plain backtracking with the marks checked after
|
|
22
|
+
* each assignment took twelve seconds to prove a hard 7×7 unique — a Latin
|
|
23
|
+
* square with few givens has a great many near-answers, and only the marks'
|
|
24
|
+
* pruning, applied as candidates are narrowed rather than after a value is
|
|
25
|
+
* placed, cuts them off early.
|
|
26
|
+
*/
|
|
27
|
+
export function countSolutions(grid, size, marks, limit = 2, first) {
|
|
28
|
+
let found = 0;
|
|
29
|
+
const step = (at) => {
|
|
30
|
+
if (found >= limit)
|
|
31
|
+
return;
|
|
32
|
+
const singles = applySingles(at, size, marks);
|
|
33
|
+
if (singles.contradiction)
|
|
34
|
+
return;
|
|
35
|
+
if (singles.solved) {
|
|
36
|
+
if (found === 0)
|
|
37
|
+
first?.(singles.grid);
|
|
38
|
+
found += 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let best = -1;
|
|
42
|
+
let bestCount = size + 1;
|
|
43
|
+
singles.candidates.forEach((mask, index) => {
|
|
44
|
+
if (singles.grid[index] !== 0)
|
|
45
|
+
return;
|
|
46
|
+
const count = bitCount(mask);
|
|
47
|
+
if (count < bestCount) {
|
|
48
|
+
best = index;
|
|
49
|
+
bestCount = count;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
for (let mask = singles.candidates[best]; mask !== 0; mask &= mask - 1) {
|
|
53
|
+
const next = [...singles.grid];
|
|
54
|
+
next[best] = lowestBit(mask);
|
|
55
|
+
step(next);
|
|
56
|
+
if (found >= limit)
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
step(grid);
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
/** The one answer the marks allow, or null when they allow none or more than one: see `groupSolve.ts`'s `solutionOf`. */
|
|
64
|
+
export function solutionOf(grid, size, marks) {
|
|
65
|
+
let answer = null;
|
|
66
|
+
return countSolutions(grid, size, marks, 2, (first) => (answer = [...first])) === 1 ? answer : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What a person can see without trying anything: the marks narrowing both
|
|
70
|
+
* ends, a cell with one candidate, a value with one place in its row or
|
|
71
|
+
* column. Runs to a fixpoint. Returns a new grid; the input is left alone.
|
|
72
|
+
*/
|
|
73
|
+
export function applySingles(grid, size, marks) {
|
|
74
|
+
const work = [...grid];
|
|
75
|
+
const candidates = work.map((value, index) => {
|
|
76
|
+
if (value !== 0)
|
|
77
|
+
return 1 << value;
|
|
78
|
+
let mask = ALL(size);
|
|
79
|
+
const row = Math.floor(index / size);
|
|
80
|
+
const col = index % size;
|
|
81
|
+
work.forEach((other, at) => {
|
|
82
|
+
if (other !== 0 && (Math.floor(at / size) === row || at % size === col))
|
|
83
|
+
mask &= ~(1 << other);
|
|
84
|
+
});
|
|
85
|
+
return mask;
|
|
86
|
+
});
|
|
87
|
+
const set = (index, value) => {
|
|
88
|
+
work[index] = value;
|
|
89
|
+
candidates[index] = 1 << value;
|
|
90
|
+
const row = Math.floor(index / size);
|
|
91
|
+
const col = index % size;
|
|
92
|
+
for (let at = 0; at < work.length; at += 1) {
|
|
93
|
+
if (at !== index && (Math.floor(at / size) === row || at % size === col))
|
|
94
|
+
candidates[at] &= ~(1 << value);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
let changed = true;
|
|
98
|
+
while (changed) {
|
|
99
|
+
changed = false;
|
|
100
|
+
for (const mark of marks) {
|
|
101
|
+
const lessMask = candidates[mark.less] & below(highestBit(candidates[mark.more]));
|
|
102
|
+
const moreMask = candidates[mark.more] & above(lowestBit(candidates[mark.less]), size);
|
|
103
|
+
if (lessMask !== candidates[mark.less]) {
|
|
104
|
+
candidates[mark.less] = lessMask;
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
if (moreMask !== candidates[mark.more]) {
|
|
108
|
+
candidates[mark.more] = moreMask;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (let index = 0; index < work.length; index += 1) {
|
|
113
|
+
if (candidates[index] === 0)
|
|
114
|
+
return { grid: work, solved: false, contradiction: true, candidates };
|
|
115
|
+
if (work[index] === 0 && bitCount(candidates[index]) === 1) {
|
|
116
|
+
set(index, lowestBit(candidates[index]));
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (let unit = 0; unit < size; unit += 1) {
|
|
121
|
+
for (let value = 1; value <= size; value += 1) {
|
|
122
|
+
const bit = 1 << value;
|
|
123
|
+
for (const kind of ["row", "col"]) {
|
|
124
|
+
let place = -1;
|
|
125
|
+
let places = 0;
|
|
126
|
+
for (let k = 0; k < size; k += 1) {
|
|
127
|
+
const index = kind === "row" ? unit * size + k : k * size + unit;
|
|
128
|
+
if ((candidates[index] & bit) !== 0) {
|
|
129
|
+
place = index;
|
|
130
|
+
places += 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (places === 0)
|
|
134
|
+
return { grid: work, solved: false, contradiction: true, candidates };
|
|
135
|
+
if (places === 1 && work[place] === 0) {
|
|
136
|
+
set(place, value);
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { grid: work, solved: work.every((value) => value !== 0), contradiction: false, candidates };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* How many guesses, each followed by every single it lets loose, a solver
|
|
147
|
+
* needs: 0 when reasoning finishes it, `Infinity` when there is no answer —
|
|
148
|
+
* or when it needs more than `limit`, which is all a caller asking "is this
|
|
149
|
+
* within the level" needs to know, and what keeps a hard 7×7 from being
|
|
150
|
+
* searched to the bottom for an answer nobody asked for.
|
|
151
|
+
*/
|
|
152
|
+
export function guessDepth(grid, size, marks, limit = Infinity) {
|
|
153
|
+
const singles = applySingles(grid, size, marks);
|
|
154
|
+
if (singles.contradiction)
|
|
155
|
+
return Infinity;
|
|
156
|
+
if (singles.solved)
|
|
157
|
+
return 0;
|
|
158
|
+
if (limit <= 0)
|
|
159
|
+
return Infinity;
|
|
160
|
+
let best = -1;
|
|
161
|
+
let bestCount = size + 1;
|
|
162
|
+
singles.candidates.forEach((mask, index) => {
|
|
163
|
+
if (singles.grid[index] !== 0)
|
|
164
|
+
return;
|
|
165
|
+
const count = bitCount(mask);
|
|
166
|
+
if (count < bestCount) {
|
|
167
|
+
best = index;
|
|
168
|
+
bestCount = count;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
let deepest = Infinity;
|
|
172
|
+
for (let mask = singles.candidates[best]; mask !== 0; mask &= mask - 1) {
|
|
173
|
+
const next = [...singles.grid];
|
|
174
|
+
next[best] = lowestBit(mask);
|
|
175
|
+
const depth = guessDepth(next, size, marks, limit - 1);
|
|
176
|
+
if (depth < deepest)
|
|
177
|
+
deepest = depth;
|
|
178
|
+
}
|
|
179
|
+
return deepest === Infinity ? Infinity : deepest + 1;
|
|
180
|
+
}
|
package/dist/mount.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type KazuGame } from "./game.ts";
|
|
2
|
+
import { type KazuHint } from "./hint.ts";
|
|
3
|
+
import type { KazuKind, KazuLevel } from "./kinds.ts";
|
|
4
|
+
import { type KazuLanguage } from "./strings.ts";
|
|
5
|
+
/**
|
|
6
|
+
* A PLAYABLE KAZU BOARD IN ANY PAGE: `mountKazu(host, options)` draws a puzzle into an element and plays
|
|
7
|
+
* it by touch, mouse and keyboard. Tap a cell and tap a number on the pad (or type it); tap the chosen
|
|
8
|
+
* cell again to step it on, 1, 2, 3 … and back to empty; turn Pencil on and the pad writes small notes
|
|
9
|
+
* instead; Undo takes the last change back; Hint says which cell to fill next and why; Check says how
|
|
10
|
+
* many are wrong, never which. A clock starts on the first entry and stops when the last cell is right.
|
|
11
|
+
*
|
|
12
|
+
* The keys: the arrows move, a number (1 to 9, and A to G on the 16×16) fills the chosen cell, Shift
|
|
13
|
+
* with a number writes it as a pencil mark, Backspace empties the cell, N turns Pencil on or off,
|
|
14
|
+
* Ctrl or Cmd with Z undoes, and Escape lets the cell go.
|
|
15
|
+
*
|
|
16
|
+
* What happens is told in events, on the host as DOM events and to the callbacks given: `kazu-change`
|
|
17
|
+
* for every change to the grid, `kazu-hint` for each hint, `kazu-check` for each check, and `kazu-solve`
|
|
18
|
+
* once, with the answer ready for `checkKazu`. Every button is also a method of the returned handle.
|
|
19
|
+
* The rules it plays by are `game.ts`'s, and the drawing is `drawKazu`'s: both are usable alone.
|
|
20
|
+
*
|
|
21
|
+
* Needs a page. Its words are English and Japanese and follow the page's `lang`.
|
|
22
|
+
*/
|
|
23
|
+
/** What every event tells of the board. */
|
|
24
|
+
export type KazuEventDetail = {
|
|
25
|
+
kind: KazuKind;
|
|
26
|
+
size: number;
|
|
27
|
+
level?: KazuLevel;
|
|
28
|
+
seed?: number;
|
|
29
|
+
/** What the player has written, as a run's code (`decodeRun` brings it back): to keep a puzzle half done. */
|
|
30
|
+
run: string;
|
|
31
|
+
/** The pencil marks, as a code (`decodeNotes`). Empty for none. */
|
|
32
|
+
notes: string;
|
|
33
|
+
/** The whole grid, printed numbers and the player's: what `checkKazu` takes as an answer. */
|
|
34
|
+
answer: string;
|
|
35
|
+
progress: {
|
|
36
|
+
filled: number;
|
|
37
|
+
total: number;
|
|
38
|
+
};
|
|
39
|
+
/** The time on the clock, in milliseconds. */
|
|
40
|
+
elapsedMs: number;
|
|
41
|
+
/** How many hints were taken, and how many times Check was pressed. A solve with any hint is a helped one. */
|
|
42
|
+
hints: number;
|
|
43
|
+
checks: number;
|
|
44
|
+
helped: boolean;
|
|
45
|
+
solved: boolean;
|
|
46
|
+
/** For `kazu-hint`: what the hint said. */
|
|
47
|
+
hint?: KazuHint;
|
|
48
|
+
};
|
|
49
|
+
export type KazuMountOptions = {
|
|
50
|
+
kind: KazuKind;
|
|
51
|
+
size: number;
|
|
52
|
+
/** The puzzle's givens code. */
|
|
53
|
+
givens: string;
|
|
54
|
+
/** The puzzle's one answer, as a cells code. Left out, it is worked out from the givens when Hint or Check needs it. */
|
|
55
|
+
solution?: string;
|
|
56
|
+
/** Which level and seed made it, to carry in the events. */
|
|
57
|
+
level?: KazuLevel;
|
|
58
|
+
seed?: number;
|
|
59
|
+
/** A run to start from: the entries of a puzzle half done (`decodeRun`'s code). */
|
|
60
|
+
run?: string;
|
|
61
|
+
/** Pencil marks to start from (`decodeNotes`'s code). */
|
|
62
|
+
notes?: string;
|
|
63
|
+
/** Milliseconds already on the clock, to carry on a puzzle kept half done. */
|
|
64
|
+
elapsed?: number;
|
|
65
|
+
/** Show the clock, which starts on the first entry. Default true. */
|
|
66
|
+
clock?: boolean;
|
|
67
|
+
/** The number pad and the buttons under the board. Default true. */
|
|
68
|
+
controls?: boolean;
|
|
69
|
+
/** What Hint does: `place` writes the number it found (default), `show` only points at the cell and says why, `off` takes the button away. */
|
|
70
|
+
hints?: "place" | "show" | "off";
|
|
71
|
+
/** What Check does: `count` says how many cells are wrong (default), `show` marks them too, `off` takes the button away. */
|
|
72
|
+
check?: "count" | "show" | "off";
|
|
73
|
+
/** Draw the cells that break a rule in red as they are made. Default true. */
|
|
74
|
+
conflicts?: boolean;
|
|
75
|
+
/** Wash the chosen cell's row, column and group, and the cells holding its number. Default true. */
|
|
76
|
+
peers?: boolean;
|
|
77
|
+
/** A number written takes itself out of the pencil marks of the cells it shares a group with. Default true. */
|
|
78
|
+
tidy?: boolean;
|
|
79
|
+
/** A tap on the chosen cell steps its number on. Default true. */
|
|
80
|
+
tapToStep?: boolean;
|
|
81
|
+
/** The language the words are in. Left out, the host's own `lang`, or the page's, and it follows the page's. */
|
|
82
|
+
language?: KazuLanguage;
|
|
83
|
+
onChange?: (detail: KazuEventDetail) => void;
|
|
84
|
+
onHint?: (detail: KazuEventDetail) => void;
|
|
85
|
+
onCheck?: (detail: KazuEventDetail) => void;
|
|
86
|
+
onSolve?: (detail: KazuEventDetail) => void;
|
|
87
|
+
};
|
|
88
|
+
/** Everything about how a mounted board plays that can change while it is on the page. */
|
|
89
|
+
export type KazuMountSettings = Pick<KazuMountOptions, "hints" | "check" | "conflicts" | "peers" | "tidy" | "tapToStep" | "language" | "clock" | "controls">;
|
|
90
|
+
export type KazuMount = {
|
|
91
|
+
readonly host: HTMLElement;
|
|
92
|
+
/** The game as it stands. */
|
|
93
|
+
game: () => KazuGame;
|
|
94
|
+
/** What the events would tell now. */
|
|
95
|
+
detail: () => KazuEventDetail;
|
|
96
|
+
/** Play another puzzle (or the same one again, fresh). `run`, `notes` and `elapsed` carry on a kept one. */
|
|
97
|
+
load: (puzzle: Pick<KazuMountOptions, "kind" | "size" | "givens" | "solution" | "level" | "seed" | "run" | "notes" | "elapsed">) => boolean;
|
|
98
|
+
/** Change how it plays: hints, check, conflicts, peers, tidy, tapToStep, language, clock, controls. */
|
|
99
|
+
set: (changes: KazuMountSettings) => void;
|
|
100
|
+
/** Choose a cell (or none, with null). */
|
|
101
|
+
select: (cell: number | null) => void;
|
|
102
|
+
/** Write a number into the chosen cell, or a pencil mark if Pencil is on; 0 empties the cell. */
|
|
103
|
+
enter: (value: number) => void;
|
|
104
|
+
/** Turn Pencil on or off. */
|
|
105
|
+
pencil: (on?: boolean) => void;
|
|
106
|
+
undo: () => void;
|
|
107
|
+
hint: () => void;
|
|
108
|
+
check: () => void;
|
|
109
|
+
/** Start again: every entry gone and the clock at nothing. */
|
|
110
|
+
restart: () => void;
|
|
111
|
+
/** Take the board down: its listeners, its timers and everything it put in the host. */
|
|
112
|
+
destroy: () => void;
|
|
113
|
+
};
|
|
114
|
+
/** Put the style in the page once: in the document's head, or in the shadow root the host is in. */
|
|
115
|
+
export declare function ensureKazuPlayStyle(host: Element): void;
|
|
116
|
+
/** Draw a puzzle into `host` and play it. Returns the handle that drives it, or null for givens that are not a puzzle of that kind and side. */
|
|
117
|
+
export declare function mountKazu(host: HTMLElement, options: KazuMountOptions): KazuMount | null;
|