@johnmorrisdotca/kazu 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +58 -16
  3. package/dist/akari-entry.d.ts +1 -0
  4. package/dist/akari-entry.js +1 -0
  5. package/dist/akari.constants.d.ts +5 -0
  6. package/dist/akari.constants.js +5 -0
  7. package/dist/akari.types.d.ts +19 -0
  8. package/dist/akariGenerate.d.ts +8 -5
  9. package/dist/akariGenerate.js +88 -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/csp.d.ts +63 -0
  19. package/dist/csp.js +162 -0
  20. package/dist/fillomino-entry.d.ts +1 -0
  21. package/dist/fillomino-entry.js +1 -0
  22. package/dist/fillomino.constants.d.ts +6 -2
  23. package/dist/fillomino.constants.js +6 -2
  24. package/dist/fillomino.types.d.ts +18 -1
  25. package/dist/fillominoBuild.d.ts +10 -0
  26. package/dist/fillominoBuild.js +78 -0
  27. package/dist/fillominoGenerate.d.ts +7 -1
  28. package/dist/fillominoGenerate.js +75 -107
  29. package/dist/fillominoLogic.d.ts +14 -0
  30. package/dist/fillominoLogic.js +177 -0
  31. package/dist/fillominoMount.js +4 -1
  32. package/dist/fillominoRate.d.ts +3 -0
  33. package/dist/fillominoRate.js +57 -0
  34. package/dist/fillominoSolve.js +15 -88
  35. package/dist/hitori-entry.d.ts +1 -0
  36. package/dist/hitori-entry.js +1 -0
  37. package/dist/hitori.constants.d.ts +7 -1
  38. package/dist/hitori.constants.js +7 -1
  39. package/dist/hitori.types.d.ts +18 -1
  40. package/dist/hitoriBoard.js +2 -1
  41. package/dist/hitoriBuild.d.ts +12 -0
  42. package/dist/hitoriBuild.js +117 -0
  43. package/dist/hitoriGenerate.d.ts +10 -3
  44. package/dist/hitoriGenerate.js +59 -157
  45. package/dist/hitoriLogic.d.ts +12 -0
  46. package/dist/hitoriLogic.js +189 -0
  47. package/dist/hitoriRate.d.ts +3 -0
  48. package/dist/hitoriRate.js +32 -0
  49. package/dist/hitoriSolve.d.ts +4 -1
  50. package/dist/hitoriSolve.js +11 -62
  51. package/dist/kakuro-entry.d.ts +1 -0
  52. package/dist/kakuro-entry.js +1 -0
  53. package/dist/kakuro.constants.d.ts +6 -1
  54. package/dist/kakuro.constants.js +6 -1
  55. package/dist/kakuro.types.d.ts +19 -0
  56. package/dist/kakuroBuild.d.ts +30 -0
  57. package/dist/kakuroBuild.js +337 -0
  58. package/dist/kakuroGenerate.d.ts +10 -3
  59. package/dist/kakuroGenerate.js +54 -122
  60. package/dist/kakuroLogic.d.ts +13 -0
  61. package/dist/kakuroLogic.js +153 -0
  62. package/dist/kakuroRate.d.ts +3 -0
  63. package/dist/kakuroRate.js +40 -0
  64. package/dist/kakuroSolve.js +17 -69
  65. package/dist/kakuroTemplate.d.ts +3 -0
  66. package/dist/kakuroTemplate.js +130 -0
  67. package/dist/shikaku-entry.d.ts +1 -0
  68. package/dist/shikaku-entry.js +1 -0
  69. package/dist/shikaku.constants.d.ts +5 -2
  70. package/dist/shikaku.constants.js +5 -2
  71. package/dist/shikaku.types.d.ts +18 -1
  72. package/dist/shikakuBuild.d.ts +21 -0
  73. package/dist/shikakuBuild.js +135 -0
  74. package/dist/shikakuGenerate.d.ts +6 -1
  75. package/dist/shikakuGenerate.js +50 -35
  76. package/dist/shikakuLogic.d.ts +13 -0
  77. package/dist/shikakuLogic.js +84 -0
  78. package/dist/shikakuRate.d.ts +3 -0
  79. package/dist/shikakuRate.js +27 -0
  80. package/dist/shikakuSolve.d.ts +1 -1
  81. package/dist/shikakuSolve.js +23 -44
  82. package/dist/shikakuTemplate.d.ts +3 -0
  83. package/dist/shikakuTemplate.js +43 -0
  84. package/dist/slitherlink-entry.d.ts +1 -0
  85. package/dist/slitherlink-entry.js +1 -0
  86. package/dist/slitherlink.constants.d.ts +5 -0
  87. package/dist/slitherlink.constants.js +5 -0
  88. package/dist/slitherlink.types.d.ts +18 -0
  89. package/dist/slitherlinkGenerate.d.ts +10 -3
  90. package/dist/slitherlinkGenerate.js +161 -80
  91. package/dist/slitherlinkLogic.d.ts +10 -0
  92. package/dist/slitherlinkLogic.js +267 -0
  93. package/dist/slitherlinkRate.d.ts +3 -0
  94. package/dist/slitherlinkRate.js +26 -0
  95. package/dist/slitherlinkSolve.d.ts +1 -1
  96. package/dist/slitherlinkSolve.js +9 -117
  97. package/dist/slitherlinkTemplate.d.ts +3 -0
  98. package/dist/slitherlinkTemplate.js +101 -0
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.js +1 -1
  101. package/package.json +1 -1
