@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +568 -0
  4. package/dist/cells.d.ts +28 -0
  5. package/dist/cells.js +65 -0
  6. package/dist/check.d.ts +15 -0
  7. package/dist/check.js +189 -0
  8. package/dist/clock.d.ts +2 -0
  9. package/dist/clock.js +9 -0
  10. package/dist/conflicts.d.ts +14 -0
  11. package/dist/conflicts.js +79 -0
  12. package/dist/draw-entry.d.ts +14 -0
  13. package/dist/draw-entry.js +10 -0
  14. package/dist/draw.d.ts +47 -0
  15. package/dist/draw.js +219 -0
  16. package/dist/element-define.d.ts +1 -0
  17. package/dist/element-define.js +13 -0
  18. package/dist/element.d.ts +48 -0
  19. package/dist/element.js +150 -0
  20. package/dist/game.d.ts +65 -0
  21. package/dist/game.js +120 -0
  22. package/dist/generate.d.ts +11 -0
  23. package/dist/generate.js +35 -0
  24. package/dist/geometry.d.ts +33 -0
  25. package/dist/geometry.js +27 -0
  26. package/dist/givens.d.ts +30 -0
  27. package/dist/givens.js +43 -0
  28. package/dist/groupSolve.d.ts +68 -0
  29. package/dist/groupSolve.js +284 -0
  30. package/dist/hint.d.ts +39 -0
  31. package/dist/hint.js +158 -0
  32. package/dist/index.d.ts +36 -0
  33. package/dist/index.js +31 -0
  34. package/dist/jigsaw.d.ts +21 -0
  35. package/dist/jigsaw.js +144 -0
  36. package/dist/kinds.d.ts +51 -0
  37. package/dist/kinds.js +40 -0
  38. package/dist/layout.d.ts +63 -0
  39. package/dist/layout.js +128 -0
  40. package/dist/moreOrLess.d.ts +7 -0
  41. package/dist/moreOrLess.js +90 -0
  42. package/dist/moreOrLessCode.d.ts +22 -0
  43. package/dist/moreOrLessCode.js +52 -0
  44. package/dist/moreOrLessSolve.d.ts +45 -0
  45. package/dist/moreOrLessSolve.js +180 -0
  46. package/dist/mount.d.ts +117 -0
  47. package/dist/mount.js +558 -0
  48. package/dist/names.d.ts +41 -0
  49. package/dist/names.js +163 -0
  50. package/dist/numberPlace.d.ts +14 -0
  51. package/dist/numberPlace.js +123 -0
  52. package/dist/play-entry.d.ts +9 -0
  53. package/dist/play-entry.js +8 -0
  54. package/dist/playStyle.d.ts +11 -0
  55. package/dist/playStyle.js +47 -0
  56. package/dist/progress.d.ts +29 -0
  57. package/dist/progress.js +96 -0
  58. package/dist/random.d.ts +25 -0
  59. package/dist/random.js +42 -0
  60. package/dist/solve.d.ts +22 -0
  61. package/dist/solve.js +60 -0
  62. package/dist/strings.d.ts +17 -0
  63. package/dist/strings.js +143 -0
  64. package/dist/style.d.ts +13 -0
  65. package/dist/style.js +61 -0
  66. package/dist/sumCages.d.ts +60 -0
  67. package/dist/sumCages.js +190 -0
  68. package/dist/towers.d.ts +3 -0
  69. package/dist/towers.js +48 -0
  70. package/dist/towersCode.d.ts +31 -0
  71. package/dist/towersCode.js +79 -0
  72. package/dist/towersSolve.d.ts +65 -0
  73. package/dist/towersSolve.js +276 -0
  74. package/dist/version.d.ts +2 -0
  75. package/dist/version.js +2 -0
  76. package/package.json +104 -0
