@johnmorrisdotca/kazu 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ All notable changes to this project are written here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.0] - 2026-10-05
10
+
11
+ ### Added
12
+
13
+ - **A 25×25 Sudoku, the Colossus.** `number-place` takes a side of 25 (`KAZU_SPECS["number-place"].sizes` is now 4, 6, 9, 16, 25; boxes of 5×5; `mostCells` 625) at `easy`, `medium` and `hard`, every puzzle with exactly one answer. Its symbols go on from the 16×16's: 1 to 9, then A to P for 10 to 25 (`symbolOf`, `valueOfSymbol`, `decodeCells` and the run and note codes read A to P). Easy yields to singles alone (366 numbers printed), medium to one guess (305), hard to two nested guesses (283). The player's pad has 25 keys and the eraser in three rows, and the keyboard types the letters up to P; because N is the number 23 there, Pencil moves to the slash key on this size (`keysColossus` says so in English and Japanese). Made in a browser's time, over seeds 1 to 50 on a Mac: easy a median 13 ms (slowest 30), medium 81 ms (161), hard 308 ms (590). Hard is the one a phone may take a few seconds over, so a page that deals one for a person should do it in a worker, as the demo's slower generators do.
14
+ - A 25×25 is made and checked by proof, not by counting every answer: `provedByGuessing(grid, layout, guesses)` is what a person's reasoning proves (singles, then guesses at the most constrained cell, each shown wrong or finished by singles), and `countKazuSolutions`, `solveKazu` and `kazuGuessDepth` use singles inside the search from the 25×25 up, which takes a medium or hard 25×25 from minutes to a few milliseconds. Every smaller size is searched exactly as before.
15
+
16
+ ### Fixed
17
+
18
+ - **Akari never returns the old fixed lattice.** When a level found no random board within its 60 attempts, 1.3.0 returned the fixed rooms of 1.2.0 without saying so: 74% black squares and five full black rows at 14×14, which was about 45% of 14×14 easy seeds (and 17% of 12×12, 35% of 13×13, 65% of 15×15 and 83% of 16×16 easy seeds, and some medium ones at 13×13 and larger). The generator now tries again with more black squares (1.5, 1.8, 2.1 and 2.4 times the usual share, thirty attempts each), which a large board needs to be solvable by its numbers. Every seed that made a random board in 1.3.0 makes the identical one (`src/levels.pins.test.js`, and a comparison over 12×12 to 16×16 at every level); only the seeds that used to return the lattice make a different board, and they make a random one. `src/akariLattice.test.ts` asserts, over every size from 3 to 16 and every level, that no seed returns the lattice, and that the black share and the boards vary. Generation time is unchanged where it was already random (14×14 extra-hard: median 212 ms, slowest 362 ms) and the old lattice cases take a median of 9 ms at 14×14 easy.
19
+
9
20
  ## [1.3.0] - 2026-10-05
10
21
 
11
22
  ### Added
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">Kazu <sub>数</sub></h1>
2
2
 
3
3
  <p align="center"><strong>Grid number and logic puzzles for JavaScript and TypeScript.</strong><br>
4
- Sudoku (4×4 to a 16×16 Giant), Jigsaw, Diagonal and Killer Sudoku, Futoshiki, Skyscrapers, Shikaku, Hitori, Nurikabe, Akari, Juosan, Slitherlink, Masyu, Yajilin, Ripple Effect, Kakuro, Fillomino and Heyawake. Dedicated typed engines, independently checked puzzles, SVG drawing, saved progress, and English and Japanese players for touch, mouse and keyboard. No runtime dependencies.</p>
4
+ Sudoku (4×4 to a 25×25 Colossus), Jigsaw, Diagonal and Killer Sudoku, Futoshiki, Skyscrapers, Shikaku, Hitori, Nurikabe, Akari, Juosan, Slitherlink, Masyu, Yajilin, Ripple Effect, Kakuro, Fillomino and Heyawake. Dedicated typed engines, independently checked puzzles, SVG drawing, saved progress, and English and Japanese players for touch, mouse and keyboard. No runtime dependencies.</p>
5
5
 
6
6
  <p align="center">
7
7
  <a href="https://github.com/johnmorrisdotca/kazu/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/johnmorrisdotca/kazu/actions/workflows/ci.yml/badge.svg"></a>
@@ -60,8 +60,8 @@ And in a page, a puzzle to play, by touch, mouse and keyboard, with nothing else
60
60
 
61
61
  ## Features
62
62
 
