@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +69 -27
  3. package/dist/akari-entry.d.ts +1 -0
  4. package/dist/akari-entry.js +1 -0
  5. package/dist/akari.constants.d.ts +12 -0
  6. package/dist/akari.constants.js +12 -0
  7. package/dist/akari.types.d.ts +19 -0
  8. package/dist/akariGenerate.d.ts +10 -5
  9. package/dist/akariGenerate.js +101 -65
  10. package/dist/akariLogic.d.ts +10 -0
  11. package/dist/akariLogic.js +84 -0
  12. package/dist/akariRate.d.ts +6 -0
  13. package/dist/akariRate.js +34 -0
  14. package/dist/akariSolve.d.ts +1 -1
  15. package/dist/akariSolve.js +8 -96
  16. package/dist/akariTemplate.d.ts +7 -0
  17. package/dist/akariTemplate.js +78 -0
  18. package/dist/cells.d.ts +4 -4
  19. package/dist/cells.js +6 -6
  20. package/dist/csp.d.ts +63 -0
  21. package/dist/csp.js +162 -0
  22. package/dist/fillomino-entry.d.ts +1 -0
  23. package/dist/fillomino-entry.js +1 -0
  24. package/dist/fillomino.constants.d.ts +6 -2
  25. package/dist/fillomino.constants.js +6 -2
  26. package/dist/fillomino.types.d.ts +18 -1
  27. package/dist/fillominoBuild.d.ts +10 -0
  28. package/dist/fillominoBuild.js +78 -0
  29. package/dist/fillominoGenerate.d.ts +7 -1
  30. package/dist/fillominoGenerate.js +75 -107
  31. package/dist/fillominoLogic.d.ts +14 -0
  32. package/dist/fillominoLogic.js +177 -0
  33. package/dist/fillominoMount.js +4 -1
  34. package/dist/fillominoRate.d.ts +3 -0
  35. package/dist/fillominoRate.js +57 -0
  36. package/dist/fillominoSolve.js +15 -88
  37. package/dist/groupSolve.d.ts +9 -0
  38. package/dist/groupSolve.js +66 -0
  39. package/dist/hitori-entry.d.ts +1 -0
  40. package/dist/hitori-entry.js +1 -0
  41. package/dist/hitori.constants.d.ts +7 -1
  42. package/dist/hitori.constants.js +7 -1
  43. package/dist/hitori.types.d.ts +18 -1
  44. package/dist/hitoriBoard.js +2 -1
  45. package/dist/hitoriBuild.d.ts +12 -0
  46. package/dist/hitoriBuild.js +117 -0
  47. package/dist/hitoriGenerate.d.ts +10 -3
  48. package/dist/hitoriGenerate.js +59 -157
  49. package/dist/hitoriLogic.d.ts +12 -0
  50. package/dist/hitoriLogic.js +189 -0
  51. package/dist/hitoriRate.d.ts +3 -0
  52. package/dist/hitoriRate.js +32 -0
  53. package/dist/hitoriSolve.d.ts +4 -1
  54. package/dist/hitoriSolve.js +11 -62
  55. package/dist/kakuro-entry.d.ts +1 -0
  56. package/dist/kakuro-entry.js +1 -0
  57. package/dist/kakuro.constants.d.ts +6 -1
  58. package/dist/kakuro.constants.js +6 -1
  59. package/dist/kakuro.types.d.ts +19 -0
  60. package/dist/kakuroBuild.d.ts +30 -0
  61. package/dist/kakuroBuild.js +337 -0
  62. package/dist/kakuroGenerate.d.ts +10 -3
  63. package/dist/kakuroGenerate.js +54 -122
  64. package/dist/kakuroLogic.d.ts +13 -0
  65. package/dist/kakuroLogic.js +153 -0
  66. package/dist/kakuroRate.d.ts +3 -0
  67. package/dist/kakuroRate.js +40 -0
  68. package/dist/kakuroSolve.js +17 -69
  69. package/dist/kakuroTemplate.d.ts +3 -0
  70. package/dist/kakuroTemplate.js +130 -0
  71. package/dist/kinds.js +1 -1
  72. package/dist/layout.js +1 -0
  73. package/dist/mount.d.ts +0 -18
  74. package/dist/mount.js +24 -3
  75. package/dist/names.js +3 -3
  76. package/dist/numberPlace.d.ts +1 -1
  77. package/dist/numberPlace.js +21 -8
  78. package/dist/shikaku-entry.d.ts +1 -0
  79. package/dist/shikaku-entry.js +1 -0
  80. package/dist/shikaku.constants.d.ts +5 -2
  81. package/dist/shikaku.constants.js +5 -2
  82. package/dist/shikaku.types.d.ts +18 -1
  83. package/dist/shikakuBuild.d.ts +21 -0
  84. package/dist/shikakuBuild.js +135 -0
  85. package/dist/shikakuGenerate.d.ts +6 -1
  86. package/dist/shikakuGenerate.js +50 -35
  87. package/dist/shikakuLogic.d.ts +13 -0
  88. package/dist/shikakuLogic.js +84 -0
  89. package/dist/shikakuRate.d.ts +3 -0
  90. package/dist/shikakuRate.js +27 -0
  91. package/dist/shikakuSolve.d.ts +1 -1
  92. package/dist/shikakuSolve.js +23 -44
  93. package/dist/shikakuTemplate.d.ts +3 -0
  94. package/dist/shikakuTemplate.js +43 -0
  95. package/dist/slitherlink-entry.d.ts +1 -0
  96. package/dist/slitherlink-entry.js +1 -0
  97. package/dist/slitherlink.constants.d.ts +5 -0
  98. package/dist/slitherlink.constants.js +5 -0
  99. package/dist/slitherlink.types.d.ts +18 -0
  100. package/dist/slitherlinkGenerate.d.ts +10 -3
  101. package/dist/slitherlinkGenerate.js +161 -80
  102. package/dist/slitherlinkLogic.d.ts +10 -0
  103. package/dist/slitherlinkLogic.js +267 -0
  104. package/dist/slitherlinkRate.d.ts +3 -0
  105. package/dist/slitherlinkRate.js +26 -0
  106. package/dist/slitherlinkSolve.d.ts +1 -1
  107. package/dist/slitherlinkSolve.js +9 -117
  108. package/dist/slitherlinkTemplate.d.ts +3 -0
  109. package/dist/slitherlinkTemplate.js +101 -0
  110. package/dist/strings.js +2 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.js +1 -1
  113. 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-G])$/.exec(event.code)?.[1];