@@ -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;
@@ -0,0 +1,27 @@
1
+ import { isShikakuBoard } from "./shikakuBoard.js";
2
+ import { shikakuModel } from "./shikakuLogic.js";
3
+ import { logicCsp, openSlots, solveCsp } from "./csp.js";
4
+ /** Rates a board with one answer by solving it with the first rule, with the first two, with all three, then by supposing. */
5
+ export function rateShikaku(board) {
6
+ if (!isShikakuBoard(board))
7
+ throw new RangeError("Invalid Shikaku board");
8
+ const first = shikakuModel(board, 0), second = shikakuModel(board, 1), strong = shikakuModel(board, 2);
9
+ const proof = solveCsp(strong.csp, openSlots(strong.csp), 2, 100000);
10
+ if (proof.count !== 1 || proof.exhausted)
11
+ throw new RangeError("Shikaku rating needs a board with one answer");
12
+ const one = logicCsp(first.csp, openSlots(first.csp), 0);
13
+ const two = one.solved ? one : logicCsp(second.csp, openSlots(second.csp), 0);
14
+ const rules = two.solved ? two : logicCsp(strong.csp, openSlots(strong.csp), 0);
15
+ const probing = rules.solved ? rules : logicCsp(strong.csp, openSlots(strong.csp), 1);
16
+ const areas = strong.clues.map(cell => board.clues[cell]);
17
+ const options = strong.clues.map((_, v) => strong.csp.starts[v + 1] - strong.csp.starts[v]);
18
+ return {
19
+ depth: rules.solved ? 0 : probing.solved ? 1 : 2,
20
+ rules: one.solved ? 1 : two.solved ? 2 : 3,
21
+ probes: rules.solved ? 0 : probing.probes,
22
+ rectangles: areas.length,
23
+ meanArea: board.clues.length / areas.length,
24
+ largest: Math.max(...areas),
25
+ ambiguity: options.reduce((a, b) => a + b, 0) / options.length,
26
+ };
27
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ShikakuBoard, ShikakuRectangle, ShikakuSolve } from "./shikaku.types.ts";
2
- /** Exact cover over cells. A bounded search reports incompleteness instead of claiming uniqueness. */
2
+ /** Exact cover over squares. A bounded search reports incompleteness instead of claiming uniqueness. */
3
3
  export declare function solveShikaku(board: ShikakuBoard, placed?: readonly ShikakuRectangle[], options?: {
4
4
  limit?: number;
5
5
  nodes?: number;
@@ -1,54 +1,33 @@
1
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. */
2
+ import { checkShikaku, isShikakuBoard } from "./shikakuBoard.js";
3
+ import { shikakuModel } from "./shikakuLogic.js";
4
+ import { decide, openSlots, solveCsp } from "./csp.js";
5
+ /** Exact cover over squares. A bounded search reports incompleteness instead of claiming uniqueness. */
4
6
  export function solveShikaku(board, placed = [], options = {}) {
5
7
  if (!isShikakuBoard(board))
6
8
  throw new RangeError("Invalid Shikaku board");
7
9
  const limit = options.limit ?? 2, budget = options.nodes ?? SHIKAKU_MOST_NODES;
8
10
  if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1)
9
11
  throw new RangeError("Invalid search bounds");
10
- const maskOf = (r) => shikakuCells(board, r).reduce((m, c) => m | 1n << BigInt(c), 0n);
11
12
  if (checkShikaku(board, placed).errors.length)
12
13
  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 };
14
+ const { csp, rectangles, clues } = shikakuModel(board);
15
+ const start = openSlots(csp);
16
+ for (const r of placed) {
17
+ const slot = rectangles.findIndex(c => c.x === r.x && c.y === r.y && c.width === r.width && c.height === r.height);
18
+ if (slot < 0)
19
+ return { count: 0, solution: null, complete: true, nodes: 0 };
20
+ decide(csp, start, clues.indexOf(clueOf(board, r)), slot);
21
+ }
22
+ const found = solveCsp(csp, start, limit, budget);
23
+ const solution = found.solution ? [...placed.map(r => ({ ...r })), ...rectangles.filter((_, slot) => found.solution[slot] === 1 && !placed.some(r => sameRectangle(r, rectangles[slot])))] : null;
24
+ return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
54
25
  }
