@johnmorrisdotca/kazu 1.0.0 → 1.1.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 +7 -0
- package/README.md +54 -2
- package/dist/draw-entry.d.ts +1 -0
- package/dist/draw-entry.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/play-entry.d.ts +1 -0
- package/dist/play-entry.js +1 -0
- package/dist/shikaku-draw-entry.d.ts +2 -0
- package/dist/shikaku-draw-entry.js +1 -0
- package/dist/shikaku-entry.d.ts +7 -0
- package/dist/shikaku-entry.js +7 -0
- package/dist/shikaku-play-entry.d.ts +4 -0
- package/dist/shikaku-play-entry.js +3 -0
- package/dist/shikaku.constants.d.ts +5 -0
- package/dist/shikaku.constants.js +5 -0
- package/dist/shikaku.types.d.ts +36 -0
- package/dist/shikaku.types.js +1 -0
- package/dist/shikakuBoard.d.ts +9 -0
- package/dist/shikakuBoard.js +59 -0
- package/dist/shikakuDraw.d.ts +4 -0
- package/dist/shikakuDraw.js +23 -0
- package/dist/shikakuGame.d.ts +14 -0
- package/dist/shikakuGame.js +61 -0
- package/dist/shikakuGenerate.d.ts +3 -0
- package/dist/shikakuGenerate.js +43 -0
- package/dist/shikakuMount.d.ts +3 -0
- package/dist/shikakuMount.js +143 -0
- package/dist/shikakuPlay.types.d.ts +26 -0
- package/dist/shikakuPlay.types.js +1 -0
- package/dist/shikakuSolve.d.ts +6 -0
- package/dist/shikakuSolve.js +54 -0
- package/dist/shikakuStrings.d.ts +102 -0
- package/dist/shikakuStrings.js +5 -0
- package/dist/shikakuStyle.d.ts +1 -0
- package/dist/shikakuStyle.js +12 -0
- package/dist/shikakuWorker.d.ts +1 -0
- package/dist/shikakuWorker.js +10 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +19 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.1.0] - 2026-10-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Shikaku: unique seeded rectangle puzzles, exact-cover solving, immutable play and progress, SVG drawing, accessible bilingual controls and worker-backed generation.
|
|
14
|
+
- Square, wide, tall and custom rectangle demos using the shared family styling. Existing six number-entry kinds and saved codes remain compatible.
|
|
15
|
+
|
|
9
16
|
## [1.0.0] - 2026-10-01
|
|
10
17
|
|
|
11
18
|
The Numbers family of grid puzzles, taken out of itsutsu.com so that the site can import them as it
|
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 puzzles for JavaScript and TypeScript.</strong><br>
|
|
4
|
-
Sudoku (4×4 to a 16×16 Giant), Jigsaw, Diagonal and Killer Sudoku, Futoshiki and
|
|
4
|
+
Sudoku (4×4 to a 16×16 Giant), Jigsaw, Diagonal and Killer Sudoku, Futoshiki, Skyscrapers and Shikaku. A seeded generator whose every puzzle has exactly one answer, at three levels; a solver that counts answers; a check that reads a finished grid in O(cells); a hint that says which cell to fill next and why; puzzles and runs as short codes; the grid drawn as SVG; and played by touch, mouse and keyboard in any page, with pencil marks, undo and a clock, as one call or one tag. No 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>
|
|
@@ -17,7 +17,7 @@ Sudoku (4×4 to a 16×16 Giant), Jigsaw, Diagonal and Killer Sudoku, Futoshiki a
|
|
|
17
17
|
<img src="docs/phone.jpg" alt="A 6×6 Skyscrapers puzzle part filled in, on a phone in dark mode and in Japanese: the clues round the edge, the number pad, the buttons and the first of the settings under it" width="200">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
-
Kazu is
|
|
20
|
+
Kazu is a family of grid puzzles built around numbers. Sudoku and its companions fill cells with numbers; Shikaku divides cells into numbered rectangles. It is
|
|
21
21
|
played at [itsutsu.com](https://itsutsu.com), which this package was taken out of, and in
|
|
22
22
|
[the demo](https://johnmorrisdotca.github.io/kazu/), with nothing to install.
|
|
23
23
|
|
|
@@ -442,6 +442,43 @@ Not here yet, and each welcome as an [issue](https://github.com/johnmorrisdotca/
|
|
|
442
442
|
|
|
443
443
|
Left out on purpose: a puzzle with more than one answer, and any account, ranking or storage. A page keeps its own runs: `onChange` hands them over.
|
|
444
444
|
|
|
445
|
+
## Shikaku — rectangles in Kazu
|
|
446
|
+
|
|
447
|
+
The demo includes square, wide (10 × 6), tall (6 × 10) and custom rectangular boards, with width and height from 2 to 16. Shares and saved settings preserve both dimensions. It uses Kazu’s unchanged shared family stylesheet and header controls. Irregular outlines are not part of this classic rectangle-partition game.
|
|
448
|
+
|
|
449
|
+
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.
|
|
450
|
+
|
|
451
|
+
```js
|
|
452
|
+
import { generateShikaku, newShikaku, placeShikaku, checkShikaku } from "@johnmorrisdotca/kazu/shikaku";
|
|
453
|
+
import { mountShikaku } from "@johnmorrisdotca/kazu/shikaku/play";
|
|
454
|
+
|
|
455
|
+
const puzzle = generateShikaku(7, 7, "medium", 42);
|
|
456
|
+
const player = mountShikaku(document.querySelector("#board"), {
|
|
457
|
+
board: puzzle, material: "ivory", pieces: "ink", language: "en",
|
|
458
|
+
onFinish: game => console.log(game.helped ? "Solved with help" : "Solved"),
|
|
459
|
+
});
|
|
460
|
+
// player.progress() saves public clues and rectangles; player.destroy() removes the player.
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Use `@johnmorrisdotca/kazu/shikaku`, `@johnmorrisdotca/kazu/shikaku/play`, or `@johnmorrisdotca/kazu/shikaku/draw`.
|
|
464
|
+
|
|
465
|
+
The root entry re-exports the engine, `/play` re-exports `mountShikaku`, and `/draw` re-exports `drawShikaku`. The dedicated entries let a consumer load only Shikaku. There are no runtime dependencies.
|
|
466
|
+
|
|
467
|
+
- `ShikakuBoard`: `width`, `height`, and row-major `clues` (zero for an empty cell). Dimensions are 2–16; clue areas sum to the grid area.
|
|
468
|
+
- `ShikakuRectangle`: zero-based `x`, `y`, `width`, `height`.
|
|
469
|
+
- `generateShikaku(width, height, level, seed)`: deterministic puzzle and solution. The answer is counted independently. Generation rejects ambiguous boards and stops after 200 attempts with an error; it never silently returns an unproved board. The seed range matches Kazu. Levels `easy`, `medium`, `hard` favor small, mixed and larger rectangles; these are generation profiles, not calibrated human difficulty ratings.
|
|
470
|
+
- `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.
|
|
471
|
+
- `checkShikaku(board, rectangles)`: coverage and rectangle rule errors, independent of a stored answer. It accepts any valid completion.
|
|
472
|
+
- `newShikaku`, `placeShikaku`, `removeShikaku`, `undoShikaku`: immutable game operations. A placement replaces intersecting rectangles, and rule errors are allowed until checked. The game strips generated solutions.
|
|
473
|
+
- `hintShikaku(game)`: a rectangle from the single proved remaining partition, or `null`. Hints in the mounted player mark the run as helped.
|
|
474
|
+
- `encodeShikaku`, `decodeShikaku`: versioned JSON with only public puzzle data and placements, validated on restore. Undo history and the clock are session-only.
|
|
475
|
+
- `drawShikaku(board, options)`: SVG. Materials `ivory`, `wood`, `slate`; numbers `ink`, `tiles`; language `en`, `ja`.
|
|
476
|
+
- `mountShikaku(host, options)`: tap two opposite corner cells, or use arrows and Enter/Space. Delete removes a selected rectangle; Escape cancels a pending corner. Undo, Check, Hint, Restart and a modal board view are built in. The handle has `game`, `progress`, `set`, `restart`, `destroy`; callbacks and bubbling `shikaku-change` / `shikaku-finish` events carry copies of public state. Mount once per puzzle; use `set` for appearance changes.
|
|
477
|
+
|
|
478
|
+
The demo is `site/shikaku.html` after `pnpm site`; its generator runs in a module worker at `dist/shikakuWorker.js`. Deploy the worker with the built files and permit same-origin module workers. The npm player accepts a board synchronously; hosts can use their own worker when generating large custom boards. The demo stores progress locally and shares settings through the address. Shared seeds start fresh; they do not share your placements. Restart and page reload reset the session clock.
|
|
479
|
+
|
|
480
|
+
[Shikaku's rules are described by Nikoli](https://www.nikoli.co.jp/en/puzzles/shikaku/). This implementation generates its own puzzles and does not copy Nikoli's puzzle grids, wording or artwork.
|
|
481
|
+
|
|
445
482
|
## Architecture
|
|
446
483
|
|
|
447
484
|
The generators, the solvers, the check, the hint and the game are plain functions over short codes, with no
|
|
@@ -449,6 +486,21 @@ DOM. The drawing is SVG text in an entry of its own, so a server that only check
|
|
|
449
486
|
the page's part (the mount and the element) is another.
|
|
450
487
|
|
|
451
488
|
```text
|
|
489
|
+
├── shikaku-draw-entry.ts
|
|
490
|
+
├── shikaku-entry.ts
|
|
491
|
+
├── shikaku-play-entry.ts
|
|
492
|
+
├── shikaku.constants.ts
|
|
493
|
+
├── shikaku.types.ts
|
|
494
|
+
├── shikakuBoard.ts
|
|
495
|
+
├── shikakuDraw.ts
|
|
496
|
+
├── shikakuGame.ts
|
|
497
|
+
├── shikakuGenerate.ts
|
|
498
|
+
├── shikakuMount.ts
|
|
499
|
+
├── shikakuPlay.types.ts
|
|
500
|
+
├── shikakuSolve.ts
|
|
501
|
+
├── shikakuStrings.ts
|
|
502
|
+
├── shikakuStyle.ts
|
|
503
|
+
├── shikakuWorker.ts
|
|
452
504
|
src/
|
|
453
505
|
├── index.ts the main entry: everything but the drawing and the page
|
|
454
506
|
├── kinds.ts the six puzzles' keys, sizes and levels, and the shape of a puzzle
|
package/dist/draw-entry.d.ts
CHANGED
|
@@ -12,3 +12,4 @@ export { KAZU_STRINGS, kazuLanguageOf, kazuNameOf, kazuSay } from "./strings.ts"
|
|
|
12
12
|
export type { KazuLanguage } from "./strings.ts";
|
|
13
13
|
export { KAZU_NAMES, KAZU_SIZE_NAMES } from "./names.ts";
|
|
14
14
|
export type { KazuName } from "./names.ts";
|
|
15
|
+
export * from "./shikaku-draw-entry.ts";
|
package/dist/draw-entry.js
CHANGED
|
@@ -8,3 +8,4 @@ export { kazuGeometry, KAZU_CELL } from "./geometry.js";
|
|
|
8
8
|
export { KAZU_STYLE } from "./style.js";
|
|
9
9
|
export { KAZU_STRINGS, kazuLanguageOf, kazuNameOf, kazuSay } from "./strings.js";
|
|
10
10
|
export { KAZU_NAMES, KAZU_SIZE_NAMES } from "./names.js";
|
|
11
|
+
export * from "./shikaku-draw-entry.js";
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -29,3 +29,4 @@ export { encodeTowers, decodeTowers, cluesOf, lineFrom, towersSeen, noClues, TOW
|
|
|
29
29
|
export { boxedLayout, boxOf, KAZU_BOXES, neighbours, regionsAreSound } from "./layout.js";
|
|
30
30
|
export { seededRandom, shuffled, freshKazuSeed, isKazuSeed, KAZU_SEED_MOST } from "./random.js";
|
|
31
31
|
export { VERSION } from "./version.js";
|
|
32
|
+
export * from "./shikaku-entry.js";
|
package/dist/play-entry.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { ensureKazuPlayStyle, mountKazu } from "./mount.ts";
|
|
|
7
7
|
export type { KazuEventDetail, KazuMount, KazuMountOptions, KazuMountSettings } from "./mount.ts";
|
|
8
8
|
export { KAZU_PLAY_STYLE } from "./playStyle.ts";
|
|
9
9
|
export { kazuClockText } from "./clock.ts";
|
|
10
|
+
export * from "./shikaku-play-entry.ts";
|
package/dist/play-entry.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { drawShikaku } from "./shikakuDraw.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Shikaku shares Kazu's number puzzle package, with its own rectangle model. */
|
|
2
|
+
export * from "./shikaku.types.ts";
|
|
3
|
+
export * from "./shikaku.constants.ts";
|
|
4
|
+
export * from "./shikakuBoard.ts";
|
|
5
|
+
export * from "./shikakuSolve.ts";
|
|
6
|
+
export * from "./shikakuGenerate.ts";
|
|
7
|
+
export * from "./shikakuGame.ts";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Shikaku shares Kazu's number puzzle package, with its own rectangle model. */
|
|
2
|
+
export * from "./shikaku.types.js";
|
|
3
|
+
export * from "./shikaku.constants.js";
|
|
4
|
+
export * from "./shikakuBoard.js";
|
|
5
|
+
export * from "./shikakuSolve.js";
|
|
6
|
+
export * from "./shikakuGenerate.js";
|
|
7
|
+
export * from "./shikakuGame.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Bounds keep custom boards and searches finite. */
|
|
2
|
+
export declare const SHIKAKU_MOST_SIDE = 16;
|
|
3
|
+
export declare const SHIKAKU_MOST_NODES = 100000;
|
|
4
|
+
export declare const SHIKAKU_MOST_ATTEMPTS = 200;
|
|
5
|
+
export declare const SHIKAKU_LEVELS: readonly ["easy", "medium", "hard"];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** A rectangle uses zero-based column and row coordinates. */
|
|
2
|
+
export type ShikakuRectangle = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
/** One area clue per rectangle; zero is an unnumbered cell. */
|
|
9
|
+
export type ShikakuBoard = {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
clues: readonly number[];
|
|
13
|
+
};
|
|
14
|
+
export type ShikakuLevel = "easy" | "medium" | "hard";
|
|
15
|
+
export type ShikakuPuzzle = ShikakuBoard & {
|
|
16
|
+
seed: number;
|
|
17
|
+
level: ShikakuLevel;
|
|
18
|
+
solution: readonly ShikakuRectangle[];
|
|
19
|
+
};
|
|
20
|
+
export type ShikakuCheck = {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
covered: number;
|
|
23
|
+
errors: readonly number[];
|
|
24
|
+
};
|
|
25
|
+
export type ShikakuSolve = {
|
|
26
|
+
count: number;
|
|
27
|
+
solution: readonly ShikakuRectangle[] | null;
|
|
28
|
+
complete: boolean;
|
|
29
|
+
nodes: number;
|
|
30
|
+
};
|
|
31
|
+
export type ShikakuGame = {
|
|
32
|
+
board: ShikakuBoard;
|
|
33
|
+
rectangles: readonly ShikakuRectangle[];
|
|
34
|
+
history: readonly (readonly ShikakuRectangle[])[];
|
|
35
|
+
helped: boolean;
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ShikakuBoard, ShikakuCheck, ShikakuRectangle } from "./shikaku.types.ts";
|
|
2
|
+
/** Whether dimensions and area clues describe a possible partition. */
|
|
3
|
+
export declare function isShikakuBoard(value: unknown): value is ShikakuBoard;
|
|
4
|
+
/** Cells of a bounded rectangle, or null for invalid coordinates. */
|
|
5
|
+
export declare function shikakuCells(board: ShikakuBoard, r: ShikakuRectangle): number[] | null;
|
|
6
|
+
/** A finished partition is checked against the rules, never against a stored answer. */
|
|
7
|
+
export declare function checkShikaku(board: ShikakuBoard, rectangles: readonly ShikakuRectangle[]): ShikakuCheck;
|
|
8
|
+
/** All legal rectangles for a numbered cell, in a stable order. */
|
|
9
|
+
export declare function shikakuCandidates(board: ShikakuBoard, cell: number): ShikakuRectangle[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SHIKAKU_MOST_SIDE } from "./shikaku.constants.js";
|
|
2
|
+
/** Whether dimensions and area clues describe a possible partition. */
|
|
3
|
+
export function isShikakuBoard(value) {
|
|
4
|
+
if (!value || typeof value !== "object")
|
|
5
|
+
return false;
|
|
6
|
+
const b = value;
|
|
7
|
+
return Number.isInteger(b.width) && b.width >= 2 && b.width <= SHIKAKU_MOST_SIDE
|
|
8
|
+
&& Number.isInteger(b.height) && b.height >= 2 && b.height <= SHIKAKU_MOST_SIDE
|
|
9
|
+
&& Array.isArray(b.clues) && b.clues.length === b.width * b.height
|
|
10
|
+
&& b.clues.every(n => Number.isInteger(n) && n >= 0 && n <= b.clues.length)
|
|
11
|
+
&& b.clues.reduce((a, n) => a + n, 0) === b.clues.length;
|
|
12
|
+
}
|
|
13
|
+
/** Cells of a bounded rectangle, or null for invalid coordinates. */
|
|
14
|
+
export function shikakuCells(board, r) {
|
|
15
|
+
if (!r || ![r.x, r.y, r.width, r.height].every(Number.isInteger)
|
|
16
|
+
|| r.x < 0 || r.y < 0 || r.width < 1 || r.height < 1
|
|
17
|
+
|| r.x + r.width > board.width || r.y + r.height > board.height)
|
|
18
|
+
return null;
|
|
19
|
+
return Array.from({ length: r.width * r.height }, (_, i) => (r.y + Math.floor(i / r.width)) * board.width + r.x + i % r.width);
|
|
20
|
+
}
|
|
21
|
+
/** A finished partition is checked against the rules, never against a stored answer. */
|
|
22
|
+
export function checkShikaku(board, rectangles) {
|
|
23
|
+
if (!isShikakuBoard(board))
|
|
24
|
+
throw new RangeError("Invalid Shikaku board");
|
|
25
|
+
const occupied = new Set(), errors = [];
|
|
26
|
+
rectangles.forEach((r, index) => {
|
|
27
|
+
const cells = shikakuCells(board, r);
|
|
28
|
+
const clues = cells?.filter(c => board.clues[c] > 0) ?? [];
|
|
29
|
+
if (!cells || clues.length !== 1 || board.clues[clues[0]] !== cells.length
|
|
30
|
+
|| cells.some(c => occupied.has(c)))
|
|
31
|
+
errors.push(index);
|
|
32
|
+
cells?.forEach(c => occupied.add(c));
|
|
33
|
+
});
|
|
34
|
+
return { ok: errors.length === 0 && occupied.size === board.clues.length, covered: occupied.size, errors };
|
|
35
|
+
}
|
|
36
|
+
/** All legal rectangles for a numbered cell, in a stable order. */
|
|
37
|
+
export function shikakuCandidates(board, cell) {
|
|
38
|
+
if (!isShikakuBoard(board))
|
|
39
|
+
throw new RangeError("Invalid Shikaku board");
|
|
40
|
+
const area = board.clues[cell], out = [];
|
|
41
|
+
if (!area)
|
|
42
|
+
return out;
|
|
43
|
+
const cx = cell % board.width, cy = Math.floor(cell / board.width);
|
|
44
|
+
for (let w = 1; w <= board.width; w += 1) {
|
|
45
|
+
if (area % w)
|
|
46
|
+
continue;
|
|
47
|
+
const h = area / w;
|
|
48
|
+
if (h > board.height)
|
|
49
|
+
continue;
|
|
50
|
+
for (let y = Math.max(0, cy - h + 1); y <= Math.min(cy, board.height - h); y += 1) {
|
|
51
|
+
for (let x = Math.max(0, cx - w + 1); x <= Math.min(cx, board.width - w); x += 1) {
|
|
52
|
+
const r = { x, y, width: w, height: h };
|
|
53
|
+
if (shikakuCells(board, r).every(c => c === cell || board.clues[c] === 0))
|
|
54
|
+
out.push(r);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ShikakuBoard } from "./shikaku.types.ts";
|
|
2
|
+
import type { ShikakuDrawOptions } from "./shikakuPlay.types.ts";
|
|
3
|
+
/** SVG text with only the public clues and the player's rectangles. */
|
|
4
|
+
export declare function drawShikaku(board: ShikakuBoard, options?: ShikakuDrawOptions): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isShikakuBoard, shikakuCells } from "./shikakuBoard.js";
|
|
2
|
+
import { shikakuWords } from "./shikakuStrings.js";
|
|
3
|
+
/** SVG text with only the public clues and the player's rectangles. */
|
|
4
|
+
export function drawShikaku(board, options = {}) {
|
|
5
|
+
if (!isShikakuBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Shikaku board");
|
|
7
|
+
const words = shikakuWords(options.language), size = 48;
|
|
8
|
+
const palettes = { ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"], wood: ["#e5cda6", "#493e2f", "#bca582"], slate: ["#262a27", "#ece8dc", "#454a44"] };
|
|
9
|
+
const [paper, ink, grid] = palettes[options.material ?? "ivory"];
|
|
10
|
+
const marks = [];
|
|
11
|
+
for (const [index, r] of (options.rectangles ?? []).entries()) {
|
|
12
|
+
if (!shikakuCells(board, r))
|
|
13
|
+
continue;
|
|
14
|
+
const colour = options.errors?.includes(index) ? "var(--kz-bad,#b5452c)" : ink;
|
|
15
|
+
marks.push(`<rect x="${r.x * size + 3}" y="${r.y * size + 3}" width="${r.width * size - 6}" height="${r.height * size - 6}" rx="3" fill="${colour}" fill-opacity=".10" stroke="${colour}" stroke-width="2.5"/>`);
|
|
16
|
+
}
|
|
17
|
+
const cells = board.clues.map((n, c) => {
|
|
18
|
+
const x = c % board.width * size, y = Math.floor(c / board.width) * size;
|
|
19
|
+
const active = c === options.selected || c === options.anchor;
|
|
20
|
+
return `<g data-shikaku-cell="${c}"><rect x="${x}" y="${y}" width="48" height="48" fill="${active ? ink : "transparent"}" fill-opacity="${active ? ".15" : "1"}" stroke="${grid}" stroke-width="1"/>${n ? `${options.pieces === "tiles" ? `<rect x="${x + 10}" y="${y + 10}" width="28" height="28" rx="6" fill="${paper}" stroke="${grid}"/>` : ""}<text x="${x + 24}" y="${y + 25}" dominant-baseline="middle" text-anchor="middle" fill="${ink}" font-size="21" font-weight="700" font-family="system-ui,sans-serif">${n}</text>` : ""}</g>`;
|
|
21
|
+
}).join("");
|
|
22
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 ${board.width * size + 4} ${board.height * size + 4}" role="img" aria-label="${words.title}" style="display:block;width:100%;height:auto;background:${paper}">${cells}${marks.join("")}<rect width="${board.width * size}" height="${board.height * size}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="2"/></svg>`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShikakuBoard, ShikakuGame, ShikakuRectangle } from "./shikaku.types.ts";
|
|
2
|
+
/** Begin with a copy of just the public clues; a generated answer is never kept in play. */
|
|
3
|
+
export declare function newShikaku(board: ShikakuBoard): ShikakuGame;
|
|
4
|
+
/** Place a rectangle, replacing any it touches. Local rule errors are allowed for the player to check. */
|
|
5
|
+
export declare function placeShikaku(game: ShikakuGame, rectangle: ShikakuRectangle): ShikakuGame;
|
|
6
|
+
/** Remove the rectangle containing a cell. */
|
|
7
|
+
export declare function removeShikaku(game: ShikakuGame, cell: number): ShikakuGame;
|
|
8
|
+
export declare function undoShikaku(game: ShikakuGame): ShikakuGame;
|
|
9
|
+
/** A rectangle is offered only when the remaining partition has one proved answer. */
|
|
10
|
+
export declare function hintShikaku(game: ShikakuGame): ShikakuRectangle | null;
|
|
11
|
+
export declare function shikakuFinished(game: ShikakuGame): boolean;
|
|
12
|
+
/** Versioned public puzzle and placement data, with no hidden answer. */
|
|
13
|
+
export declare function encodeShikaku(game: ShikakuGame): string;
|
|
14
|
+
export declare function decodeShikaku(code: string): ShikakuGame | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { checkShikaku, isShikakuBoard, shikakuCells } from "./shikakuBoard.js";
|
|
2
|
+
import { solveShikaku } from "./shikakuSolve.js";
|
|
3
|
+
/** Begin with a copy of just the public clues; a generated answer is never kept in play. */
|
|
4
|
+
export function newShikaku(board) {
|
|
5
|
+
if (!isShikakuBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Shikaku board");
|
|
7
|
+
return { board: { width: board.width, height: board.height, clues: [...board.clues] }, rectangles: [], history: [], helped: false };
|
|
8
|
+
}
|
|
9
|
+
/** Place a rectangle, replacing any it touches. Local rule errors are allowed for the player to check. */
|
|
10
|
+
export function placeShikaku(game, rectangle) {
|
|
11
|
+
const cells = shikakuCells(game.board, rectangle);
|
|
12
|
+
if (!cells)
|
|
13
|
+
return game;
|
|
14
|
+
const covered = new Set(cells);
|
|
15
|
+
return { ...game, rectangles: [...game.rectangles.filter(r => !shikakuCells(game.board, r).some(c => covered.has(c))), { ...rectangle }],
|
|
16
|
+
history: [...game.history, game.rectangles.map(r => ({ ...r }))] };
|
|
17
|
+
}
|
|
18
|
+
/** Remove the rectangle containing a cell. */
|
|
19
|
+
export function removeShikaku(game, cell) {
|
|
20
|
+
const rectangles = game.rectangles.filter(r => !shikakuCells(game.board, r).includes(cell));
|
|
21
|
+
return rectangles.length === game.rectangles.length ? game : { ...game, rectangles, history: [...game.history, game.rectangles] };
|
|
22
|
+
}
|
|
23
|
+
export function undoShikaku(game) {
|
|
24
|
+
const previous = game.history.at(-1);
|
|
25
|
+
return previous ? { ...game, rectangles: previous.map(r => ({ ...r })), history: game.history.slice(0, -1) } : game;
|
|
26
|
+
}
|
|
27
|
+
/** A rectangle is offered only when the remaining partition has one proved answer. */
|
|
28
|
+
export function hintShikaku(game) {
|
|
29
|
+
const solved = solveShikaku(game.board, game.rectangles);
|
|
30
|
+
if (!solved.complete || solved.count !== 1)
|
|
31
|
+
return null;
|
|
32
|
+
return solved.solution.find(r => !game.rectangles.some(p => p.x === r.x && p.y === r.y && p.width === r.width && p.height === r.height)) ?? null;
|
|
33
|
+
}
|
|
34
|
+
export function shikakuFinished(game) { return checkShikaku(game.board, game.rectangles).ok; }
|
|
35
|
+
/** Versioned public puzzle and placement data, with no hidden answer. */
|
|
36
|
+
export function encodeShikaku(game) {
|
|
37
|
+
return JSON.stringify({ version: 1, board: game.board, rectangles: game.rectangles, helped: game.helped });
|
|
38
|
+
}
|
|
39
|
+
export function decodeShikaku(code) {
|
|
40
|
+
try {
|
|
41
|
+
if (code.length > 40000)
|
|
42
|
+
return null;
|
|
43
|
+
const value = JSON.parse(code);
|
|
44
|
+
if (value.version !== 1 || !isShikakuBoard(value.board) || !Array.isArray(value.rectangles)
|
|
45
|
+
|| value.rectangles.length > value.board.clues.length || typeof value.helped !== "boolean")
|
|
46
|
+
return null;
|
|
47
|
+
let game = newShikaku(value.board);
|
|
48
|
+
for (const r of value.rectangles) {
|
|
49
|
+
if (!shikakuCells(game.board, r))
|
|
50
|
+
return null;
|
|
51
|
+
const next = placeShikaku(game, r);
|
|
52
|
+
if (next.rectangles.length !== game.rectangles.length + 1)
|
|
53
|
+
return null;
|
|
54
|
+
game = next;
|
|
55
|
+
}
|
|
56
|
+
return { ...game, history: [], helped: value.helped };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ShikakuLevel, ShikakuPuzzle } from "./shikaku.types.ts";
|
|
2
|
+
/** Seeded partitions with an independently counted, unique answer. Levels choose rectangle sizes, not a promised human rating. */
|
|
3
|
+
export declare function generateShikaku(width?: number, height?: number, level?: ShikakuLevel, seed?: number): ShikakuPuzzle;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SHIKAKU_LEVELS, SHIKAKU_MOST_ATTEMPTS } from "./shikaku.constants.js";
|
|
2
|
+
import { isShikakuBoard, shikakuCells } from "./shikakuBoard.js";
|
|
3
|
+
import { solveShikaku } from "./shikakuSolve.js";
|
|
4
|
+
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
+
/** Seeded partitions with an independently counted, unique answer. Levels choose rectangle sizes, not a promised human rating. */
|
|
6
|
+
export function generateShikaku(width = 7, height = width, level = "medium", seed = 1) {
|
|
7
|
+
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= 16))
|
|
8
|
+
throw new RangeError("Invalid Shikaku dimensions");
|
|
9
|
+
const empty = { width, height, clues: Array(width * height).fill(0) };
|
|
10
|
+
if (!isKazuSeed(seed) || !SHIKAKU_LEVELS.includes(level)
|
|
11
|
+
|| !isShikakuBoard({ ...empty, clues: [width * height, ...empty.clues.slice(1)] }))
|
|
12
|
+
throw new RangeError("Invalid Shikaku settings");
|
|
13
|
+
const random = seededRandom(seed), maximum = { easy: 5, medium: 9, hard: 15 }[level];
|
|
14
|
+
for (let attempt = 0; attempt < SHIKAKU_MOST_ATTEMPTS; attempt += 1) {
|
|
15
|
+
const solution = [];
|
|
16
|
+
const split = (r) => {
|
|
17
|
+
if (r.width * r.height <= maximum && (r.width * r.height < 3 || random() < .6)) {
|
|
18
|
+
solution.push(r);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const vertical = r.height === 1 || (r.width > 1 && random() < .5);
|
|
22
|
+
const cut = 1 + Math.floor(random() * ((vertical ? r.width : r.height) - 1));
|
|
23
|
+
if (vertical) {
|
|
24
|
+
split({ ...r, width: cut });
|
|
25
|
+
split({ ...r, x: r.x + cut, width: r.width - cut });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
split({ ...r, height: cut });
|
|
29
|
+
split({ ...r, y: r.y + cut, height: r.height - cut });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
split({ x: 0, y: 0, width, height });
|
|
33
|
+
const clues = [...empty.clues];
|
|
34
|
+
for (const r of solution) {
|
|
35
|
+
const cells = shikakuCells(empty, r);
|
|
36
|
+
clues[cells[Math.floor(random() * cells.length)]] = cells.length;
|
|
37
|
+
}
|
|
38
|
+
const board = { width, height, clues }, counted = solveShikaku(board);
|
|
39
|
+
if (counted.complete && counted.count === 1)
|
|
40
|
+
return { ...board, seed, level, solution: counted.solution };
|
|
41
|
+
}
|
|
42
|
+
throw new Error("No unique Shikaku found within the generation budget; try another seed");
|
|
43
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ShikakuMount, ShikakuMountOptions } from "./shikakuPlay.types.ts";
|
|
2
|
+
/** A rectangle player in any element; mount once, update appearance with set, destroy on unmount. */
|
|
3
|
+
export declare function mountShikaku(host: HTMLElement, initial: ShikakuMountOptions): ShikakuMount;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { checkShikaku, shikakuCells } from "./shikakuBoard.js";
|
|
2
|
+
import { decodeShikaku, encodeShikaku, hintShikaku, newShikaku, placeShikaku, removeShikaku, shikakuFinished, undoShikaku } from "./shikakuGame.js";
|
|
3
|
+
import { drawShikaku } from "./shikakuDraw.js";
|
|
4
|
+
import { KAZU_STYLE } from "./style.js";
|
|
5
|
+
import { SHIKAKU_PLAY_STYLE } from "./shikakuStyle.js";
|
|
6
|
+
import { shikakuWords } from "./shikakuStrings.js";
|
|
7
|
+
/** A rectangle player in any element; mount once, update appearance with set, destroy on unmount. */
|
|
8
|
+
export function mountShikaku(host, initial) {
|
|
9
|
+
const doc = host.ownerDocument;
|
|
10
|
+
let options = { ...initial }, game = initial.progress ? decodeShikaku(initial.progress) : null;
|
|
11
|
+
game ?? (game = newShikaku(initial.board));
|
|
12
|
+
let selected = 0, anchor = null, errors = [], message = "", finished = shikakuFinished(game), dead = false;
|
|
13
|
+
const root = doc.createElement("div"), style = doc.createElement("style"), board = doc.createElement("div"), status = doc.createElement("p"), tools = doc.createElement("div"), help = doc.createElement("p");
|
|
14
|
+
root.className = "ks-root";
|
|
15
|
+
style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".ks-palette") + SHIKAKU_PLAY_STYLE;
|
|
16
|
+
board.className = "ks-board";
|
|
17
|
+
status.className = "ks-status";
|
|
18
|
+
status.setAttribute("role", "status");
|
|
19
|
+
tools.className = "ks-tools";
|
|
20
|
+
help.className = "ks-help";
|
|
21
|
+
root.append(style, board, status, tools, help);
|
|
22
|
+
host.append(root);
|
|
23
|
+
const copy = () => ({ ...game, board: { ...game.board, clues: [...game.board.clues] }, rectangles: game.rectangles.map(r => ({ ...r })), history: game.history.map(turn => turn.map(r => ({ ...r }))) });
|
|
24
|
+
const words = () => shikakuWords(options.language);
|
|
25
|
+
const button = (text, action, disabled = false) => {
|
|
26
|
+
const b = doc.createElement("button");
|
|
27
|
+
b.type = "button";
|
|
28
|
+
b.textContent = text;
|
|
29
|
+
b.disabled = disabled;
|
|
30
|
+
b.onclick = action;
|
|
31
|
+
tools.append(b);
|
|
32
|
+
};
|
|
33
|
+
const changed = () => {
|
|
34
|
+
errors = [];
|
|
35
|
+
message = "";
|
|
36
|
+
anchor = null;
|
|
37
|
+
const won = shikakuFinished(game);
|
|
38
|
+
render();
|
|
39
|
+
options.onChange?.(copy());
|
|
40
|
+
root.dispatchEvent(new CustomEvent("shikaku-change", { detail: copy(), bubbles: true }));
|
|
41
|
+
if (won && !finished) {
|
|
42
|
+
options.onFinish?.(copy());
|
|
43
|
+
root.dispatchEvent(new CustomEvent("shikaku-finish", { detail: copy(), bubbles: true }));
|
|
44
|
+
}
|
|
45
|
+
finished = won;
|
|
46
|
+
};
|
|
47
|
+
const choose = (cell) => {
|
|
48
|
+
if (finished)
|
|
49
|
+
return;
|
|
50
|
+
selected = cell;
|
|
51
|
+
if (anchor === null) {
|
|
52
|
+
anchor = cell;
|
|
53
|
+
message = "";
|
|
54
|
+
render();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const a = anchor, width = game.board.width, ax = a % width, ay = Math.floor(a / width), x = cell % width, y = Math.floor(cell / width);
|
|
58
|
+
game = placeShikaku(game, { x: Math.min(ax, x), y: Math.min(ay, y), width: Math.abs(ax - x) + 1, height: Math.abs(ay - y) + 1 });
|
|
59
|
+
changed();
|
|
60
|
+
};
|
|
61
|
+
const remove = () => { game = removeShikaku(game, selected); changed(); };
|
|
62
|
+
const restart = () => { game = newShikaku(game.board); finished = false; changed(); };
|
|
63
|
+
const render = () => {
|
|
64
|
+
if (dead)
|
|
65
|
+
return;
|
|
66
|
+
const w = words(), b = game.board, focused = board.contains(doc.activeElement);
|
|
67
|
+
board.classList.toggle("ks-palette", (options.material ?? "ivory") === "ivory");
|
|
68
|
+
board.innerHTML = drawShikaku(b, { ...options, rectangles: game.rectangles, selected, anchor, errors });
|
|
69
|
+
const cells = doc.createElement("div");
|
|
70
|
+
cells.className = "ks-cells";
|
|
71
|
+
cells.style.gridTemplateColumns = `repeat(${b.width},1fr)`;
|
|
72
|
+
for (let c = 0; c < b.clues.length; c += 1) {
|
|
73
|
+
const cell = doc.createElement("button");
|
|
74
|
+
cell.type = "button";
|
|
75
|
+
cell.className = "ks-cell";
|
|
76
|
+
cell.dataset.cell = String(c);
|
|
77
|
+
cell.tabIndex = c === selected ? 0 : -1;
|
|
78
|
+
const inside = game.rectangles.find(r => shikakuCells(b, r).includes(c));
|
|
79
|
+
cell.setAttribute("aria-label", `${w.row} ${Math.floor(c / b.width) + 1}, ${w.column} ${c % b.width + 1}: ${b.clues[c] ? `${w.clue} ${b.clues[c]}` : w.blank}${inside ? `, ${w.covered}` : ""}`);
|
|
80
|
+
cell.onclick = () => choose(c);
|
|
81
|
+
cell.onkeydown = e => {
|
|
82
|
+
const steps = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -b.width, ArrowDown: b.width };
|
|
83
|
+
if (e.key in steps) {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
selected = Math.max(0, Math.min(b.clues.length - 1, c + steps[e.key]));
|
|
86
|
+
render();
|
|
87
|
+
}
|
|
88
|
+
else if (e.key === "Escape") {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
anchor = null;
|
|
91
|
+
render();
|
|
92
|
+
}
|
|
93
|
+
else if (e.key === "Delete" || e.key === "Backspace") {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
selected = c;
|
|
96
|
+
remove();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
cells.append(cell);
|
|
100
|
+
}
|
|
101
|
+
board.append(cells);
|
|
102
|
+
if (focused)
|
|
103
|
+
board.querySelector(`[data-cell="${selected}"]`)?.focus();
|
|
104
|
+
status.textContent = shikakuFinished(game) ? (game.helped ? w.helped : w.won) : message || (anchor === null ? w.ready : w.corner);
|
|
105
|
+
help.textContent = w.help;
|
|
106
|
+
tools.replaceChildren();
|
|
107
|
+
button(w.undo, () => { game = undoShikaku(game); finished = shikakuFinished(game); changed(); }, !game.history.length);
|
|
108
|
+
button(w.remove, remove, finished || !game.rectangles.some(r => shikakuCells(b, r).includes(selected)));
|
|
109
|
+
button(w.cancel, () => { anchor = null; message = ""; render(); }, anchor === null);
|
|
110
|
+
button(w.check, () => { errors = checkShikaku(b, game.rectangles).errors; message = errors.length ? w.wrong : w.checked; render(); });
|
|
111
|
+
button(w.hint, () => {
|
|
112
|
+
const r = hintShikaku(game);
|
|
113
|
+
if (!r) {
|
|
114
|
+
message = w.noHint;
|
|
115
|
+
render();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
game = { ...placeShikaku(game, r), helped: true };
|
|
119
|
+
changed();
|
|
120
|
+
message = w.hintWhy;
|
|
121
|
+
render();
|
|
122
|
+
}, finished);
|
|
123
|
+
button(w.restart, restart);
|
|
124
|
+
button(w.expand, () => {
|
|
125
|
+
if (root.parentElement !== host)
|
|
126
|
+
return;
|
|
127
|
+
const dialog = doc.createElement("dialog"), close = doc.createElement("button");
|
|
128
|
+
dialog.className = "ks-dialog";
|
|
129
|
+
close.className = "ks-close";
|
|
130
|
+
close.textContent = w.close;
|
|
131
|
+
close.onclick = () => dialog.close();
|
|
132
|
+
dialog.append(close, root);
|
|
133
|
+
host.append(dialog);
|
|
134
|
+
dialog.onclose = () => { host.append(root); dialog.remove(); };
|
|
135
|
+
dialog.showModal();
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
render();
|
|
139
|
+
return { game: copy, progress: () => encodeShikaku(game), restart,
|
|
140
|
+
set: (next) => { if (next.language && next.language !== options.language)
|
|
141
|
+
message = ""; options = { ...options, ...next }; render(); },
|
|
142
|
+
destroy: () => { dead = true; const dialog = root.closest("dialog"); root.remove(); dialog?.remove(); } };
|
|
143
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ShikakuBoard, ShikakuGame } from "./shikaku.types.ts";
|
|
2
|
+
export type ShikakuMaterial = "ivory" | "wood" | "slate";
|
|
3
|
+
export type ShikakuPieces = "ink" | "tiles";
|
|
4
|
+
export type ShikakuLanguage = "en" | "ja";
|
|
5
|
+
export type ShikakuDrawOptions = {
|
|
6
|
+
rectangles?: ShikakuGame["rectangles"];
|
|
7
|
+
selected?: number | null;
|
|
8
|
+
anchor?: number | null;
|
|
9
|
+
errors?: readonly number[];
|
|
10
|
+
material?: ShikakuMaterial;
|
|
11
|
+
pieces?: ShikakuPieces;
|
|
12
|
+
language?: ShikakuLanguage;
|
|
13
|
+
};
|
|
14
|
+
export type ShikakuMountOptions = ShikakuDrawOptions & {
|
|
15
|
+
board: ShikakuBoard;
|
|
16
|
+
progress?: string;
|
|
17
|
+
onChange?: (game: ShikakuGame) => void;
|
|
18
|
+
onFinish?: (game: ShikakuGame) => void;
|
|
19
|
+
};
|
|
20
|
+
export type ShikakuMount = {
|
|
21
|
+
game: () => ShikakuGame;
|
|
22
|
+
progress: () => string;
|
|
23
|
+
set: (options: ShikakuDrawOptions) => void;
|
|
24
|
+
restart: () => void;
|
|
25
|
+
destroy: () => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ShikakuBoard, ShikakuRectangle, ShikakuSolve } from "./shikaku.types.ts";
|
|
2
|
+
/** Exact cover over cells. A bounded search reports incompleteness instead of claiming uniqueness. */
|
|
3
|
+
export declare function solveShikaku(board: ShikakuBoard, placed?: readonly ShikakuRectangle[], options?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
nodes?: number;
|
|
6
|
+
}): ShikakuSolve;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SHIKAKU_MOST_NODES } from "./shikaku.constants.js";
|
|
2
|
+
import { checkShikaku, isShikakuBoard, shikakuCandidates, shikakuCells } from "./shikakuBoard.js";
|
|
3
|
+
/** Exact cover over cells. A bounded search reports incompleteness instead of claiming uniqueness. */
|
|
4
|
+
export function solveShikaku(board, placed = [], options = {}) {
|
|
5
|
+
if (!isShikakuBoard(board))
|
|
6
|
+
throw new RangeError("Invalid Shikaku board");
|
|
7
|
+
const limit = options.limit ?? 2, budget = options.nodes ?? SHIKAKU_MOST_NODES;
|
|
8
|
+
if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1)
|
|
9
|
+
throw new RangeError("Invalid search bounds");
|
|
10
|
+
const maskOf = (r) => shikakuCells(board, r).reduce((m, c) => m | 1n << BigInt(c), 0n);
|
|
11
|
+
if (checkShikaku(board, placed).errors.length)
|
|
12
|
+
return { count: 0, solution: null, complete: true, nodes: 0 };
|
|
13
|
+
const all = (1n << BigInt(board.clues.length)) - 1n;
|
|
14
|
+
let occupied = 0n;
|
|
15
|
+
for (const r of placed)
|
|
16
|
+
occupied |= maskOf(r);
|
|
17
|
+
const candidates = board.clues.flatMap((n, c) => n ? shikakuCandidates(board, c).map(r => ({ r, mask: maskOf(r) })) : []);
|
|
18
|
+
const byCell = board.clues.map((_, c) => candidates.filter(r => (r.mask & 1n << BigInt(c)) !== 0n));
|
|
19
|
+
let count = 0, nodes = 0, complete = true, solution = null;
|
|
20
|
+
const visit = (mask, rectangles) => {
|
|
21
|
+
if (++nodes > budget) {
|
|
22
|
+
complete = false;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (mask === all) {
|
|
26
|
+
count += 1;
|
|
27
|
+
solution ?? (solution = rectangles.map(r => ({ ...r })));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let choices = null;
|
|
31
|
+
for (let c = 0; c < board.clues.length; c += 1) {
|
|
32
|
+
if ((mask & 1n << BigInt(c)) !== 0n)
|
|
33
|
+
continue;
|
|
34
|
+
const available = byCell[c].filter(r => (mask & r.mask) === 0n);
|
|
35
|
+
if (!available.length)
|
|
36
|
+
return;
|
|
37
|
+
if (!choices || available.length < choices.length)
|
|
38
|
+
choices = available;
|
|
39
|
+
if (choices.length === 1)
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
for (const next of choices ?? []) {
|
|
43
|
+
visit(mask | next.mask, [...rectangles, next.r]);
|
|
44
|
+
if (!complete)
|
|
45
|
+
return;
|
|
46
|
+
if (count >= limit) {
|
|
47
|
+
complete = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
visit(occupied, placed);
|
|
53
|
+
return { count, solution, complete, nodes };
|
|
54
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ShikakuLanguage } from "./shikakuPlay.types.ts";
|
|
2
|
+
export declare const SHIKAKU_STRINGS: {
|
|
3
|
+
readonly en: {
|
|
4
|
+
readonly title: "Shikaku";
|
|
5
|
+
readonly help: "Choose two opposite corners to draw a rectangle. Each rectangle must contain one number equal to its area. Cover the whole board. Arrow keys move; Enter chooses a corner; Delete removes a rectangle; Escape cancels.";
|
|
6
|
+
readonly undo: "Undo";
|
|
7
|
+
readonly hint: "Hint";
|
|
8
|
+
readonly check: "Check";
|
|
9
|
+
readonly restart: "Restart";
|
|
10
|
+
readonly cancel: "Cancel";
|
|
11
|
+
readonly expand: "Just the board";
|
|
12
|
+
readonly close: "Close";
|
|
13
|
+
readonly ready: "Choose the first corner.";
|
|
14
|
+
readonly corner: "Choose the opposite corner.";
|
|
15
|
+
readonly won: "Every rectangle fits. Solved!";
|
|
16
|
+
readonly helped: "Every rectangle fits. Solved with help.";
|
|
17
|
+
readonly hintWhy: "This rectangle occurs in the only remaining complete partition.";
|
|
18
|
+
readonly noHint: "No proved hint: undo a conflicting rectangle or continue drawing.";
|
|
19
|
+
readonly checked: "All drawn rectangles follow the rules. Keep filling the board.";
|
|
20
|
+
readonly wrong: "Some rectangles need changing: each must hold one number equal to its area.";
|
|
21
|
+
readonly remove: "Remove selected rectangle";
|
|
22
|
+
readonly row: "Row";
|
|
23
|
+
readonly column: "column";
|
|
24
|
+
readonly clue: "area";
|
|
25
|
+
readonly blank: "no clue";
|
|
26
|
+
readonly covered: "in a rectangle";
|
|
27
|
+
};
|
|
28
|
+
readonly ja: {
|
|
29
|
+
readonly title: "四角に切れ";
|
|
30
|
+
readonly help: "向かい合う二つの角を選んで長方形を描きます。各長方形に数字を一つ入れ、その数字と面積を同じにします。盤全体を埋めましょう。矢印キーで移動、Enterで角を選び、Deleteで長方形を消し、Escapeで取り消します。";
|
|
31
|
+
readonly undo: "戻す";
|
|
32
|
+
readonly hint: "ヒント";
|
|
33
|
+
readonly check: "確かめる";
|
|
34
|
+
readonly restart: "やり直す";
|
|
35
|
+
readonly cancel: "取り消す";
|
|
36
|
+
readonly expand: "盤だけ";
|
|
37
|
+
readonly close: "閉じる";
|
|
38
|
+
readonly ready: "最初の角を選びます。";
|
|
39
|
+
readonly corner: "向かいの角を選びます。";
|
|
40
|
+
readonly won: "全部の長方形が合いました。完成!";
|
|
41
|
+
readonly helped: "ヒントを使って完成!";
|
|
42
|
+
readonly hintWhy: "残った唯一の答えに、この長方形があります。";
|
|
43
|
+
readonly noHint: "確かなヒントがありません。合わない長方形を戻すか、続けて描きましょう。";
|
|
44
|
+
readonly checked: "描いた長方形はルールに合っています。残りも埋めましょう。";
|
|
45
|
+
readonly wrong: "直す長方形があります。数字を一つ含み、その数字と面積を同じにします。";
|
|
46
|
+
readonly remove: "選んだ長方形を消す";
|
|
47
|
+
readonly row: "行";
|
|
48
|
+
readonly column: "列";
|
|
49
|
+
readonly clue: "面積";
|
|
50
|
+
readonly blank: "数字なし";
|
|
51
|
+
readonly covered: "長方形の中";
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export declare function shikakuWords(language?: ShikakuLanguage): {
|
|
55
|
+
readonly title: "Shikaku";
|
|
56
|
+
readonly help: "Choose two opposite corners to draw a rectangle. Each rectangle must contain one number equal to its area. Cover the whole board. Arrow keys move; Enter chooses a corner; Delete removes a rectangle; Escape cancels.";
|
|
57
|
+
readonly undo: "Undo";
|
|
58
|
+
readonly hint: "Hint";
|
|
59
|
+
readonly check: "Check";
|
|
60
|
+
readonly restart: "Restart";
|
|
61
|
+
readonly cancel: "Cancel";
|
|
62
|
+
readonly expand: "Just the board";
|
|
63
|
+
readonly close: "Close";
|
|
64
|
+
readonly ready: "Choose the first corner.";
|
|
65
|
+
readonly corner: "Choose the opposite corner.";
|
|
66
|
+
readonly won: "Every rectangle fits. Solved!";
|
|
67
|
+
readonly helped: "Every rectangle fits. Solved with help.";
|
|
68
|
+
readonly hintWhy: "This rectangle occurs in the only remaining complete partition.";
|
|
69
|
+
readonly noHint: "No proved hint: undo a conflicting rectangle or continue drawing.";
|
|
70
|
+
readonly checked: "All drawn rectangles follow the rules. Keep filling the board.";
|
|
71
|
+
readonly wrong: "Some rectangles need changing: each must hold one number equal to its area.";
|
|
72
|
+
readonly remove: "Remove selected rectangle";
|
|
73
|
+
readonly row: "Row";
|
|
74
|
+
readonly column: "column";
|
|
75
|
+
readonly clue: "area";
|
|
76
|
+
readonly blank: "no clue";
|
|
77
|
+
readonly covered: "in a rectangle";
|
|
78
|
+
} | {
|
|
79
|
+
readonly title: "四角に切れ";
|
|
80
|
+
readonly help: "向かい合う二つの角を選んで長方形を描きます。各長方形に数字を一つ入れ、その数字と面積を同じにします。盤全体を埋めましょう。矢印キーで移動、Enterで角を選び、Deleteで長方形を消し、Escapeで取り消します。";
|
|
81
|
+
readonly undo: "戻す";
|
|
82
|
+
readonly hint: "ヒント";
|
|
83
|
+
readonly check: "確かめる";
|
|
84
|
+
readonly restart: "やり直す";
|
|
85
|
+
readonly cancel: "取り消す";
|
|
86
|
+
readonly expand: "盤だけ";
|
|
87
|
+
readonly close: "閉じる";
|
|
88
|
+
readonly ready: "最初の角を選びます。";
|
|
89
|
+
readonly corner: "向かいの角を選びます。";
|
|
90
|
+
readonly won: "全部の長方形が合いました。完成!";
|
|
91
|
+
readonly helped: "ヒントを使って完成!";
|
|
92
|
+
readonly hintWhy: "残った唯一の答えに、この長方形があります。";
|
|
93
|
+
readonly noHint: "確かなヒントがありません。合わない長方形を戻すか、続けて描きましょう。";
|
|
94
|
+
readonly checked: "描いた長方形はルールに合っています。残りも埋めましょう。";
|
|
95
|
+
readonly wrong: "直す長方形があります。数字を一つ含み、その数字と面積を同じにします。";
|
|
96
|
+
readonly remove: "選んだ長方形を消す";
|
|
97
|
+
readonly row: "行";
|
|
98
|
+
readonly column: "列";
|
|
99
|
+
readonly clue: "面積";
|
|
100
|
+
readonly blank: "数字なし";
|
|
101
|
+
readonly covered: "長方形の中";
|
|
102
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const SHIKAKU_STRINGS = {
|
|
2
|
+
en: { title: "Shikaku", help: "Choose two opposite corners to draw a rectangle. Each rectangle must contain one number equal to its area. Cover the whole board. Arrow keys move; Enter chooses a corner; Delete removes a rectangle; Escape cancels.", undo: "Undo", hint: "Hint", check: "Check", restart: "Restart", cancel: "Cancel", expand: "Just the board", close: "Close", ready: "Choose the first corner.", corner: "Choose the opposite corner.", won: "Every rectangle fits. Solved!", helped: "Every rectangle fits. Solved with help.", hintWhy: "This rectangle occurs in the only remaining complete partition.", noHint: "No proved hint: undo a conflicting rectangle or continue drawing.", checked: "All drawn rectangles follow the rules. Keep filling the board.", wrong: "Some rectangles need changing: each must hold one number equal to its area.", remove: "Remove selected rectangle", row: "Row", column: "column", clue: "area", blank: "no clue", covered: "in a rectangle" },
|
|
3
|
+
ja: { title: "四角に切れ", help: "向かい合う二つの角を選んで長方形を描きます。各長方形に数字を一つ入れ、その数字と面積を同じにします。盤全体を埋めましょう。矢印キーで移動、Enterで角を選び、Deleteで長方形を消し、Escapeで取り消します。", undo: "戻す", hint: "ヒント", check: "確かめる", restart: "やり直す", cancel: "取り消す", expand: "盤だけ", close: "閉じる", ready: "最初の角を選びます。", corner: "向かいの角を選びます。", won: "全部の長方形が合いました。完成!", helped: "ヒントを使って完成!", hintWhy: "残った唯一の答えに、この長方形があります。", noHint: "確かなヒントがありません。合わない長方形を戻すか、続けて描きましょう。", checked: "描いた長方形はルールに合っています。残りも埋めましょう。", wrong: "直す長方形があります。数字を一つ含み、その数字と面積を同じにします。", remove: "選んだ長方形を消す", row: "行", column: "列", clue: "面積", blank: "数字なし", covered: "長方形の中" }
|
|
4
|
+
};
|
|
5
|
+
export function shikakuWords(language = "en") { return SHIKAKU_STRINGS[language]; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SHIKAKU_PLAY_STYLE = "\n.ks-root{color:var(--ks-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto}\n.ks-board{position:relative;width:100%;min-width:240px;touch-action:manipulation}\n.ks-cells{position:absolute;inset:0;display:grid;padding:2px}\n.ks-cell{background:transparent;border:0;min-width:0;min-height:0;padding:0;cursor:crosshair;touch-action:manipulation}\n.ks-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px;border-radius:4px}\n.ks-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}\n.ks-tools button,.ks-close{font:inherit;border:1px solid var(--ks-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--ks-surface,#fbf8f1);color:var(--ks-ink,#1f2320);cursor:pointer}\n.ks-tools button:disabled{opacity:.45;cursor:default}.ks-status{min-height:1.5em}.ks-help{font-size:14px;opacity:.8}\n.ks-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,760px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--ks-ink:var(--ink,#1f2320);--ks-surface:var(--surface,#fbf8f1);--ks-rule:var(--rule,#ddd6c6)}.ks-dialog::backdrop{background:#172c28bb}\n.ks-close{float:right;margin-bottom:10px}.ks-dialog .ks-root{clear:both}\n";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const SHIKAKU_PLAY_STYLE = `
|
|
2
|
+
.ks-root{color:var(--ks-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:680px;margin:auto}
|
|
3
|
+
.ks-board{position:relative;width:100%;min-width:240px;touch-action:manipulation}
|
|
4
|
+
.ks-cells{position:absolute;inset:0;display:grid;padding:2px}
|
|
5
|
+
.ks-cell{background:transparent;border:0;min-width:0;min-height:0;padding:0;cursor:crosshair;touch-action:manipulation}
|
|
6
|
+
.ks-cell:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-4px;border-radius:4px}
|
|
7
|
+
.ks-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}
|
|
8
|
+
.ks-tools button,.ks-close{font:inherit;border:1px solid var(--ks-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--ks-surface,#fbf8f1);color:var(--ks-ink,#1f2320);cursor:pointer}
|
|
9
|
+
.ks-tools button:disabled{opacity:.45;cursor:default}.ks-status{min-height:1.5em}.ks-help{font-size:14px;opacity:.8}
|
|
10
|
+
.ks-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,760px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--ks-ink:var(--ink,#1f2320);--ks-surface:var(--surface,#fbf8f1);--ks-rule:var(--rule,#ddd6c6)}.ks-dialog::backdrop{background:#172c28bb}
|
|
11
|
+
.ks-close{float:right;margin-bottom:10px}.ks-dialog .ks-root{clear:both}
|
|
12
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { generateShikaku } from "./shikakuGenerate.js";
|
|
2
|
+
self.onmessage = (event) => {
|
|
3
|
+
try {
|
|
4
|
+
const { width, height, level, seed } = event.data;
|
|
5
|
+
self.postMessage({ puzzle: generateShikaku(width, height, level, seed) });
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
self.postMessage({ error: error instanceof Error ? error.message : String(error) });
|
|
9
|
+
}
|
|
10
|
+
};
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export declare const VERSION = "1.
|
|
2
|
+
export declare const VERSION = "1.1.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version. */
|
|
2
|
-
export const VERSION = "1.
|
|
2
|
+
export const VERSION = "1.1.0";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kazu",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Kazu
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Kazu \u6570, grid number puzzles for JavaScript and TypeScript: Sudoku (4\u00d74 to a 16\u00d716 Giant), Jigsaw, Diagonal and Killer Sudoku, Futoshiki, Skyscrapers and Shikaku. Shikaku uses a dedicated rectangle model, exact-cover solver and player. A seeded generator whose every puzzle has exactly one answer, at three levels; a solver that counts answers; a check that reads a finished grid in O(cells); a hint that says which cell to fill next and why; puzzles as short codes; the grid drawn as SVG; and played by touch, mouse and keyboard in any page, with pencil marks, undo and a clock, as a function call or a tag. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"puzzle",
|
|
7
7
|
"sudoku",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"zero-dependencies",
|
|
24
24
|
"browser",
|
|
25
25
|
"node",
|
|
26
|
-
"itsutsu"
|
|
26
|
+
"itsutsu",
|
|
27
|
+
"shikaku"
|
|
27
28
|
],
|
|
28
29
|
"homepage": "https://github.com/johnmorrisdotca/kazu#readme",
|
|
29
30
|
"repository": {
|
|
@@ -67,6 +68,21 @@
|
|
|
67
68
|
"types": "./dist/element-define.d.ts",
|
|
68
69
|
"import": "./dist/element-define.js",
|
|
69
70
|
"default": "./dist/element-define.js"
|
|
71
|
+
},
|
|
72
|
+
"./shikaku": {
|
|
73
|
+
"types": "./dist/shikaku-entry.d.ts",
|
|
74
|
+
"import": "./dist/shikaku-entry.js",
|
|
75
|
+
"default": "./dist/shikaku-entry.js"
|
|
76
|
+
},
|
|
77
|
+
"./shikaku/play": {
|
|
78
|
+
"types": "./dist/shikaku-play-entry.d.ts",
|
|
79
|
+
"import": "./dist/shikaku-play-entry.js",
|
|
80
|
+
"default": "./dist/shikaku-play-entry.js"
|
|
81
|
+
},
|
|
82
|
+
"./shikaku/draw": {
|
|
83
|
+
"types": "./dist/shikaku-draw-entry.d.ts",
|
|
84
|
+
"import": "./dist/shikaku-draw-entry.js",
|
|
85
|
+
"default": "./dist/shikaku-draw-entry.js"
|
|
70
86
|
}
|
|
71
87
|
},
|
|
72
88
|
"files": [
|