package/dist/hint.js ADDED
@@ -0,0 +1,158 @@
1
+ import { decodeCells } from "./cells.js";
2
+ import { layoutOfGivens, readGivens } from "./givens.js";
3
+ import { bitCount, candidatesAt, lowestBit, used } from "./groupSolve.js";
4
+ import { solveKazu } from "./solve.js";
5
+ import { cluedLines, narrowEdges } from "./towersSolve.js";
6
+ const ALL = (size) => (1 << (size + 1)) - 2;
7
+ /** The type and number of the group at a place in a layout's list of groups (`layout.ts`'s order): only the groups that hold every number, never a cage. */
8
+ function groupInfo(givens, g) {
9
+ const { size } = givens;
10
+ if (g < size)
11
+ return { type: "row", index: g };
12
+ if (g < 2 * size)
13
+ return { type: "column", index: g - size };
14
+ if (givens.regions !== null && g < 3 * size)
15
+ return { type: givens.kind === "jigsaw" ? "region" : "box", index: g - 2 * size };
16
+ return { type: "diagonal", index: g - 3 * size };
17
+ }
18
+ /** Candidate masks for every cell of a grid, and which rule beyond the groups narrowed each. */
19
+ function candidatesFor(givens, grid) {
20
+ const { size } = givens;
21
+ const layout = layoutOfGivens(givens);
22
+ if (layout !== null) {
23
+ const taken = used(grid, layout);
24
+ const masks = grid.map((value, index) => (value !== 0 ? 0 : candidatesAt(layout, index, taken, grid)));
25
+ const by = grid.map((value, index) => {
26
+ if (value !== 0)
27
+ return undefined;
28
+ let blocked = 0;
29
+ for (const group of layout.groupsOf[index])
30
+ blocked |= taken[group];
31
+ const open = ALL(size) & ~blocked;
32
+ return bitCount(open) > 1 && bitCount(masks[index]) === 1 && layout.cages !== undefined ? "cage" : undefined;
33
+ });
34
+ return { masks, by, groups: layout.groups };
35
+ }
36
+ const rows = Array.from({ length: size }, (_, r) => Array.from({ length: size }, (_, c) => r * size + c));
37
+ const cols = Array.from({ length: size }, (_, c) => Array.from({ length: size }, (_, r) => r * size + c));
38
+ const groups = [...rows, ...cols];
39
+ const masks = grid.map((value, index) => {
40
+ if (value !== 0)
41
+ return 0;
42
+ let mask = ALL(size);
43
+ for (const other of rows[Math.floor(index / size)])
44
+ if (grid[other] !== 0)
45
+ mask &= ~(1 << grid[other]);
46
+ for (const other of cols[index % size])
47
+ if (grid[other] !== 0)
48
+ mask &= ~(1 << grid[other]);
49
+ return mask;
50
+ });
51
+ const plain = [...masks];
52
+ // What the marks or the clues rule out is narrowed to a fixpoint, as a person works along a chain of them.
53
+ if (givens.kind === "more-or-less") {
54
+ const candidates = grid.map((value, index) => (value !== 0 ? 1 << value : masks[index]));
55
+ const highest = (mask) => 31 - Math.clz32(mask);
56
+ for (let changed = true; changed;) {
57
+ changed = false;
58
+ for (const mark of givens.marks) {
59
+ const lessMask = candidates[mark.less] & ((1 << highest(candidates[mark.more])) - 1);
60
+ const moreMask = candidates[mark.more] & (ALL(size) & ~((1 << (lowestBit(candidates[mark.less]) + 1)) - 1));
61
+ if (lessMask !== candidates[mark.less]) {
62
+ candidates[mark.less] = lessMask;
63
+ changed = true;
64
+ }
65
+ if (moreMask !== candidates[mark.more]) {
66
+ candidates[mark.more] = moreMask;
67
+ changed = true;
68
+ }
69
+ }
70
+ if (candidates.some((mask) => mask === 0))
71
+ return null;
72
+ }
73
+ return { masks: grid.map((value, index) => (value !== 0 ? 0 : candidates[index])), by: grid.map((value, index) => (value === 0 && bitCount(plain[index]) > 1 && bitCount(candidates[index]) === 1 ? "marks" : undefined)), groups };
74
+ }
75
+ if (givens.clues !== null) {
76
+ const candidates = grid.map((value, index) => (value !== 0 ? 1 << value : masks[index]));
77
+ const lines = cluedLines(size, givens.clues);
78
+ for (let changed = true; changed;) {
79
+ changed = false;
80
+ for (const line of lines) {
81
+ const narrowed = narrowEdges(line, candidates, size);
82
+ if (narrowed === null)
83
+ return null;
84
+ if (narrowed)
85
+ changed = true;
86
+ }
87
+ }
88
+ return { masks: grid.map((value, index) => (value !== 0 ? 0 : candidates[index])), by: grid.map((value, index) => (value === 0 && bitCount(plain[index]) > 1 && bitCount(candidates[index]) === 1 ? "clues" : undefined)), groups };
89
+ }
90
+ return { masks, by: grid.map(() => undefined), groups };
91
+ }
92
+ /**
93
+ * The next cell a person could fill in, and why (see `KazuHint`). `entries` is what the player has
94
+ * written, row-major (0 for empty; printed cells are ignored). `answer` is the puzzle's solution as a
95
+ * cells code; left out, it is worked out from the givens. Null when every cell is right, or when
96
+ * the givens are not a puzzle with exactly one answer: there is nothing true to say.
97
+ */
98
+ export function hintKazu(kind, size, givens, entries, answer) {
99
+ const read = readGivens(kind, size, givens);
100
+ if (read === null)
101
+ return null;
102
+ const solved = decodeCells(answer ?? solveKazu(kind, size, givens) ?? "", size);
103
+ if (solved === null)
104
+ return null;
105
+ const right = read.cells.map((printed, index) => (printed !== 0 ? printed : entries[index] === solved[index] ? solved[index] : 0));
106
+ if (right.every((value) => value !== 0))
107
+ return null;
108
+ const replaces = (cell) => (entries[cell] ?? 0) !== 0 && read.cells[cell] === 0;
109
+ const found = candidatesFor(read, right);
110
+ if (found !== null) {
111
+ const empty = right.flatMap((value, index) => (value === 0 ? [index] : []));
112
+ // A cell that is empty is a gentler hint than one that holds a wrong number, so those come first.
113
+ const ordered = [...empty.filter((cell) => !replaces(cell)), ...empty.filter(replaces)];
114
+ for (const cell of ordered) {
115
+ if (bitCount(found.masks[cell]) === 1)
116
+ return { cell, value: lowestBit(found.masks[cell]), why: "only-number", by: found.by[cell], replaces: replaces(cell) };
117
+ }
118
+ for (const rank of [false, true]) {
119
+ for (const [g, group] of found.groups.entries()) {
120
+ // A cage holds some of the numbers, not all, so a number with one place left in it is no reason to put it there.
121
+ if (group.length !== size)
122
+ continue;
123
+ let present = 0;
124
+ for (const index of group)
125
+ if (right[index] !== 0)
126
+ present |= 1 << right[index];
127
+ for (let value = 1; value <= size; value += 1) {
128
+ if (present & (1 << value))
129
+ continue;
130
+ const places = group.filter((index) => right[index] === 0 && (found.masks[index] & (1 << value)) !== 0);
131
+ if (places.length === 1 && replaces(places[0]) === rank)
132
+ return { cell: places[0], value, why: "only-place", group: groupInfo(read, g), replaces: rank };
133
+ }
134
+ }
135
+ }
136
+ }
137
+ // Nothing follows by a single step: the tightest cell, where the most of its row and column are right already.
138
+ let best = -1;
139
+ let bestPeers = -1;
140
+ for (let index = 0; index < size * size; index += 1) {
141
+ if (right[index] !== 0)
142
+ continue;
143
+ const row = Math.floor(index / size);
144
+ const col = index % size;
145
+ let peers = 0;
146
+ for (let k = 0; k < size; k += 1) {
147
+ if (k !== col && right[row * size + k] !== 0)
148
+ peers += 1;
149
+ if (k !== row && right[k * size + col] !== 0)
150
+ peers += 1;
151
+ }
152
+ if (peers > bestPeers) {
153
+ best = index;
154
+ bestPeers = peers;
155
+ }
156
+ }
157
+ return best === -1 ? null : { cell: best, value: solved[best], why: "answer", replaces: replaces(best) };
158
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Kazu 数: the Numbers family of grid puzzles. Sudoku, Jigsaw Sudoku, Diagonal Sudoku, Killer Sudoku
3
+ * (Sum Cages), Futoshiki (More or Less) and Skyscrapers (Towers): a seeded generator whose every puzzle
4
+ * has exactly one answer, at three levels; a solver that counts answers; a check that reads a finished
5
+ * grid in O(cells); a hint that says which cell to fill next and why; puzzles and runs as short codes;
6
+ * and a game in play as pure functions. The drawing is `@johnmorrisdotca/kazu/draw`, playing in a page
7
+ * is `@johnmorrisdotca/kazu/play`, and the tag is `@johnmorrisdotca/kazu/element/define`.
8
+ */
9
+ export * from "./kinds.ts";
10
+ export * from "./generate.ts";
11
+ export * from "./solve.ts";
12
+ export * from "./check.ts";
13
+ export * from "./hint.ts";
14
+ export * from "./givens.ts";
15
+ export * from "./conflicts.ts";
16
+ export * from "./game.ts";
17
+ export * from "./cells.ts";
18
+ export * from "./progress.ts";
19
+ export { kazuClockText } from "./clock.ts";
20
+ export { generateNumberPlace, generateDiagonal } from "./numberPlace.ts";
21
+ export { generateJigsaw } from "./jigsaw.ts";
22
+ export { generateSumCages } from "./sumCages.ts";
23
+ export { generateMoreOrLess } from "./moreOrLess.ts";
24
+ export { generateTowers } from "./towers.ts";
25
+ export { encodeJigsaw, decodeJigsaw, encodeRegions, decodeRegions } from "./jigsaw.ts";
26
+ export { encodeKiller, decodeKiller, cageOutline, CAGE_LETTERS } from "./sumCages.ts";
27
+ export type { Cage, Segment } from "./sumCages.ts";
28
+ export { encodeMoreOrLess, decodeMoreOrLess, NO_MARK } from "./moreOrLessCode.ts";
29
+ export type { Mark } from "./moreOrLessCode.ts";
30
+ export { encodeTowers, decodeTowers, cluesOf, lineFrom, towersSeen, noClues, TOWER_SIDES } from "./towersCode.ts";
31
+ export type { TowerClues, TowerSide } from "./towersCode.ts";
32
+ export { boxedLayout, boxOf, KAZU_BOXES, neighbours, regionsAreSound } from "./layout.ts";
33
+ export type { Boxes } from "./layout.ts";
34
+ export { seededRandom, shuffled, freshKazuSeed, isKazuSeed, KAZU_SEED_MOST } from "./random.ts";
35
+ export type { Random } from "./random.ts";
36
+ export { VERSION } from "./version.ts";
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Kazu 数: the Numbers family of grid puzzles. Sudoku, Jigsaw Sudoku, Diagonal Sudoku, Killer Sudoku
3
+ * (Sum Cages), Futoshiki (More or Less) and Skyscrapers (Towers): a seeded generator whose every puzzle
4
+ * has exactly one answer, at three levels; a solver that counts answers; a check that reads a finished
5
+ * grid in O(cells); a hint that says which cell to fill next and why; puzzles and runs as short codes;
6
+ * and a game in play as pure functions. The drawing is `@johnmorrisdotca/kazu/draw`, playing in a page
7
+ * is `@johnmorrisdotca/kazu/play`, and the tag is `@johnmorrisdotca/kazu/element/define`.
8
+ */
9
+ export * from "./kinds.js";
10
+ export * from "./generate.js";
11
+ export * from "./solve.js";
12
+ export * from "./check.js";
13
+ export * from "./hint.js";
14
+ export * from "./givens.js";
15
+ export * from "./conflicts.js";
16
+ export * from "./game.js";
17
+ export * from "./cells.js";
18
+ export * from "./progress.js";
19
+ export { kazuClockText } from "./clock.js";
20
+ export { generateNumberPlace, generateDiagonal } from "./numberPlace.js";
21
+ export { generateJigsaw } from "./jigsaw.js";
22
+ export { generateSumCages } from "./sumCages.js";
23
+ export { generateMoreOrLess } from "./moreOrLess.js";
24
+ export { generateTowers } from "./towers.js";
25
+ export { encodeJigsaw, decodeJigsaw, encodeRegions, decodeRegions } from "./jigsaw.js";
26
+ export { encodeKiller, decodeKiller, cageOutline, CAGE_LETTERS } from "./sumCages.js";
27
+ export { encodeMoreOrLess, decodeMoreOrLess, NO_MARK } from "./moreOrLessCode.js";
28
+ export { encodeTowers, decodeTowers, cluesOf, lineFrom, towersSeen, noClues, TOWER_SIDES } from "./towersCode.js";
29
+ export { boxedLayout, boxOf, KAZU_BOXES, neighbours, regionsAreSound } from "./layout.js";
30
+ export { seededRandom, shuffled, freshKazuSeed, isKazuSeed, KAZU_SEED_MOST } from "./random.js";
31
+ export { VERSION } from "./version.js";
@@ -0,0 +1,21 @@
1
+ import type { KazuLevel, KazuPuzzle } from "./kinds.ts";
2
+ import { type Random } from "./random.ts";
3
+ /** The region of every cell as a letter, `a` for the first region and so on: one character a cell. */
4
+ export declare function encodeRegions(regions: readonly number[]): string;
5
+ /** Regions row-major, one letter each, or null for a string that is not that. */
6
+ export declare function decodeRegions(code: string, size: number): number[] | null;
7
+ /**
8
+ * A Jigsaw's givens: the cells, then the regions, one letter a cell. The regions ride in the givens
9
+ * because they are the puzzle: a check reads a finished grid against the regions it was handed, in
10
+ * one pass, and never has to make them again.
11
+ */
12
+ export declare function encodeJigsaw(cells: readonly number[], regions: readonly number[]): string;
13
+ /** The cells and regions a Jigsaw's givens say, or null for a string that is not a Jigsaw of this side. */
14
+ export declare function decodeJigsaw(code: string, size: number): {
15
+ cells: number[];
16
+ regions: number[];
17
+ } | null;
18
+ /** Irregular regions for a grid of this side, shaken from the boxes (or the rows) by exchanges, in a seeded order. Every region is joined and none is a straight line. */
19
+ export declare function shakeRegions(size: number, random: Random): number[];
20
+ /** A Jigsaw Sudoku of this side, level and seed: 5, 6, 7 or 9. */
21
+ export declare function generateJigsaw(size: number, level: KazuLevel, seed: number): KazuPuzzle;
package/dist/jigsaw.js ADDED
@@ -0,0 +1,144 @@
1
+ import { decodeCells, encodeCells } from "./cells.js";
2
+ import { fillLayout } from "./groupSolve.js";
3
+ import { KAZU_BOXES, neighbours, regionLayout, regionsAreSound } from "./layout.js";
4
+ import { carve } from "./numberPlace.js";
5
+ import { seededRandom } from "./random.js";
6
+ /**
7
+ * Making a Jigsaw Sudoku from a seed: Sudoku with the boxes traded for irregular regions of the
8
+ * same size.
9
+ *
10
+ * The regions come first. They start as a regular partition (the boxes where the side has boxes,
11
+ * the rows where it does not) and are shaken by exchanges: a cell passes to a neighbouring region
12
+ * and a cell of that region touching the first passes back, kept only while both regions stay
13
+ * joined edge to edge, so the sizes never change. A shaken layout with a region that is still a
14
+ * whole row or column is drawn again, because that region would ask nothing the row does not.
15
+ *
16
+ * Then a full grid for those regions, by the solver's own search with a step budget: some layouts
17
+ * fill slowly, and one that runs past the budget is swapped for another rather than waited on.
18
+ * Then the givens, carved exactly as Sudoku's are (`carve`). Deterministic in the seed.
19
+ */
20
+ const REGION_FIRST = "a".charCodeAt(0);
21
+ /** The region of every cell as a letter, `a` for the first region and so on: one character a cell. */
22
+ export function encodeRegions(regions) {
23
+ return regions.map((region) => String.fromCharCode(REGION_FIRST + region)).join("");
24
+ }
25
+ /** Regions row-major, one letter each, or null for a string that is not that. */
26
+ export function decodeRegions(code, size) {
27
+ if (typeof code !== "string" || code.length !== size * size)
28
+ return null;
29
+ const regions = [];
30
+ for (const letter of code) {
31
+ const region = letter.charCodeAt(0) - REGION_FIRST;
32
+ if (letter.length !== 1 || region < 0 || region >= size)
33
+ return null;
34
+ regions.push(region);
35
+ }
36
+ return regions;
37
+ }
38
+ /**
39
+ * A Jigsaw's givens: the cells, then the regions, one letter a cell. The regions ride in the givens
40
+ * because they are the puzzle: a check reads a finished grid against the regions it was handed, in
41
+ * one pass, and never has to make them again.
42
+ */
43
+ export function encodeJigsaw(cells, regions) {
44
+ return encodeCells(cells) + encodeRegions(regions);
45
+ }
46
+ /** The cells and regions a Jigsaw's givens say, or null for a string that is not a Jigsaw of this side. */
47
+ export function decodeJigsaw(code, size) {
48
+ if (typeof code !== "string" || code.length !== 2 * size * size)
49
+ return null;
50
+ const cells = decodeCells(code.slice(0, size * size), size);
51
+ const regions = decodeRegions(code.slice(size * size), size);
52
+ return cells === null || regions === null ? null : { cells, regions };
53
+ }
54
+ /** Where the removal stops, by level and side: the same proportions as Sudoku's floors. */
55
+ const FLOOR = {
56
+ easy: { 5: 12, 6: 18, 7: 25, 9: 38 },
57
+ medium: { 5: 9, 6: 14, 7: 20, 9: 30 },
58
+ hard: { 5: 7, 6: 11, 7: 16, 9: 24 },
59
+ };
60
+ /** Exchanges tried per cell when shaking the regions. */
61
+ const SHAKES_PER_CELL = 40;
62
+ /** Layouts tried before giving up; a layout that cannot be filled inside the budget is replaced. */
63
+ const LAYOUTS_TRIED = 40;
64
+ function startingRegions(size) {
65
+ const boxes = KAZU_BOXES[size];
66
+ return Array.from({ length: size * size }, (_, index) => {
67
+ const row = Math.floor(index / size);
68
+ const col = index % size;
69
+ return boxes === undefined ? row : Math.floor(row / boxes.rows) * (size / boxes.cols) + Math.floor(col / boxes.cols);
70
+ });
71
+ }
72
+ function joined(size, region, group) {
73
+ const start = region.indexOf(group);
74
+ const seen = new Set([start]);
75
+ const stack = [start];
76
+ while (stack.length > 0) {
77
+ const index = stack.pop();
78
+ for (const next of neighbours(size, index)) {
79
+ if (!seen.has(next) && region[next] === group) {
80
+ seen.add(next);
81
+ stack.push(next);
82
+ }
83
+ }
84
+ }
85
+ return seen.size === size;
86
+ }
87
+ /** Whether some region is exactly one row or one column. */
88
+ function hasStraightRegion(size, region) {
89
+ for (let group = 0; group < size; group += 1) {
90
+ const cells = region.flatMap((value, index) => (value === group ? [index] : []));
91
+ const rows = new Set(cells.map((index) => Math.floor(index / size)));
92
+ const cols = new Set(cells.map((index) => index % size));
93
+ if (rows.size === 1 || cols.size === 1)
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+ /** Irregular regions for a grid of this side, shaken from the boxes (or the rows) by exchanges, in a seeded order. Every region is joined and none is a straight line. */
99
+ export function shakeRegions(size, random) {
100
+ for (;;) {
101
+ const region = startingRegions(size);
102
+ for (let shake = 0; shake < SHAKES_PER_CELL * size * size; shake += 1) {
103
+ /*
104
+ * One cell passes from its region A to a neighbouring region B, and then any cell of B that
105
+ * touches A passes back, so both keep their size. A straight swap of two neighbours almost
106
+ * always cuts one region in two, and starting from rows it never can succeed at all.
107
+ */
108
+ const a = Math.floor(random() * size * size);
109
+ const ra = region[a];
110
+ const across = neighbours(size, a).filter((next) => region[next] !== ra);
111
+ if (across.length === 0)
112
+ continue;
113
+ const rb = region[across[Math.floor(random() * across.length)]];
114
+ region[a] = rb;
115
+ const back = region.flatMap((value, index) => value === rb && index !== a && neighbours(size, index).some((next) => region[next] === ra) ? [index] : []);
116
+ if (back.length === 0) {
117
+ region[a] = ra;
118
+ continue;
119
+ }
120
+ const b = back[Math.floor(random() * back.length)];
121
+ region[b] = ra;
122
+ if (!joined(size, region, ra) || !joined(size, region, rb)) {
123
+ region[a] = ra;
124
+ region[b] = rb;
125
+ }
126
+ }
127
+ if (!hasStraightRegion(size, region) && regionsAreSound(size, region))
128
+ return region;
129
+ }
130
+ }
131
+ /** A Jigsaw Sudoku of this side, level and seed: 5, 6, 7 or 9. */
132
+ export function generateJigsaw(size, level, seed) {
133
+ const random = seededRandom(seed);
134
+ for (let tried = 0; tried < LAYOUTS_TRIED; tried += 1) {
135
+ const regions = shakeRegions(size, random);
136
+ const layout = regionLayout(size, regions);
137
+ const solution = fillLayout(layout, random);
138
+ if (solution === null)
139
+ continue;
140
+ const givens = carve(solution, layout, level, FLOOR[level][size], random);
141
+ return { kind: "jigsaw", size, level, seed, givens: encodeJigsaw(givens, regions), solution: encodeCells(solution) };
142
+ }
143
+ throw new Error(`No jigsaw could be filled from seed ${seed} at ${size}×${size}.`);
144
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * THE SIX PUZZLES, and what each offers: the keys, the sizes and the levels.
3
+ *
4
+ * A key is kebab case and is the name of the puzzle everywhere in this package: the
5
+ * address of one, the `kind` of a puzzle, the value of the element's `kind` attribute.
6
+ * itsutsu.com spelled the same six `numberPlace`, `jigsaw`, `diagonal`, `sumCages`,
7
+ * `moreOrLess` and `towers`; `KAZU_KIND_OF_SITE_KIND` maps one to the other.
8
+ */
9
+ export declare const KAZU_KINDS: readonly ["number-place", "jigsaw", "diagonal", "sum-cages", "more-or-less", "towers"];
10
+ export type KazuKind = (typeof KAZU_KINDS)[number];
11
+ /** How hard a puzzle is made: what the solver needed. Easy yields to singles alone, medium to one guess, hard to whatever it takes. */
12
+ export declare const KAZU_LEVELS: readonly ["easy", "medium", "hard"];
13
+ export type KazuLevel = (typeof KAZU_LEVELS)[number];
14
+ /** What one puzzle offers. */
15
+ export type KazuSpec = {
16
+ /** Every side it can be made at, smallest first. */
17
+ sizes: readonly number[];
18
+ /** The side a first visit opens on. */
19
+ defaultSize: number;
20
+ /** The most characters one of its codes can hold, for a route to refuse anything larger. */
21
+ mostCells: number;
22
+ };
23
+ export declare const KAZU_SPECS: Record<KazuKind, KazuSpec>;
24
+ /** The names itsutsu.com gave the six in its code, to the keys here: what a site's stored kind becomes. */
25
+ export declare const KAZU_KIND_OF_SITE_KIND: Record<string, KazuKind>;
26
+ /** Whether a value is one of the six keys. */
27
+ export declare function isKazuKind(value: unknown): value is KazuKind;
28
+ /** Whether a value is one of the three levels. */
29
+ export declare function isKazuLevel(value: unknown): value is KazuLevel;
30
+ /** Whether this puzzle can be made at this side. */
31
+ export declare function isKazuSize(kind: KazuKind, size: number): boolean;
32
+ /**
33
+ * One puzzle, made from a seed or read back from its codes. `givens` and `solution` are the strings
34
+ * `cells.ts` and the kinds' own codes write: row-major cells, one character each, and for a Jigsaw, Sum
35
+ * Cages, More or Less or Towers the rest of what the puzzle is printed with, after the cells.
36
+ */
37
+ export type KazuPuzzle = {
38
+ kind: KazuKind;
39
+ size: number;
40
+ level: KazuLevel;
41
+ seed: number;
42
+ givens: string;
43
+ solution: string;
44
+ };
45
+ /** The verdict on a submitted answer: `{ ok: true }`, or `{ ok: false, reason }` in the words the site has always used. */
46
+ export type KazuCheck = {
47
+ ok: true;
48
+ } | {
49
+ ok: false;
50
+ reason: string;
51
+ };
package/dist/kinds.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * THE SIX PUZZLES, and what each offers: the keys, the sizes and the levels.
3
+ *
4
+ * A key is kebab case and is the name of the puzzle everywhere in this package: the
5
+ * address of one, the `kind` of a puzzle, the value of the element's `kind` attribute.
6
+ * itsutsu.com spelled the same six `numberPlace`, `jigsaw`, `diagonal`, `sumCages`,
7
+ * `moreOrLess` and `towers`; `KAZU_KIND_OF_SITE_KIND` maps one to the other.
8
+ */
9
+ export const KAZU_KINDS = ["number-place", "jigsaw", "diagonal", "sum-cages", "more-or-less", "towers"];
10
+ /** How hard a puzzle is made: what the solver needed. Easy yields to singles alone, medium to one guess, hard to whatever it takes. */
11
+ export const KAZU_LEVELS = ["easy", "medium", "hard"];
12
+ export const KAZU_SPECS = {
13
+ "number-place": { sizes: [4, 6, 9, 16], defaultSize: 9, mostCells: 256 },
14
+ jigsaw: { sizes: [5, 6, 7, 9], defaultSize: 7, mostCells: 162 },
15
+ diagonal: { sizes: [6, 9], defaultSize: 9, mostCells: 81 },
16
+ "sum-cages": { sizes: [6, 9], defaultSize: 9, mostCells: 286 },
17
+ "more-or-less": { sizes: [4, 5, 6, 7], defaultSize: 5, mostCells: 133 },
18
+ towers: { sizes: [4, 5, 6, 7], defaultSize: 5, mostCells: 77 },
19
+ };
20
+ /** The names itsutsu.com gave the six in its code, to the keys here: what a site's stored kind becomes. */
21
+ export const KAZU_KIND_OF_SITE_KIND = {
22
+ numberPlace: "number-place",
23
+ jigsaw: "jigsaw",
24
+ diagonal: "diagonal",
25
+ sumCages: "sum-cages",
26
+ moreOrLess: "more-or-less",
27
+ towers: "towers",
28
+ };
29
+ /** Whether a value is one of the six keys. */
30
+ export function isKazuKind(value) {
31
+ return KAZU_KINDS.includes(value);
32
+ }
33
+ /** Whether a value is one of the three levels. */
34
+ export function isKazuLevel(value) {
35
+ return KAZU_LEVELS.includes(value);
36
+ }
37
+ /** Whether this puzzle can be made at this side. */
38
+ export function isKazuSize(kind, size) {
39
+ return isKazuKind(kind) && KAZU_SPECS[kind].sizes.includes(size);
40
+ }
@@ -0,0 +1,63 @@
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
+ /** Where the boxes are on a Sudoku grid: how many rows and columns of cells each box holds. */
14
+ export type Boxes = {
15
+ rows: number;
16
+ cols: number;
17
+ };
18
+ /**
19
+ * 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,
20
+ * which is the one people get wrong and the reason this is a table rather than a square root.
21
+ */
22
+ export declare const KAZU_BOXES: Record<number, Boxes>;
23
+ /** The box a cell is in, numbered row-major from 0. */
24
+ export declare function boxOf(size: number, index: number): number;
25
+ export type Layout = {
26
+ size: number;
27
+ /** Every group of cells that holds 1..size once each. */
28
+ groups: number[][];
29
+ /** For each cell, the groups it is in. */
30
+ groupsOf: number[][];
31
+ /** For each cell, the region it is drawn in: a box, or a jigsaw region. */
32
+ region: number[];
33
+ /** What a region is called when a check says which group repeats: a box, or a jigsaw's region. */
34
+ regionWord: "box" | "region";
35
+ diagonal: boolean;
36
+ /**
37
+ * Sum Cages' cages: cells that hold different numbers adding to `sum`. Each is also one of
38
+ * `groups`, so "different" is the solver's ordinary rule; the sum is the one thing the solver
39
+ * reads from here. Absent for every other puzzle, which is what keeps their search as it was.
40
+ */
41
+ cages?: {
42
+ cells: number[];
43
+ sum: number;
44
+ }[];
45
+ /** For each cell, the index of its cage in `cages`. */
46
+ cageOf?: number[];
47
+ };
48
+ /** Rows, columns and boxes; with `diagonal`, the two long diagonals as well. */
49
+ export declare function boxedLayout(size: number, diagonal?: boolean): Layout;
50
+ /** Rows, columns and the given regions, numbered 0..size-1, each `size` cells. */
51
+ export declare function regionLayout(size: number, region: readonly number[]): Layout;
52
+ /** Rows, columns and boxes, and the cages over them, each cage a group of its own as well. */
53
+ export declare function cagedLayout(size: number, cages: readonly {
54
+ cells: readonly number[];
55
+ sum: number;
56
+ }[]): Layout;
57
+ /** The cells sharing an edge with `index`. */
58
+ export declare function neighbours(size: number, index: number): number[];
59
+ /**
60
+ * Whether `region` divides a size×size grid into `size` regions of `size` cells each, every one of
61
+ * them joined edge to edge. O(cells): a check of a Jigsaw asks it of whatever regions it was sent.
62
+ */
63
+ export declare function regionsAreSound(size: number, region: readonly number[]): boolean;