26
+ const sameRectangle = (a, b) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
27
+ const clueOf = (board, r) => {
28
+ for (let y = r.y; y < r.y + r.height; y += 1)
29
+ for (let x = r.x; x < r.x + r.width; x += 1)
30
+ if (board.clues[y * board.width + x])
31
+ return y * board.width + x;
32
+ return -1;
33
+ };
@@ -0,0 +1,3 @@
1
+ import type { ShikakuLevel, ShikakuPuzzle } from "./shikaku.types.ts";
2
+ /** The first generator, kept as the fallback that cannot fail on a board it can make: seeded cuts into rectangles, accepted when the answer is single. */
3
+ export declare function templateShikaku(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
+ /** The first generator, kept as the fallback that cannot fail on a board it can make: seeded cuts into rectangles, accepted when the answer is single. */
6
+ export function templateShikaku(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, "extra-hard": 20 }[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
+ }
@@ -4,6 +4,7 @@ export * from "./slitherlink.constants.ts";
4
4
  export * from "./slitherlinkBoard.ts";
5
5
  export * from "./slitherlinkSolve.ts";
6
6
  export * from "./slitherlinkGenerate.ts";
7
+ export * from "./slitherlinkRate.ts";
7
8
  export * from "./slitherlinkGame.ts";
8
9
  export { drawSlitherlink } from "./slitherlinkDraw.ts";
9
10
  export type { SlitherlinkDrawOptions } from "./slitherlinkPlay.types.ts";
@@ -4,6 +4,7 @@ export * from "./slitherlink.constants.js";
4
4
  export * from "./slitherlinkBoard.js";
5
5
  export * from "./slitherlinkSolve.js";
6
6
  export * from "./slitherlinkGenerate.js";
7
+ export * from "./slitherlinkRate.js";
7
8
  export * from "./slitherlinkGame.js";
8
9
  export { drawSlitherlink } from "./slitherlinkDraw.js";
9
10
  export { mountSlitherlink } from "./slitherlinkMount.js";
@@ -1,2 +1,7 @@
1
1
  export declare const SLITHERLINK_MOST_SIDE = 10;
2
2
  export declare const SLITHERLINK_MOST_NODES = 300000;
3
+ /** How hard a puzzle is made, by what a person must do to solve it; see `rateSlitherlink`. */
4
+ export declare const SLITHERLINK_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
5
+ /** The sides on offer for a square board; any side from 2 to `SLITHERLINK_MOST_SIDE` can be made. */
6
+ export declare const SLITHERLINK_SIZES: readonly [5, 7, 10];
7
+ export declare const SLITHERLINK_MOST_ATTEMPTS = 40;
@@ -1,2 +1,7 @@
1
1
  export const SLITHERLINK_MOST_SIDE = 10;
2
2
  export const SLITHERLINK_MOST_NODES = 300000;
3
+ /** How hard a puzzle is made, by what a person must do to solve it; see `rateSlitherlink`. */
4
+ export const SLITHERLINK_LEVELS = ["easy", "medium", "hard", "extra-hard"];
5
+ /** The sides on offer for a square board; any side from 2 to `SLITHERLINK_MOST_SIDE` can be made. */
6
+ export const SLITHERLINK_SIZES = [5, 7, 10];
7
+ export const SLITHERLINK_MOST_ATTEMPTS = 40;
@@ -3,10 +3,28 @@ export type SlitherlinkBoard = {
3
3
  height: number;
4
4
  clues: readonly (number | null)[];
5
5
  };
6
+ export type SlitherlinkLevel = "easy" | "medium" | "hard" | "extra-hard";
6
7
  export type SlitherlinkPuzzle = SlitherlinkBoard & {
7
8
  seed: number;
9
+ level: SlitherlinkLevel;
8
10
  solution: readonly number[];
9
11
  };
12
+ /**
13
+ * How hard a board is, measured by solving it. `depth` 0 means the plain rules solve it, 1 that somebody has to
14
+ * suppose an edge drawn or crossed and watch it break, 2 that more than that is needed; `probes` is how many
15
+ * suppositions depth 1 needed. The rest describes the board.
16
+ */
17
+ export type SlitherlinkRating = {
18
+ depth: 0 | 1 | 2;
19
+ probes: number;
20
+ clues: number;
21
+ /** Numbered squares as a share of all squares. */
22
+ clueShare: number;
23
+ /** Squares numbered 0 as a share of the numbered ones. */
24
+ zeroShare: number;
25
+ /** Edges in the loop. */
26
+ loop: number;
27
+ };
10
28
  export type SlitherlinkCheck = {
11
29
  ok: boolean;
12
30
  errors: readonly number[];
@@ -1,3 +1,10 @@
1
- import type { SlitherlinkPuzzle } from "./slitherlink.types.ts";
2
- /** Makes a proved puzzle from a seeded rectangle or L-shaped tile-region loop. */
3
- export declare function generateSlitherlink(width?: number, height?: number, seed?: number): SlitherlinkPuzzle;
1
+ import type { SlitherlinkLevel, SlitherlinkPuzzle } from "./slitherlink.types.ts";
2
+ /**
3
+ * Makes a seeded puzzle and proves it has one answer. A random winding loop is grown cell by cell, every square is
4
+ * numbered with how many of its edges the loop uses, and then numbers are taken away for as long as the puzzle can
5
+ * still be solved the way the level asks: easy and medium by the rules alone (easy keeps most numbers), hard by
6
+ * supposing one edge at a time, extra-hard the most-supposing of several such boards. Numbers that say 0 are
7
+ * taken away first, because they say least. If no board of the level is found within the attempts the next level
8
+ * down is tried, and the first generator, which cannot fail, is the last.
9
+ */
10
+ export declare function generateSlitherlink(width?: number, height?: number, seed?: number, level?: SlitherlinkLevel): SlitherlinkPuzzle;