@johnmorrisdotca/kazu 1.2.0 → 1.3.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 +23 -0
- package/README.md +58 -16
- package/dist/akari-entry.d.ts +1 -0
- package/dist/akari-entry.js +1 -0
- package/dist/akari.constants.d.ts +5 -0
- package/dist/akari.constants.js +5 -0
- package/dist/akari.types.d.ts +19 -0
- package/dist/akariGenerate.d.ts +8 -5
- package/dist/akariGenerate.js +88 -65
- package/dist/akariLogic.d.ts +10 -0
- package/dist/akariLogic.js +84 -0
- package/dist/akariRate.d.ts +6 -0
- package/dist/akariRate.js +34 -0
- package/dist/akariSolve.d.ts +1 -1
- package/dist/akariSolve.js +8 -96
- package/dist/akariTemplate.d.ts +7 -0
- package/dist/akariTemplate.js +78 -0
- package/dist/csp.d.ts +63 -0
- package/dist/csp.js +162 -0
- package/dist/fillomino-entry.d.ts +1 -0
- package/dist/fillomino-entry.js +1 -0
- package/dist/fillomino.constants.d.ts +6 -2
- package/dist/fillomino.constants.js +6 -2
- package/dist/fillomino.types.d.ts +18 -1
- package/dist/fillominoBuild.d.ts +10 -0
- package/dist/fillominoBuild.js +78 -0
- package/dist/fillominoGenerate.d.ts +7 -1
- package/dist/fillominoGenerate.js +75 -107
- package/dist/fillominoLogic.d.ts +14 -0
- package/dist/fillominoLogic.js +177 -0
- package/dist/fillominoMount.js +4 -1
- package/dist/fillominoRate.d.ts +3 -0
- package/dist/fillominoRate.js +57 -0
- package/dist/fillominoSolve.js +15 -88
- package/dist/hitori-entry.d.ts +1 -0
- package/dist/hitori-entry.js +1 -0
- package/dist/hitori.constants.d.ts +7 -1
- package/dist/hitori.constants.js +7 -1
- package/dist/hitori.types.d.ts +18 -1
- package/dist/hitoriBoard.js +2 -1
- package/dist/hitoriBuild.d.ts +12 -0
- package/dist/hitoriBuild.js +117 -0
- package/dist/hitoriGenerate.d.ts +10 -3
- package/dist/hitoriGenerate.js +59 -157
- package/dist/hitoriLogic.d.ts +12 -0
- package/dist/hitoriLogic.js +189 -0
- package/dist/hitoriRate.d.ts +3 -0
- package/dist/hitoriRate.js +32 -0
- package/dist/hitoriSolve.d.ts +4 -1
- package/dist/hitoriSolve.js +11 -62
- package/dist/kakuro-entry.d.ts +1 -0
- package/dist/kakuro-entry.js +1 -0
- package/dist/kakuro.constants.d.ts +6 -1
- package/dist/kakuro.constants.js +6 -1
- package/dist/kakuro.types.d.ts +19 -0
- package/dist/kakuroBuild.d.ts +30 -0
- package/dist/kakuroBuild.js +337 -0
- package/dist/kakuroGenerate.d.ts +10 -3
- package/dist/kakuroGenerate.js +54 -122
- package/dist/kakuroLogic.d.ts +13 -0
- package/dist/kakuroLogic.js +153 -0
- package/dist/kakuroRate.d.ts +3 -0
- package/dist/kakuroRate.js +40 -0
- package/dist/kakuroSolve.js +17 -69
- package/dist/kakuroTemplate.d.ts +3 -0
- package/dist/kakuroTemplate.js +130 -0
- package/dist/shikaku-entry.d.ts +1 -0
- package/dist/shikaku-entry.js +1 -0
- package/dist/shikaku.constants.d.ts +5 -2
- package/dist/shikaku.constants.js +5 -2
- package/dist/shikaku.types.d.ts +18 -1
- package/dist/shikakuBuild.d.ts +21 -0
- package/dist/shikakuBuild.js +135 -0
- package/dist/shikakuGenerate.d.ts +6 -1
- package/dist/shikakuGenerate.js +50 -35
- package/dist/shikakuLogic.d.ts +13 -0
- package/dist/shikakuLogic.js +84 -0
- package/dist/shikakuRate.d.ts +3 -0
- package/dist/shikakuRate.js +27 -0
- package/dist/shikakuSolve.d.ts +1 -1
- package/dist/shikakuSolve.js +23 -44
- package/dist/shikakuTemplate.d.ts +3 -0
- package/dist/shikakuTemplate.js +43 -0
- package/dist/slitherlink-entry.d.ts +1 -0
- package/dist/slitherlink-entry.js +1 -0
- package/dist/slitherlink.constants.d.ts +5 -0
- package/dist/slitherlink.constants.js +5 -0
- package/dist/slitherlink.types.d.ts +18 -0
- package/dist/slitherlinkGenerate.d.ts +10 -3
- package/dist/slitherlinkGenerate.js +161 -80
- package/dist/slitherlinkLogic.d.ts +10 -0
- package/dist/slitherlinkLogic.js +267 -0
- package/dist/slitherlinkRate.d.ts +3 -0
- package/dist/slitherlinkRate.js +26 -0
- package/dist/slitherlinkSolve.d.ts +1 -1
- package/dist/slitherlinkSolve.js +9 -117
- package/dist/slitherlinkTemplate.d.ts +3 -0
- package/dist/slitherlinkTemplate.js +101 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.3.0] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Four levels on six grid puzzles.** Shikaku, Akari, Slitherlink, Hitori, Fillomino and Kakuro each make `easy`, `medium`, `hard` and `extra-hard` boards (the level types and `SHIKAKU_LEVELS`, `AKARI_LEVELS`, `SLITHERLINK_LEVELS`, `HITORI_LEVELS`, `FILLOMINO_LEVELS` and `KAKURO_LEVELS`), every one with exactly one answer, and every puzzle now carries its `level`. Easy and medium are solved by plain rules (easy keeps more of its numbers, medium has as few as the rules allow); hard needs supposing something and seeing it break; extra-hard needs the most of that.
|
|
14
|
+
- **More sizes.** `AKARI_SIZES` (5, 7, 10, 14), `SLITHERLINK_SIZES` (5, 7, 10), `HITORI_SIZES` (5, 6, 7, 8, 9, 10, 12), `FILLOMINO_SIZES` (6, 8, 10, 12), `KAKURO_SIZES` (6, 8, 10, 12) and `SHIKAKU_SIZES` (5, 7, 10, 14) list what the demo offers. Hitori takes any side from 4 to 12 (was 5 and 7), Kakuro any side from 5 to 12 (was one size, 10), Fillomino any side from 4 to 12 (was 4 to 8 and 36 squares).
|
|
15
|
+
- **A measured difficulty.** `rateShikaku`, `rateAkari`, `rateSlitherlink`, `rateHitori`, `rateFillomino` and `rateKakuro` solve a board with one answer the way a person does (the rules alone, then supposing one thing, then more) and report how deep that went (`depth`, `probes`) and what the board is made of (numbers, runs, regions, loop length). `docs/LEVELS.md` defines each level per kind and tables the measures and the generation times by size and level; `node scripts/measure-levels.mjs` makes the tables again, and draws a puzzle as text with `--show`.
|
|
16
|
+
- A shared engine (`src/csp.ts`) under the six kinds: counting answers, reasoning with and without supposing, and proving one answer by reasoning when that is enough.
|
|
17
|
+
- `generateAkari`, `generateSlitherlink`, `generateHitori` and `generateKakuro` take the level as a last argument (`generateAkari(width, height, seed, level?)`, `generateSlitherlink(width, height, seed, level?)`, `generateHitori(size, seed, level?)`, `generateKakuro(seed, level?, size?)`), `"medium"` if left out; Shikaku and Fillomino keep `(width, height, level, seed)` and add `extra-hard`.
|
|
18
|
+
- The demo has a Level choice, in English and Japanese, on all six pages, and the sizes above.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **The six generators make new boards.** Akari, Slitherlink, Hitori and Kakuro used fixed layouts (the Akari rooms, a loop of one or two blocks with nearly every square numbered and most of them 0, three or four shaded squares in Hitori, a 10×10 of 2×2 to 3×3 blocks) and made the same few motifs again and again; they now build random boards: Akari scatters black squares and takes numbers away, Slitherlink grows a winding loop and takes numbers away (few say 0), Hitori shades about a quarter of the squares and repairs the numbers until the answer is single, Kakuro lays runs out row by row and repairs digits, and Shikaku packs interlocking rectangles instead of cutting straight lines. Fillomino's generator is new and no longer slow (a 6×6 took up to 1.7 s). **A seed makes a different puzzle from the one 1.2.0 made** for Shikaku, Akari, Slitherlink, Hitori, Kakuro and Fillomino; a progress code carries its board, so a saved game is unaffected, but a site that keeps a game as kind, size, level and seed will find that seed is another puzzle. The six number puzzles are untouched, and `src/site.fixture.json` still makes all 3,600 of them again, byte for byte.
|
|
23
|
+
- The six solvers count answers with a shared engine that reasons first: a board that the rules (or one supposition) settle is proved with no search at all, and others are searched from what reasoning left. Counts are the same as before on every board the tests compare (exhaustive enumeration of small boards, and the old solvers); only the node counts, and so what a given `nodes` budget reaches, are different, and answers are found in far fewer nodes. `hint*` calls are quicker as a result.
|
|
24
|
+
- If a generator cannot make a board of the level within its attempts it makes one of the next level down, and finally a plain one, rather than throw; the rating of the board shows what it is. On the 20,800 boards of `docs/LEVELS.md` (seeds 1 to 200 at every size and level) this happened on none.
|
|
25
|
+
- Fillomino's number choice in the player offers only numbers a board can hold (up to the biggest given, or the biggest stretch of squares with none), not up to the square count.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `generateKakuro(97)` threw "No uniquely solvable Kakuro board was proved within the generation budget"; no seed throws now, at any level or size.
|
|
30
|
+
- `solveHitori` could count one shade pattern twice (so a board with two answers could be reported as having three, and a limit could be reached early); it counts each distinct pattern once.
|
|
31
|
+
|
|
9
32
|
## [1.2.0] - 2026-10-05
|
|
10
33
|
|
|
11
34
|
### Added
|
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ And in a page, a puzzle to play, by touch, mouse and keyboard, with nothing else
|
|
|
62
62
|
|
|
63
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
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.
|
|
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).
|
|
65
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.
|
|
66
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.
|
|
67
68
|
- **A hint that is a reason.** Which cell to fill next, with the rule that says so (a cell with one number left, a number with one place left), never built on a wrong entry.
|
|
@@ -481,7 +482,7 @@ Import the engine, player, and drawing from `@johnmorrisdotca/kazu/yajilin`, `@j
|
|
|
481
482
|
|
|
482
483
|
## Shikaku — rectangles in Kazu
|
|
483
484
|
|
|
484
|
-
The demo includes square, wide (10 × 6), tall (6 × 10) and custom rectangular boards, with width and height from 2 to 16. The named Courtyard (square), Long Table (wide) and Narrow Garden (tall) packs each hold three uniquely proved challenges with useful titles. Shares and saved settings preserve both dimensions. It uses Kazu’s shared materials and pieces palette.
|
|
485
|
+
The demo includes square boards (5, 7, 10 and 14 on a side), wide (10 × 6), tall (6 × 10) and custom rectangular boards, with width and height from 2 to 16, at four levels. The named Courtyard (square), Long Table (wide) and Narrow Garden (tall) packs each hold three uniquely proved challenges with useful titles. Shares and saved settings preserve both dimensions. It uses Kazu’s shared materials and pieces palette.
|
|
485
486
|
|
|
486
487
|
Shikaku belongs to the number-and-grid family. Its moves are rectangles rather than number entries, so it has a dedicated model and optional entry points; the existing six `KazuKind` values and saved Sudoku codes remain compatible.
|
|
487
488
|
|
|
@@ -522,7 +523,8 @@ Juosan has a dedicated immutable engine and package paths: `@johnmorrisdotca/kaz
|
|
|
522
523
|
|
|
523
524
|
- `ShikakuBoard`: `width`, `height`, and row-major `clues` (zero for an empty cell). Dimensions are 2–16; clue areas sum to the grid area.
|
|
524
525
|
- `ShikakuRectangle`: zero-based `x`, `y`, `width`, `height`.
|
|
525
|
-
- `generateShikaku(width, height, level, seed)`: deterministic puzzle and solution. The answer is counted independently
|
|
526
|
+
- `generateShikaku(width, height, level, seed)`: deterministic puzzle and solution; `level` is `easy`, `medium`, `hard` or `extra-hard` (`SHIKAKU_LEVELS`), and `SHIKAKU_SIZES` lists the square sides the demo offers (5, 7, 10, 14). The board is cut into interlocking rectangles by packing, not by straight cuts, each carries one number, and the answer is counted independently; every level is checked by solving the board (see [Levels](#levels-of-the-grid-puzzles)). It never returns an unproved board. If no board of a level is found within its attempts the next level down is made instead, which the rating shows.
|
|
527
|
+
- `rateShikaku(board)`: how hard a board is, measured by solving it: `depth` (0 rules alone, 1 supposing one rectangle, 2 more), `rules` (how many of the three rules a depth-0 solve needed), `probes`, and the number, area and ambiguity of the rectangles.
|
|
526
528
|
- `solveShikaku(board, placements?, {limit?, nodes?})`: exact-cover count, first answer, nodes visited and `complete`. The default limit is two answers and 100,000 nodes. Only `complete && count === 1` proves uniqueness; a stopped search is explicitly incomplete.
|
|
527
529
|
- `checkShikaku(board, rectangles)`: coverage and rectangle rule errors, independent of a stored answer. It accepts any valid completion.
|
|
528
530
|
- `newShikaku`, `placeShikaku`, `removeShikaku`, `undoShikaku`: immutable game operations. A placement replaces intersecting rectangles, and rule errors are allowed until checked. The game strips generated solutions.
|
|
@@ -537,13 +539,13 @@ The demo is `site/shikaku.html` after `pnpm site`; its generator runs in a modul
|
|
|
537
539
|
|
|
538
540
|
## Akari — light the grid
|
|
539
541
|
|
|
540
|
-
Akari (美術館) places bulbs in white squares. Each bulb lights in straight lines until a black square or the edge. Every white square must be lit, bulbs cannot see each other, and a numbered black square must touch exactly that many bulbs. Boards may be square, wide, tall or custom, with each side from 2 to 16. The seeded generator
|
|
542
|
+
Akari (美術館) places bulbs in white squares. Each bulb lights in straight lines until a black square or the edge. Every white square must be lit, bulbs cannot see each other, and a numbered black square must touch exactly that many bulbs. Boards may be square, wide, tall or custom, with each side from 2 to 16. The seeded generator scatters black squares at random (half the time in rotating pairs), lights them with random bulbs, numbers every black square that touches a white one, and then takes numbers away for as long as the board can still be solved the way the level asks, so the layouts are not a fixed motif. It returns a board only when its answer is proved single.
|
|
541
543
|
|
|
542
544
|
```js
|
|
543
545
|
import { generateAkari, checkAkari } from "@johnmorrisdotca/kazu/akari";
|
|
544
546
|
import { mountAkari } from "@johnmorrisdotca/kazu/akari/play";
|
|
545
547
|
|
|
546
|
-
const puzzle = generateAkari(7, 7, 42);
|
|
548
|
+
const puzzle = generateAkari(7, 7, 42, "hard"); // width, height, seed, level ("medium" if left out)
|
|
547
549
|
const player = mountAkari(document.querySelector("#board"), {
|
|
548
550
|
board: puzzle, material: "ivory", pieces: "ink", language: "en",
|
|
549
551
|
});
|
|
@@ -553,7 +555,8 @@ const player = mountAkari(document.querySelector("#board"), {
|
|
|
553
555
|
Use `@johnmorrisdotca/kazu/akari`, `@johnmorrisdotca/kazu/akari/play`, or `@johnmorrisdotca/kazu/akari/draw`. The root package also re-exports the engine; the dedicated drawing and player entries keep those features optional. There are no runtime dependencies.
|
|
554
556
|
|
|
555
557
|
- `AkariBoard`: width, height and row-major `cells`: `null` is white, `false` is an unnumbered black square, and `0`–`4` are numbered black squares.
|
|
556
|
-
- `generateAkari(width, height, seed)`: deterministic puzzle and its solution,
|
|
558
|
+
- `generateAkari(width, height, seed, level?)`: deterministic puzzle and its solution at `easy`, `medium`, `hard` or `extra-hard` (`AKARI_LEVELS`; `AKARI_SIZES` lists the square sides on offer: 5, 7, 10, 14). Easy keeps most of its numbers, medium is solved by the rules alone with as few as it can, hard needs supposing a bulb or an empty square, extra-hard needs the most of that. It returns only when an independent count proves exactly one answer; if no board of the level is found within its attempts the next level down is made, and the first generator, which cannot fail, is the last resort.
|
|
559
|
+
- `rateAkari(board)`: how hard a board is, measured by solving it: `depth` (0 rules alone, 1 supposing one square, 2 more), `probes`, and the numbers, bulbs and white squares.
|
|
557
560
|
- `solveAkari(board, {limit?, nodes?})`: counts placements, returns the first answer, visited nodes and `complete`; only `complete && count === 1` proves uniqueness. The default answer limit is two and the node budget is 250,000.
|
|
558
561
|
- `checkAkari(board, bulbs)`: checks a complete placement from the rules, independently of the generated answer. `progressAkari` reports dark squares and immediate conflicts while permitting unfinished numbered clues.
|
|
559
562
|
- `newAkari`, `toggleAkari`, `undoAkari`, `akariFinished`, `hintAkari`: immutable play operations. Hints require a proved unique answer and mark the game as helped.
|
|
@@ -571,14 +574,14 @@ The demo is `site/akari.html` after `pnpm site`. It shares Kazu's family header,
|
|
|
571
574
|
import { generateSlitherlink } from "@johnmorrisdotca/kazu/slitherlink";
|
|
572
575
|
import { mountSlitherlink } from "@johnmorrisdotca/kazu/slitherlink/play";
|
|
573
576
|
|
|
574
|
-
const puzzle = generateSlitherlink(7, 7, 42);
|
|
577
|
+
const puzzle = generateSlitherlink(7, 7, 42, "hard"); // width, height, seed, level ("medium" if left out)
|
|
575
578
|
const player = mountSlitherlink(document.querySelector("#board"), {
|
|
576
579
|
board: puzzle, material: "ivory", language: "en",
|
|
577
580
|
});
|
|
578
581
|
// player.progress() saves the public clues and selected edges.
|
|
579
582
|
```
|
|
580
583
|
|
|
581
|
-
The Slitherlink engine has its own edge model, checker, progress checker, bounded solution counter, seeded generator and immutable play state. `solveSlitherlink` distinguishes an exhausted search from a proved count; the generator returns only boards proved to have one loop. Boards may be 2–10 cells wide and high. The
|
|
584
|
+
The Slitherlink engine has its own edge model, checker, progress checker, bounded solution counter, seeded generator and immutable play state. `solveSlitherlink` distinguishes an exhausted search from a proved count; the generator returns only boards proved to have one loop. `generateSlitherlink(width, height, seed, level?)` makes `easy`, `medium`, `hard` or `extra-hard` (`SLITHERLINK_LEVELS`) boards, and `SLITHERLINK_SIZES` lists the square sides on offer (5, 7, 10). `rateSlitherlink(board)` measures a board by solving it: `depth` (0 rules alone, 1 supposing one edge, 2 more), `probes`, the numbers, how many of them say 0, and the loop's length. Boards may be 2–10 cells wide and high. The generator grows a random winding loop (a connected region without holes whose outline never touches itself), numbers every square with how many of its edges the loop uses, and takes numbers away, squares numbered 0 first, for as long as the board can still be solved the way the level asks, so boards are not a few shapes and few squares say 0. The player supports touch and mouse edge toggles, arrow-key focus, Enter/Space, undo, restart, checking, proved hints, save/restore, and ivory, wood and slate materials in English and Japanese.
|
|
582
585
|
|
|
583
586
|
Use `@johnmorrisdotca/kazu/slitherlink`, `@johnmorrisdotca/kazu/slitherlink/play`, or `@johnmorrisdotca/kazu/slitherlink/draw`. The demo is `site/slitherlink.html` after `pnpm site`. The rules are described by [Nikoli](https://www.nikoli.co.jp/en/puzzles/slitherlink/). This implementation uses original generated layouts and does not copy Nikoli puzzle grids, wording or artwork.
|
|
584
587
|
|
|
@@ -608,13 +611,13 @@ Kakuro fills white cells with digits 1–9. Each across and down run must match
|
|
|
608
611
|
import { generateKakuro, solveKakuro, checkKakuro } from "@johnmorrisdotca/kazu/kakuro";
|
|
609
612
|
import { mountKakuro } from "@johnmorrisdotca/kazu/kakuro/play";
|
|
610
613
|
|
|
611
|
-
const puzzle = generateKakuro(42);
|
|
614
|
+
const puzzle = generateKakuro(42, "hard", 8); // seed, level ("medium"), size including the totals' row and column (10)
|
|
612
615
|
const proof = solveKakuro(puzzle); // uniqueness only when complete && count === 1
|
|
613
616
|
const player = mountKakuro(document.querySelector("#board"), { board: puzzle, language: "en" });
|
|
614
617
|
player.progress(); // public clues, entries and pencil marks; no answer
|
|
615
618
|
```
|
|
616
619
|
|
|
617
|
-
`@johnmorrisdotca/kazu/kakuro/draw` provides standalone SVG drawing.
|
|
620
|
+
`@johnmorrisdotca/kazu/kakuro/draw` provides standalone SVG drawing. `generateKakuro(seed, level?, size?)` makes a board of any side from 5 to 12 (`KAKURO_SIZES` lists those on offer: 6, 8, 10, 12) at `easy`, `medium`, `hard` or `extra-hard` (`KAKURO_LEVELS`). It lays out the black squares row by row so that no run is a single square or longer than the level allows, fills random digits, and changes digits or darkens squares until the answer is single; easy and medium also ease the board until the rules they promise are enough, and hard and extra-hard ask for supposing. A board is accepted only after a bounded exact count proves one answer, and a seed never throws: if a level is not found within its attempts the next level down is made, and the first generator is the last resort on a 10×10. `rateKakuro(board)` measures a board by solving it: `depth`, `plain` (the single-run rules were enough), `probes`, the runs, the longest run and the share of totals that can be made one way only. `solveKakuro` reports `complete: false` when its node budget or answer limit stops counting. `checkKakuro` validates completed runs independently; `progressKakuro` permits blanks while marking impossible totals and repeats. The bilingual player supports touch, arrows, digits, pencil mode, Undo, Hint, Check, Restart and versioned saved progress.
|
|
618
621
|
|
|
619
622
|
The Kakuro entries are `@johnmorrisdotca/kazu/kakuro`, `@johnmorrisdotca/kazu/kakuro/play` and `@johnmorrisdotca/kazu/kakuro/draw`.
|
|
620
623
|
|
|
@@ -628,21 +631,36 @@ Each cell holds a number. All orthogonally connected cells with the same number
|
|
|
628
631
|
import { generateFillomino, checkFillomino, solveFillomino } from "@johnmorrisdotca/kazu/fillomino";
|
|
629
632
|
import { mountFillomino } from "@johnmorrisdotca/kazu/fillomino/play";
|
|
630
633
|
|
|
631
|
-
const puzzle = generateFillomino(
|
|
634
|
+
const puzzle = generateFillomino(6, 6, "hard", 17);
|
|
632
635
|
const result = solveFillomino(puzzle);
|
|
633
636
|
if (!result.complete || result.count !== 1) throw new Error("The answer was not proved unique");
|
|
634
637
|
checkFillomino(puzzle, result.solution);
|
|
635
638
|
mountFillomino(document.querySelector("#board"), { board: puzzle });
|
|
636
639
|
```
|
|
637
640
|
|
|
638
|
-
`FillominoBoard` contains `width`, `height`, and row-major `givens`, with zero for an empty cell. Engine validation
|
|
641
|
+
`FillominoBoard` contains `width`, `height`, and row-major `givens`, with zero for an empty cell. Engine validation and the seeded generator both support rectangular boards from 4 to 12 cells per side (`FILLOMINO_SIZES` lists the square sides on offer: 6, 8, 10, 12). A seed reproduces its puzzle. The levels are `easy`, `medium`, `hard` and `extra-hard` (`FILLOMINO_LEVELS`), and `rateFillomino(board)` measures a board by solving it: `depth` (0 rules alone, 1 supposing one number, 2 more), `probes`, the givens and their share, the regions, how many have no given and how big they are. The generator cuts the board into connected regions with no two of one size touching, gives every square, and takes givens away while the board can still be solved the way the level asks. Search bounds report when counting stopped rather than treating a partial search as a uniqueness proof.
|
|
639
642
|
|
|
640
643
|
`checkFillomino(board, entries)` checks givens, oversized connected groups and completion independently of the generated answer. An unfinished group smaller than its number can still grow. `solveFillomino(board, entries?, { limit?, nodes? })` counts filled solutions by growing connected regions, including regions with no given. Only `complete && count === 1` proves uniqueness. `newFillomino`, `setFillominoCell`, `undoFillomino`, `restartFillomino`, `hintFillomino`, and `fillominoFinished` are immutable game helpers. Progress codes contain public clues, entries, and the persistent assisted flag; they contain no stored answer.
|
|
641
644
|
|
|
642
|
-
The player accepts touch, mouse, and keyboard input, with undo, check, a proved hint, restart, and local progress codes. Hints persistently mark a run as assisted. The English and Japanese player uses the same board materials and number styles as Shikaku. The demo offers 4×4 through
|
|
645
|
+
The player accepts touch, mouse, and keyboard input, with undo, check, a proved hint, restart, and local progress codes. Hints persistently mark a run as assisted. The English and Japanese player uses the same board materials and number styles as Shikaku. The demo offers 4×4 through 12×12 settings at four levels. It is at [fillomino.html](https://johnmorrisdotca.github.io/kazu/fillomino.html).
|
|
643
646
|
|
|
644
647
|
[Nikoli's Fillomino rules](https://www.nikoli.co.jp/en/puzzles/fillomino/) describe numbered connected regions, exact area, and separation between equal-area regions. This implementation generates original puzzles and does not reuse published grids or artwork.
|
|
645
648
|
|
|
649
|
+
## Levels of the grid puzzles
|
|
650
|
+
|
|
651
|
+
Shikaku, Akari, Slitherlink, Hitori, Fillomino and Kakuro make boards at `easy`, `medium`, `hard` and `extra-hard`. Every board has exactly one answer, and a level says what a person has to do to solve it, measured by solving the board with the package's own rules: easy and medium need only the rules (easy keeps more numbers, medium as few as the rules allow), hard needs supposing something and watching it break, and extra-hard needs the most of that. `rateShikaku`, `rateAkari`, `rateSlitherlink`, `rateHitori`, `rateFillomino` and `rateKakuro` return the measure of a board (`depth`, `probes` and what it is made of), so a site can show it or pick boards by it.
|
|
652
|
+
|
|
653
|
+
| Kind | Call | Sizes on offer | Largest size, extra-hard: median / slowest to make |
|
|
654
|
+
| --- | --- | --- | --- |
|
|
655
|
+
| Shikaku | `generateShikaku(width, height, level, seed)` | 5, 7, 10, 14 (any side 2–16) | 14 × 14: 89 ms / 302 ms |
|
|
656
|
+
| Akari | `generateAkari(width, height, seed, level?)` | 5, 7, 10, 14 (any side 2–16) | 14 × 14: 212 ms / 366 ms |
|
|
657
|
+
| Slitherlink | `generateSlitherlink(width, height, seed, level?)` | 5, 7, 10 (any side 2–10) | 10 × 10: 231 ms / 286 ms |
|
|
658
|
+
| Hitori | `generateHitori(size, seed, level?)` | 5, 6, 7, 8, 9, 10, 12 (any side 4–12) | 12 × 12: 157 ms / 511 ms |
|
|
659
|
+
| Fillomino | `generateFillomino(width, height, level, seed)` | 6, 8, 10, 12 (any side 4–12) | 12 × 12: 187 ms / 422 ms |
|
|
660
|
+
| Kakuro | `generateKakuro(seed, level?, size?)` | 6, 8, 10, 12 (any side 5–12) | 12 × 12: 273 ms / 1,254 ms |
|
|
661
|
+
|
|
662
|
+
[docs/LEVELS.md](docs/LEVELS.md) defines each level for each kind, defines the measure, and tables it by size and level over 200 seeds, with the median, 95th percentile and slowest time to make a board; `node scripts/measure-levels.mjs` makes the tables again. These are the same boards in every browser and every Node for a given kind, size, level and seed, but they are **not** the boards 1.2.0 made for that seed.
|
|
663
|
+
|
|
646
664
|
## Heyawake — rooms and white paths
|
|
647
665
|
|
|
648
666
|
The Heyawake demo supports rectangular room boards, black/white/blank marking, keyboard and touch play, undo, a contradiction check, unique-solution hints, restart, local progress, and English/Japanese labels. Use `@johnmorrisdotca/kazu/heyawake`, `@johnmorrisdotca/kazu/heyawake/play`, or `@johnmorrisdotca/kazu/heyawake/draw`; generated answers are never included in progress data.
|
|
@@ -675,6 +693,9 @@ the page's part (the mount and the element) is another.
|
|
|
675
693
|
├── hitoriDraw.ts
|
|
676
694
|
├── hitoriGame.ts
|
|
677
695
|
├── hitoriGenerate.ts
|
|
696
|
+
├── hitoriBuild.ts
|
|
697
|
+
├── hitoriLogic.ts
|
|
698
|
+
├── hitoriRate.ts
|
|
678
699
|
├── hitoriMount.ts
|
|
679
700
|
├── hitoriPlay.types.ts
|
|
680
701
|
├── hitoriSolve.ts
|
|
@@ -745,6 +766,9 @@ the page's part (the mount and the element) is another.
|
|
|
745
766
|
├── fillominoDraw.ts
|
|
746
767
|
├── fillominoGame.ts
|
|
747
768
|
├── fillominoGenerate.ts
|
|
769
|
+
├── fillominoBuild.ts
|
|
770
|
+
├── fillominoLogic.ts
|
|
771
|
+
├── fillominoRate.ts
|
|
748
772
|
├── fillominoMount.ts
|
|
749
773
|
├── fillominoPlay.types.ts
|
|
750
774
|
├── fillominoSolve.ts
|
|
@@ -763,6 +787,10 @@ the page's part (the mount and the element) is another.
|
|
|
763
787
|
├── kakuroDraw.ts
|
|
764
788
|
├── kakuroGame.ts
|
|
765
789
|
├── kakuroGenerate.ts
|
|
790
|
+
├── kakuroBuild.ts
|
|
791
|
+
├── kakuroLogic.ts
|
|
792
|
+
├── kakuroRate.ts
|
|
793
|
+
├── kakuroTemplate.ts
|
|
766
794
|
├── kakuroMount.ts
|
|
767
795
|
├── kakuroPlay.types.ts
|
|
768
796
|
├── kakuroSolve.ts
|
|
@@ -774,6 +802,10 @@ the page's part (the mount and the element) is another.
|
|
|
774
802
|
├── shikakuDraw.ts
|
|
775
803
|
├── shikakuGame.ts
|
|
776
804
|
├── shikakuGenerate.ts
|
|
805
|
+
├── shikakuBuild.ts
|
|
806
|
+
├── shikakuLogic.ts
|
|
807
|
+
├── shikakuRate.ts
|
|
808
|
+
├── shikakuTemplate.ts
|
|
777
809
|
├── shikakuMount.ts
|
|
778
810
|
├── shikakuPacks.ts
|
|
779
811
|
├── shikakuPlay.types.ts
|
|
@@ -790,6 +822,9 @@ the page's part (the mount and the element) is another.
|
|
|
790
822
|
├── akariDraw.ts
|
|
791
823
|
├── akariGame.ts
|
|
792
824
|
├── akariGenerate.ts
|
|
825
|
+
├── akariLogic.ts
|
|
826
|
+
├── akariRate.ts
|
|
827
|
+
├── akariTemplate.ts
|
|
793
828
|
├── akariMount.ts
|
|
794
829
|
├── akariPlay.types.ts
|
|
795
830
|
├── akariSolve.ts
|
|
@@ -804,6 +839,9 @@ the page's part (the mount and the element) is another.
|
|
|
804
839
|
├── slitherlinkDraw.ts
|
|
805
840
|
├── slitherlinkGame.ts
|
|
806
841
|
├── slitherlinkGenerate.ts
|
|
842
|
+
├── slitherlinkLogic.ts
|
|
843
|
+
├── slitherlinkRate.ts
|
|
844
|
+
├── slitherlinkTemplate.ts
|
|
807
845
|
├── slitherlinkMount.ts
|
|
808
846
|
├── slitherlinkPlay.types.ts
|
|
809
847
|
├── slitherlinkSolve.ts
|
|
@@ -842,6 +880,7 @@ src/
|
|
|
842
880
|
├── index.ts the main entry: everything but the drawing and the page
|
|
843
881
|
├── kinds.ts the six puzzles' keys, sizes and levels, and the shape of a puzzle
|
|
844
882
|
├── random.ts the seeded random numbers every puzzle is made from
|
|
883
|
+
├── csp.ts the one small engine under the six grid kinds: counting answers, and reasoning with and without supposing
|
|
845
884
|
├── cells.ts a grid of numbers as a string, 1 to 9 and A to G
|
|
846
885
|
├── layout.ts the groups that must each hold every number once: rows, columns, boxes, regions, diagonals, cages
|
|
847
886
|
├── groupSolve.ts the solver for puzzles made of groups: counting, singles, depth
|
|
@@ -905,7 +944,7 @@ Using Kazu in something? Open an *Add my project* issue and we will add you.
|
|
|
905
944
|
### The family
|
|
906
945
|
|
|
907
946
|
<!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
|
|
908
|
-
Kazu is one of
|
|
947
|
+
Kazu is one of twenty-two packages, each made for the same site, each at
|
|
909
948
|
[github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
|
|
910
949
|
|
|
911
950
|
- [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. [Demo](https://johnmorrisdotca.github.io/korokoro/).
|
|
@@ -927,8 +966,11 @@ Kazu is one of nineteen packages, each made for the same site, each at
|
|
|
927
966
|
- [Hikidashi](https://github.com/johnmorrisdotca/hikidashi) (引き出し): a drawer of small Japanese text tools: era dates, kanji numerals, readings and sentence difficulty. [Demo](https://johnmorrisdotca.github.io/hikidashi/).
|
|
928
967
|
- [Chizu](https://github.com/johnmorrisdotca/chizu) (地図): maps of the world and of countries' regions, in English and Japanese, with a quiz and callouts. [Demo](https://johnmorrisdotca.github.io/chizu/).
|
|
929
968
|
- [Bushu](https://github.com/johnmorrisdotca/bushu) (部首): find a kanji by the parts it is made of. [Demo](https://johnmorrisdotca.github.io/bushu/).
|
|
969
|
+
- [Tobiishi](https://github.com/johnmorrisdotca/tobiishi) (飛び石): peg solitaire with nine boards and seeded solvable challenges. [Demo](https://johnmorrisdotca.github.io/tobiishi/).
|
|
970
|
+
- [Jirai](https://github.com/johnmorrisdotca/jirai) (地雷): minesweeper on shaped grids with verified no-guess boards. [Demo](https://johnmorrisdotca.github.io/jirai/).
|
|
971
|
+
- [Gunjin](https://github.com/johnmorrisdotca/gunjin) (軍人): five hidden-rank strategy games with pass-the-device play. [Demo](https://johnmorrisdotca.github.io/gunjin/).
|
|
930
972
|
|
|
931
|
-
**This package is Kazu.** The demos of all
|
|
973
|
+
**This package is Kazu.** The demos of all twenty-two share one header and footer, so each links the rest.
|
|
932
974
|
<!-- family:end -->
|
|
933
975
|
|
|
934
976
|
## Development
|
|
@@ -958,14 +1000,14 @@ MIT, © John Morris. The puzzles are made in code and the drawing is SVG; there
|
|
|
958
1000
|
|
|
959
1001
|
## Hitori
|
|
960
1002
|
|
|
961
|
-
Hitori is included as a small standalone rules engine, drawing and player. Its public board has a `size`
|
|
1003
|
+
Hitori is included as a small standalone rules engine, drawing and player. Its public board has a `size` from 4 to 12 (`HITORI_SIZES` lists those on offer: 5, 6, 7, 8, 9, 10, 12) and a flat row-major `numbers` array. A solution is a Boolean shade mask: `true` means black. The solver counts minimal shade patterns, excluding redundant extra black cells; `complete: true` means the search finished, while a node-budget stop never claims uniqueness. `generateHitori(size, seed, level?)` makes `easy`, `medium`, `hard` or `extra-hard` (`HITORI_LEVELS`) puzzles: a random set of shaded squares that never touch and leave the rest in one piece, white squares numbered from a random Latin square so nothing repeats among them, and every shaded square numbered like a white one in its row or column, repaired until the answer is single. Easy is solved by the duplicates, pairs and sandwiches alone, medium once the whites must stay connected, hard by supposing, extra-hard needs the most supposing of several boards. The generator returns only puzzles proved to have one minimal answer, and `rateHitori(board)` measures a board by solving it: `depth`, `reach`, `probes` and how much is shaded and repeated.
|
|
962
1004
|
|
|
963
1005
|
```ts
|
|
964
1006
|
import { generateHitori, checkHitori, solveHitori } from "@johnmorrisdotca/kazu/hitori";
|
|
965
1007
|
import { drawHitori } from "@johnmorrisdotca/kazu/hitori/draw";
|
|
966
1008
|
import { mountHitori } from "@johnmorrisdotca/kazu/hitori/play";
|
|
967
1009
|
|
|
968
|
-
const puzzle = generateHitori(
|
|
1010
|
+
const puzzle = generateHitori(9, 42, "hard"); // size, seed, level ("medium" if left out)
|
|
969
1011
|
checkHitori(puzzle, puzzle.solution); // { ok: true, errors: [] }
|
|
970
1012
|
solveHitori(puzzle); // count: 1, complete: true
|
|
971
1013
|
```
|
package/dist/akari-entry.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./akari.constants.ts";
|
|
|
3
3
|
export * from "./akariBoard.ts";
|
|
4
4
|
export * from "./akariSolve.ts";
|
|
5
5
|
export * from "./akariGenerate.ts";
|
|
6
|
+
export * from "./akariRate.ts";
|
|
6
7
|
export * from "./akariGame.ts";
|
|
7
8
|
export { drawAkari } from "./akariDraw.ts";
|
|
8
9
|
export type { AkariDrawOptions } from "./akariPlay.types.ts";
|
package/dist/akari-entry.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./akari.constants.js";
|
|
|
3
3
|
export * from "./akariBoard.js";
|
|
4
4
|
export * from "./akariSolve.js";
|
|
5
5
|
export * from "./akariGenerate.js";
|
|
6
|
+
export * from "./akariRate.js";
|
|
6
7
|
export * from "./akariGame.js";
|
|
7
8
|
export { drawAkari } from "./akariDraw.js";
|
|
8
9
|
export { mountAkari } from "./akariMount.js";
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export declare const AKARI_MOST_SIDE = 16;
|
|
2
2
|
export declare const AKARI_MOST_NODES = 250000;
|
|
3
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateAkari`. */
|
|
4
|
+
export declare const AKARI_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
|
|
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
|
+
export declare const AKARI_SIZES: readonly [5, 7, 10, 14];
|
|
7
|
+
export declare const AKARI_MOST_ATTEMPTS = 60;
|
package/dist/akari.constants.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export const AKARI_MOST_SIDE = 16;
|
|
2
2
|
export const AKARI_MOST_NODES = 250000;
|
|
3
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateAkari`. */
|
|
4
|
+
export const AKARI_LEVELS = ["easy", "medium", "hard", "extra-hard"];
|
|
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
|
+
export const AKARI_SIZES = [5, 7, 10, 14];
|
|
7
|
+
export const AKARI_MOST_ATTEMPTS = 60;
|
package/dist/akari.types.d.ts
CHANGED
|
@@ -4,10 +4,29 @@ export type AkariBoard = {
|
|
|
4
4
|
height: number;
|
|
5
5
|
cells: readonly (number | null | false)[];
|
|
6
6
|
};
|
|
7
|
+
export type AkariLevel = "easy" | "medium" | "hard" | "extra-hard";
|
|
7
8
|
export type AkariPuzzle = AkariBoard & {
|
|
8
9
|
seed: number;
|
|
10
|
+
level: AkariLevel;
|
|
9
11
|
solution: readonly number[];
|
|
10
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* How hard a board is, measured by solving it. `depth` 0 means the plain rules solve it, 1 means somebody has to
|
|
15
|
+
* suppose a bulb or an empty square and see it break, 2 means more than that. `probes` is how many suppositions
|
|
16
|
+
* the depth-1 reasoning needed. The rest describes the board.
|
|
17
|
+
*/
|
|
18
|
+
export type AkariRating = {
|
|
19
|
+
depth: 0 | 1 | 2;
|
|
20
|
+
probes: number;
|
|
21
|
+
whites: number;
|
|
22
|
+
blacks: number;
|
|
23
|
+
clues: number;
|
|
24
|
+
bulbs: number;
|
|
25
|
+
/** Numbered squares as a share of the black squares that touch a white one. */
|
|
26
|
+
clueShare: number;
|
|
27
|
+
/** White squares as a share of the board. */
|
|
28
|
+
openShare: number;
|
|
29
|
+
};
|
|
11
30
|
export type AkariCheck = {
|
|
12
31
|
ok: boolean;
|
|
13
32
|
illuminated: number;
|
package/dist/akariGenerate.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { AkariPuzzle } from "./akari.types.ts";
|
|
1
|
+
import type { AkariLevel, AkariPuzzle } from "./akari.types.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Makes a seeded board
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Makes a seeded board and proves it has one answer. A random wall of black squares is lit by randomly placed
|
|
4
|
+
* bulbs, every black square that touches a white one is numbered, and then numbers are taken away for as long as
|
|
5
|
+
* the puzzle can still be solved the way the level asks: easy and medium by the rules alone (easy keeps most
|
|
6
|
+
* of its numbers), hard by supposing one square at a time, extra-hard for as long as the answer stays single,
|
|
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.
|
|
6
9
|
*/
|
|
7
|
-
export declare function generateAkari(width?: number, height?: number, seed?: number): AkariPuzzle;
|
|
10
|
+
export declare function generateAkari(width?: number, height?: number, seed?: number, level?: AkariLevel): AkariPuzzle;
|
package/dist/akariGenerate.js
CHANGED
|
@@ -1,78 +1,101 @@
|
|
|
1
|
-
import { AKARI_MOST_SIDE } from "./akari.constants.js";
|
|
2
|
-
import { checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
1
|
+
import { AKARI_LEVELS, AKARI_MOST_ATTEMPTS, AKARI_MOST_SIDE } from "./akari.constants.js";
|
|
2
|
+
import { akariNeighbours, akariVisible, checkAkari, isAkariBoard } from "./akariBoard.js";
|
|
3
|
+
import { akariModel } from "./akariLogic.js";
|
|
3
4
|
import { solveAkari } from "./akariSolve.js";
|
|
4
|
-
import {
|
|
5
|
+
import { templateAkari } from "./akariTemplate.js";
|
|
6
|
+
import { logicCsp, openSlots } from "./csp.js";
|
|
7
|
+
import { isKazuSeed, seededRandom, shuffled } from "./random.js";
|
|
5
8
|
/**
|
|
6
|
-
* Makes a seeded board
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* Makes a seeded board and proves it has one answer. A random wall of black squares is lit by randomly placed
|
|
10
|
+
* bulbs, every black square that touches a white one is numbered, and then numbers are taken away for as long as
|
|
11
|
+
* the puzzle can still be solved the way the level asks: easy and medium by the rules alone (easy keeps most
|
|
12
|
+
* of its numbers), hard by supposing one square at a time, extra-hard for as long as the answer stays single,
|
|
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.
|
|
9
15
|
*/
|
|
10
|
-
export function generateAkari(width = 7, height = width, seed = 1) {
|
|
16
|
+
export function generateAkari(width = 7, height = width, seed = 1, level = "medium") {
|
|
11
17
|
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= AKARI_MOST_SIDE)
|
|
12
|
-
|| !isKazuSeed(seed))
|
|
18
|
+
|| !isKazuSeed(seed) || !AKARI_LEVELS.includes(level))
|
|
13
19
|
throw new RangeError("Invalid Akari settings");
|
|
14
20
|
const random = seededRandom(seed);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for (let
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
cells[room] = null;
|
|
33
|
-
cells[clueY * width + x] = 1;
|
|
34
|
-
bulbs.push(room);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
21
|
+
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 };
|
|
37
36
|
}
|
|
37
|
+
if (best)
|
|
38
|
+
return { ...best.board, seed, level, solution: best.solution };
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else {
|
|
53
|
-
cells[room] = null;
|
|
54
|
-
cells[y * width + clueX] = 1;
|
|
55
|
-
bulbs.push(room);
|
|
56
|
-
}
|
|
40
|
+
const fallback = templateAkari(width, height, seed);
|
|
41
|
+
return { ...fallback, level };
|
|
42
|
+
}
|
|
43
|
+
function attemptAkari(width, height, level, random) {
|
|
44
|
+
const size = width * height;
|
|
45
|
+
const density = { easy: .2, medium: .22, hard: .24, "extra-hard": .26 }[level] * (.85 + random() * .3);
|
|
46
|
+
const cells = Array.from({ length: size }, () => random() < density ? false : null);
|
|
47
|
+
if (random() < .5)
|
|
48
|
+
for (let y = 0; y < height; y += 1)
|
|
49
|
+
for (let x = 0; x < width; x += 1) {
|
|
50
|
+
const mirror = (height - 1 - y) * width + width - 1 - x;
|
|
51
|
+
if (y * width + x < mirror)
|
|
52
|
+
cells[mirror] = cells[y * width + x];
|
|
57
53
|
}
|
|
58
|
-
|
|
54
|
+
if (!cells.includes(null))
|
|
55
|
+
return null;
|
|
56
|
+
const base = { width, height, cells };
|
|
57
|
+
// Light every white square with randomly chosen bulbs that never see one another.
|
|
58
|
+
const lit = new Set(), bulbs = new Set();
|
|
59
|
+
for (const cell of shuffled(cells.flatMap((value, at) => value === null ? [at] : []), random)) {
|
|
60
|
+
if (lit.has(cell))
|
|
61
|
+
continue;
|
|
62
|
+
bulbs.add(cell);
|
|
63
|
+
for (const seen of akariVisible(base, cell))
|
|
64
|
+
lit.add(seen);
|
|
59
65
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const numbered = cells.flatMap((value, at) => value === false && akariNeighbours(base, at).some(n => cells[n] === null) ? [at] : []);
|
|
67
|
+
for (const at of numbered)
|
|
68
|
+
cells[at] = akariNeighbours(base, at).filter(n => bulbs.has(n)).length;
|
|
69
|
+
if (!numbered.length)
|
|
70
|
+
return null;
|
|
71
|
+
const accepts = () => {
|
|
72
|
+
const { csp } = akariModel({ width, height, cells });
|
|
73
|
+
return logicCsp(csp, openSlots(csp), level === "easy" || level === "medium" ? 0 : 1).solved;
|
|
66
74
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
if (!accepts())
|
|
76
|
+
return null;
|
|
77
|
+
const keepAtLeast = level === "easy" ? Math.ceil(numbered.length * .7) : 0;
|
|
78
|
+
let left = numbered.length;
|
|
79
|
+
for (const at of shuffled(numbered, random)) {
|
|
80
|
+
if (left <= keepAtLeast)
|
|
81
|
+
break;
|
|
82
|
+
const was = cells[at];
|
|
83
|
+
cells[at] = false;
|
|
84
|
+
if (accepts())
|
|
85
|
+
left -= 1;
|
|
86
|
+
else
|
|
87
|
+
cells[at] = was;
|
|
76
88
|
}
|
|
77
|
-
|
|
89
|
+
const board = { width, height, cells };
|
|
90
|
+
if (!isAkariBoard(board))
|
|
91
|
+
return null;
|
|
92
|
+
const { csp } = akariModel(board);
|
|
93
|
+
if (level === "easy" || level === "medium")
|
|
94
|
+
return { board, probes: 0 };
|
|
95
|
+
if (logicCsp(csp, openSlots(csp), 0).solved)
|
|
96
|
+
return null;
|
|
97
|
+
const probing = logicCsp(csp, openSlots(csp), 1);
|
|
98
|
+
if (!probing.solved)
|
|
99
|
+
return level === "extra-hard" ? { board, probes: 1000 } : null;
|
|
100
|
+
return probing.probes >= Math.max(2, Math.round(size * .02)) ? { board, probes: probing.probes } : null;
|
|
78
101
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { AkariBoard } from "./akari.types.ts";
|
|
3
|
+
/** An Akari board as variables: one per white square, slot 0 for no bulb and slot 1 for a bulb. */
|
|
4
|
+
export type AkariModel = {
|
|
5
|
+
csp: Csp;
|
|
6
|
+
whites: readonly number[];
|
|
7
|
+
index: Int32Array;
|
|
8
|
+
};
|
|
9
|
+
/** Builds the pruning rules of Akari: a bulb shades its lines, a number counts its neighbours, every white square needs a lamp. */
|
|
10
|
+
export declare function akariModel(board: AkariBoard): AkariModel;
|