63
- - **Seven puzzles, three levels.** Sudoku (4×4, 6×6, 9×9 and a 16×16 Giant), Jigsaw, Diagonal and Killer Sudoku, Futoshiki and Skyscrapers, each at `easy`, `medium` and `hard`, named by kebab-case keys.
64
- - **Six number puzzles, three levels.** Sudoku (4×4, 6×6, 9×9 and a 16×16 Giant), Jigsaw, Diagonal and Killer Sudoku, Futoshiki and Skyscrapers, each at `easy`, `medium` and `hard`, named by kebab-case keys. Shikaku and Juosan use dedicated rectangle and territory models.
63
+ - **Seven puzzles, three levels.** Sudoku (4×4, 6×6, 9×9, a 16×16 Giant and a 25×25 Colossus), Jigsaw, Diagonal and Killer Sudoku, Futoshiki and Skyscrapers, each at `easy`, `medium` and `hard`, named by kebab-case keys.
64
+ - **Six number puzzles, three levels.** Sudoku (4×4, 6×6, 9×9, a 16×16 Giant and a 25×25 Colossus), Jigsaw, Diagonal and Killer Sudoku, Futoshiki and Skyscrapers, each at `easy`, `medium` and `hard`, named by kebab-case keys. Shikaku and Juosan use dedicated rectangle and territory models.
65
65
  - **Four levels on the grid puzzles.** Shikaku, Akari, Slitherlink, Hitori, Fillomino and Kakuro each make `easy`, `medium`, `hard` and `extra-hard` boards, at three or more sizes each, every one with exactly one answer and rated by what a person must do to solve it: [Levels of the grid puzzles](#levels-of-the-grid-puzzles).
66
66
  - **Exactly one answer.** A generator makes puzzles from a seed, and a solver that counts answers confirms there is one. The same kind, size, level and seed make the same puzzle in every browser and every Node, for ever.
67
67
  - **A check a server can trust.** `checkKazu` reads a finished grid in O(cells), with no search, and says the first thing wrong in words.
@@ -177,7 +177,7 @@ exactly one answer. Every kind is named by a kebab-case key.
177
177
 
178
178
  | Key | Called | In Japanese | Sizes | What it prints | What is new |
179
179
  | --- | --- | --- | --- | --- | --- |
180
- | `number-place` | Sudoku | ナンプレ | 4×4, 6×6, 9×9, 16×16 | numbers | every row, column and box holds each number once; the 16×16 Giant uses 1 to 9 and then A to G |
180
+ | `number-place` | Sudoku | ナンプレ | 4×4, 6×6, 9×9, 16×16, 25×25 | numbers | every row, column and box holds each number once; the 16×16 Giant uses 1 to 9 and then A to G, and the 25×25 Colossus goes on to P |
181
181
  | `jigsaw` | Jigsaw Sudoku | 変形ナンプレ | 5×5, 6×6, 7×7, 9×9 | numbers, and the regions | the boxes are irregular regions, each joined and none a row or a column |
182
182
  | `diagonal` | Diagonal Sudoku | 対角ナンプレ | 6×6, 9×9 | numbers | the two long diagonals hold each number once too |
183
183
  | `sum-cages` | Killer Sudoku | サムナンプレ | 6×6, 9×9 | cages and their sums | next to no numbers: dashed cages each add to a sum, and repeat nothing |
@@ -218,7 +218,7 @@ has always used. It restates the rules rather than reading the solver's mind, so
218
218
  A solver and a generator never run on a server unless you ask them to.
219
219
 
220
220
  A run is kept as short strings the site's own stored runs decode as they are: `encodeRun(entries)` and
221
- `decodeRun(code, size)` (one character a cell, `.` for empty, A to G past nine), `encodeSteps` and
221
+ `decodeRun(code, size)` (one character a cell, `.` for empty, A to P past nine), `encodeSteps` and
222
222
  `decodeSteps` for a step log, `kazuHash(givens)` for a fingerprint of a puzzle, and `encodeNotes` and
223
223
  `decodeNotes` for Kazu's own pencil marks.
224
224
 
@@ -293,8 +293,8 @@ out of the notes of the cells it shares a group with. **Undo** takes the last ch
293
293
  cell to fill next and why, **Check** says how many cells are wrong, never which. A clock starts on the first entry
294
294
  and stops when the last cell is right, and waits while the page is hidden.
295
295
 
296
- The keys: the arrows move, a number (1 to 9, and A to G on the 16×16) fills the chosen cell, Shift with a
297
- number writes it as a pencil mark, Backspace empties the cell, N turns Pencil on or off, Ctrl or Cmd with Z
296
+ The keys: the arrows move, a number (1 to 9, then A to G on the 16×16 and on to P on the 25×25) fills the chosen cell, Shift with a
297
+ number writes it as a pencil mark, Backspace empties the cell, N turns Pencil on or off (the slash key on the 25×25, where N is the number 23), Ctrl or Cmd with Z
298
298
  undoes, Escape lets the cell go. The board's box keeps one steady square, and the lines of words under it keep
299
299
  the room their longest wording takes, so nothing moves as numbers are written or messages come and go. Nothing
300
300
  the player touches can be selected. Its words are English and Japanese and follow the page's `lang`.
@@ -417,10 +417,10 @@ All of these are held by tests, and the ones with a name are exported.
417
417
  | --- | --- | --- |
418
418
  | Puzzles | the six keys of `KAZU_KINDS` | the table under [The puzzles](#the-puzzles) |
419
419
  | Levels | `easy`, `medium`, `hard` | `KAZU_LEVELS` |
420
- | Sizes | each puzzle's own, 4×4 to 16×16 | `KAZU_SPECS[kind].sizes` |
420
+ | Sizes | each puzzle's own, 4×4 to 25×25 | `KAZU_SPECS[kind].sizes` |
421
421
  | A seed | a whole number from 1 to 2,147,483,647 | `KAZU_SEED_MOST`, `isKazuSeed` |
422
- | Symbols in a grid | `1` to `9`, then `A` to `G` for the 16×16 | `symbolOf`, `valueOfSymbol` |
423
- | The longest givens code | Sudoku 256 characters, Jigsaw 162, Diagonal 81, Killer Sudoku 286, Futoshiki 133, Skyscrapers 77 | `KAZU_SPECS[kind].mostCells`, for a route that must refuse anything larger |
422
+ | Symbols in a grid | `1` to `9`, then `A` to `G` for the 16×16 and on to `P` for the 25×25 | `symbolOf`, `valueOfSymbol` |
423
+ | The longest givens code | Sudoku 625 characters, Jigsaw 162, Diagonal 81, Killer Sudoku 286, Futoshiki 133, Skyscrapers 77 | `KAZU_SPECS[kind].mostCells`, for a route that must refuse anything larger |
424
424
  | Answers counted | two, so that "many" costs no more than "two" | the `limit` argument of `countKazuSolutions` |
425
425
  | The solver's work | 2,000,000 steps, then it says it cannot say (`null`) | the `budget` argument of `solveKazu` |
426
426
  | A step log | the newest 400 steps | `KAZU_STEPS_KEPT` |
@@ -881,7 +881,7 @@ src/
881
881
  ├── kinds.ts the six puzzles' keys, sizes and levels, and the shape of a puzzle
882
882
  ├── random.ts the seeded random numbers every puzzle is made from
883
883
  ├── csp.ts the one small engine under the six grid kinds: counting answers, and reasoning with and without supposing
884
- ├── cells.ts a grid of numbers as a string, 1 to 9 and A to G
884
+ ├── cells.ts a grid of numbers as a string, 1 to 9 and A to P
885
885
  ├── layout.ts the groups that must each hold every number once: rows, columns, boxes, regions, diagonals, cages
886
886
  ├── groupSolve.ts the solver for puzzles made of groups: counting, singles, depth
887
887
  ├── numberPlace.ts Sudoku and Diagonal Sudoku: the generator and how givens are carved
@@ -5,3 +5,10 @@ export declare const AKARI_LEVELS: readonly ["easy", "medium", "hard", "extra-ha
5
5
  /** The sides the demo and the site offer for a square board; any side from 2 to `AKARI_MOST_SIDE` can be made. */
6
6
  export declare const AKARI_SIZES: readonly [5, 7, 10, 14];
7
7
  export declare const AKARI_MOST_ATTEMPTS = 60;
8
+ /**
9
+ * When a level finds no board in `AKARI_MOST_ATTEMPTS` plain attempts (a large board of an easy level, mostly: one attempt in a
10
+ * hundred succeeds on a 14×14), the generator tries again with more black squares, each of these multiples of the usual share in
11
+ * turn, `AKARI_DENSER_ATTEMPTS` attempts at each. Black squares are what make a large board solvable by its numbers.
12
+ */
13
+ export declare const AKARI_DENSER_CROWDS: readonly [1.5, 1.8, 2.1, 2.4];
14
+ export declare const AKARI_DENSER_ATTEMPTS = 30;
@@ -5,3 +5,10 @@ export const AKARI_LEVELS = ["easy", "medium", "hard", "extra-hard"];
5
5
  /** The sides the demo and the site offer for a square board; any side from 2 to `AKARI_MOST_SIDE` can be made. */
6
6
  export const AKARI_SIZES = [5, 7, 10, 14];
7
7
  export const AKARI_MOST_ATTEMPTS = 60;
8
+ /**
9
+ * When a level finds no board in `AKARI_MOST_ATTEMPTS` plain attempts (a large board of an easy level, mostly: one attempt in a
10
+ * hundred succeeds on a 14×14), the generator tries again with more black squares, each of these multiples of the usual share in
11
+ * turn, `AKARI_DENSER_ATTEMPTS` attempts at each. Black squares are what make a large board solvable by its numbers.
12
+ */
13
+ export const AKARI_DENSER_CROWDS = [1.5, 1.8, 2.1, 2.4];
14
+ export const AKARI_DENSER_ATTEMPTS = 30;
@@ -5,6 +5,8 @@ import type { AkariLevel, AkariPuzzle } from "./akari.types.ts";
5
5
  * the puzzle can still be solved the way the level asks: easy and medium by the rules alone (easy keeps most
6
6
  * of its numbers), hard by supposing one square at a time, extra-hard for as long as the answer stays single,
7
7
  * which leaves a board that needs more supposing than that. If no board of the level is found within the
8
- * attempts, the next one down is tried, and the first generator, which cannot fail, is the last.
8
+ * attempts, the next one down is tried; if none of the levels finds one, the same is tried again with more
9
+ * black squares, which a large board needs to be solvable at all (see `AKARI_DENSER_CROWDS`). The fixed lattice
10
+ * of the first generator is only what is left if even that fails, which no seed does at any size offered.
9
11
  */
10
12
  export declare function generateAkari(width?: number, height?: number, seed?: number, level?: AkariLevel): AkariPuzzle;
@@ -1,4 +1,4 @@
1
- import { AKARI_LEVELS, AKARI_MOST_ATTEMPTS, AKARI_MOST_SIDE } from "./akari.constants.js";
1
+ import { AKARI_DENSER_ATTEMPTS, AKARI_DENSER_CROWDS, AKARI_LEVELS, AKARI_MOST_ATTEMPTS, AKARI_MOST_SIDE } from "./akari.constants.js";
2
2
  import { akariNeighbours, akariVisible, checkAkari, isAkariBoard } from "./akariBoard.js";
3
3
  import { akariModel } from "./akariLogic.js";
4
4
  import { solveAkari } from "./akariSolve.js";
@@ -11,38 +11,51 @@ import { isKazuSeed, seededRandom, shuffled } from "./random.js";
11
11
  * the puzzle can still be solved the way the level asks: easy and medium by the rules alone (easy keeps most
12
12
  * of its numbers), hard by supposing one square at a time, extra-hard for as long as the answer stays single,
13
13
  * which leaves a board that needs more supposing than that. If no board of the level is found within the
14
- * attempts, the next one down is tried, and the first generator, which cannot fail, is the last.
14
+ * attempts, the next one down is tried; if none of the levels finds one, the same is tried again with more
15
+ * black squares, which a large board needs to be solvable at all (see `AKARI_DENSER_CROWDS`). The fixed lattice
16
+ * of the first generator is only what is left if even that fails, which no seed does at any size offered.
15
17
  */
16
18
  export function generateAkari(width = 7, height = width, seed = 1, level = "medium") {
17
19
  if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= AKARI_MOST_SIDE)
18
20
  || !isKazuSeed(seed) || !AKARI_LEVELS.includes(level))
19
21
  throw new RangeError("Invalid Akari settings");
20
22
  const random = seededRandom(seed);
23
+ // The plain attempts come first, as they always have, so every seed that made a board before makes the same one.
21
24
  for (let aim = AKARI_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
22
- const aimed = AKARI_LEVELS[aim];
23
- // Extra-hard is the hardest of several boards that need supposing: the most suppositions wins.
24
- const wanted = aimed === "extra-hard" ? 8 : 1;
25
- let best = null, found = 0;
26
- for (let attempt = 0; attempt < AKARI_MOST_ATTEMPTS && found < wanted; attempt += 1) {
27
- const made = attemptAkari(width, height, aimed, random);
28
- if (!made)
29
- continue;
30
- const proof = solveAkari(made.board, { limit: 2 });
31
- if (!proof.complete || proof.count !== 1 || !proof.solution || !checkAkari(made.board, proof.solution).ok)
32
- continue;
33
- found += 1;
34
- if (!best || made.probes > best.probes)
35
- best = { board: made.board, probes: made.probes, solution: proof.solution };
36
- }
37
- if (best)
38
- return { ...best.board, seed, level, solution: best.solution };
25
+ const found = bestAkari(width, height, AKARI_LEVELS[aim], random, 1, AKARI_MOST_ATTEMPTS);
26
+ if (found)
27
+ return { ...found.board, seed, level, solution: found.solution };
39
28
  }
29
+ for (const crowd of AKARI_DENSER_CROWDS)
30
+ for (let aim = AKARI_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
31
+ const found = bestAkari(width, height, AKARI_LEVELS[aim], random, crowd, AKARI_DENSER_ATTEMPTS);
32
+ if (found)
33
+ return { ...found.board, seed, level, solution: found.solution };
34
+ }
40
35
  const fallback = templateAkari(width, height, seed);
41
36
  return { ...fallback, level };
42
37
  }
43
- function attemptAkari(width, height, level, random) {
38
+ /** The best board of the level found within `attempts` tries at this share of black squares, with its answer, or null. */
39
+ function bestAkari(width, height, level, random, crowd, attempts) {
40
+ // Extra-hard is the hardest of several boards that need supposing: the most suppositions wins.
41
+ const wanted = level === "extra-hard" ? 8 : 1;
42
+ let best = null, found = 0;
43
+ for (let attempt = 0; attempt < attempts && found < wanted; attempt += 1) {
44
+ const made = attemptAkari(width, height, level, random, crowd);
45
+ if (!made)
46
+ continue;
47
+ const proof = solveAkari(made.board, { limit: 2 });
48
+ if (!proof.complete || proof.count !== 1 || !proof.solution || !checkAkari(made.board, proof.solution).ok)
49
+ continue;
50
+ found += 1;
51
+ if (!best || made.probes > best.probes)
52
+ best = { board: made.board, probes: made.probes, solution: proof.solution };
53
+ }
54
+ return best;
55
+ }
56
+ function attemptAkari(width, height, level, random, crowd) {
44
57
  const size = width * height;
45
- const density = { easy: .2, medium: .22, hard: .24, "extra-hard": .26 }[level] * (.85 + random() * .3);
58
+ const density = { easy: .2, medium: .22, hard: .24, "extra-hard": .26 }[level] * (.85 + random() * .3) * crowd;
46
59
  const cells = Array.from({ length: size }, () => random() < density ? false : null);
47
60
  if (random() < .5)
48
61
  for (let y = 0; y < height; y += 1)
package/dist/cells.d.ts CHANGED
@@ -2,14 +2,14 @@
2
2
  * A GRID OF NUMBERS AS A STRING: for an address, a POST body, and a kept run.
3
3
  *
4
4
  * Row-major, one character per cell: a digit for a value, `.` for an empty cell. Past nine the
5
- * values are letters, A for 10 up to G for 16, as a 16×16 Sudoku is printed (`symbolOf`), so one
6
- * character is still one cell. Upper case only in a code, so one grid has one spelling. These are
5
+ * values are letters, A for 10 up to G for 16 as a 16×16 Sudoku is printed, and on to P for 25 on a
6
+ * 25×25 (`symbolOf`), so one character is still one cell. Upper case only in a code, so one grid has one spelling. These are
7
7
  * the spellings itsutsu.com has always stored, and they decode here unchanged.
8
8
  */
9
9
  export declare const EMPTY_CELL = ".";
10
- /** How a value is written, in a code and on a cell: 1–9, then A–G. */
10
+ /** How a value is written, in a code and on a cell: 1–9, then A–P. */
11
11
  export declare function symbolOf(value: number): string;
12
- /** The value a symbol names, or 0 for one that is not 1–9 or A–G (either case). */
12
+ /** The value a symbol names, or 0 for one that is not 1–9 or A–P (either case). */
13
13
  export declare function valueOfSymbol(symbol: string): number;
14
14
  /** `[0, 3, 0, 1]` → `".3.1"`. */
15
15
  export declare function encodeCells(cells: readonly number[]): string;
package/dist/cells.js CHANGED
@@ -2,18 +2,18 @@
2
2
  * A GRID OF NUMBERS AS A STRING: for an address, a POST body, and a kept run.
3
3
  *
4
4
  * Row-major, one character per cell: a digit for a value, `.` for an empty cell. Past nine the
5
- * values are letters, A for 10 up to G for 16, as a 16×16 Sudoku is printed (`symbolOf`), so one
6
- * character is still one cell. Upper case only in a code, so one grid has one spelling. These are
5
+ * values are letters, A for 10 up to G for 16 as a 16×16 Sudoku is printed, and on to P for 25 on a
6
+ * 25×25 (`symbolOf`), so one character is still one cell. Upper case only in a code, so one grid has one spelling. These are
7
7
  * the spellings itsutsu.com has always stored, and they decode here unchanged.
8
8
  */
9
9
  export const EMPTY_CELL = ".";
10
- /** The letters after 9, in order: A is 10, G is 16. */
11
- const PAST_NINE = "ABCDEFG";
12
- /** How a value is written, in a code and on a cell: 1–9, then A–G. */
10
+ /** The letters after 9, in order: A is 10, G is 16, P is 25. */
11
+ const PAST_NINE = "ABCDEFGHIJKLMNOP";
12
+ /** How a value is written, in a code and on a cell: 1–9, then A–P. */
13
13
  export function symbolOf(value) {
14
14
  return value <= 9 ? String(value) : PAST_NINE[value - 10];
15
15
  }
16
- /** The value a symbol names, or 0 for one that is not 1–9 or A–G (either case). */
16
+ /** The value a symbol names, or 0 for one that is not 1–9 or A–P (either case). */
17
17
  export function valueOfSymbol(symbol) {
18
18
  if (symbol.length !== 1)
19
19
  return 0;
@@ -60,6 +60,15 @@ export declare function applySingles(grid: Grid, layout: Layout): SinglesResult;
60
60
  * has no answer. Meant for a grid already known to have exactly one.
61
61
  */
62
62
  export declare function guessDepth(grid: Grid, layout: Layout): number;
63
+ /**
64
+ * What reasoning with at most `guesses` guesses proves about a grid: 0 when it proves there is no answer, 1
65
+ * when it proves there is exactly one, 2 when it proves neither (it could not finish, or there are several).
66
+ * A guess is made at the most constrained empty cell and every value is tried, each followed by everything
67
+ * singles find; the grid has one answer when exactly one value leads to one and every other leads to none.
68
+ * This is a person's proof, and it stops where theirs would, so it costs a bounded few hundred passes of
69
+ * singles however sparse and large the grid is: counting every answer of a sparse 25×25 grid does not.
70
+ */
71
+ export declare function provedByGuessing(grid: Grid, layout: Layout, guesses: number): 0 | 1 | 2;
63
72
  /**
64
73
  * A full grid for this layout, drawn at random, or null when the search runs
65
74
  * past `budget` steps — which for a jigsaw means these regions are a poor
@@ -121,6 +121,8 @@ export function countSolutions(grid, layout, limit = 2) {
121
121
  * rather than keeping a browser waiting.
122
122
  */
123
123
  export function countSolutionsWithin(grid, layout, limit, budget, first) {
124
+ if (layout.size > LARGEST_SEARCHED_BARE)
125
+ return countByReasoning(grid, layout, limit, budget, first);
124
126
  const work = [...grid];
125
127
  const taken = used(work, layout);
126
128
  let found = 0;
@@ -150,6 +152,41 @@ export function countSolutionsWithin(grid, layout, limit, budget, first) {
150
152
  step();
151
153
  return steps > budget && found < limit ? null : found;
152
154
  }
155
+ /**
156
+ * Up to this side the search guesses at the most constrained cell and nothing else, which is how every
157
+ * 4×4 to 16×16 grid has always been counted. Past it (the 25×25) a sparse grid thrashes that way for
158
+ * minutes, so a search there lets singles finish whatever they can before each guess.
159
+ */
160
+ const LARGEST_SEARCHED_BARE = 16;
161
+ /** The same count for a large grid: at every step all the singles are filled in first, and only what they leave is guessed at. */
162
+ function countByReasoning(grid, layout, limit, budget, first) {
163
+ let found = 0;
164
+ let steps = 0;
165
+ const step = (from) => {
166
+ if (found >= limit || steps > budget)
167
+ return;
168
+ steps += 1;
169
+ const singles = applySingles(from, layout);
170
+ if (singles.contradiction)
171
+ return;
172
+ if (singles.solved) {
173
+ if (found === 0)
174
+ first?.([...singles.grid]);
175
+ found += 1;
176
+ return;
177
+ }
178
+ const { index, mask } = mostConstrained(singles.grid, layout, used(singles.grid, layout));
179
+ for (let left = mask; left !== 0; left &= left - 1) {
180
+ const next = [...singles.grid];
181
+ next[index] = lowestBit(left);
182
+ step(next);
183
+ if (found >= limit || steps > budget)
184
+ return;
185
+ }
186
+ };
187
+ step(grid);
188
+ return steps > budget && found < limit ? null : found;
189
+ }
153
190
  /**
154
191
  * THE ANSWER, WORKED OUT FROM THE GIVENS: a finished puzzle kept before its
155
192
  * grid was, drawn solved rather than as dealt. Every puzzle made here has
@@ -245,6 +282,35 @@ export function guessDepth(grid, layout) {
245
282
  }
246
283
  return deepest === Infinity ? Infinity : deepest + 1;
247
284
  }
285
+ /**
286
+ * What reasoning with at most `guesses` guesses proves about a grid: 0 when it proves there is no answer, 1
287
+ * when it proves there is exactly one, 2 when it proves neither (it could not finish, or there are several).
288
+ * A guess is made at the most constrained empty cell and every value is tried, each followed by everything
289
+ * singles find; the grid has one answer when exactly one value leads to one and every other leads to none.
290
+ * This is a person's proof, and it stops where theirs would, so it costs a bounded few hundred passes of
291
+ * singles however sparse and large the grid is: counting every answer of a sparse 25×25 grid does not.
292
+ */
293
+ export function provedByGuessing(grid, layout, guesses) {
294
+ const singles = applySingles(grid, layout);
295
+ if (singles.contradiction)
296
+ return 0;
297
+ if (singles.solved)
298
+ return 1;
299
+ if (guesses < 1)
300
+ return 2;
301
+ const work = singles.grid;
302
+ const { index, mask } = mostConstrained(work, layout, used(work, layout));
303
+ let ones = 0;
304
+ for (let left = mask; left !== 0; left &= left - 1) {
305
+ const next = [...work];
306
+ next[index] = lowestBit(left);
307
+ const outcome = provedByGuessing(next, layout, guesses - 1);
308
+ if (outcome === 2)
309
+ return 2;
310
+ ones += outcome;
311
+ }
312
+ return ones === 0 ? 0 : ones === 1 ? 1 : 2;
313
+ }
248
314
  /**
249
315
  * A full grid for this layout, drawn at random, or null when the search runs
250
316
  * past `budget` steps — which for a jigsaw means these regions are a poor
package/dist/kinds.js CHANGED
@@ -10,7 +10,7 @@ export const KAZU_KINDS = ["number-place", "jigsaw", "diagonal", "sum-cages", "m
10
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
11
  export const KAZU_LEVELS = ["easy", "medium", "hard"];
12
12
  export const KAZU_SPECS = {
13
- "number-place": { sizes: [4, 6, 9, 16], defaultSize: 9, mostCells: 256 },
13
+ "number-place": { sizes: [4, 6, 9, 16, 25], defaultSize: 9, mostCells: 625 },
14
14
  jigsaw: { sizes: [5, 6, 7, 9], defaultSize: 7, mostCells: 162 },
15
15
  diagonal: { sizes: [6, 9], defaultSize: 9, mostCells: 81 },
16
16
  "sum-cages": { sizes: [6, 9], defaultSize: 9, mostCells: 286 },
package/dist/layout.js CHANGED
@@ -19,6 +19,7 @@ export const KAZU_BOXES = {
19
19
  6: { rows: 2, cols: 3 },
20
20
  9: { rows: 3, cols: 3 },
21
21
  16: { rows: 4, cols: 4 },
22
+ 25: { rows: 5, cols: 5 },
22
23
  };
23
24
  /** The box a cell is in, numbered row-major from 0. */
24
25
  export function boxOf(size, index) {
package/dist/mount.d.ts CHANGED
@@ -2,24 +2,6 @@ import { type KazuGame } from "./game.ts";
2
2
  import { type KazuHint } from "./hint.ts";
3
3
  import type { KazuKind, KazuLevel } from "./kinds.ts";
4
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
5
  /** What every event tells of the board. */
24
6
  export type KazuEventDetail = {
25
7
  kind: KazuKind;
package/dist/mount.js CHANGED
@@ -7,6 +7,27 @@ import { KAZU_PLAY_STYLE } from "./playStyle.js";
7
7
  import { decodeNotes, decodeRun, encodeNotes, encodeRun, notesOf } from "./progress.js";
8
8
  import { solveKazu } from "./solve.js";
9
9
  import { kazuLanguageOf, kazuNameOf, kazuSay } from "./strings.js";
10
+ /**
11
+ * A PLAYABLE KAZU BOARD IN ANY PAGE: `mountKazu(host, options)` draws a puzzle into an element and plays
12
+ * it by touch, mouse and keyboard. Tap a cell and tap a number on the pad (or type it); tap the chosen
13
+ * cell again to step it on, 1, 2, 3 … and back to empty; turn Pencil on and the pad writes small notes
14
+ * instead; Undo takes the last change back; Hint says which cell to fill next and why; Check says how
15
+ * many are wrong, never which. A clock starts on the first entry and stops when the last cell is right.
16
+ *
17
+ * The keys: the arrows move, a number (1 to 9, then A to G on the 16×16 and on to P on the 25×25) fills
18
+ * the chosen cell, Shift with a number writes it as a pencil mark, Backspace empties the cell, N turns
19
+ * Pencil on or off (the slash key on the 25×25, where N is the number 23), Ctrl or Cmd with Z undoes,
20
+ * and Escape lets the cell go.
21
+ *
22
+ * What happens is told in events, on the host as DOM events and to the callbacks given: `kazu-change`
23
+ * for every change to the grid, `kazu-hint` for each hint, `kazu-check` for each check, and `kazu-solve`
24
+ * once, with the answer ready for `checkKazu`. Every button is also a method of the returned handle.
25
+ * The rules it plays by are `game.ts`'s, and the drawing is `drawKazu`'s: both are usable alone.
26
+ *
27
+ * Needs a page. Its words are English and Japanese and follow the page's `lang`.
28
+ */
29
+ /** From this side up the letter N is a number (A is 10, so N is 23), and Pencil moves to the slash key. */
30
+ const NOTES_KEY_IS_A_NUMBER = 23;
10
31
  /** Put the style in the page once: in the document's head, or in the shadow root the host is in. */
11
32
  export function ensureKazuPlayStyle(host) {
12
33
  const root = host.getRootNode();
@@ -210,7 +231,7 @@ export function mountKazu(host, options) {
210
231
  host.dataset.kind = game.kind;
211
232
  host.dataset.size = String(game.size);
212
233
  box.setAttribute("aria-label", say("board", { name: kazuNameOf(game.kind, language), size: game.size }));
213
- keysNote.textContent = say("keys");
234
+ keysNote.textContent = say(game.size >= NOTES_KEY_IS_A_NUMBER ? "keysColossus" : "keys");
214
235
  const counts = numberCounts(game);
215
236
  padKeys.forEach((key, at) => {
216
237
  const value = at + 1;
@@ -328,13 +349,13 @@ export function mountKazu(host, options) {
328
349
  event.preventDefault();
329
350
  api.enter(0);
330
351
  }
331
- else if (event.key.toLowerCase() === "n") {
352
+ else if (event.key.toLowerCase() === (game.size >= NOTES_KEY_IS_A_NUMBER ? "/" : "n")) {
332
353
  event.preventDefault();
333
354
  api.pencil();
334
355
  }
335
356
  else if (event.shiftKey && selected !== null) {
336
357
  // Shift with a number is a pencil mark, whatever the keyboard writes for a shifted digit: read the key's place, not its symbol.
337
- const code = /^Digit([1-9])$/.exec(event.code)?.[1] ?? /^Key([A-G])$/.exec(event.code)?.[1];
358
+ const code = /^Digit([1-9])$/.exec(event.code)?.[1] ?? /^Key([A-P])$/.exec(event.code)?.[1];
338
359
  const value = code === undefined ? 0 : valueOfSymbol(code);
339
360
  if (value >= 1 && value <= game.size) {
340
361
  event.preventDefault();
package/dist/names.js CHANGED
@@ -11,13 +11,13 @@ export const KAZU_NAMES = {
11
11
  en: [
12
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
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.",
14
+ "The 16×16 Giant has sixteen symbols: 1 to 9, then A to G for 10 to 16. The 25×25 Colossus has twenty-five: A to P for 10 to 25.",
15
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
16
  ],
17
17
  ja: [
18
18
  "空いているマスに、1から盤の一辺の数までの数字を入れます。どの行、列、ブロックにも、同じ数字が一つずつ入ります。",
19
19
  "最初から書かれている数字はそのままです。どの問題も、答えはちょうど一つだけです。",
20
- "16×16の「特大」は、1から9までの数字に、10から16までを表すAからGを足した16種類を使います。",
20
+ "16×16の「特大」は、1から9までの数字に、10から16までを表すAからGを足した16種類を使います。25×25の「巨大」は、10から25までを表すAからPまでを使う25種類です。",
21
21
  "やさしい問題は、推理だけで全部のマスが決まります。ふつうとむずかしい問題では、試してみる場面があります。",
22
22
  ],
23
23
  },
@@ -154,7 +154,7 @@ export const KAZU_NAMES = {
154
154
  };
155
155
  /** What each side is for, under its size on a chooser: the quick one, the usual one, the long one. */
156
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: "特大" } },
157
+ "number-place": { 4: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" }, 16: { en: "Giant", ja: "特大" }, 25: { en: "Colossus", ja: "巨大" } },
158
158
  jigsaw: { 5: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 7: { en: "Standard", ja: "定番" }, 9: { en: "Classic", ja: "本格" } },
159
159
  diagonal: { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
160
160
  "sum-cages": { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
@@ -8,7 +8,7 @@ import { type Random } from "./random.ts";
8
8
  * puzzle on a layout (Sudoku, Diagonal and Jigsaw).
9
9
  */
10
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. */
11
+ /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9, 16 or 25. */
12
12
  export declare function generateNumberPlace(size: number, level: KazuLevel, seed: number): KazuPuzzle;
13
13
  /** A Diagonal Sudoku (Sudoku X): Sudoku with the two long diagonals as groups too. 6 or 9. */
14
14
  export declare function generateDiagonal(size: number, level: KazuLevel, seed: number): KazuPuzzle;
@@ -1,5 +1,5 @@
1
1
  import { encodeCells } from "./cells.js";
2
- import { countSolutions, guessDepth } from "./groupSolve.js";
2
+ import { countSolutions, guessDepth, provedByGuessing } from "./groupSolve.js";
3
3
  import { boxedLayout } from "./layout.js";
4
4
  import { seededRandom, shuffled } from "./random.js";
5
5
  /**
@@ -22,10 +22,21 @@ import { seededRandom, shuffled } from "./random.js";
22
22
  * hard puzzle would be a medium one with a different label.
23
23
  */
24
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 } },
25
+ easy: { depth: 0, floor: { 4: 9, 6: 20, 9: 40, 16: 150, 25: 366 } },
26
+ medium: { depth: 1, floor: { 4: 7, 6: 15, 9: 31, 16: 125, 25: 305 } },
27
+ hard: { depth: Infinity, floor: { 4: 5, 6: 11, 9: 24, 16: 116, 25: 283 } },
28
28
  };
29
+ /** Past this side the exact count of answers and the exact depth are not asked, only a proof (see `LARGE_GUESSES`). */
30
+ const LARGEST_COUNTED = 16;
31
+ /**
32
+ * The 25×25 is carved by proof, not by counting. Counting every answer of a sparse 625-cell grid can run for
33
+ * minutes (a single removal took 20 s), and the exact guess depth of one is as bad. What a person does is
34
+ * what is asked instead: singles, then at most this many guesses at the most constrained cell, each value
35
+ * of it either finished by singles or shown wrong by them. A puzzle that passes has exactly one answer, and
36
+ * the work for each removal is bounded however the grid looks. Hard allows two nested guesses and no more,
37
+ * so a 25×25 hard puzzle is one a person can finish, and easy is singles alone, as it is at every size.
38
+ */
39
+ const LARGE_GUESSES = { easy: 0, medium: 1, hard: 2 };
29
40
  /**
30
41
  * A whole grid, filled cell by cell in reading order with the values tried in a seeded order. The
31
42
  * groups come from the layout, so the diagonals are honoured the same way.
@@ -65,7 +76,7 @@ function fillInOrder(layout, random) {
65
76
  * into a dead end deep in a 256-cell grid and take seconds to climb out. A grid that is right by
66
77
  * construction (each row the one above it shifted a box's width, each band shifted by one), then
67
78
  * 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
79
+ * bands, columns within a stack, the stacks), is as varied and costs nothing. Only 16×16 and 25×25 are made
69
80
  * this way, so every smaller grid comes out of its seed exactly as it always has.
70
81
  */
71
82
  function fillByPattern(size, random) {
@@ -91,7 +102,9 @@ export function carve(solution, layout, level, floor, random) {
91
102
  break;
92
103
  const value = givens[index];
93
104
  givens[index] = 0;
94
- const stillOne = countSolutions(givens, layout, 2) === 1 && guessDepth(givens, layout) <= depth;
105
+ const stillOne = layout.size > LARGEST_COUNTED
106
+ ? provedByGuessing(givens, layout, LARGE_GUESSES[level]) === 1
107
+ : countSolutions(givens, layout, 2) === 1 && guessDepth(givens, layout) <= depth;
95
108
  if (stillOne)
96
109
  left -= 1;
97
110
  else
@@ -99,11 +112,11 @@ export function carve(solution, layout, level, floor, random) {
99
112
  }
100
113
  return givens;
101
114
  }
102
- /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9 or 16. */
115
+ /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9, 16 or 25. */
103
116
  export function generateNumberPlace(size, level, seed) {
104
117
  const random = seededRandom(seed);
105
118
  const layout = boxedLayout(size);
106
- const solution = size === 16 ? fillByPattern(size, random) : fillInOrder(layout, random);
119
+ const solution = size >= 16 ? fillByPattern(size, random) : fillInOrder(layout, random);
107
120
  const givens = carve(solution, layout, level, LEVELS[level].floor[size], random);
108
121
  return { kind: "number-place", size, level, seed, givens: encodeCells(givens), solution: encodeCells(solution) };
109
122
  }
package/dist/strings.js CHANGED
@@ -19,6 +19,7 @@ export const KAZU_STRINGS = {
19
19
  tap: "Tap a cell, then a number.",
20
20
  tapPencil: "Pencil marks are on: tap a number to note it in the cell.",
21
21
  keys: "Arrow keys move, a number fills the cell, Backspace empties it, N turns pencil marks on or off.",
22
+ keysColossus: "Arrow keys move, a number or a letter from A to P fills the cell, Backspace empties it, the slash key turns pencil marks on or off (N is a number here).",
22
23
  undo: "Undo",
23
24
  pencil: "Pencil",
24
25
  pencilTitle: "Write small notes in a cell instead of a number",
@@ -83,6 +84,7 @@ export const KAZU_STRINGS = {
83
84
  tap: "マスをタップして、数字を選びます。",
84
85
  tapPencil: "メモがオンです。数字をタップすると、マスにメモします。",
85
86
  keys: "矢印キーで移動、数字キーで入力、Backspaceで消去、Nでメモのオンとオフ。",
87
+ keysColossus: "矢印キーで移動、数字またはAからPのキーで入力、Backspaceで消去、スラッシュキーでメモのオンとオフ(ここではNは数字です)。",
86
88
  undo: "元に戻す",
87
89
  pencil: "メモ",
88
90
  pencilTitle: "数字のかわりに、マスに小さくメモを書きます",
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version. */
2
- export declare const VERSION = "1.3.0";
2
+ export declare const VERSION = "1.4.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version. */
2
- export const VERSION = "1.3.0";
2
+ export const VERSION = "1.4.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kazu",
3
- "version": "1.3.0",
4
- "description": "Grid number and logic puzzles for JavaScript and TypeScript: Sudoku and its variants, Futoshiki, Skyscrapers, Shikaku, Hitori, Nurikabe, Akari, Juosan, Slitherlink, Masyu, Yajilin, Ripple Effect, Kakuro, Fillomino and Heyawake. Dedicated typed engines, independently checked puzzles, SVG boards and bilingual touch and keyboard players. Zero runtime dependencies.",
3
+ "version": "1.4.0",
4
+ "description": "Grid number and logic puzzles for JavaScript and TypeScript: Sudoku (up to 25×25) and its variants, Futoshiki, Skyscrapers, Shikaku, Hitori, Nurikabe, Akari, Juosan, Slitherlink, Masyu, Yajilin, Ripple Effect, Kakuro, Fillomino and Heyawake. Dedicated typed engines, independently checked puzzles, SVG boards and bilingual touch and keyboard players. Zero runtime dependencies.",
5
5
  "keywords": [
6
6
  "puzzle",
7
7
  "sudoku",