@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
@@ -1,130 +1,62 @@
1
- import { KAKURO_GENERATION_ATTEMPTS, KAKURO_GENERATION_NODES, KAKURO_MAX_ATTEMPTS, KAKURO_MAX_NODES } from "./kakuro.constants.js";
2
- import { isKakuroBoard } from "./kakuroBoard.js";
1
+ import { KAKURO_LEAST_GENERATED_SIDE, KAKURO_LEVELS, KAKURO_MAX_SIDE } from "./kakuro.constants.js";
2
+ import { checkKakuro } from "./kakuroBoard.js";
3
+ import { candidateKakuro } from "./kakuroBuild.js";
4
+ import { kakuroModel } from "./kakuroLogic.js";
3
5
  import { solveKakuro } from "./kakuroSolve.js";
6
+ import { templateKakuro } from "./kakuroTemplate.js";
7
+ import { logicCsp, openSlots } from "./csp.js";
4
8
  import { isKazuSeed, seededRandom } from "./random.js";
5
- /** Makes a seeded 10×10 family of crossing runs and returns only a uniquely proved puzzle. */
6
- export function generateKakuro(seed = 1) {
7
- if (!isKazuSeed(seed))
8
- throw new RangeError("Invalid Kakuro seed.");
9
+ const MOST_ATTEMPTS = 400;
10
+ /**
11
+ * Makes a seeded board of `size` squares a side (counting the row and column of totals) and proves it has one
12
+ * answer. A random pattern of black squares sets the runs, random digits fill them, and digits are changed until
13
+ * the answer is single. The board is then rated by solving it: easy by the single-run rules, medium once a digit
14
+ * that must appear has to be traced, hard by supposing, and extra-hard the most-supposing of several boards.
15
+ * If no board of the level is found within the attempts the next level down is tried, and the first generator
16
+ * (a 10×10 only) is the last resort, so a seed never throws.
17
+ */
18
+ export function generateKakuro(seed = 1, level = "medium", size = 10) {
19
+ if (!isKazuSeed(seed) || !KAKURO_LEVELS.includes(level) || !Number.isInteger(size)
20
+ || size < KAKURO_LEAST_GENERATED_SIDE || size > KAKURO_MAX_SIDE)
21
+ throw new RangeError("Invalid Kakuro settings");
9
22
  const random = seededRandom(seed);
10
- const budget = { attempts: KAKURO_GENERATION_ATTEMPTS, nodes: KAKURO_GENERATION_NODES };
11
- for (let attempt = 0; attempt < KAKURO_MAX_ATTEMPTS && budget.attempts > 0 && budget.nodes > 0; attempt += 1) {
12
- budget.attempts -= 1;
13
- const rowGroups = random() < .5 ? [2, 2, 2] : random() < .5 ? [2, 3] : [3, 2];
14
- const columnGroups = rowGroups.length === 3 ? [2, 2, 2] : [2, 3];
15
- if (random() < .5)
16
- columnGroups.reverse();
17
- const rowStarts = groupStarts(rowGroups, random);
18
- const columnStarts = groupStarts(columnGroups, random);
19
- const cells = Array.from({ length: 100 }, () => ({ kind: "black", across: null, down: null }));
20
- const solution = Array(100).fill(0);
21
- const board = { width: 10, height: 10, cells };
22
- let failed = false;
23
- for (const [rowIndex, rowStart] of rowStarts.entries()) {
24
- for (const [columnIndex, columnStart] of columnStarts.entries()) {
25
- const shape = { height: rowGroups[rowIndex], width: columnGroups[columnIndex] };
26
- const local = chooseUniqueTile(shape.width, shape.height, random, budget);
27
- if (!local) {
28
- failed = true;
29
- break;
30
- }
31
- placeTile(board, solution, rowStart, columnStart, local);
32
- }
33
- if (failed)
34
- break;
35
- }
36
- if (failed)
37
- continue;
38
- if (!isKakuroBoard(board))
39
- throw new Error(JSON.stringify(board));
40
- const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(KAKURO_MAX_NODES, budget.nodes) });
41
- budget.nodes -= Math.min(budget.nodes, proof.nodes);
42
- if (proof.complete && proof.count === 1 && proof.solution) {
43
- return { ...board, seed, solution: proof.solution };
23
+ for (let aim = KAKURO_LEVELS.indexOf(level); aim >= 0; aim -= 1) {
24
+ const aimed = KAKURO_LEVELS[aim];
25
+ const wanted = aimed === "extra-hard" ? 3 : 1;
26
+ let best = null, found = 0;
27
+ for (let attempt = 0; attempt < MOST_ATTEMPTS && found < wanted; attempt += 1) {
28
+ const built = candidateKakuro(size, aimed, random);
29
+ if (!built)
30
+ continue;
31
+ const score = scored(built.board, aimed);
32
+ if (score === null)
33
+ continue;
34
+ found += 1;
35
+ if (!best || score > best.score)
36
+ best = { board: built.board, score };
44
37
  }
45
- }
46
- throw new Error("No uniquely solvable Kakuro board was proved within the generation budget.");
47
- }
48
- function groupStarts(groups, random) {
49
- const used = groups.reduce((sum, size) => sum + size, 0) + groups.length - 1;
50
- let at = 1 + (8 - used > 0 && random() < .5 ? 8 - used : 0);
51
- return groups.map(size => { const start = at; at += size + 1; return start; });
52
- }
53
- function chooseUniqueTile(width, height, random, budget) {
54
- for (let attempt = 0; attempt < KAKURO_MAX_ATTEMPTS && budget.attempts > 0 && budget.nodes > 0; attempt += 1) {
55
- budget.attempts -= 1;
56
- const candidate = randomRectangle(width, height, random);
57
- if (!candidate)
58
- continue;
59
- const board = tileBoard(candidate);
60
- const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(25000, budget.nodes) });
61
- budget.nodes -= Math.min(budget.nodes, proof.nodes);
62
- if (!budget.nodes)
63
- return null;
64
- if (proof.complete && proof.count === 1)
65
- return proof.solution ? splitSolution(proof.solution, width, height) : candidate;
66
- }
67
- return null;
68
- }
69
- function randomRectangle(width, height, random) {
70
- const rows = [];
71
- for (let y = 0; y < height; y += 1) {
72
- let row = null;
73
- for (let attempt = 0; attempt < 100; attempt += 1) {
74
- const available = shuffled([1, 2, 3, 4, 5, 6, 7, 8, 9], random);
75
- const candidate = available.slice(0, width);
76
- if (candidate.every((digit, x) => rows.every(previous => previous[x] !== digit))) {
77
- row = candidate;
78
- break;
38
+ if (best) {
39
+ const proof = solveKakuro(best.board);
40
+ if (proof.complete && proof.count === 1 && proof.solution && checkKakuro(best.board, proof.solution).ok) {
41
+ return { ...best.board, seed, level, solution: proof.solution };
79
42
  }
80
43
  }
81
- if (!row)
82
- return null;
83
- rows.push(row);
84
- }
85
- return rows;
86
- }
87
- function tileBoard(solution) {
88
- const height = solution.length + 1, width = solution[0].length + 1;
89
- const cells = Array.from({ length: width * height }, () => ({ kind: "black", across: null, down: null }));
90
- for (let y = 0; y < solution.length; y += 1)
91
- for (let x = 0; x < solution[y].length; x += 1)
92
- cells[(y + 1) * width + x + 1] = { kind: "white" };
93
- for (let y = 0; y < solution.length; y += 1)
94
- cells[(y + 1) * width].across = solution[y].reduce((sum, value) => sum + value, 0);
95
- for (let x = 0; x < solution[0].length; x += 1)
96
- cells[x + 1].down = solution.reduce((sum, row) => sum + row[x], 0);
97
- return { width, height, cells };
98
- }
99
- function placeTile(board, full, top, left, solution) {
100
- const width = board.width;
101
- const cells = board.cells;
102
- for (let y = 0; y < solution.length; y += 1)
103
- for (let x = 0; x < solution[y].length; x += 1)
104
- full[(top + y) * width + left + x] = solution[y][x];
105
- for (let y = 0; y < solution.length; y += 1) {
106
- const clue = (top + y) * width + left - 1;
107
- cells[clue].across = solution[y].reduce((sum, value) => sum + value, 0);
108
- }
109
- for (let x = 0; x < solution[0].length; x += 1) {
110
- const clue = (top - 1) * width + left + x;
111
- cells[clue].down = solution.reduce((sum, row) => sum + row[x], 0);
112
- }
113
- for (let y = 0; y < solution.length; y += 1)
114
- for (let x = 0; x < solution[y].length; x += 1)
115
- cells[(top + y) * width + left + x] = { kind: "white" };
116
- }
117
- function splitSolution(values, width, height) {
118
- const result = [];
119
- for (let y = 1; y <= height; y += 1)
120
- result.push(values.slice(y * (width + 1) + 1, y * (width + 1) + width + 1));
121
- return result;
122
- }
123
- function shuffled(items, random) {
124
- const result = [...items];
125
- for (let i = result.length - 1; i > 0; i -= 1) {
126
- const j = Math.floor(random() * (i + 1));
127
- [result[i], result[j]] = [result[j], result[i]];
128
44
  }
129
- return result;
45
+ if (size === 10)
46
+ return { ...templateKakuro(seed), level };
47
+ throw new Error("No uniquely solvable Kakuro board was found within the generation budget; try another seed");
48
+ }
49
+ /** Whether a board suits a level, and how well: higher is harder. */
50
+ function scored(board, level) {
51
+ const weak = kakuroModel(board, 0), strong = kakuroModel(board, 1);
52
+ if (logicCsp(weak.csp, openSlots(weak.csp), 0).solved)
53
+ return level === "easy" ? 0 : null;
54
+ if (level === "easy")
55
+ return null;
56
+ if (logicCsp(strong.csp, openSlots(strong.csp), 0).solved)
57
+ return level === "medium" ? 0 : null;
58
+ if (level === "medium")
59
+ return null;
60
+ const probing = logicCsp(strong.csp, openSlots(strong.csp), 1);
61
+ return probing.solved ? probing.probes : level === "extra-hard" ? 1000 : null;
130
62
  }
@@ -0,0 +1,13 @@
1
+ import type { Csp } from "./csp.ts";
2
+ import type { KakuroBoard } from "./kakuro.types.ts";
3
+ /**
4
+ * Kakuro as variables: one per white square, nine slots for the digits 1 to 9. A run's pruning keeps only the digit
5
+ * sets that add up to its total and fit what its squares can still be, so a square keeps only digits some such
6
+ * set uses; a digit that every such set uses must appear somewhere (`strength` 1), and a digit that only one square
7
+ * can take is that square's.
8
+ */
9
+ export declare function kakuroModel(board: KakuroBoard, strength?: 0 | 1, random?: () => number): {
10
+ csp: Csp;
11
+ whites: readonly number[];
12
+ index: Int32Array;
13
+ };
@@ -0,0 +1,153 @@
1
+ import { kakuroRuns } from "./kakuroBoard.js";
2
+ /** Every way to choose `length` different digits that add to `sum`, as 9-bit masks (bit d - 1 for digit d), by length then sum. */
3
+ const COMBINATIONS = (() => {
4
+ const table = Array.from({ length: 10 }, () => Array.from({ length: 46 }, () => []));
5
+ for (let mask = 1; mask < 512; mask += 1) {
6
+ let length = 0, sum = 0;
7
+ for (let d = 1; d <= 9; d += 1)
8
+ if (mask & 1 << d - 1) {
9
+ length += 1;
10
+ sum += d;
11
+ }
12
+ table[length][sum].push(mask);
13
+ }
14
+ return table;
15
+ })();
16
+ /**
17
+ * Kakuro as variables: one per white square, nine slots for the digits 1 to 9. A run's pruning keeps only the digit
18
+ * sets that add up to its total and fit what its squares can still be, so a square keeps only digits some such
19
+ * set uses; a digit that every such set uses must appear somewhere (`strength` 1), and a digit that only one square
20
+ * can take is that square's.
21
+ */
22
+ export function kakuroModel(board, strength = 1, random) {
23
+ const whites = board.cells.flatMap((tile, cell) => tile.kind === "white" ? [cell] : []);
24
+ const index = new Int32Array(board.cells.length).fill(-1);
25
+ whites.forEach((cell, i) => { index[cell] = i; });
26
+ const runs = kakuroRuns(board).map(run => ({ cells: Int32Array.from(run.cells.map(cell => index[cell])), sum: run.sum, combos: COMBINATIONS[run.cells.length][run.sum] }));
27
+ const runsOf = whites.map(() => []);
28
+ runs.forEach((run, r) => run.cells.forEach(cell => runsOf[cell].push(r)));
29
+ const starts = Int32Array.from({ length: whites.length + 1 }, (_, i) => i * 9);
30
+ const domain = new Int16Array(whites.length), loaded = new Int32Array(whites.length), touched = new Int32Array(whites.length), changedCells = new Int32Array(whites.length);
31
+ const queue = new Int32Array(runs.length * 4 + 4), queued = new Uint8Array(runs.length);
32
+ let stamp = 0;
33
+ const propagate = (alive, decided) => {
34
+ stamp += 1;
35
+ let changes = 0;
36
+ // A square's digits are read from `alive` the first time a run asks, so a call after one decision touches only what that reaches.
37
+ const read = (i) => {
38
+ if (loaded[i] !== stamp) {
39
+ let mask = 0;
40
+ for (let d = 0; d < 9; d += 1)
41
+ if (alive[9 * i + d])
42
+ mask |= 1 << d;
43
+ domain[i] = mask;
44
+ loaded[i] = stamp;
45
+ }
46
+ return domain[i];
47
+ };
48
+ let head = 0, tail = 0;
49
+ const push = (r) => { if (!queued[r]) {
50
+ queued[r] = 1;
51
+ queue[tail++ % queue.length] = r;
52
+ } };
53
+ if (decided === undefined)
54
+ for (let r = 0; r < runs.length; r += 1)
55
+ push(r);
56
+ else
57
+ for (const r of runsOf[decided])
58
+ push(r);
59
+ while (head < tail) {
60
+ const r = queue[head++ % queue.length], run = runs[r], cells = run.cells;
61
+ queued[r] = 0;
62
+ let known = 0, union = 0;
63
+ for (let k = 0; k < cells.length; k += 1) {
64
+ const mask = read(cells[k]);
65
+ if (!mask)
66
+ return fail();
67
+ union |= mask;
68
+ if (!(mask & mask - 1)) {
69
+ if (known & mask)
70
+ return fail();
71
+ known |= mask;
72
+ }
73
+ }
74
+ let usable = 0, always = 511, viable = 0;
75
+ for (let c = 0; c < run.combos.length; c += 1) {
76
+ const combo = run.combos[c];
77
+ if (combo & ~union || known & ~combo)
78
+ continue;
79
+ let fits = true;
80
+ for (let k = 0; k < cells.length && fits; k += 1)
81
+ if (!(domain[cells[k]] & combo))
82
+ fits = false;
83
+ if (!fits)
84
+ continue;
85
+ viable += 1;
86
+ usable |= combo;
87
+ always &= combo;
88
+ }
89
+ if (!viable)
90
+ return fail();
91
+ for (let k = 0; k < cells.length; k += 1) {
92
+ const cell = cells[k], was = domain[cell];
93
+ let mask = was & usable;
94
+ // Squares not yet decided cannot take the digits the decided ones of the run hold.
95
+ if (was & was - 1)
96
+ mask &= ~known;
97
+ if (strength > 0 && always && mask & mask - 1) {
98
+ // A digit every viable set uses must go somewhere: if only this square can hold it, it does.
99
+ for (let d = 0; d < 9; d += 1) {
100
+ if (!(always & 1 << d) || !(mask & 1 << d))
101
+ continue;
102
+ let holders = 0;
103
+ for (let j = 0; j < cells.length; j += 1)
104
+ if (domain[cells[j]] & 1 << d)
105
+ holders += 1;
106
+ if (holders === 1) {
107
+ mask = 1 << d;
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ if (!mask)
113
+ return fail();
114
+ if (mask !== was) {
115
+ if (touched[cell] !== stamp) {
116
+ touched[cell] = stamp;
117
+ changedCells[changes++] = cell;
118
+ }
119
+ domain[cell] = mask;
120
+ for (const other of runsOf[cell])
121
+ push(other);
122
+ }
123
+ }
124
+ }
125
+ for (let c = 0; c < changes; c += 1) {
126
+ const i = changedCells[c];
127
+ for (let d = 0; d < 9; d += 1)
128
+ alive[9 * i + d] = domain[i] & 1 << d ? 1 : 0;
129
+ }
130
+ return true;
131
+ };
132
+ const fail = () => { queued.fill(0); return false; };
133
+ // A search that picks at random among the squares with fewest digits left can be restarted when it gets lost.
134
+ const choose = random ? (alive) => {
135
+ let best = 10, count = 0, pick = -1;
136
+ for (let i = 0; i < whites.length; i += 1) {
137
+ let left = 0;
138
+ for (let d = 0; d < 9; d += 1)
139
+ left += alive[9 * i + d];
140
+ if (left < 2 || left > best)
141
+ continue;
142
+ if (left < best) {
143
+ best = left;
144
+ count = 0;
145
+ }
146
+ count += 1;
147
+ if (random() * count < 1)
148
+ pick = i;
149
+ }
150
+ return pick;
151
+ } : undefined;
152
+ return { csp: { starts, propagate, choose }, whites, index };
153
+ }
@@ -0,0 +1,3 @@
1
+ import type { KakuroBoard, KakuroRating } from "./kakuro.types.ts";
2
+ /** Rates a board with one answer by solving it: with the single-run rules, with the digit-that-must-appear rule, then by supposing. */
3
+ export declare function rateKakuro(board: KakuroBoard): KakuroRating;
@@ -0,0 +1,40 @@
1
+ import { isKakuroBoard, kakuroRuns } from "./kakuroBoard.js";
2
+ import { kakuroModel } from "./kakuroLogic.js";
3
+ import { logicCsp, openSlots, solveCsp } from "./csp.js";
4
+ const COMBOS = (length, sum) => {
5
+ let count = 0;
6
+ for (let mask = 1; mask < 512; mask += 1) {
7
+ let n = 0, total = 0;
8
+ for (let d = 1; d <= 9; d += 1)
9
+ if (mask & 1 << d - 1) {
10
+ n += 1;
11
+ total += d;
12
+ }
13
+ if (n === length && total === sum)
14
+ count += 1;
15
+ }
16
+ return count;
17
+ };
18
+ /** Rates a board with one answer by solving it: with the single-run rules, with the digit-that-must-appear rule, then by supposing. */
19
+ export function rateKakuro(board) {
20
+ if (!isKakuroBoard(board))
21
+ throw new RangeError("Invalid Kakuro board");
22
+ const weak = kakuroModel(board, 0), strong = kakuroModel(board, 1);
23
+ const proof = solveCsp(strong.csp, openSlots(strong.csp), 2, 250000);
24
+ if (proof.count !== 1 || proof.exhausted)
25
+ throw new RangeError("Kakuro rating needs a board with one answer");
26
+ const plain = logicCsp(weak.csp, openSlots(weak.csp), 0);
27
+ const rules = plain.solved ? plain : logicCsp(strong.csp, openSlots(strong.csp), 0);
28
+ const probing = rules.solved ? rules : logicCsp(strong.csp, openSlots(strong.csp), 1);
29
+ const runs = kakuroRuns(board);
30
+ return {
31
+ depth: rules.solved ? 0 : probing.solved ? 1 : 2,
32
+ plain: plain.solved,
33
+ probes: rules.solved ? 0 : probing.probes,
34
+ whites: strong.whites.length,
35
+ runs: runs.length,
36
+ longest: Math.max(...runs.map(run => run.cells.length)),
37
+ meanRun: runs.reduce((total, run) => total + run.cells.length, 0) / runs.length,
38
+ fixedShare: runs.filter(run => COMBOS(run.cells.length, run.sum) === 1).length / runs.length,
39
+ };
40
+ }
@@ -1,5 +1,7 @@
1
1
  import { KAKURO_MAX_NODES } from "./kakuro.constants.js";
2
- import { canReach, isKakuroBoard, kakuroRuns } from "./kakuroBoard.js";
2
+ import { isKakuroBoard } from "./kakuroBoard.js";
3
+ import { kakuroModel } from "./kakuroLogic.js";
4
+ import { decide, openSlots, solveCsp } from "./csp.js";
3
5
  /** Bounded exact counter over across and down runs; an interrupted search never proves uniqueness. */
4
6
  export function solveKakuro(board, entries = [], options = {}) {
5
7
  if (!isKakuroBoard(board))
@@ -9,77 +11,23 @@ export function solveKakuro(board, entries = [], options = {}) {
9
11
  throw new RangeError("Invalid search bounds");
10
12
  if (entries.length && entries.length !== board.cells.length)
11
13
  throw new RangeError("Invalid Kakuro entries");
12
- const runs = kakuroRuns(board), values = entries.length ? [...entries] : Array(board.cells.length).fill(0);
13
- const memberships = board.cells.map(() => []);
14
- runs.forEach((run, index) => run.cells.forEach(cell => memberships[cell].push(index)));
15
- for (let cell = 0; cell < values.length; cell += 1) {
16
- const value = values[cell];
14
+ const { csp, whites } = kakuroModel(board);
15
+ const start = openSlots(csp);
16
+ for (let cell = 0; cell < entries.length; cell += 1) {
17
+ const value = entries[cell];
17
18
  if (board.cells[cell].kind === "black" ? value !== 0 : !Number.isInteger(value) || value < 0 || value > 9) {
18
19
  return { count: 0, solution: null, complete: true, nodes: 0 };
19
20
  }
20
21
  }
21
- let count = 0, nodes = 0, complete = true, solution = null;
22
- const domains = (cell) => {
23
- const possible = [];
24
- for (let digit = 1; digit <= 9; digit += 1) {
25
- if (memberships[cell].every(index => runAllows(runs[index], values, cell, digit)))
26
- possible.push(digit);
27
- }
28
- return possible;
29
- };
30
- const visit = () => {
31
- if (++nodes > budget) {
32
- complete = false;
33
- return;
34
- }
35
- let chosen = -1, optionsForCell = [];
36
- for (let cell = 0; cell < values.length; cell += 1) {
37
- if (board.cells[cell].kind === "black" || values[cell])
38
- continue;
39
- const possible = domains(cell);
40
- if (!possible.length)
41
- return;
42
- if (chosen < 0 || possible.length < optionsForCell.length) {
43
- chosen = cell;
44
- optionsForCell = possible;
45
- if (possible.length === 1)
46
- break;
47
- }
48
- }
49
- if (chosen < 0) {
50
- count += 1;
51
- solution ?? (solution = [...values]);
52
- return;
53
- }
54
- for (const digit of optionsForCell) {
55
- values[chosen] = digit;
56
- visit();
57
- values[chosen] = 0;
58
- if (!complete || count >= limit) {
59
- complete = false;
60
- return;
61
- }
62
- }
63
- };
64
- // Reject contradictory entries before branching.
65
- for (const run of runs)
66
- if (!runAllows(run, values, -1, 0))
67
- return { count: 0, solution: null, complete: true, nodes: 0 };
68
- visit();
69
- return { count, solution, complete, nodes };
70
- }
71
- function runAllows(run, values, targetCell, candidate) {
72
- let sum = 0;
73
- const used = new Set();
74
- for (const cell of run.cells) {
75
- const value = cell === targetCell ? candidate : values[cell];
76
- if (!value)
77
- continue;
78
- if (used.has(value))
79
- return false;
80
- used.add(value);
81
- sum += value;
22
+ whites.forEach((cell, i) => { const value = entries[cell]; if (value)
23
+ decide(csp, start, i, 9 * i + value - 1); });
24
+ const found = solveCsp(csp, start, limit, budget);
25
+ let solution = null;
26
+ if (found.solution) {
27
+ solution = Array(board.cells.length).fill(0);
28
+ whites.forEach((cell, i) => { for (let d = 0; d < 9; d += 1)
29
+ if (found.solution[9 * i + d])
30
+ solution[cell] = d + 1; });
82
31
  }
83
- const slots = run.cells.filter(cell => !values[cell] && cell !== targetCell).length;
84
- return sum <= run.sum && canReach(run.sum - sum, slots, [...used]);
32
+ return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
85
33
  }
@@ -0,0 +1,3 @@
1
+ import type { KakuroPuzzle } from "./kakuro.types.ts";
2
+ /** The first generator, kept as the fallback that cannot fail on a 10×10: a seeded family of crossing runs, returned only when uniquely proved. */
3
+ export declare function templateKakuro(seed?: number): Omit<KakuroPuzzle, "level">;
@@ -0,0 +1,130 @@
1
+ import { KAKURO_GENERATION_ATTEMPTS, KAKURO_GENERATION_NODES, KAKURO_MAX_ATTEMPTS, KAKURO_MAX_NODES } from "./kakuro.constants.js";
2
+ import { isKakuroBoard } from "./kakuroBoard.js";
3
+ import { solveKakuro } from "./kakuroSolve.js";
4
+ import { isKazuSeed, seededRandom } from "./random.js";
5
+ /** The first generator, kept as the fallback that cannot fail on a 10×10: a seeded family of crossing runs, returned only when uniquely proved. */
6
+ export function templateKakuro(seed = 1) {
7
+ if (!isKazuSeed(seed))
8
+ throw new RangeError("Invalid Kakuro seed.");
9
+ const random = seededRandom(seed);
10
+ const budget = { attempts: KAKURO_GENERATION_ATTEMPTS, nodes: KAKURO_GENERATION_NODES };
11
+ for (let attempt = 0; attempt < KAKURO_MAX_ATTEMPTS && budget.attempts > 0 && budget.nodes > 0; attempt += 1) {
12
+ budget.attempts -= 1;
13
+ const rowGroups = random() < .5 ? [2, 2, 2] : random() < .5 ? [2, 3] : [3, 2];
14
+ const columnGroups = rowGroups.length === 3 ? [2, 2, 2] : [2, 3];
15
+ if (random() < .5)
16
+ columnGroups.reverse();
17
+ const rowStarts = groupStarts(rowGroups, random);
18
+ const columnStarts = groupStarts(columnGroups, random);
19
+ const cells = Array.from({ length: 100 }, () => ({ kind: "black", across: null, down: null }));
20
+ const solution = Array(100).fill(0);
21
+ const board = { width: 10, height: 10, cells };
22
+ let failed = false;
23
+ for (const [rowIndex, rowStart] of rowStarts.entries()) {
24
+ for (const [columnIndex, columnStart] of columnStarts.entries()) {
25
+ const shape = { height: rowGroups[rowIndex], width: columnGroups[columnIndex] };
26
+ const local = chooseUniqueTile(shape.width, shape.height, random, budget);
27
+ if (!local) {
28
+ failed = true;
29
+ break;
30
+ }
31
+ placeTile(board, solution, rowStart, columnStart, local);
32
+ }
33
+ if (failed)
34
+ break;
35
+ }
36
+ if (failed)
37
+ continue;
38
+ if (!isKakuroBoard(board))
39
+ throw new Error(JSON.stringify(board));
40
+ const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(KAKURO_MAX_NODES, budget.nodes) });
41
+ budget.nodes -= Math.min(budget.nodes, proof.nodes);
42
+ if (proof.complete && proof.count === 1 && proof.solution) {
43
+ return { ...board, seed, solution: proof.solution };
44
+ }
45
+ }
46
+ throw new Error("No uniquely solvable Kakuro board was proved within the generation budget.");
47
+ }
48
+ function groupStarts(groups, random) {
49
+ const used = groups.reduce((sum, size) => sum + size, 0) + groups.length - 1;
50
+ let at = 1 + (8 - used > 0 && random() < .5 ? 8 - used : 0);
51
+ return groups.map(size => { const start = at; at += size + 1; return start; });
52
+ }
53
+ function chooseUniqueTile(width, height, random, budget) {
54
+ for (let attempt = 0; attempt < KAKURO_MAX_ATTEMPTS && budget.attempts > 0 && budget.nodes > 0; attempt += 1) {
55
+ budget.attempts -= 1;
56
+ const candidate = randomRectangle(width, height, random);
57
+ if (!candidate)
58
+ continue;
59
+ const board = tileBoard(candidate);
60
+ const proof = solveKakuro(board, [], { limit: 2, nodes: Math.min(25000, budget.nodes) });
61
+ budget.nodes -= Math.min(budget.nodes, proof.nodes);
62
+ if (!budget.nodes)
63
+ return null;
64
+ if (proof.complete && proof.count === 1)
65
+ return proof.solution ? splitSolution(proof.solution, width, height) : candidate;
66
+ }
67
+ return null;
68
+ }
69
+ function randomRectangle(width, height, random) {
70
+ const rows = [];
71
+ for (let y = 0; y < height; y += 1) {
72
+ let row = null;
73
+ for (let attempt = 0; attempt < 100; attempt += 1) {
74
+ const available = shuffled([1, 2, 3, 4, 5, 6, 7, 8, 9], random);
75
+ const candidate = available.slice(0, width);
76
+ if (candidate.every((digit, x) => rows.every(previous => previous[x] !== digit))) {
77
+ row = candidate;
78
+ break;
79
+ }
80
+ }
81
+ if (!row)
82
+ return null;
83
+ rows.push(row);
84
+ }
85
+ return rows;
86
+ }
87
+ function tileBoard(solution) {
88
+ const height = solution.length + 1, width = solution[0].length + 1;
89
+ const cells = Array.from({ length: width * height }, () => ({ kind: "black", across: null, down: null }));
90
+ for (let y = 0; y < solution.length; y += 1)
91
+ for (let x = 0; x < solution[y].length; x += 1)
92
+ cells[(y + 1) * width + x + 1] = { kind: "white" };
93
+ for (let y = 0; y < solution.length; y += 1)
94
+ cells[(y + 1) * width].across = solution[y].reduce((sum, value) => sum + value, 0);
95
+ for (let x = 0; x < solution[0].length; x += 1)
96
+ cells[x + 1].down = solution.reduce((sum, row) => sum + row[x], 0);
97
+ return { width, height, cells };
98
+ }
99
+ function placeTile(board, full, top, left, solution) {
100
+ const width = board.width;
101
+ const cells = board.cells;
102
+ for (let y = 0; y < solution.length; y += 1)
103
+ for (let x = 0; x < solution[y].length; x += 1)
104
+ full[(top + y) * width + left + x] = solution[y][x];
105
+ for (let y = 0; y < solution.length; y += 1) {
106
+ const clue = (top + y) * width + left - 1;
107
+ cells[clue].across = solution[y].reduce((sum, value) => sum + value, 0);
108
+ }
109
+ for (let x = 0; x < solution[0].length; x += 1) {
110
+ const clue = (top - 1) * width + left + x;
111
+ cells[clue].down = solution.reduce((sum, row) => sum + row[x], 0);
112
+ }
113
+ for (let y = 0; y < solution.length; y += 1)
114
+ for (let x = 0; x < solution[y].length; x += 1)
115
+ cells[(top + y) * width + left + x] = { kind: "white" };
116
+ }
117
+ function splitSolution(values, width, height) {
118
+ const result = [];
119
+ for (let y = 1; y <= height; y += 1)
120
+ result.push(values.slice(y * (width + 1) + 1, y * (width + 1) + width + 1));
121
+ return result;
122
+ }
123
+ function shuffled(items, random) {
124
+ const result = [...items];
125
+ for (let i = result.length - 1; i > 0; i -= 1) {
126
+ const j = Math.floor(random() * (i + 1));
127
+ [result[i], result[j]] = [result[j], result[i]];
128
+ }
129
+ return result;
130
+ }
package/dist/kinds.js CHANGED
@@ -10,7 +10,7 @@ export const KAZU_KINDS = ["number-place", "jigsaw", "diagonal", "sum-cages", "m
10
10
  /** How hard a puzzle is made: what the solver needed. Easy yields to singles alone, medium to one guess, hard to whatever it takes. */
11
11
  export const KAZU_LEVELS = ["easy", "medium", "hard"];
12
12
  export const KAZU_SPECS = {
13
- "number-place": { sizes: [4, 6, 9, 16], defaultSize: 9, mostCells: 256 },
13
+ "number-place": { sizes: [4, 6, 9, 16, 25], defaultSize: 9, mostCells: 625 },
14
14
  jigsaw: { sizes: [5, 6, 7, 9], defaultSize: 7, mostCells: 162 },
15
15
  diagonal: { sizes: [6, 9], defaultSize: 9, mostCells: 81 },
16
16
  "sum-cages": { sizes: [6, 9], defaultSize: 9, mostCells: 286 },
package/dist/layout.js CHANGED
@@ -19,6 +19,7 @@ export const KAZU_BOXES = {
19
19
  6: { rows: 2, cols: 3 },
20
20
  9: { rows: 3, cols: 3 },
21
21
  16: { rows: 4, cols: 4 },
22
+ 25: { rows: 5, cols: 5 },
22
23
  };
23
24
  /** The box a cell is in, numbered row-major from 0. */
24
25
  export function boxOf(size, index) {