@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/names.js ADDED
@@ -0,0 +1,163 @@
1
+ export const KAZU_NAMES = {
2
+ "number-place": {
3
+ en: "Sudoku",
4
+ ja: "ナンプレ",
5
+ alsoKnownAs: ["Number Place", "Nanpure"],
6
+ tagline: {
7
+ en: "Fill the grid so every row, column and box holds each number once.",
8
+ ja: "どの行、列、ブロックにも、同じ数字が一つずつ入るように、盤を埋めます。",
9
+ },
10
+ rules: {
11
+ en: [
12
+ "Fill every empty cell with a number from 1 up to the side of the grid, so that each row, each column and each box holds every number exactly once.",
13
+ "The numbers already printed are the givens. They stay where they are, and every puzzle has exactly one answer that fits them.",
14
+ "The 16×16 Giant has sixteen symbols: 1 to 9, then A to G for 10 to 16.",
15
+ "Easy yields to reasoning alone: every cell can be found from what is already there. Medium and hard ask you to try something and see.",
16
+ ],
17
+ ja: [
18
+ "空いているマスに、1から盤の一辺の数までの数字を入れます。どの行、列、ブロックにも、同じ数字が一つずつ入ります。",
19
+ "最初から書かれている数字はそのままです。どの問題も、答えはちょうど一つだけです。",
20
+ "16×16の「特大」は、1から9までの数字に、10から16までを表すAからGを足した16種類を使います。",
21
+ "やさしい問題は、推理だけで全部のマスが決まります。ふつうとむずかしい問題では、試してみる場面があります。",
22
+ ],
23
+ },
24
+ origin: {
25
+ en: "Howard Garns's Number Place, printed by Dell in 1979; Nikoli took it to Japan in 1984 and named it Sudoku.",
26
+ ja: "1979年にデル社が載せた、ハワード・ガーンズの「ナンバー・プレース」です。1984年にニコリが日本に紹介し、数独と名づけました。",
27
+ },
28
+ },
29
+ jigsaw: {
30
+ en: "Jigsaw Sudoku",
31
+ ja: "変形ナンプレ",
32
+ alsoKnownAs: ["Nonomino", "Irregular Sudoku"],
33
+ tagline: {
34
+ en: "Sudoku with the boxes cut into irregular regions.",
35
+ ja: "ブロックが、いびつな形の領域に変わったナンプレです。",
36
+ },
37
+ rules: {
38
+ en: [
39
+ "Fill every empty cell with a number from 1 up to the side of the grid, so that each row, each column and each outlined region holds every number exactly once.",
40
+ "The regions are drawn in heavier lines, and each has as many cells as the grid is wide, in a shape of its own.",
41
+ "Every puzzle has exactly one answer. Easy yields to reasoning alone.",
42
+ ],
43
+ ja: [
44
+ "空いているマスに、1から盤の一辺の数までの数字を入れます。どの行、列、太線で囲まれた領域にも、同じ数字が一つずつ入ります。",
45
+ "領域は太い線で描かれ、どれも盤の一辺と同じ数のマスでできていて、形はそれぞれ違います。",
46
+ "どの問題も、答えはちょうど一つです。やさしい問題は、推理だけで解けます。",
47
+ ],
48
+ },
49
+ origin: {
50
+ en: "Sudoku with its boxes traded for irregular shapes, printed under names such as Nonomino and Jigsaw Sudoku. With no boxes it is not tied to sides that divide evenly, so it comes at five and seven as well.",
51
+ ja: "ブロックをいびつな形に置きかえたナンプレで、ノノミノやジグソー数独などの名前で載っています。ブロックがないので、5×5や7×7もあります。",
52
+ },
53
+ },
54
+ diagonal: {
55
+ en: "Diagonal Sudoku",
56
+ ja: "対角ナンプレ",
57
+ alsoKnownAs: ["Sudoku X", "X-Sudoku"],
58
+ tagline: {
59
+ en: "Sudoku where the two long diagonals must hold each number once too.",
60
+ ja: "二本の対角線にも、同じ数字が一つずつ入るナンプレです。",
61
+ },
62
+ rules: {
63
+ en: [
64
+ "Fill every empty cell with a number from 1 up to the side of the grid, so that each row, each column and each box holds every number exactly once.",
65
+ "The two long diagonals, shaded corner to corner, must each hold every number exactly once as well.",
66
+ "Every puzzle has exactly one answer, and the diagonals are part of reaching it: fewer numbers are printed than a plain grid would need.",
67
+ ],
68
+ ja: [
69
+ "空いているマスに、1から盤の一辺の数までの数字を入れます。どの行、列、ブロックにも、同じ数字が一つずつ入ります。",
70
+ "角から角へ色のついた二本の対角線にも、同じ数字が一つずつ入ります。",
71
+ "どの問題も、答えはちょうど一つです。対角線も手がかりなので、ふつうの盤より書かれている数字は少なめです。",
72
+ ],
73
+ },
74
+ origin: {
75
+ en: "The most common extra rule laid on Sudoku: the two diagonals count as groups as well. Newspapers print it as Sudoku X.",
76
+ ja: "ナンプレにいちばんよく足される決まりで、二本の対角線もグループとして数えます。新聞では「数独X」の名で載っています。",
77
+ },
78
+ },
79
+ "sum-cages": {
80
+ en: "Killer Sudoku",
81
+ ja: "サムナンプレ",
82
+ alsoKnownAs: ["Sumdoku", "Sum Number Place"],
83
+ tagline: {
84
+ en: "Sudoku with almost no numbers printed: dashed cages each give the sum of the numbers inside them.",
85
+ ja: "数字はほとんど書かれていません。点線の囲みごとに、中の数字の合計だけが示されます。",
86
+ },
87
+ rules: {
88
+ en: [
89
+ "Fill every cell with a number from 1 up to the side of the grid, so that each row, each column and each box holds every number exactly once.",
90
+ "The dashed outlines are cages. The small number in a cage's corner is the sum of the numbers inside it, and no number appears twice in one cage.",
91
+ "Almost nothing is printed: the sums are the clues. Every puzzle has exactly one answer, and the harder levels have fewer, bigger cages.",
92
+ ],
93
+ ja: [
94
+ "すべてのマスに、1から盤の一辺の数までの数字を入れます。どの行、列、ブロックにも、同じ数字が一つずつ入ります。",
95
+ "点線で囲まれた部分が「囲み」です。囲みの角の小さな数字は、中の数字の合計で、同じ数字は一つの囲みに二度入りません。",
96
+ "ほとんど何も書かれておらず、合計が手がかりです。どの問題も答えはちょうど一つで、むずかしいほど囲みは大きく、数は少なくなります。",
97
+ ],
98
+ },
99
+ origin: {
100
+ en: "Played in Japan in the 1990s as sum number place, and made famous as Killer Sudoku by The Times in 2005.",
101
+ ja: "1990年代の日本で「サムナンプレ」として遊ばれ、2005年に英国のタイムズ紙が「キラー数独」として広めました。",
102
+ },
103
+ },
104
+ "more-or-less": {
105
+ en: "Futoshiki",
106
+ ja: "不等式",
107
+ alsoKnownAs: ["Unequal", "Greater Than Sudoku"],
108
+ tagline: {
109
+ en: "Fill the square so every row and column holds each number once, and every more-than mark is true.",
110
+ ja: "どの行と列にも同じ数字が一つずつ入り、不等号がすべて正しくなるように、盤を埋めます。",
111
+ },
112
+ rules: {
113
+ en: [
114
+ "Fill every cell with a number from 1 up to the side of the square, so that each row and each column holds every number exactly once.",
115
+ "A mark between two cells says which is the bigger: the open end faces the larger number, the point the smaller.",
116
+ "Every puzzle has exactly one answer, and every mark and given is needed to reach it.",
117
+ ],
118
+ ja: [
119
+ "すべてのマスに、1から盤の一辺の数までの数字を入れます。どの行にも列にも、同じ数字が一つずつ入ります。",
120
+ "二つのマスのあいだの印は、どちらが大きいかを示します。開いているほうが大きい数字、とがっているほうが小さい数字です。",
121
+ "どの問題も、答えはちょうど一つです。書かれている数字と印は、すべて必要なものだけです。",
122
+ ],
123
+ },
124
+ origin: {
125
+ en: "Futoshiki 不等式, Tamaki Seimiya's puzzle of 2001, which Nikoli published.",
126
+ ja: "2001年に日本で考案された不等式のパズルで、ニコリが載せました。",
127
+ },
128
+ },
129
+ towers: {
130
+ en: "Skyscrapers",
131
+ ja: "摩天楼",
132
+ alsoKnownAs: ["Towers", "Building Heights"],
133
+ tagline: {
134
+ en: "Every number is a tower's height. The clues around the edge say how many towers you can see from there.",
135
+ ja: "数字は塔の高さです。盤の外の数字は、そこから見える塔の数を表します。",
136
+ },
137
+ rules: {
138
+ en: [
139
+ "Fill every cell with a tower from 1 up to the side of the square, so that each row and each column holds every height exactly once.",
140
+ "A number outside the square says how many towers can be seen looking in from there. A taller tower hides every shorter one behind it.",
141
+ "So a 1 means the tallest tower stands right beside the clue, and a clue as big as the square means the towers climb one step at a time.",
142
+ ],
143
+ ja: [
144
+ "すべてのマスに、1から盤の一辺の数までの高さの塔を置きます。どの行にも列にも、同じ高さが一つずつ入ります。",
145
+ "盤の外の数字は、そこから中を見たときに見える塔の数です。高い塔は、その後ろの低い塔を隠します。",
146
+ "つまり、1は一番高い塔がすぐ隣にあること、盤の一辺と同じ数は、塔が一段ずつ高くなることを意味します。",
147
+ ],
148
+ },
149
+ origin: {
150
+ en: "A Japanese logic puzzle known in English as Skyscrapers, set at the first World Puzzle Championship in 1992; Simon Tatham's puzzle collection calls it Towers.",
151
+ ja: "英語ではスカイスクレイパーと呼ばれる日本のロジックパズルで、1992年の第1回世界パズル選手権で出題されました。",
152
+ },
153
+ },
154
+ };
155
+ /** What each side is for, under its size on a chooser: the quick one, the usual one, the long one. */
156
+ export const KAZU_SIZE_NAMES = {
157
+ "number-place": { 4: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" }, 16: { en: "Giant", ja: "特大" } },
158
+ jigsaw: { 5: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 7: { en: "Standard", ja: "定番" }, 9: { en: "Classic", ja: "本格" } },
159
+ diagonal: { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
160
+ "sum-cages": { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
161
+ "more-or-less": { 4: { en: "Quick", ja: "速" }, 5: { en: "Standard", ja: "定番" }, 6: { en: "Longer", ja: "長め" }, 7: { en: "Long", ja: "長" } },
162
+ towers: { 4: { en: "Quick", ja: "速" }, 5: { en: "Standard", ja: "定番" }, 6: { en: "Longer", ja: "長め" }, 7: { en: "Long", ja: "長" } },
163
+ };
@@ -0,0 +1,14 @@
1
+ import { type Grid } from "./groupSolve.ts";
2
+ import type { KazuLevel, KazuPuzzle } from "./kinds.ts";
3
+ import { type Layout } from "./layout.ts";
4
+ import { type Random } from "./random.ts";
5
+ /**
6
+ * The givens: the solution with cells taken away in a seeded order, each removal kept only while the
7
+ * puzzle still has one answer and stays within the level, down to the level's floor. Shared by every
8
+ * puzzle on a layout (Sudoku, Diagonal and Jigsaw).
9
+ */
10
+ export declare function carve(solution: Grid, layout: Layout, level: KazuLevel, floor: number, random: Random): Grid;
11
+ /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9 or 16. */
12
+ export declare function generateNumberPlace(size: number, level: KazuLevel, seed: number): KazuPuzzle;
13
+ /** A Diagonal Sudoku (Sudoku X): Sudoku with the two long diagonals as groups too. 6 or 9. */
14
+ export declare function generateDiagonal(size: number, level: KazuLevel, seed: number): KazuPuzzle;
@@ -0,0 +1,123 @@
1
+ import { encodeCells } from "./cells.js";
2
+ import { countSolutions, guessDepth } from "./groupSolve.js";
3
+ import { boxedLayout } from "./layout.js";
4
+ import { seededRandom, shuffled } from "./random.js";
5
+ /**
6
+ * Making a Sudoku (Number Place) puzzle, and its Diagonal variant, from a seed.
7
+ *
8
+ * Two steps. Fill a whole grid by backtracking with the values tried in a seeded order, so the seed
9
+ * decides the grid. Then take cells away in a seeded order, keeping each removal only while the
10
+ * puzzle still has exactly one answer AND stays within the level: an easy puzzle must go on
11
+ * yielding to singles, a medium one to a single guess, a hard one to whatever it takes. The
12
+ * removal stops at the level's floor of givens, so a hard 9×9 does not run every one of its 81
13
+ * cells past the solver for the sake of two more blanks.
14
+ *
15
+ * Everything here is deterministic in the seed. Change this file and every seed makes a different
16
+ * puzzle, and a solve kept as (kind, size, level, seed) is a different one: `site.fixture.test.ts`
17
+ * holds every puzzle itsutsu.com made from its seeds, byte for byte.
18
+ */
19
+ /**
20
+ * How deep a guess the level allows, and where its removal stops. The floor is a count of givens
21
+ * left, by side. Below it the solver's answer rarely changes and the time does; above it a 9×9
22
+ * hard puzzle would be a medium one with a different label.
23
+ */
24
+ const LEVELS = {
25
+ easy: { depth: 0, floor: { 4: 9, 6: 20, 9: 40, 16: 150 } },
26
+ medium: { depth: 1, floor: { 4: 7, 6: 15, 9: 31, 16: 125 } },
27
+ hard: { depth: Infinity, floor: { 4: 5, 6: 11, 9: 24, 16: 116 } },
28
+ };
29
+ /**
30
+ * A whole grid, filled cell by cell in reading order with the values tried in a seeded order. The
31
+ * groups come from the layout, so the diagonals are honoured the same way.
32
+ */
33
+ function fillInOrder(layout, random) {
34
+ const { size } = layout;
35
+ const grid = new Array(size * size).fill(0);
36
+ const values = Array.from({ length: size }, (_, i) => i + 1);
37
+ const taken = new Array(layout.groups.length).fill(0);
38
+ const fill = (index) => {
39
+ if (index === grid.length)
40
+ return true;
41
+ const groups = layout.groupsOf[index];
42
+ let blocked = 0;
43
+ for (const group of groups)
44
+ blocked |= taken[group];
45
+ for (const value of shuffled(values, random)) {
46
+ const bit = 1 << value;
47
+ if (blocked & bit)
48
+ continue;
49
+ grid[index] = value;
50
+ for (const group of groups)
51
+ taken[group] |= bit;
52
+ if (fill(index + 1))
53
+ return true;
54
+ for (const group of groups)
55
+ taken[group] &= ~bit;
56
+ grid[index] = 0;
57
+ }
58
+ return false;
59
+ };
60
+ fill(0);
61
+ return grid;
62
+ }
63
+ /**
64
+ * A FILLED 16×16 FROM A PATTERN, SHUFFLED BY THE SEED. Cell by cell with a random order can wander
65
+ * into a dead end deep in a 256-cell grid and take seconds to climb out. A grid that is right by
66
+ * construction (each row the one above it shifted a box's width, each band shifted by one), then
67
+ * shuffled in every way that keeps it right (the numbers relabelled, rows within a band, the
68
+ * bands, columns within a stack, the stacks), is as varied and costs nothing. Only 16×16 is made
69
+ * this way, so every smaller grid comes out of its seed exactly as it always has.
70
+ */
71
+ function fillByPattern(size, random) {
72
+ const box = Math.sqrt(size);
73
+ const labels = shuffled(Array.from({ length: size }, (_, i) => i + 1), random);
74
+ const order = () => shuffled(Array.from({ length: box }, (_, b) => b), random).flatMap((band) => shuffled(Array.from({ length: box }, (_, r) => band * box + r), random));
75
+ const rows = order();
76
+ const cols = order();
77
+ const base = (r, c) => (box * (r % box) + Math.floor(r / box) + c) % size;
78
+ return Array.from({ length: size * size }, (_, index) => labels[base(rows[Math.floor(index / size)], cols[index % size])]);
79
+ }
80
+ /**
81
+ * The givens: the solution with cells taken away in a seeded order, each removal kept only while the
82
+ * puzzle still has one answer and stays within the level, down to the level's floor. Shared by every
83
+ * puzzle on a layout (Sudoku, Diagonal and Jigsaw).
84
+ */
85
+ export function carve(solution, layout, level, floor, random) {
86
+ const { depth } = LEVELS[level];
87
+ const givens = [...solution];
88
+ let left = givens.length;
89
+ for (const index of shuffled(givens.map((_, i) => i), random)) {
90
+ if (left <= floor)
91
+ break;
92
+ const value = givens[index];
93
+ givens[index] = 0;
94
+ const stillOne = countSolutions(givens, layout, 2) === 1 && guessDepth(givens, layout) <= depth;
95
+ if (stillOne)
96
+ left -= 1;
97
+ else
98
+ givens[index] = value;
99
+ }
100
+ return givens;
101
+ }
102
+ /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9 or 16. */
103
+ export function generateNumberPlace(size, level, seed) {
104
+ const random = seededRandom(seed);
105
+ const layout = boxedLayout(size);
106
+ const solution = size === 16 ? fillByPattern(size, random) : fillInOrder(layout, random);
107
+ const givens = carve(solution, layout, level, LEVELS[level].floor[size], random);
108
+ return { kind: "number-place", size, level, seed, givens: encodeCells(givens), solution: encodeCells(solution) };
109
+ }
110
+ /** Where a Diagonal's removal stops, by level and side: a few below the classic floors, because the extra groups need fewer givens for one answer. */
111
+ const DIAGONAL_FLOOR = {
112
+ easy: { 6: 16, 9: 34 },
113
+ medium: { 6: 12, 9: 27 },
114
+ hard: { 6: 9, 9: 21 },
115
+ };
116
+ /** A Diagonal Sudoku (Sudoku X): Sudoku with the two long diagonals as groups too. 6 or 9. */
117
+ export function generateDiagonal(size, level, seed) {
118
+ const random = seededRandom(seed);
119
+ const layout = boxedLayout(size, true);
120
+ const solution = fillInOrder(layout, random);
121
+ const givens = carve(solution, layout, level, DIAGONAL_FLOOR[level][size], random);
122
+ return { kind: "diagonal", size, level, seed, givens: encodeCells(givens), solution: encodeCells(solution) };
123
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Kazu, played in a page: `mountKazu` draws a puzzle into any element and plays it by touch, mouse and
3
+ * keyboard, with a number pad, pencil marks, Undo, Hint, Check and a clock, and the words in English and
4
+ * Japanese. A separate entry (`@johnmorrisdotca/kazu/play`), so a server never loads any of it.
5
+ */
6
+ export { ensureKazuPlayStyle, mountKazu } from "./mount.ts";
7
+ export type { KazuEventDetail, KazuMount, KazuMountOptions, KazuMountSettings } from "./mount.ts";
8
+ export { KAZU_PLAY_STYLE } from "./playStyle.ts";
9
+ export { kazuClockText } from "./clock.ts";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Kazu, played in a page: `mountKazu` draws a puzzle into any element and plays it by touch, mouse and
3
+ * keyboard, with a number pad, pencil marks, Undo, Hint, Check and a clock, and the words in English and
4
+ * Japanese. A separate entry (`@johnmorrisdotca/kazu/play`), so a server never loads any of it.
5
+ */
6
+ export { ensureKazuPlayStyle, mountKazu } from "./mount.js";
7
+ export { KAZU_PLAY_STYLE } from "./playStyle.js";
8
+ export { kazuClockText } from "./clock.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * THE STYLE a playable Kazu board wears (`mountKazu`, `<kazu-board>`): the drawing's own (`KAZU_STYLE`)
3
+ * and the board's box, its number pad, its buttons and its lines of words. Colours are custom properties
4
+ * on `.kazu-play` (`--kzp-ink`, `--kzp-muted`, `--kzp-rule`, `--kzp-surface`, `--kzp-accent`,
5
+ * `--kzp-good`) so a page sets only what it wants different.
6
+ *
7
+ * Nothing moves when something is chosen: the board is one square box, the lines of words keep the room
8
+ * their longest wording takes, and the buttons are one size. Nothing the player touches can be
9
+ * selected, and no tap on it zooms the page.
10
+ */
11
+ export declare const KAZU_PLAY_STYLE = "\n.kazu {\n --kz-paper: #fbf8f1; --kz-ink: #1f2320; --kz-given: #1f2320; --kz-entry: #1d5fa8; --kz-note: #5b6b7d;\n --kz-grid: #cfc6b2; --kz-box: #3a3d38; --kz-frame: #a98954; --kz-cage: #6a5a8e; --kz-clue: #7a4b14;\n --kz-diagonal: #e9dfc6; --kz-peer: #efe8d8; --kz-same: #dcd0f2; --kz-select: #ffe08a; --kz-hint: #b9e3c4;\n --kz-conflict: #f4b8ad; --kz-wrong: #f4b8ad; --kz-bad: #b5452c; --kz-good: #2f7a4f;\n --kz-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n display: block; width: 100%; height: auto;\n user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; touch-action: manipulation; -webkit-tap-highlight-color: transparent;\n overflow: visible;\n}\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) .kazu {\n --kz-paper: #262a27; --kz-ink: #ece8dc; --kz-given: #ece8dc; --kz-entry: #8fc1ff; --kz-note: #9fb0c2;\n --kz-grid: #454a44; --kz-box: #c9c5b8; --kz-frame: #6b5632; --kz-cage: #b8a5e6; --kz-clue: #e8c48f;\n --kz-diagonal: #34382f; --kz-peer: #2f332f; --kz-same: #433a5c; --kz-select: #6b5a1f; --kz-hint: #25503a;\n --kz-conflict: #6e2f26; --kz-wrong: #6e2f26; --kz-bad: #ff8a6b; --kz-good: #6fcf97;\n }\n}\n:root[data-theme=\"dark\"] .kazu {\n --kz-paper: #262a27; --kz-ink: #ece8dc; --kz-given: #ece8dc; --kz-entry: #8fc1ff; --kz-note: #9fb0c2;\n --kz-grid: #454a44; --kz-box: #c9c5b8; --kz-frame: #6b5632; --kz-cage: #b8a5e6; --kz-clue: #e8c48f;\n --kz-diagonal: #34382f; --kz-peer: #2f332f; --kz-same: #433a5c; --kz-select: #6b5a1f; --kz-hint: #25503a;\n --kz-conflict: #6e2f26; --kz-wrong: #6e2f26; --kz-bad: #ff8a6b; --kz-good: #6fcf97;\n}\n.kazu * { user-select: none; -webkit-user-select: none; }\n.kazu .kz-frame { fill: var(--kz-frame); }\n.kazu .kz-paper { fill: var(--kz-paper); }\n.kazu .kz-diagonal { fill: var(--kz-diagonal); }\n.kazu .kz-peer { fill: var(--kz-peer); }\n.kazu .kz-same { fill: var(--kz-same); }\n.kazu .kz-select { fill: var(--kz-select); }\n.kazu .kz-hint { fill: var(--kz-hint); }\n.kazu .kz-conflict, .kazu .kz-wrong { fill: var(--kz-conflict); }\n.kazu .kz-solved { fill: var(--kz-good); opacity: .14; pointer-events: none; }\n.kazu .kz-grid { fill: none; stroke: var(--kz-grid); stroke-width: 1px; vector-effect: non-scaling-stroke; }\n.kazu .kz-box { fill: none; stroke: var(--kz-box); stroke-width: 2.5px; stroke-linecap: square; vector-effect: non-scaling-stroke; }\n.kazu .kz-cage { stroke: var(--kz-cage); stroke-width: 1.5px; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }\n.kazu .kz-digit { font-family: var(--kz-font); font-weight: 600; text-anchor: middle; dominant-baseline: central; font-variant-numeric: tabular-nums; pointer-events: none; }\n.kazu .kz-given { fill: var(--kz-given); font-weight: 700; }\n.kazu .kz-entry { fill: var(--kz-entry); }\n.kazu .kz-digit.kz-bad { fill: var(--kz-bad); }\n.kazu .kz-note { font-family: var(--kz-font); font-weight: 600; fill: var(--kz-note); text-anchor: middle; dominant-baseline: central; pointer-events: none; }\n.kazu .kz-cage-sum { font-family: var(--kz-font); font-weight: 600; fill: var(--kz-cage); text-anchor: start; dominant-baseline: hanging; pointer-events: none; }\n.kazu .kz-clue { font-family: var(--kz-font); font-weight: 700; fill: var(--kz-clue); text-anchor: middle; dominant-baseline: central; pointer-events: none; }\n.kazu .kz-mark-bg { fill: var(--kz-paper); stroke: none; pointer-events: none; }\n.kazu .kz-mark { fill: none; stroke: var(--kz-ink); stroke-width: 2px; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; pointer-events: none; }\n.kazu .kz-hit { fill: transparent; cursor: pointer; }\n\n.kazu-play {\n --kzp-ink: #1f2320; --kzp-muted: #6b6f68; --kzp-rule: #ddd6c6; --kzp-surface: #fbf8f1; --kzp-accent: #b5452c; --kzp-good: #2f7a4f;\n display: block; max-width: 100%; box-sizing: border-box; color: var(--kzp-ink); font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;\n}\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) .kazu-play { --kzp-ink: #ece8dc; --kzp-muted: #a09d93; --kzp-rule: #3a3d38; --kzp-surface: #1d201e; --kzp-accent: #ff8a6b; --kzp-good: #6fcf97; }\n}\n:root[data-theme=\"dark\"] .kazu-play { --kzp-ink: #ece8dc; --kzp-muted: #a09d93; --kzp-rule: #3a3d38; --kzp-surface: #1d201e; --kzp-accent: #ff8a6b; --kzp-good: #6fcf97; }\n.kazu-play *, .kazu-play *::before, .kazu-play *::after { box-sizing: border-box; }\n.kazu-play .kzp-bar { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 8px; min-height: 1.5em; font-variant-numeric: tabular-nums; }\n.kazu-play .kzp-clock { font-weight: 700; font-size: 1.05rem; min-width: 4.5ch; }\n.kazu-play .kzp-progress { color: var(--kzp-muted); font-size: .85rem; }\n.kazu-play .kzp-box { position: relative; width: 100%; aspect-ratio: 1; overflow: hidden; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; cursor: pointer; border-radius: 8px; }\n.kazu-play .kzp-box:focus-visible { outline: 3px solid var(--kzp-accent); outline-offset: 2px; }\n.kazu-play .kzp-box[data-over=\"true\"] { cursor: default; }\n.kazu-play .kzp-inner { position: absolute; inset: 0; }\n.kazu-play .kzp-inner .kazu { position: absolute; inset: 0; width: 100%; height: 100%; }\n.kazu-play .kzp-pad { display: grid; grid-template-columns: repeat(var(--kzp-columns, 5), minmax(0, 1fr)); gap: 6px; margin-top: 10px; }\n.kazu-play .kzp-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }\n.kazu-play [hidden] { display: none !important; }\n.kazu-play button { font: inherit; color: inherit; user-select: none; -webkit-user-select: none; touch-action: manipulation; }\n.kazu-play .kzp-key, .kazu-play .kzp-button { border: 1px solid var(--kzp-rule); background: var(--kzp-surface); color: var(--kzp-ink); border-radius: 12px; min-height: 44px; min-width: 44px; padding: 0 12px; font-size: 1.05rem; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; }\n.kazu-play .kzp-button { border-radius: 999px; font-size: .85rem; font-weight: 600; }\n.kazu-play .kzp-key:hover:not(:disabled), .kazu-play .kzp-button:hover:not(:disabled) { border-color: var(--kzp-ink); }\n.kazu-play .kzp-key:disabled, .kazu-play .kzp-button:disabled { opacity: .32; cursor: default; }\n.kazu-play .kzp-key[data-done=\"true\"] { opacity: .4; }\n.kazu-play .kzp-key[data-erase=\"true\"] { color: var(--kzp-muted); }\n.kazu-play .kzp-button[aria-pressed=\"true\"] { background: var(--kzp-ink); color: var(--kzp-surface); border-color: var(--kzp-ink); }\n.kazu-play .kzp-key:focus-visible, .kazu-play .kzp-button:focus-visible { outline: 3px solid var(--kzp-accent); outline-offset: 2px; }\n.kazu-play .kzp-says { margin: 10px 0 0; min-height: 3.9em; font-size: .85rem; line-height: 1.4; color: var(--kzp-muted); }\n.kazu-play .kzp-says[data-warn=\"true\"] { color: var(--kzp-accent); font-weight: 600; }\n.kazu-play[data-solved=\"true\"] .kzp-says, .kazu-play[data-solved=\"true\"] .kzp-clock { color: var(--kzp-good); font-weight: 600; }\n.kazu-play .kzp-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n";
@@ -0,0 +1,47 @@
1
+ import { KAZU_STYLE } from "./style.js";
2
+ /**
3
+ * THE STYLE a playable Kazu board wears (`mountKazu`, `<kazu-board>`): the drawing's own (`KAZU_STYLE`)
4
+ * and the board's box, its number pad, its buttons and its lines of words. Colours are custom properties
5
+ * on `.kazu-play` (`--kzp-ink`, `--kzp-muted`, `--kzp-rule`, `--kzp-surface`, `--kzp-accent`,
6
+ * `--kzp-good`) so a page sets only what it wants different.
7
+ *
8
+ * Nothing moves when something is chosen: the board is one square box, the lines of words keep the room
9
+ * their longest wording takes, and the buttons are one size. Nothing the player touches can be
10
+ * selected, and no tap on it zooms the page.
11
+ */
12
+ export const KAZU_PLAY_STYLE = `${KAZU_STYLE}
13
+ .kazu-play {
14
+ --kzp-ink: #1f2320; --kzp-muted: #6b6f68; --kzp-rule: #ddd6c6; --kzp-surface: #fbf8f1; --kzp-accent: #b5452c; --kzp-good: #2f7a4f;
15
+ display: block; max-width: 100%; box-sizing: border-box; color: var(--kzp-ink); font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
16
+ user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;
17
+ }
18
+ @media (prefers-color-scheme: dark) {
19
+ :root:not([data-theme="light"]) .kazu-play { --kzp-ink: #ece8dc; --kzp-muted: #a09d93; --kzp-rule: #3a3d38; --kzp-surface: #1d201e; --kzp-accent: #ff8a6b; --kzp-good: #6fcf97; }
20
+ }
21
+ :root[data-theme="dark"] .kazu-play { --kzp-ink: #ece8dc; --kzp-muted: #a09d93; --kzp-rule: #3a3d38; --kzp-surface: #1d201e; --kzp-accent: #ff8a6b; --kzp-good: #6fcf97; }
22
+ .kazu-play *, .kazu-play *::before, .kazu-play *::after { box-sizing: border-box; }
23
+ .kazu-play .kzp-bar { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 8px; min-height: 1.5em; font-variant-numeric: tabular-nums; }
24
+ .kazu-play .kzp-clock { font-weight: 700; font-size: 1.05rem; min-width: 4.5ch; }
25
+ .kazu-play .kzp-progress { color: var(--kzp-muted); font-size: .85rem; }
26
+ .kazu-play .kzp-box { position: relative; width: 100%; aspect-ratio: 1; overflow: hidden; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; cursor: pointer; border-radius: 8px; }
27
+ .kazu-play .kzp-box:focus-visible { outline: 3px solid var(--kzp-accent); outline-offset: 2px; }
28
+ .kazu-play .kzp-box[data-over="true"] { cursor: default; }
29
+ .kazu-play .kzp-inner { position: absolute; inset: 0; }
30
+ .kazu-play .kzp-inner .kazu { position: absolute; inset: 0; width: 100%; height: 100%; }
31
+ .kazu-play .kzp-pad { display: grid; grid-template-columns: repeat(var(--kzp-columns, 5), minmax(0, 1fr)); gap: 6px; margin-top: 10px; }
32
+ .kazu-play .kzp-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
33
+ .kazu-play [hidden] { display: none !important; }
34
+ .kazu-play button { font: inherit; color: inherit; user-select: none; -webkit-user-select: none; touch-action: manipulation; }
35
+ .kazu-play .kzp-key, .kazu-play .kzp-button { border: 1px solid var(--kzp-rule); background: var(--kzp-surface); color: var(--kzp-ink); border-radius: 12px; min-height: 44px; min-width: 44px; padding: 0 12px; font-size: 1.05rem; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; }
36
+ .kazu-play .kzp-button { border-radius: 999px; font-size: .85rem; font-weight: 600; }
37
+ .kazu-play .kzp-key:hover:not(:disabled), .kazu-play .kzp-button:hover:not(:disabled) { border-color: var(--kzp-ink); }
38
+ .kazu-play .kzp-key:disabled, .kazu-play .kzp-button:disabled { opacity: .32; cursor: default; }
39
+ .kazu-play .kzp-key[data-done="true"] { opacity: .4; }
40
+ .kazu-play .kzp-key[data-erase="true"] { color: var(--kzp-muted); }
41
+ .kazu-play .kzp-button[aria-pressed="true"] { background: var(--kzp-ink); color: var(--kzp-surface); border-color: var(--kzp-ink); }
42
+ .kazu-play .kzp-key:focus-visible, .kazu-play .kzp-button:focus-visible { outline: 3px solid var(--kzp-accent); outline-offset: 2px; }
43
+ .kazu-play .kzp-says { margin: 10px 0 0; min-height: 3.9em; font-size: .85rem; line-height: 1.4; color: var(--kzp-muted); }
44
+ .kazu-play .kzp-says[data-warn="true"] { color: var(--kzp-accent); font-weight: 600; }
45
+ .kazu-play[data-solved="true"] .kzp-says, .kazu-play[data-solved="true"] .kzp-clock { color: var(--kzp-good); font-weight: 600; }
46
+ .kazu-play .kzp-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
47
+ `;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * WHAT HAS BEEN WRITTEN ON AN UNFINISHED PUZZLE, as strings a page can keep and bring back.
3
+ *
4
+ * A run is the player's entries the way a puzzle's cells are written (`cells.ts`): one character a
5
+ * cell, the printed cells left empty. These are the spellings itsutsu.com keeps (`PuzzleRun.progress`
6
+ * and its step log), and they read here unchanged. Pencil marks are Kazu's own and ride beside a run
7
+ * as a second string.
8
+ */
9
+ /** The entries of a run as its code. */
10
+ export declare function encodeRun(entries: readonly number[]): string;
11
+ /** The entries a run's code says, or null for a code that is not a run of this side. */
12
+ export declare function decodeRun(code: string, size: number): number[] | null;
13
+ /** Every value of a pencil-mark mask, smallest first. */
14
+ export declare function notesOf(mask: number): number[];
15
+ /** Pencil marks as a code: for each cell that has any, its index in base 36 (two characters) and its marks as one number in base 36 (four characters); nothing at all for a grid with none. */
16
+ export declare function encodeNotes(notes: readonly number[]): string;
17
+ /** The pencil marks a code says (each cell a bit mask, bit `v` for the number `v`), or null for a code that is not one for a grid of this side. */
18
+ export declare function decodeNotes(code: string, size: number): number[] | null;
19
+ /** How many grids a step log keeps: the newest, so a long puzzle's log has a ceiling. */
20
+ export declare const KAZU_STEPS_KEPT = 400;
21
+ /**
22
+ * THE STEPS OF A PUZZLE, written down so a scrubber has them when the puzzle is picked up again. Every
23
+ * step is a grid in a run's code. The first is written whole; each after it as the cells that changed
24
+ * (a cell's place in base 36, two characters, and its new character), the steps parted by "~", which
25
+ * no run uses. A long puzzle's log is a few hundred characters, not a grid per step.
26
+ */
27
+ export declare function encodeSteps(codes: readonly string[]): string;
28
+ /** The grids a step log holds, each `cells` characters long, or null for a log that does not read as one. */
29
+ export declare function decodeSteps(log: string, cells: number): string[] | null;
@@ -0,0 +1,96 @@
1
+ import { decodeCells, encodeCells } from "./cells.js";
2
+ /**
3
+ * WHAT HAS BEEN WRITTEN ON AN UNFINISHED PUZZLE, as strings a page can keep and bring back.
4
+ *
5
+ * A run is the player's entries the way a puzzle's cells are written (`cells.ts`): one character a
6
+ * cell, the printed cells left empty. These are the spellings itsutsu.com keeps (`PuzzleRun.progress`
7
+ * and its step log), and they read here unchanged. Pencil marks are Kazu's own and ride beside a run
8
+ * as a second string.
9
+ */
10
+ /** The entries of a run as its code. */
11
+ export function encodeRun(entries) {
12
+ return encodeCells(entries);
13
+ }
14
+ /** The entries a run's code says, or null for a code that is not a run of this side. */
15
+ export function decodeRun(code, size) {
16
+ return code.length === size * size ? decodeCells(code, size) : null;
17
+ }
18
+ /** Every value of a pencil-mark mask, smallest first. */
19
+ export function notesOf(mask) {
20
+ const out = [];
21
+ for (let value = 1; mask >>> value !== 0; value += 1)
22
+ if ((mask >>> value) & 1)
23
+ out.push(value);
24
+ return out;
25
+ }
26
+ /** Pencil marks as a code: for each cell that has any, its index in base 36 (two characters) and its marks as one number in base 36 (four characters); nothing at all for a grid with none. */
27
+ export function encodeNotes(notes) {
28
+ let code = "";
29
+ notes.forEach((mask, index) => {
30
+ if (mask !== 0)
31
+ code += index.toString(36).padStart(2, "0") + (mask >>> 1).toString(36).padStart(4, "0");
32
+ });
33
+ return code;
34
+ }
35
+ /** The pencil marks a code says (each cell a bit mask, bit `v` for the number `v`), or null for a code that is not one for a grid of this side. */
36
+ export function decodeNotes(code, size) {
37
+ if (code.length % 6 !== 0)
38
+ return null;
39
+ const notes = new Array(size * size).fill(0);
40
+ for (let at = 0; at < code.length; at += 6) {
41
+ const index = Number.parseInt(code.slice(at, at + 2), 36);
42
+ const bits = Number.parseInt(code.slice(at + 2, at + 6), 36);
43
+ if (!Number.isInteger(index) || index < 0 || index >= size * size || !Number.isInteger(bits) || bits <= 0 || bits >= 2 ** size)
44
+ return null;
45
+ notes[index] = bits << 1;
46
+ }
47
+ return notes;
48
+ }
49
+ /** How many grids a step log keeps: the newest, so a long puzzle's log has a ceiling. */
50
+ export const KAZU_STEPS_KEPT = 400;
51
+ const PARTED = "~";
52
+ /**
53
+ * THE STEPS OF A PUZZLE, written down so a scrubber has them when the puzzle is picked up again. Every
54
+ * step is a grid in a run's code. The first is written whole; each after it as the cells that changed
55
+ * (a cell's place in base 36, two characters, and its new character), the steps parted by "~", which
56
+ * no run uses. A long puzzle's log is a few hundred characters, not a grid per step.
57
+ */
58
+ export function encodeSteps(codes) {
59
+ const kept = codes.slice(-KAZU_STEPS_KEPT);
60
+ if (kept.length === 0)
61
+ return "";
62
+ const parts = [kept[0]];
63
+ for (let at = 1; at < kept.length; at += 1) {
64
+ const before = kept[at - 1];
65
+ const after = kept[at];
66
+ let changed = "";
67
+ for (let cell = 0; cell < after.length; cell += 1) {
68
+ if (after[cell] !== before[cell])
69
+ changed += cell.toString(36).padStart(2, "0") + after[cell];
70
+ }
71
+ parts.push(changed);
72
+ }
73
+ return parts.join(PARTED);
74
+ }
75
+ /** The grids a step log holds, each `cells` characters long, or null for a log that does not read as one. */
76
+ export function decodeSteps(log, cells) {
77
+ if (log === "")
78
+ return null;
79
+ const [first, ...changes] = log.split(PARTED);
80
+ if (first === undefined || first.length !== cells || changes.length >= KAZU_STEPS_KEPT)
81
+ return null;
82
+ const codes = [first];
83
+ for (const change of changes) {
84
+ if (change.length % 3 !== 0)
85
+ return null;
86
+ const grid = [...codes.at(-1)];
87
+ for (let at = 0; at < change.length; at += 3) {
88
+ const cell = Number.parseInt(change.slice(at, at + 2), 36);
89
+ if (!Number.isInteger(cell) || cell < 0 || cell >= cells)
90
+ return null;
91
+ grid[cell] = change[at + 2];
92
+ }
93
+ codes.push(grid.join(""));
94
+ }
95
+ return codes;
96
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * SEEDED RANDOMNESS, the one thing every puzzle is made from.
3
+ *
4
+ * mulberry32 (Tommy Ettinger, 2017): one 32-bit word of state, a few multiplies
5
+ * a draw, the same stream for the same seed in every browser and every Node. It
6
+ * decides every shuffle, so it must never change: a solve kept as its kind, size,
7
+ * level and seed is made again from them, and a changed stream would hand somebody
8
+ * a different puzzle from the one they were playing. It is the same stream
9
+ * `@johnmorrisdotca/tane` draws, written out here so this package depends on
10
+ * nothing; `site.fixture.test.ts` pins every puzzle the site made from it.
11
+ *
12
+ * Not a credential: it is for fair-looking puzzles, never for secrets.
13
+ */
14
+ /** A number in [0, 1), like `Math.random`, from a stream a seed fixes. */
15
+ export type Random = () => number;
16
+ /** A stream of numbers in [0, 1) fixed by a seed. The seed is read as an unsigned 32-bit integer. */
17
+ export declare function seededRandom(seed: number): Random;
18
+ /** A copy of the list in a random order (Fisher–Yates, from the end); the list given is left alone. */
19
+ export declare function shuffled<T>(items: readonly T[], random: Random): T[];
20
+ /** The most a seed can be: a whole number from 1 to 2³¹ − 1, so it travels in an address as a plain integer. */
21
+ export declare const KAZU_SEED_MOST: number;
22
+ /** Whether a value is a seed this package takes: a whole number from 1 to `KAZU_SEED_MOST`. */
23
+ export declare function isKazuSeed(value: unknown): value is number;
24
+ /** A new seed, from `Math.random` or the stream given: anywhere in the range. */
25
+ export declare function freshKazuSeed(random?: Random): number;
package/dist/random.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * SEEDED RANDOMNESS, the one thing every puzzle is made from.
3
+ *
4
+ * mulberry32 (Tommy Ettinger, 2017): one 32-bit word of state, a few multiplies
5
+ * a draw, the same stream for the same seed in every browser and every Node. It
6
+ * decides every shuffle, so it must never change: a solve kept as its kind, size,
7
+ * level and seed is made again from them, and a changed stream would hand somebody
8
+ * a different puzzle from the one they were playing. It is the same stream
9
+ * `@johnmorrisdotca/tane` draws, written out here so this package depends on
10
+ * nothing; `site.fixture.test.ts` pins every puzzle the site made from it.
11
+ *
12
+ * Not a credential: it is for fair-looking puzzles, never for secrets.
13
+ */
14
+ /** A stream of numbers in [0, 1) fixed by a seed. The seed is read as an unsigned 32-bit integer. */
15
+ export function seededRandom(seed) {
16
+ let state = seed >>> 0;
17
+ return () => {
18
+ state = (state + 0x6d2b79f5) >>> 0;
19
+ let t = Math.imul(state ^ (state >>> 15), state | 1);
20
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
21
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
22
+ };
23
+ }
24
+ /** A copy of the list in a random order (Fisher–Yates, from the end); the list given is left alone. */
25
+ export function shuffled(items, random) {
26
+ const out = [...items];
27
+ for (let i = out.length - 1; i > 0; i -= 1) {
28
+ const j = Math.floor(random() * (i + 1));
29
+ [out[i], out[j]] = [out[j], out[i]];
30
+ }
31
+ return out;
32
+ }
33
+ /** The most a seed can be: a whole number from 1 to 2³¹ − 1, so it travels in an address as a plain integer. */
34
+ export const KAZU_SEED_MOST = 2 ** 31 - 1;
35
+ /** Whether a value is a seed this package takes: a whole number from 1 to `KAZU_SEED_MOST`. */
36
+ export function isKazuSeed(value) {
37
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= KAZU_SEED_MOST;
38
+ }
39
+ /** A new seed, from `Math.random` or the stream given: anywhere in the range. */
40
+ export function freshKazuSeed(random = Math.random) {
41
+ return 1 + Math.floor(random() * KAZU_SEED_MOST);
42
+ }
@@ -0,0 +1,22 @@
1
+ import type { KazuKind } from "./kinds.ts";
2
+ /**
3
+ * How many answers a puzzle has, up to `limit` (two by default, so "many" costs no more than "two"):
4
+ * 1 is a puzzle, 0 is a grid that cannot be finished, 2 is a guessing game. Null, never a number,
5
+ * for givens that are not a puzzle of this kind and side, and for a search that ran past `budget`
6
+ * steps (Sudoku, Jigsaw, Diagonal and Sum Cages count steps; the other two have no budget): "I
7
+ * could not say" is not "there are none".
8
+ */
9
+ export declare function countKazuSolutions(kind: KazuKind, size: number, givens: string, limit?: number, budget?: number): number | null;
10
+ /**
11
+ * The one answer a puzzle's givens allow, as a cells code, or null when they allow none, more than
12
+ * one, or the search ran past `budget` steps (Sudoku, Jigsaw, Diagonal and Sum Cages). A grid this
13
+ * cannot vouch for is never handed back as though it were the answer.
14
+ */
15
+ export declare function solveKazu(kind: KazuKind, size: number, givens: string, budget?: number): string | null;
16
+ /**
17
+ * How many guesses, each followed by everything reasoning then finds, a person needs to finish the
18
+ * puzzle: 0 when reasoning alone finishes it (easy), 1 when one guess does (medium), more when more
19
+ * (hard); Infinity when there is no answer. Null for givens that are not a puzzle. Meant for a
20
+ * puzzle already known to have exactly one answer.
21
+ */
22
+ export declare function kazuGuessDepth(kind: KazuKind, size: number, givens: string): number | null;