358
+ const code = /^Digit([1-9])$/.exec(event.code)?.[1] ?? /^Key([A-P])$/.exec(event.code)?.[1];
338
359
  const value = code === undefined ? 0 : valueOfSymbol(code);
339
360
  if (value >= 1 && value <= game.size) {
340
361
  event.preventDefault();
package/dist/names.js CHANGED
@@ -11,13 +11,13 @@ export const KAZU_NAMES = {
11
11
  en: [
12
12
  "Fill every empty cell with a number from 1 up to the side of the grid, so that each row, each column and each box holds every number exactly once.",
13
13
  "The numbers already printed are the givens. They stay where they are, and every puzzle has exactly one answer that fits them.",
14
- "The 16×16 Giant has sixteen symbols: 1 to 9, then A to G for 10 to 16.",
14
+ "The 16×16 Giant has sixteen symbols: 1 to 9, then A to G for 10 to 16. The 25×25 Colossus has twenty-five: A to P for 10 to 25.",
15
15
  "Easy yields to reasoning alone: every cell can be found from what is already there. Medium and hard ask you to try something and see.",
16
16
  ],
17
17
  ja: [
18
18
  "空いているマスに、1から盤の一辺の数までの数字を入れます。どの行、列、ブロックにも、同じ数字が一つずつ入ります。",
19
19
  "最初から書かれている数字はそのままです。どの問題も、答えはちょうど一つだけです。",
20
- "16×16の「特大」は、1から9までの数字に、10から16までを表すAからGを足した16種類を使います。",
20
+ "16×16の「特大」は、1から9までの数字に、10から16までを表すAからGを足した16種類を使います。25×25の「巨大」は、10から25までを表すAからPまでを使う25種類です。",
21
21
  "やさしい問題は、推理だけで全部のマスが決まります。ふつうとむずかしい問題では、試してみる場面があります。",
22
22
  ],
23
23
  },
@@ -154,7 +154,7 @@ export const KAZU_NAMES = {
154
154
  };
155
155
  /** What each side is for, under its size on a chooser: the quick one, the usual one, the long one. */
156
156
  export const KAZU_SIZE_NAMES = {
157
- "number-place": { 4: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" }, 16: { en: "Giant", ja: "特大" } },
157
+ "number-place": { 4: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" }, 16: { en: "Giant", ja: "特大" }, 25: { en: "Colossus", ja: "巨大" } },
158
158
  jigsaw: { 5: { en: "Quick", ja: "速" }, 6: { en: "Short", ja: "短" }, 7: { en: "Standard", ja: "定番" }, 9: { en: "Classic", ja: "本格" } },
159
159
  diagonal: { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
160
160
  "sum-cages": { 6: { en: "Short", ja: "短" }, 9: { en: "Classic", ja: "定番" } },
@@ -8,7 +8,7 @@ import { type Random } from "./random.ts";
8
8
  * puzzle on a layout (Sudoku, Diagonal and Jigsaw).
9
9
  */
10
10
  export declare function carve(solution: Grid, layout: Layout, level: KazuLevel, floor: number, random: Random): Grid;
11
- /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9 or 16. */
11
+ /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9, 16 or 25. */
12
12
  export declare function generateNumberPlace(size: number, level: KazuLevel, seed: number): KazuPuzzle;
13
13
  /** A Diagonal Sudoku (Sudoku X): Sudoku with the two long diagonals as groups too. 6 or 9. */
14
14
  export declare function generateDiagonal(size: number, level: KazuLevel, seed: number): KazuPuzzle;
@@ -1,5 +1,5 @@
1
1
  import { encodeCells } from "./cells.js";
2
- import { countSolutions, guessDepth } from "./groupSolve.js";
2
+ import { countSolutions, guessDepth, provedByGuessing } from "./groupSolve.js";
3
3
  import { boxedLayout } from "./layout.js";
4
4
  import { seededRandom, shuffled } from "./random.js";
5
5
  /**
@@ -22,10 +22,21 @@ import { seededRandom, shuffled } from "./random.js";
22
22
  * hard puzzle would be a medium one with a different label.
23
23
  */
24
24
  const LEVELS = {
25
- easy: { depth: 0, floor: { 4: 9, 6: 20, 9: 40, 16: 150 } },
26
- medium: { depth: 1, floor: { 4: 7, 6: 15, 9: 31, 16: 125 } },
27
- hard: { depth: Infinity, floor: { 4: 5, 6: 11, 9: 24, 16: 116 } },
25
+ easy: { depth: 0, floor: { 4: 9, 6: 20, 9: 40, 16: 150, 25: 366 } },
26
+ medium: { depth: 1, floor: { 4: 7, 6: 15, 9: 31, 16: 125, 25: 305 } },
27
+ hard: { depth: Infinity, floor: { 4: 5, 6: 11, 9: 24, 16: 116, 25: 283 } },
28
28
  };
29
+ /** Past this side the exact count of answers and the exact depth are not asked, only a proof (see `LARGE_GUESSES`). */
30
+ const LARGEST_COUNTED = 16;
31
+ /**
32
+ * The 25×25 is carved by proof, not by counting. Counting every answer of a sparse 625-cell grid can run for
33
+ * minutes (a single removal took 20 s), and the exact guess depth of one is as bad. What a person does is
34
+ * what is asked instead: singles, then at most this many guesses at the most constrained cell, each value
35
+ * of it either finished by singles or shown wrong by them. A puzzle that passes has exactly one answer, and
36
+ * the work for each removal is bounded however the grid looks. Hard allows two nested guesses and no more,
37
+ * so a 25×25 hard puzzle is one a person can finish, and easy is singles alone, as it is at every size.
38
+ */
39
+ const LARGE_GUESSES = { easy: 0, medium: 1, hard: 2 };
29
40
  /**
30
41
  * A whole grid, filled cell by cell in reading order with the values tried in a seeded order. The
31
42
  * groups come from the layout, so the diagonals are honoured the same way.
@@ -65,7 +76,7 @@ function fillInOrder(layout, random) {
65
76
  * into a dead end deep in a 256-cell grid and take seconds to climb out. A grid that is right by
66
77
  * construction (each row the one above it shifted a box's width, each band shifted by one), then
67
78
  * shuffled in every way that keeps it right (the numbers relabelled, rows within a band, the
68
- * bands, columns within a stack, the stacks), is as varied and costs nothing. Only 16×16 is made
79
+ * bands, columns within a stack, the stacks), is as varied and costs nothing. Only 16×16 and 25×25 are made
69
80
  * this way, so every smaller grid comes out of its seed exactly as it always has.
70
81
  */
71
82
  function fillByPattern(size, random) {
@@ -91,7 +102,9 @@ export function carve(solution, layout, level, floor, random) {
91
102
  break;
92
103
  const value = givens[index];
93
104
  givens[index] = 0;
94
- const stillOne = countSolutions(givens, layout, 2) === 1 && guessDepth(givens, layout) <= depth;
105
+ const stillOne = layout.size > LARGEST_COUNTED
106
+ ? provedByGuessing(givens, layout, LARGE_GUESSES[level]) === 1
107
+ : countSolutions(givens, layout, 2) === 1 && guessDepth(givens, layout) <= depth;
95
108
  if (stillOne)
96
109
  left -= 1;
97
110
  else
@@ -99,11 +112,11 @@ export function carve(solution, layout, level, floor, random) {
99
112
  }
100
113
  return givens;
101
114
  }
102
- /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9 or 16. */
115
+ /** A Sudoku (Number Place) of this side, level and seed: 4, 6, 9, 16 or 25. */
103
116
  export function generateNumberPlace(size, level, seed) {
104
117
  const random = seededRandom(seed);
105
118
  const layout = boxedLayout(size);
106
- const solution = size === 16 ? fillByPattern(size, random) : fillInOrder(layout, random);
119
+ const solution = size >= 16 ? fillByPattern(size, random) : fillInOrder(layout, random);
107
120
  const givens = carve(solution, layout, level, LEVELS[level].floor[size], random);
108
121
  return { kind: "number-place", size, level, seed, givens: encodeCells(givens), solution: encodeCells(solution) };
109
122
  }
@@ -4,5 +4,6 @@ export * from "./shikaku.constants.ts";
4
4
  export * from "./shikakuBoard.ts";
5
5
  export * from "./shikakuSolve.ts";
6
6
  export * from "./shikakuGenerate.ts";
7
+ export * from "./shikakuRate.ts";
7
8
  export * from "./shikakuGame.ts";
8
9
  export * from "./shikakuPacks.ts";
@@ -4,5 +4,6 @@ export * from "./shikaku.constants.js";
4
4
  export * from "./shikakuBoard.js";
5
5
  export * from "./shikakuSolve.js";
6
6
  export * from "./shikakuGenerate.js";
7
+ export * from "./shikakuRate.js";
7
8
  export * from "./shikakuGame.js";
8
9
  export * from "./shikakuPacks.js";
@@ -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 = 200;
5
- export declare const SHIKAKU_LEVELS: readonly ["easy", "medium", "hard"];
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 = 200;
5
- export const SHIKAKU_LEVELS = ["easy", "medium", "hard"];
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];
@@ -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
- /** Seeded partitions with an independently counted, unique answer. Levels choose rectangle sizes, not a promised human rating. */
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;
@@ -1,43 +1,58 @@
1
- import { SHIKAKU_LEVELS, SHIKAKU_MOST_ATTEMPTS } from "./shikaku.constants.js";
2
- import { isShikakuBoard, shikakuCells } from "./shikakuBoard.js";
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
- /** Seeded partitions with an independently counted, unique answer. Levels choose rectangle sizes, not a promised human rating. */
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 <= 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)] }))
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), 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 });
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
- throw new Error("No unique Shikaku found within the generation budget; try another seed");
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
+ }
@@ -0,0 +1,3 @@
1
+ import type { ShikakuBoard, ShikakuRating } from "./shikaku.types.ts";
2
+ /** Rates a board with one answer by solving it with the first rule, with the first two, with all three, then by supposing. */
3
+ export declare function rateShikaku(board: ShikakuBoard): ShikakuRating;