@johnmorrisdotca/kazu 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +69 -27
- package/dist/akari-entry.d.ts +1 -0
- package/dist/akari-entry.js +1 -0
- package/dist/akari.constants.d.ts +12 -0
- package/dist/akari.constants.js +12 -0
- package/dist/akari.types.d.ts +19 -0
- package/dist/akariGenerate.d.ts +10 -5
- package/dist/akariGenerate.js +101 -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/cells.d.ts +4 -4
- package/dist/cells.js +6 -6
- 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/groupSolve.d.ts +9 -0
- package/dist/groupSolve.js +66 -0
- 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/kinds.js +1 -1
- package/dist/layout.js +1 -0
- package/dist/mount.d.ts +0 -18
- package/dist/mount.js +24 -3
- package/dist/names.js +3 -3
- package/dist/numberPlace.d.ts +1 -1
- package/dist/numberPlace.js +21 -8
- 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/strings.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/mount.d.ts
CHANGED
|
@@ -2,24 +2,6 @@ import { type KazuGame } from "./game.ts";
|
|
|
2
2
|
import { type KazuHint } from "./hint.ts";
|
|
3
3
|
import type { KazuKind, KazuLevel } from "./kinds.ts";
|
|
4
4
|
import { type KazuLanguage } from "./strings.ts";
|
|
5
|
-
/**
|
|
6
|
-
* A PLAYABLE KAZU BOARD IN ANY PAGE: `mountKazu(host, options)` draws a puzzle into an element and plays
|
|
7
|
-
* it by touch, mouse and keyboard. Tap a cell and tap a number on the pad (or type it); tap the chosen
|
|
8
|
-
* cell again to step it on, 1, 2, 3 … and back to empty; turn Pencil on and the pad writes small notes
|
|
9
|
-
* instead; Undo takes the last change back; Hint says which cell to fill next and why; Check says how
|
|
10
|
-
* many are wrong, never which. A clock starts on the first entry and stops when the last cell is right.
|
|
11
|
-
*
|
|
12
|
-
* The keys: the arrows move, a number (1 to 9, and A to G on the 16×16) fills the chosen cell, Shift
|
|
13
|
-
* with a number writes it as a pencil mark, Backspace empties the cell, N turns Pencil on or off,
|
|
14
|
-
* Ctrl or Cmd with Z undoes, and Escape lets the cell go.
|
|
15
|
-
*
|
|
16
|
-
* What happens is told in events, on the host as DOM events and to the callbacks given: `kazu-change`
|
|
17
|
-
* for every change to the grid, `kazu-hint` for each hint, `kazu-check` for each check, and `kazu-solve`
|
|
18
|
-
* once, with the answer ready for `checkKazu`. Every button is also a method of the returned handle.
|
|
19
|
-
* The rules it plays by are `game.ts`'s, and the drawing is `drawKazu`'s: both are usable alone.
|
|
20
|
-
*
|
|
21
|
-
* Needs a page. Its words are English and Japanese and follow the page's `lang`.
|
|
22
|
-
*/
|
|
23
5
|
/** What every event tells of the board. */
|
|
24
6
|
export type KazuEventDetail = {
|
|
25
7
|
kind: KazuKind;
|
package/dist/mount.js
CHANGED
|
@@ -7,6 +7,27 @@ import { KAZU_PLAY_STYLE } from "./playStyle.js";
|
|
|
7
7
|
import { decodeNotes, decodeRun, encodeNotes, encodeRun, notesOf } from "./progress.js";
|
|
8
8
|
import { solveKazu } from "./solve.js";
|
|
9
9
|
import { kazuLanguageOf, kazuNameOf, kazuSay } from "./strings.js";
|
|
10
|
+
/**
|
|
11
|
+
* A PLAYABLE KAZU BOARD IN ANY PAGE: `mountKazu(host, options)` draws a puzzle into an element and plays
|
|
12
|
+
* it by touch, mouse and keyboard. Tap a cell and tap a number on the pad (or type it); tap the chosen
|
|
13
|
+
* cell again to step it on, 1, 2, 3 … and back to empty; turn Pencil on and the pad writes small notes
|
|
14
|
+
* instead; Undo takes the last change back; Hint says which cell to fill next and why; Check says how
|
|
15
|
+
* many are wrong, never which. A clock starts on the first entry and stops when the last cell is right.
|
|
16
|
+
*
|
|
17
|
+
* The keys: the arrows move, a number (1 to 9, then A to G on the 16×16 and on to P on the 25×25) fills
|
|
18
|
+
* the chosen cell, Shift with a number writes it as a pencil mark, Backspace empties the cell, N turns
|
|
19
|
+
* Pencil on or off (the slash key on the 25×25, where N is the number 23), Ctrl or Cmd with Z undoes,
|
|
20
|
+
* and Escape lets the cell go.
|
|
21
|
+
*
|
|
22
|
+
* What happens is told in events, on the host as DOM events and to the callbacks given: `kazu-change`
|
|
23
|
+
* for every change to the grid, `kazu-hint` for each hint, `kazu-check` for each check, and `kazu-solve`
|
|
24
|
+
* once, with the answer ready for `checkKazu`. Every button is also a method of the returned handle.
|
|
25
|
+
* The rules it plays by are `game.ts`'s, and the drawing is `drawKazu`'s: both are usable alone.
|
|
26
|
+
*
|
|
27
|
+
* Needs a page. Its words are English and Japanese and follow the page's `lang`.
|
|
28
|
+
*/
|
|
29
|
+
/** From this side up the letter N is a number (A is 10, so N is 23), and Pencil moves to the slash key. */
|
|
30
|
+
const NOTES_KEY_IS_A_NUMBER = 23;
|
|
10
31
|
/** Put the style in the page once: in the document's head, or in the shadow root the host is in. */
|
|
11
32
|
export function ensureKazuPlayStyle(host) {
|
|
12
33
|
const root = host.getRootNode();
|
|
@@ -210,7 +231,7 @@ export function mountKazu(host, options) {
|
|
|
210
231
|
host.dataset.kind = game.kind;
|
|
211
232
|
host.dataset.size = String(game.size);
|
|
212
233
|
box.setAttribute("aria-label", say("board", { name: kazuNameOf(game.kind, language), size: game.size }));
|
|
213
|
-
keysNote.textContent = say("keys");
|
|
234
|
+
keysNote.textContent = say(game.size >= NOTES_KEY_IS_A_NUMBER ? "keysColossus" : "keys");
|
|
214
235
|
const counts = numberCounts(game);
|
|
215
236
|
padKeys.forEach((key, at) => {
|
|
216
237
|
const value = at + 1;
|
|
@@ -328,13 +349,13 @@ export function mountKazu(host, options) {
|
|
|
328
349
|
event.preventDefault();
|
|
329
350
|
api.enter(0);
|
|
330
351
|
}
|
|
331
|
-
else if (event.key.toLowerCase() === "n") {
|
|
352
|
+
else if (event.key.toLowerCase() === (game.size >= NOTES_KEY_IS_A_NUMBER ? "/" : "n")) {
|
|
332
353
|
event.preventDefault();
|
|
333
354
|
api.pencil();
|
|
334
355
|
}
|
|
335
356
|
else if (event.shiftKey && selected !== null) {
|
|
336
357
|
// Shift with a number is a pencil mark, whatever the keyboard writes for a shifted digit: read the key's place, not its symbol.
|
|
337
|
-
const code = /^Digit([1-9])$/.exec(event.code)?.[1] ?? /^Key([A-
|
|
358
|
+
const code = /^Digit([1-9])$/.exec(event.code)?.[1] ?? /^Key([A-P])$/.exec(event.code)?.[1];
|
|
338
359
|
const value = code === undefined ? 0 : valueOfSymbol(code);
|
|
339
360
|
if (value >= 1 && value <= game.size) {
|
|
340
361
|
event.preventDefault();
|
package/dist/names.js
CHANGED
|
@@ -11,13 +11,13 @@ export const KAZU_NAMES = {
|
|
|
11
11
|
en: [
|
|
12
12
|
"Fill every empty cell with a number from 1 up to the side of the grid, so that each row, each column and each box holds every number exactly once.",
|
|
13
13
|
"The numbers already printed are the givens. They stay where they are, and every puzzle has exactly one answer that fits them.",
|
|
14
|
-
"The 16×16 Giant has sixteen symbols: 1 to 9, then A to G for 10 to 16.",
|
|
14
|
+
"The 16×16 Giant has sixteen symbols: 1 to 9, then A to G for 10 to 16. The 25×25 Colossus has twenty-five: A to P for 10 to 25.",
|
|
15
15
|
"Easy yields to reasoning alone: every cell can be found from what is already there. Medium and hard ask you to try something and see.",
|
|
16
16
|
],
|
|
17
17
|
ja: [
|
|
18
18
|
"空いているマスに、1から盤の一辺の数までの数字を入れます。どの行、列、ブロックにも、同じ数字が一つずつ入ります。",
|
|
19
19
|
"最初から書かれている数字はそのままです。どの問題も、答えはちょうど一つだけです。",
|
|
20
|
-
"16×16の「特大」は、1から9までの数字に、10から16までを表すAからGを足した16種類を使います。",
|
|
20
|
+
"16×16の「特大」は、1から9までの数字に、10から16までを表すAからGを足した16種類を使います。25×25の「巨大」は、10から25までを表すAからPまでを使う25種類です。",
|
|
21
21
|
"やさしい問題は、推理だけで全部のマスが決まります。ふつうとむずかしい問題では、試してみる場面があります。",
|
|
22
22
|
],
|
|
23
23
|
},
|
|
@@ -154,7 +154,7 @@ export const KAZU_NAMES = {
|
|
|
154
154
|
};
|
|
155
155
|
/** What each side is for, under its size on a chooser: the quick one, the usual one, the long one. */
|
|
156
156
|
export const KAZU_SIZE_NAMES = {
|
|
157
|
-
"number-place": { 4: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" }, 16: { en: "Giant", ja: "特大" } },
|
|
157
|
+
"number-place": { 4: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" }, 16: { en: "Giant", ja: "特大" }, 25: { en: "Colossus", ja: "巨大" } },
|
|
158
158
|
jigsaw: { 5: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 7: { en: "Standard", ja: "定番" }, 9: { en: "Classic", ja: "本格" } },
|
|
159
159
|
diagonal: { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
|
|
160
160
|
"sum-cages": { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
|
package/dist/numberPlace.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { type Random } from "./random.ts";
|
|
|
8
8
|
* puzzle on a layout (Sudoku, Diagonal and Jigsaw).
|
|
9
9
|
*/
|
|
10
10
|
export declare function carve(solution: Grid, layout: Layout, level: KazuLevel, floor: number, random: Random): Grid;
|
|
11
|
-
/** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9 or
|
|
11
|
+
/** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9, 16 or 25. */
|
|
12
12
|
export declare function generateNumberPlace(size: number, level: KazuLevel, seed: number): KazuPuzzle;
|
|
13
13
|
/** A Diagonal Sudoku (Sudoku X): Sudoku with the two long diagonals as groups too. 6 or 9. */
|
|
14
14
|
export declare function generateDiagonal(size: number, level: KazuLevel, seed: number): KazuPuzzle;
|
package/dist/numberPlace.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encodeCells } from "./cells.js";
|
|
2
|
-
import { countSolutions, guessDepth } from "./groupSolve.js";
|
|
2
|
+
import { countSolutions, guessDepth, provedByGuessing } from "./groupSolve.js";
|
|
3
3
|
import { boxedLayout } from "./layout.js";
|
|
4
4
|
import { seededRandom, shuffled } from "./random.js";
|
|
5
5
|
/**
|
|
@@ -22,10 +22,21 @@ import { seededRandom, shuffled } from "./random.js";
|
|
|
22
22
|
* hard puzzle would be a medium one with a different label.
|
|
23
23
|
*/
|
|
24
24
|
const LEVELS = {
|
|
25
|
-
easy: { depth: 0, floor: { 4: 9, 6: 20, 9: 40, 16: 150 } },
|
|
26
|
-
medium: { depth: 1, floor: { 4: 7, 6: 15, 9: 31, 16: 125 } },
|
|
27
|
-
hard: { depth: Infinity, floor: { 4: 5, 6: 11, 9: 24, 16: 116 } },
|
|
25
|
+
easy: { depth: 0, floor: { 4: 9, 6: 20, 9: 40, 16: 150, 25: 366 } },
|
|
26
|
+
medium: { depth: 1, floor: { 4: 7, 6: 15, 9: 31, 16: 125, 25: 305 } },
|
|
27
|
+
hard: { depth: Infinity, floor: { 4: 5, 6: 11, 9: 24, 16: 116, 25: 283 } },
|
|
28
28
|
};
|
|
29
|
+
/** Past this side the exact count of answers and the exact depth are not asked, only a proof (see `LARGE_GUESSES`). */
|
|
30
|
+
const LARGEST_COUNTED = 16;
|
|
31
|
+
/**
|
|
32
|
+
* The 25×25 is carved by proof, not by counting. Counting every answer of a sparse 625-cell grid can run for
|
|
33
|
+
* minutes (a single removal took 20 s), and the exact guess depth of one is as bad. What a person does is
|
|
34
|
+
* what is asked instead: singles, then at most this many guesses at the most constrained cell, each value
|
|
35
|
+
* of it either finished by singles or shown wrong by them. A puzzle that passes has exactly one answer, and
|
|
36
|
+
* the work for each removal is bounded however the grid looks. Hard allows two nested guesses and no more,
|
|
37
|
+
* so a 25×25 hard puzzle is one a person can finish, and easy is singles alone, as it is at every size.
|
|
38
|
+
*/
|
|
39
|
+
const LARGE_GUESSES = { easy: 0, medium: 1, hard: 2 };
|
|
29
40
|
/**
|
|
30
41
|
* A whole grid, filled cell by cell in reading order with the values tried in a seeded order. The
|
|
31
42
|
* groups come from the layout, so the diagonals are honoured the same way.
|
|
@@ -65,7 +76,7 @@ function fillInOrder(layout, random) {
|
|
|
65
76
|
* into a dead end deep in a 256-cell grid and take seconds to climb out. A grid that is right by
|
|
66
77
|
* construction (each row the one above it shifted a box's width, each band shifted by one), then
|
|
67
78
|
* shuffled in every way that keeps it right (the numbers relabelled, rows within a band, the
|
|
68
|
-
* bands, columns within a stack, the stacks), is as varied and costs nothing. Only 16×16
|
|
79
|
+
* bands, columns within a stack, the stacks), is as varied and costs nothing. Only 16×16 and 25×25 are made
|
|
69
80
|
* this way, so every smaller grid comes out of its seed exactly as it always has.
|
|
70
81
|
*/
|
|
71
82
|
function fillByPattern(size, random) {
|
|
@@ -91,7 +102,9 @@ export function carve(solution, layout, level, floor, random) {
|
|
|
91
102
|
break;
|
|
92
103
|
const value = givens[index];
|
|
93
104
|
givens[index] = 0;
|
|
94
|
-
const stillOne =
|
|
105
|
+
const stillOne = layout.size > LARGEST_COUNTED
|
|
106
|
+
? provedByGuessing(givens, layout, LARGE_GUESSES[level]) === 1
|
|
107
|
+
: countSolutions(givens, layout, 2) === 1 && guessDepth(givens, layout) <= depth;
|
|
95
108
|
if (stillOne)
|
|
96
109
|
left -= 1;
|
|
97
110
|
else
|
|
@@ -99,11 +112,11 @@ export function carve(solution, layout, level, floor, random) {
|
|
|
99
112
|
}
|
|
100
113
|
return givens;
|
|
101
114
|
}
|
|
102
|
-
/** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9 or
|
|
115
|
+
/** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9, 16 or 25. */
|
|
103
116
|
export function generateNumberPlace(size, level, seed) {
|
|
104
117
|
const random = seededRandom(seed);
|
|
105
118
|
const layout = boxedLayout(size);
|
|
106
|
-
const solution = size
|
|
119
|
+
const solution = size >= 16 ? fillByPattern(size, random) : fillInOrder(layout, random);
|
|
107
120
|
const givens = carve(solution, layout, level, LEVELS[level].floor[size], random);
|
|
108
121
|
return { kind: "number-place", size, level, seed, givens: encodeCells(givens), solution: encodeCells(solution) };
|
|
109
122
|
}
|
package/dist/shikaku-entry.d.ts
CHANGED
package/dist/shikaku-entry.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Bounds keep custom boards and searches finite. */
|
|
2
2
|
export declare const SHIKAKU_MOST_SIDE = 16;
|
|
3
3
|
export declare const SHIKAKU_MOST_NODES = 100000;
|
|
4
|
-
export declare const SHIKAKU_MOST_ATTEMPTS =
|
|
5
|
-
|
|
4
|
+
export declare const SHIKAKU_MOST_ATTEMPTS = 1500;
|
|
5
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateShikaku`. */
|
|
6
|
+
export declare const SHIKAKU_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
|
|
7
|
+
/** The sides on offer for a square board; any side from 2 to `SHIKAKU_MOST_SIDE` can be made. */
|
|
8
|
+
export declare const SHIKAKU_SIZES: readonly [5, 7, 10, 14];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Bounds keep custom boards and searches finite. */
|
|
2
2
|
export const SHIKAKU_MOST_SIDE = 16;
|
|
3
3
|
export const SHIKAKU_MOST_NODES = 100000;
|
|
4
|
-
export const SHIKAKU_MOST_ATTEMPTS =
|
|
5
|
-
|
|
4
|
+
export const SHIKAKU_MOST_ATTEMPTS = 1500;
|
|
5
|
+
/** How hard a puzzle is made, by what a person must do to solve it; see `rateShikaku`. */
|
|
6
|
+
export const SHIKAKU_LEVELS = ["easy", "medium", "hard", "extra-hard"];
|
|
7
|
+
/** The sides on offer for a square board; any side from 2 to `SHIKAKU_MOST_SIDE` can be made. */
|
|
8
|
+
export const SHIKAKU_SIZES = [5, 7, 10, 14];
|
package/dist/shikaku.types.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type ShikakuBoard = {
|
|
|
11
11
|
height: number;
|
|
12
12
|
clues: readonly number[];
|
|
13
13
|
};
|
|
14
|
-
export type ShikakuLevel = "easy" | "medium" | "hard";
|
|
14
|
+
export type ShikakuLevel = "easy" | "medium" | "hard" | "extra-hard";
|
|
15
15
|
export type ShikakuPuzzle = ShikakuBoard & {
|
|
16
16
|
seed: number;
|
|
17
17
|
level: ShikakuLevel;
|
|
@@ -34,3 +34,20 @@ export type ShikakuGame = {
|
|
|
34
34
|
history: readonly (readonly ShikakuRectangle[])[];
|
|
35
35
|
helped: boolean;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* How hard a board is, measured by solving it. `depth` 0 means the rules solve it, 1 that somebody has to suppose a
|
|
39
|
+
* rectangle and watch it break, 2 that more than that is needed. `rules` is how many of the three rules a depth-0
|
|
40
|
+
* solve needed: 1 for a number with one fitting rectangle (a settled rectangle clears its squares), 2 adding that a
|
|
41
|
+
* square only one rectangle can cover is covered by it, 3 adding that a square only one number can reach makes that
|
|
42
|
+
* number's rectangle cover it. `probes` is the suppositions depth 1 needed.
|
|
43
|
+
*/
|
|
44
|
+
export type ShikakuRating = {
|
|
45
|
+
depth: 0 | 1 | 2;
|
|
46
|
+
rules: 1 | 2 | 3;
|
|
47
|
+
probes: number;
|
|
48
|
+
rectangles: number;
|
|
49
|
+
meanArea: number;
|
|
50
|
+
largest: number;
|
|
51
|
+
/** Mean number of rectangles a number could be at the start. */
|
|
52
|
+
ambiguity: number;
|
|
53
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ShikakuBoard, ShikakuLevel, ShikakuRectangle } from "./shikaku.types.ts";
|
|
2
|
+
import type { Random } from "./random.ts";
|
|
3
|
+
/** The smallest and largest rectangle a level cuts the board into. */
|
|
4
|
+
export declare const SHIKAKU_AREAS: Record<ShikakuLevel, {
|
|
5
|
+
least: number;
|
|
6
|
+
most: number;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Cuts a board into rectangles by packing: the square with the fewest free neighbours is covered next, by a random
|
|
10
|
+
* rectangle of a random allowed area that fits the free squares round it. Unlike cutting the board in two over and
|
|
11
|
+
* over, this makes pinwheels and rectangles that interlock.
|
|
12
|
+
*/
|
|
13
|
+
export declare function packRectangles(width: number, height: number, level: ShikakuLevel, random: Random): ShikakuRectangle[];
|
|
14
|
+
/**
|
|
15
|
+
* A board with exactly one answer, or null. Each rectangle's number sits in a random square of it; while there is
|
|
16
|
+
* another answer, the numbers of the rectangles the two answers disagree on move to new squares.
|
|
17
|
+
*/
|
|
18
|
+
export declare function candidateShikaku(width: number, height: number, level: ShikakuLevel, random: Random): {
|
|
19
|
+
board: ShikakuBoard;
|
|
20
|
+
solution: readonly ShikakuRectangle[];
|
|
21
|
+
} | null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { shikakuModel } from "./shikakuLogic.js";
|
|
2
|
+
import { countCsp, openSlots } from "./csp.js";
|
|
3
|
+
import { shuffled } from "./random.js";
|
|
4
|
+
/** The smallest and largest rectangle a level cuts the board into. */
|
|
5
|
+
export const SHIKAKU_AREAS = {
|
|
6
|
+
easy: { least: 2, most: 6 },
|
|
7
|
+
medium: { least: 3, most: 12 },
|
|
8
|
+
hard: { least: 3, most: 9 },
|
|
9
|
+
"extra-hard": { least: 4, most: 9 },
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Cuts a board into rectangles by packing: the square with the fewest free neighbours is covered next, by a random
|
|
13
|
+
* rectangle of a random allowed area that fits the free squares round it. Unlike cutting the board in two over and
|
|
14
|
+
* over, this makes pinwheels and rectangles that interlock.
|
|
15
|
+
*/
|
|
16
|
+
export function packRectangles(width, height, level, random) {
|
|
17
|
+
const free = new Uint8Array(width * height).fill(1);
|
|
18
|
+
const { least, most: wanted } = SHIKAKU_AREAS[level];
|
|
19
|
+
const most = Math.max(least, Math.min(wanted, Math.round(width * height * .3)));
|
|
20
|
+
const freeAt = (x, y) => x >= 0 && y >= 0 && x < width && y < height && free[y * width + x] === 1;
|
|
21
|
+
const rectangles = [];
|
|
22
|
+
for (;;) {
|
|
23
|
+
let anchor = -1, fewest = 9, ties = 0;
|
|
24
|
+
for (let cell = 0; cell < free.length; cell += 1) {
|
|
25
|
+
if (!free[cell])
|
|
26
|
+
continue;
|
|
27
|
+
const x = cell % width, y = Math.floor(cell / width);
|
|
28
|
+
const around = +freeAt(x - 1, y) + +freeAt(x + 1, y) + +freeAt(x, y - 1) + +freeAt(x, y + 1);
|
|
29
|
+
if (around < fewest) {
|
|
30
|
+
fewest = around;
|
|
31
|
+
anchor = cell;
|
|
32
|
+
ties = 1;
|
|
33
|
+
}
|
|
34
|
+
else if (around === fewest && random() * ++ties < 1)
|
|
35
|
+
anchor = cell;
|
|
36
|
+
}
|
|
37
|
+
if (anchor < 0)
|
|
38
|
+
return rectangles;
|
|
39
|
+
const ax = anchor % width, ay = Math.floor(anchor / width);
|
|
40
|
+
const fits = [];
|
|
41
|
+
for (let w = 1; w <= Math.min(width, most); w += 1)
|
|
42
|
+
for (let h = 1; w * h <= most; h += 1) {
|
|
43
|
+
for (let ox = 0; ox < w; ox += 1)
|
|
44
|
+
for (let oy = 0; oy < h; oy += 1) {
|
|
45
|
+
const x = ax - ox, y = ay - oy;
|
|
46
|
+
let all = x >= 0 && y >= 0 && x + w <= width && y + h <= height;
|
|
47
|
+
for (let k = 0; all && k < w * h; k += 1)
|
|
48
|
+
if (!free[(y + Math.floor(k / w)) * width + x + k % w])
|
|
49
|
+
all = false;
|
|
50
|
+
if (all)
|
|
51
|
+
fits.push({ x, y, width: w, height: h });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Prefer rectangles that leave no square with nowhere to go, then rectangles of a decent size.
|
|
55
|
+
const leavesIsland = (r) => {
|
|
56
|
+
const inside = (x, y) => x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height;
|
|
57
|
+
for (let x = r.x - 1; x <= r.x + r.width; x += 1)
|
|
58
|
+
for (let y = r.y - 1; y <= r.y + r.height; y += 1) {
|
|
59
|
+
if (inside(x, y) || !freeAt(x, y))
|
|
60
|
+
continue;
|
|
61
|
+
const exits = [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]].filter(([nx, ny]) => freeAt(nx, ny) && !inside(nx, ny)).length;
|
|
62
|
+
if (!exits)
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
const sound = fits.filter(r => !leavesIsland(r));
|
|
68
|
+
const roomy = (sound.length ? sound : fits).filter(r => r.width * r.height >= least);
|
|
69
|
+
const pool = roomy.length ? roomy : sound.length ? sound : fits;
|
|
70
|
+
const areas = [...new Set(pool.map(r => r.width * r.height))];
|
|
71
|
+
const area = areas[Math.floor(random() * areas.length)];
|
|
72
|
+
const same = pool.filter(r => r.width * r.height === area);
|
|
73
|
+
const pick = same[Math.floor(random() * same.length)];
|
|
74
|
+
rectangles.push(pick);
|
|
75
|
+
for (let k = 0; k < pick.width * pick.height; k += 1)
|
|
76
|
+
free[(pick.y + Math.floor(k / pick.width)) * width + pick.x + k % pick.width] = 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A board with exactly one answer, or null. Each rectangle's number sits in a random square of it; while there is
|
|
81
|
+
* another answer, the numbers of the rectangles the two answers disagree on move to new squares.
|
|
82
|
+
*/
|
|
83
|
+
export function candidateShikaku(width, height, level, random) {
|
|
84
|
+
const solution = packRectangles(width, height, level, random);
|
|
85
|
+
if (solution.length < 2 || solution.some(r => r.width * r.height === 1))
|
|
86
|
+
return null;
|
|
87
|
+
const cellsOf = (r) => Array.from({ length: r.width * r.height }, (_, i) => (r.y + Math.floor(i / r.width)) * width + r.x + i % r.width);
|
|
88
|
+
// Where a number sits decides how many rectangles it could be: easy boards put it where it has the fewest, the others anywhere.
|
|
89
|
+
const freedom = (r, cell) => {
|
|
90
|
+
const area = r.width * r.height;
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (let w = 1; w <= area && w <= width; w += 1) {
|
|
93
|
+
if (area % w || area / w > height)
|
|
94
|
+
continue;
|
|
95
|
+
const h = area / w;
|
|
96
|
+
count += (Math.min(cell % width, width - w) - Math.max(0, cell % width - w + 1) + 1) * (Math.min(Math.floor(cell / width), height - h) - Math.max(0, Math.floor(cell / width) - h + 1) + 1);
|
|
97
|
+
}
|
|
98
|
+
return count;
|
|
99
|
+
};
|
|
100
|
+
const chooseSpot = (r, not) => {
|
|
101
|
+
const cells = shuffled(cellsOf(r).filter(c => c !== not), random);
|
|
102
|
+
if (!cells.length)
|
|
103
|
+
return not;
|
|
104
|
+
if (level !== "easy")
|
|
105
|
+
return cells[0];
|
|
106
|
+
const ranked = [...cells].sort((a, b) => freedom(r, a) - freedom(r, b));
|
|
107
|
+
return ranked[Math.floor(random() * Math.min(2, ranked.length))];
|
|
108
|
+
};
|
|
109
|
+
const spot = solution.map(r => chooseSpot(r));
|
|
110
|
+
for (let repair = 0; repair < 40; repair += 1) {
|
|
111
|
+
const clues = Array(width * height).fill(0);
|
|
112
|
+
solution.forEach((r, i) => { clues[spot[i]] = r.width * r.height; });
|
|
113
|
+
const board = { width, height, clues };
|
|
114
|
+
const model = shikakuModel(board);
|
|
115
|
+
const found = countCsp(model.csp, openSlots(model.csp), 2, 20000);
|
|
116
|
+
if (found.exhausted)
|
|
117
|
+
return null;
|
|
118
|
+
if (found.count === 1)
|
|
119
|
+
return { board, solution };
|
|
120
|
+
const alt = found.solutions[1];
|
|
121
|
+
let moved = false;
|
|
122
|
+
solution.forEach((r, i) => {
|
|
123
|
+
const slot = model.rectangles.findIndex((c, s) => model.owner[s] === model.clues.indexOf(spot[i]) && c.x === r.x && c.y === r.y && c.width === r.width && c.height === r.height);
|
|
124
|
+
if (slot >= 0 && alt[slot])
|
|
125
|
+
return;
|
|
126
|
+
if (cellsOf(r).length < 2)
|
|
127
|
+
return;
|
|
128
|
+
spot[i] = shuffled(cellsOf(r).filter(c => c !== spot[i]), random)[0];
|
|
129
|
+
moved = true;
|
|
130
|
+
});
|
|
131
|
+
if (!moved)
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { ShikakuLevel, ShikakuPuzzle } from "./shikaku.types.ts";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Seeded rectangles packed into the board, one number each, with an independently counted, unique answer. The board
|
|
4
|
+
* is rated by solving it: easy by the first two rules (a number with one fitting rectangle, a square one rectangle can
|
|
5
|
+
* cover), medium once the third rule is needed too, hard by supposing, extra-hard the most-supposing of several boards. If no board of the level is found within the attempts
|
|
6
|
+
* the next level down is tried, and the first generator is the last resort.
|
|
7
|
+
*/
|
|
3
8
|
export declare function generateShikaku(width?: number, height?: number, level?: ShikakuLevel, seed?: number): ShikakuPuzzle;
|
package/dist/shikakuGenerate.js
CHANGED
|
@@ -1,43 +1,58 @@
|
|
|
1
|
-
import { SHIKAKU_LEVELS, SHIKAKU_MOST_ATTEMPTS } from "./shikaku.constants.js";
|
|
2
|
-
import {
|
|
1
|
+
import { SHIKAKU_LEVELS, SHIKAKU_MOST_ATTEMPTS, SHIKAKU_MOST_SIDE } from "./shikaku.constants.js";
|
|
2
|
+
import { checkShikaku } from "./shikakuBoard.js";
|
|
3
|
+
import { candidateShikaku } from "./shikakuBuild.js";
|
|
4
|
+
import { shikakuModel } from "./shikakuLogic.js";
|
|
3
5
|
import { solveShikaku } from "./shikakuSolve.js";
|
|
6
|
+
import { templateShikaku } from "./shikakuTemplate.js";
|
|
7
|
+
import { logicCsp, openSlots } from "./csp.js";
|
|
4
8
|
import { isKazuSeed, seededRandom } from "./random.js";
|
|
5
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Seeded rectangles packed into the board, one number each, with an independently counted, unique answer. The board
|
|
11
|
+
* is rated by solving it: easy by the first two rules (a number with one fitting rectangle, a square one rectangle can
|
|
12
|
+
* cover), medium once the third rule is needed too, hard by supposing, extra-hard the most-supposing of several boards. If no board of the level is found within the attempts
|
|
13
|
+
* the next level down is tried, and the first generator is the last resort.
|
|
14
|
+
*/
|
|
6
15
|
export function generateShikaku(width = 7, height = width, level = "medium", seed = 1) {
|
|
7
|
-
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <=
|
|
8
|
-
|
|
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)] }))
|
|
16
|
+
if (![width, height].every(n => Number.isInteger(n) && n >= 2 && n <= SHIKAKU_MOST_SIDE)
|
|
17
|
+
|| !isKazuSeed(seed) || !SHIKAKU_LEVELS.includes(level))
|
|
12
18
|
throw new RangeError("Invalid Shikaku settings");
|
|
13
|
-
const random = seededRandom(seed)
|
|
14
|
-
for (let
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
const random = seededRandom(seed);
|
|
20
|
+
for (let aim = SHIKAKU_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
|
|
21
|
+
const aimed = SHIKAKU_LEVELS[aim];
|
|
22
|
+
const wanted = aimed === "extra-hard" ? 3 : 1;
|
|
23
|
+
let best = null, found = 0;
|
|
24
|
+
for (let attempt = 0; attempt < SHIKAKU_MOST_ATTEMPTS && found < wanted; attempt += 1) {
|
|
25
|
+
const built = candidateShikaku(width, height, aimed, random);
|
|
26
|
+
if (!built)
|
|
27
|
+
continue;
|
|
28
|
+
const score = scored(built.board, aimed);
|
|
29
|
+
if (score === null)
|
|
30
|
+
continue;
|
|
31
|
+
found += 1;
|
|
32
|
+
if (!best || score > best.score)
|
|
33
|
+
best = { board: built.board, score };
|
|
34
|
+
}
|
|
35
|
+
if (best) {
|
|
36
|
+
const proof = solveShikaku(best.board);
|
|
37
|
+
if (proof.complete && proof.count === 1 && proof.solution && checkShikaku(best.board, proof.solution).ok) {
|
|
38
|
+
return { ...best.board, seed, level, solution: proof.solution };
|
|
30
39
|
}
|
|
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
40
|
}
|
|
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
41
|
}
|
|
42
|
-
|
|
42
|
+
return { ...templateShikaku(width, height, level === "extra-hard" ? "hard" : level, seed), level };
|
|
43
|
+
}
|
|
44
|
+
/** Whether a board suits a level, and how well: higher is harder. */
|
|
45
|
+
function scored(board, level) {
|
|
46
|
+
const models = [shikakuModel(board, 1), shikakuModel(board, 2)];
|
|
47
|
+
const solves = (index, depth) => logicCsp(models[index].csp, openSlots(models[index].csp), depth);
|
|
48
|
+
if (solves(0, 0).solved)
|
|
49
|
+
return level === "easy" ? 0 : null;
|
|
50
|
+
if (level === "easy")
|
|
51
|
+
return null;
|
|
52
|
+
if (solves(1, 0).solved)
|
|
53
|
+
return level === "medium" ? 0 : null;
|
|
54
|
+
if (level === "medium")
|
|
55
|
+
return null;
|
|
56
|
+
const probing = solves(1, 1);
|
|
57
|
+
return probing.solved ? probing.probes : level === "extra-hard" ? 1000 : null;
|
|
43
58
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Csp } from "./csp.ts";
|
|
2
|
+
import type { ShikakuBoard, ShikakuRectangle } from "./shikaku.types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Shikaku as variables: one per numbered square, its slots the rectangles it could be the one number of. A rectangle
|
|
5
|
+
* that is settled keeps every other rectangle off its squares. With `strength` 1 a square only one rectangle can
|
|
6
|
+
* still cover is covered by it, and with 2 a square only one number can still reach forces that number's rectangle to cover it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function shikakuModel(board: ShikakuBoard, strength?: 0 | 1 | 2): {
|
|
9
|
+
csp: Csp;
|
|
10
|
+
clues: readonly number[];
|
|
11
|
+
rectangles: readonly ShikakuRectangle[];
|
|
12
|
+
owner: Int32Array;
|
|
13
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { shikakuCandidates, shikakuCells } from "./shikakuBoard.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shikaku as variables: one per numbered square, its slots the rectangles it could be the one number of. A rectangle
|
|
4
|
+
* that is settled keeps every other rectangle off its squares. With `strength` 1 a square only one rectangle can
|
|
5
|
+
* still cover is covered by it, and with 2 a square only one number can still reach forces that number's rectangle to cover it.
|
|
6
|
+
*/
|
|
7
|
+
export function shikakuModel(board, strength = 2) {
|
|
8
|
+
const clues = board.clues.flatMap((n, cell) => n ? [cell] : []);
|
|
9
|
+
const rectangles = [], owner = [], covers = [], starts = [0];
|
|
10
|
+
clues.forEach((cell, variable) => {
|
|
11
|
+
for (const rectangle of shikakuCandidates(board, cell)) {
|
|
12
|
+
rectangles.push(rectangle);
|
|
13
|
+
owner.push(variable);
|
|
14
|
+
covers.push(Int32Array.from(shikakuCells(board, rectangle)));
|
|
15
|
+
}
|
|
16
|
+
starts.push(rectangles.length);
|
|
17
|
+
});
|
|
18
|
+
const through = Array.from({ length: board.clues.length }, () => []);
|
|
19
|
+
covers.forEach((cells, slot) => cells.forEach(cell => through[cell].push(slot)));
|
|
20
|
+
const propagate = (alive) => {
|
|
21
|
+
let changed = true;
|
|
22
|
+
while (changed) {
|
|
23
|
+
changed = false;
|
|
24
|
+
for (let v = 0; v < clues.length; v += 1) {
|
|
25
|
+
let left = 0, only = -1;
|
|
26
|
+
for (let s = starts[v]; s < starts[v + 1]; s += 1)
|
|
27
|
+
if (alive[s]) {
|
|
28
|
+
left += 1;
|
|
29
|
+
only = s;
|
|
30
|
+
}
|
|
31
|
+
if (!left)
|
|
32
|
+
return false;
|
|
33
|
+
if (left !== 1)
|
|
34
|
+
continue;
|
|
35
|
+
const cells = covers[only];
|
|
36
|
+
for (let k = 0; k < cells.length; k += 1) {
|
|
37
|
+
const others = through[cells[k]];
|
|
38
|
+
for (let j = 0; j < others.length; j += 1) {
|
|
39
|
+
const t = others[j];
|
|
40
|
+
if (t !== only && alive[t] && owner[t] !== v) {
|
|
41
|
+
alive[t] = 0;
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (strength < 1)
|
|
48
|
+
continue;
|
|
49
|
+
for (let cell = 0; cell < through.length; cell += 1) {
|
|
50
|
+
const slots = through[cell];
|
|
51
|
+
let count = 0, only = -1, single = true, first = -1;
|
|
52
|
+
for (let k = 0; k < slots.length; k += 1) {
|
|
53
|
+
const s = slots[k];
|
|
54
|
+
if (!alive[s])
|
|
55
|
+
continue;
|
|
56
|
+
count += 1;
|
|
57
|
+
only = s;
|
|
58
|
+
if (first < 0)
|
|
59
|
+
first = owner[s];
|
|
60
|
+
else if (owner[s] !== first)
|
|
61
|
+
single = false;
|
|
62
|
+
}
|
|
63
|
+
if (!count)
|
|
64
|
+
return false;
|
|
65
|
+
if (count === 1) {
|
|
66
|
+
for (let s = starts[owner[only]]; s < starts[owner[only] + 1]; s += 1)
|
|
67
|
+
if (s !== only && alive[s]) {
|
|
68
|
+
alive[s] = 0;
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (single && strength > 1) {
|
|
73
|
+
for (let s = starts[first]; s < starts[first + 1]; s += 1)
|
|
74
|
+
if (alive[s] && !covers[s].includes(cell)) {
|
|
75
|
+
alive[s] = 0;
|
|
76
|
+
changed = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
return { csp: { starts: Int32Array.from(starts), propagate }, clues, rectangles, owner: Int32Array.from(owner) };
|
|
84
|
+
}
|