@johnmorrisdotca/kazu 1.0.0 → 1.2.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 (355) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +429 -5
  3. package/dist/akari-draw-entry.d.ts +2 -0
  4. package/dist/akari-draw-entry.js +1 -0
  5. package/dist/akari-entry.d.ts +12 -0
  6. package/dist/akari-entry.js +10 -0
  7. package/dist/akari-play-entry.d.ts +2 -0
  8. package/dist/akari-play-entry.js +1 -0
  9. package/dist/akari.constants.d.ts +2 -0
  10. package/dist/akari.constants.js +2 -0
  11. package/dist/akari.types.d.ts +33 -0
  12. package/dist/akari.types.js +1 -0
  13. package/dist/akariBoard.d.ts +11 -0
  14. package/dist/akariBoard.js +96 -0
  15. package/dist/akariDraw.d.ts +4 -0
  16. package/dist/akariDraw.js +38 -0
  17. package/dist/akariGame.d.ts +15 -0
  18. package/dist/akariGame.js +71 -0
  19. package/dist/akariGenerate.d.ts +7 -0
  20. package/dist/akariGenerate.js +78 -0
  21. package/dist/akariMount.d.ts +3 -0
  22. package/dist/akariMount.js +214 -0
  23. package/dist/akariPlay.types.d.ts +25 -0
  24. package/dist/akariPlay.types.js +1 -0
  25. package/dist/akariSolve.d.ts +6 -0
  26. package/dist/akariSolve.js +106 -0
  27. package/dist/akariStrings.d.ts +98 -0
  28. package/dist/akariStrings.js +53 -0
  29. package/dist/akariStyle.d.ts +1 -0
  30. package/dist/akariStyle.js +9 -0
  31. package/dist/draw-entry.d.ts +1 -0
  32. package/dist/draw-entry.js +1 -0
  33. package/dist/fillomino-draw-entry.d.ts +2 -0
  34. package/dist/fillomino-draw-entry.js +1 -0
  35. package/dist/fillomino-entry.d.ts +8 -0
  36. package/dist/fillomino-entry.js +7 -0
  37. package/dist/fillomino-play-entry.d.ts +4 -0
  38. package/dist/fillomino-play-entry.js +3 -0
  39. package/dist/fillomino.constants.d.ts +5 -0
  40. package/dist/fillomino.constants.js +5 -0
  41. package/dist/fillomino.types.d.ts +30 -0
  42. package/dist/fillomino.types.js +1 -0
  43. package/dist/fillominoBoard.d.ts +6 -0
  44. package/dist/fillominoBoard.js +84 -0
  45. package/dist/fillominoDraw.d.ts +3 -0
  46. package/dist/fillominoDraw.js +31 -0
  47. package/dist/fillominoGame.d.ts +13 -0
  48. package/dist/fillominoGame.js +65 -0
  49. package/dist/fillominoGenerate.d.ts +3 -0
  50. package/dist/fillominoGenerate.js +125 -0
  51. package/dist/fillominoMount.d.ts +2 -0
  52. package/dist/fillominoMount.js +211 -0
  53. package/dist/fillominoPlay.types.d.ts +25 -0
  54. package/dist/fillominoPlay.types.js +1 -0
  55. package/dist/fillominoSolve.d.ts +6 -0
  56. package/dist/fillominoSolve.js +111 -0
  57. package/dist/fillominoStrings.d.ts +102 -0
  58. package/dist/fillominoStrings.js +13 -0
  59. package/dist/fillominoStyle.d.ts +1 -0
  60. package/dist/fillominoStyle.js +13 -0
  61. package/dist/fillominoWorker.d.ts +1 -0
  62. package/dist/fillominoWorker.js +10 -0
  63. package/dist/heyawake-draw-entry.d.ts +2 -0
  64. package/dist/heyawake-draw-entry.js +1 -0
  65. package/dist/heyawake-entry.d.ts +13 -0
  66. package/dist/heyawake-entry.js +11 -0
  67. package/dist/heyawake-play-entry.d.ts +2 -0
  68. package/dist/heyawake-play-entry.js +1 -0
  69. package/dist/heyawake.constants.d.ts +5 -0
  70. package/dist/heyawake.constants.js +5 -0
  71. package/dist/heyawake.types.d.ts +35 -0
  72. package/dist/heyawake.types.js +1 -0
  73. package/dist/heyawakeBoard.d.ts +7 -0
  74. package/dist/heyawakeBoard.js +116 -0
  75. package/dist/heyawakeDraw.d.ts +3 -0
  76. package/dist/heyawakeDraw.js +27 -0
  77. package/dist/heyawakeGame.d.ts +12 -0
  78. package/dist/heyawakeGame.js +51 -0
  79. package/dist/heyawakeGenerate.d.ts +2 -0
  80. package/dist/heyawakeGenerate.js +114 -0
  81. package/dist/heyawakeMount.d.ts +2 -0
  82. package/dist/heyawakeMount.js +106 -0
  83. package/dist/heyawakePlay.types.d.ts +24 -0
  84. package/dist/heyawakePlay.types.js +1 -0
  85. package/dist/heyawakeSolve.d.ts +5 -0
  86. package/dist/heyawakeSolve.js +52 -0
  87. package/dist/heyawakeStrings.d.ts +19 -0
  88. package/dist/heyawakeStrings.js +7 -0
  89. package/dist/heyawakeStyle.d.ts +1 -0
  90. package/dist/heyawakeStyle.js +2 -0
  91. package/dist/heyawakeWorker.d.ts +1 -0
  92. package/dist/heyawakeWorker.js +10 -0
  93. package/dist/hitori-draw-entry.d.ts +2 -0
  94. package/dist/hitori-draw-entry.js +1 -0
  95. package/dist/hitori-entry.d.ts +6 -0
  96. package/dist/hitori-entry.js +6 -0
  97. package/dist/hitori-play-entry.d.ts +4 -0
  98. package/dist/hitori-play-entry.js +3 -0
  99. package/dist/hitori.constants.d.ts +2 -0
  100. package/dist/hitori.constants.js +2 -0
  101. package/dist/hitori.types.d.ts +20 -0
  102. package/dist/hitori.types.js +1 -0
  103. package/dist/hitoriBoard.d.ts +8 -0
  104. package/dist/hitoriBoard.js +72 -0
  105. package/dist/hitoriDraw.d.ts +4 -0
  106. package/dist/hitoriDraw.js +25 -0
  107. package/dist/hitoriGame.d.ts +10 -0
  108. package/dist/hitoriGame.js +48 -0
  109. package/dist/hitoriGenerate.d.ts +3 -0
  110. package/dist/hitoriGenerate.js +163 -0
  111. package/dist/hitoriMount.d.ts +3 -0
  112. package/dist/hitoriMount.js +111 -0
  113. package/dist/hitoriPlay.types.d.ts +25 -0
  114. package/dist/hitoriPlay.types.js +1 -0
  115. package/dist/hitoriSolve.d.ts +6 -0
  116. package/dist/hitoriSolve.js +69 -0
  117. package/dist/hitoriStrings.d.ts +70 -0
  118. package/dist/hitoriStrings.js +17 -0
  119. package/dist/hitoriStyle.d.ts +1 -0
  120. package/dist/hitoriStyle.js +9 -0
  121. package/dist/index.d.ts +12 -0
  122. package/dist/index.js +12 -0
  123. package/dist/juosan-draw-entry.d.ts +2 -0
  124. package/dist/juosan-draw-entry.js +1 -0
  125. package/dist/juosan-entry.d.ts +11 -0
  126. package/dist/juosan-entry.js +10 -0
  127. package/dist/juosan-play-entry.d.ts +4 -0
  128. package/dist/juosan-play-entry.js +3 -0
  129. package/dist/juosan.constants.d.ts +4 -0
  130. package/dist/juosan.constants.js +4 -0
  131. package/dist/juosan.types.d.ts +33 -0
  132. package/dist/juosan.types.js +1 -0
  133. package/dist/juosanBoard.d.ts +7 -0
  134. package/dist/juosanBoard.js +102 -0
  135. package/dist/juosanDraw.d.ts +4 -0
  136. package/dist/juosanDraw.js +49 -0
  137. package/dist/juosanGame.d.ts +20 -0
  138. package/dist/juosanGame.js +86 -0
  139. package/dist/juosanGenerate.d.ts +3 -0
  140. package/dist/juosanGenerate.js +31 -0
  141. package/dist/juosanMount.d.ts +3 -0
  142. package/dist/juosanMount.js +206 -0
  143. package/dist/juosanPlay.types.d.ts +24 -0
  144. package/dist/juosanPlay.types.js +1 -0
  145. package/dist/juosanSolve.d.ts +3 -0
  146. package/dist/juosanSolve.js +82 -0
  147. package/dist/juosanStrings.d.ts +77 -0
  148. package/dist/juosanStrings.js +43 -0
  149. package/dist/juosanStyle.d.ts +1 -0
  150. package/dist/juosanStyle.js +1 -0
  151. package/dist/kakuro-draw-entry.d.ts +2 -0
  152. package/dist/kakuro-draw-entry.js +1 -0
  153. package/dist/kakuro-entry.d.ts +12 -0
  154. package/dist/kakuro-entry.js +10 -0
  155. package/dist/kakuro-play-entry.d.ts +4 -0
  156. package/dist/kakuro-play-entry.js +3 -0
  157. package/dist/kakuro.constants.d.ts +6 -0
  158. package/dist/kakuro.constants.js +6 -0
  159. package/dist/kakuro.types.d.ts +46 -0
  160. package/dist/kakuro.types.js +1 -0
  161. package/dist/kakuroBoard.d.ts +8 -0
  162. package/dist/kakuroBoard.js +117 -0
  163. package/dist/kakuroDraw.d.ts +10 -0
  164. package/dist/kakuroDraw.js +21 -0
  165. package/dist/kakuroGame.d.ts +12 -0
  166. package/dist/kakuroGame.js +74 -0
  167. package/dist/kakuroGenerate.d.ts +3 -0
  168. package/dist/kakuroGenerate.js +130 -0
  169. package/dist/kakuroMount.d.ts +2 -0
  170. package/dist/kakuroMount.js +109 -0
  171. package/dist/kakuroPlay.types.d.ts +20 -0
  172. package/dist/kakuroPlay.types.js +1 -0
  173. package/dist/kakuroSolve.d.ts +6 -0
  174. package/dist/kakuroSolve.js +85 -0
  175. package/dist/kakuroStrings.d.ts +82 -0
  176. package/dist/kakuroStrings.js +5 -0
  177. package/dist/kakuroStyle.d.ts +1 -0
  178. package/dist/kakuroStyle.js +1 -0
  179. package/dist/masyu-draw-entry.d.ts +2 -0
  180. package/dist/masyu-draw-entry.js +1 -0
  181. package/dist/masyu-entry.d.ts +6 -0
  182. package/dist/masyu-entry.js +6 -0
  183. package/dist/masyu-play-entry.d.ts +4 -0
  184. package/dist/masyu-play-entry.js +3 -0
  185. package/dist/masyu.constants.d.ts +2 -0
  186. package/dist/masyu.constants.js +2 -0
  187. package/dist/masyu.types.d.ts +46 -0
  188. package/dist/masyu.types.js +1 -0
  189. package/dist/masyuBoard.d.ts +10 -0
  190. package/dist/masyuBoard.js +88 -0
  191. package/dist/masyuDraw.d.ts +2 -0
  192. package/dist/masyuDraw.js +33 -0
  193. package/dist/masyuGame.d.ts +9 -0
  194. package/dist/masyuGame.js +46 -0
  195. package/dist/masyuGenerate.d.ts +3 -0
  196. package/dist/masyuGenerate.js +56 -0
  197. package/dist/masyuMount.d.ts +2 -0
  198. package/dist/masyuMount.js +118 -0
  199. package/dist/masyuPlay.types.d.ts +1 -0
  200. package/dist/masyuPlay.types.js +1 -0
  201. package/dist/masyuSolve.d.ts +6 -0
  202. package/dist/masyuSolve.js +88 -0
  203. package/dist/masyuStrings.d.ts +28 -0
  204. package/dist/masyuStrings.js +4 -0
  205. package/dist/masyuStyle.d.ts +1 -0
  206. package/dist/masyuStyle.js +11 -0
  207. package/dist/nurikabe-draw-entry.d.ts +2 -0
  208. package/dist/nurikabe-draw-entry.js +1 -0
  209. package/dist/nurikabe-entry.d.ts +6 -0
  210. package/dist/nurikabe-entry.js +6 -0
  211. package/dist/nurikabe-play-entry.d.ts +4 -0
  212. package/dist/nurikabe-play-entry.js +3 -0
  213. package/dist/nurikabe.constants.d.ts +2 -0
  214. package/dist/nurikabe.constants.js +2 -0
  215. package/dist/nurikabe.types.d.ts +20 -0
  216. package/dist/nurikabe.types.js +1 -0
  217. package/dist/nurikabeBoard.d.ts +8 -0
  218. package/dist/nurikabeBoard.js +72 -0
  219. package/dist/nurikabeDraw.d.ts +4 -0
  220. package/dist/nurikabeDraw.js +15 -0
  221. package/dist/nurikabeGame.d.ts +8 -0
  222. package/dist/nurikabeGame.js +40 -0
  223. package/dist/nurikabeGenerate.d.ts +3 -0
  224. package/dist/nurikabeGenerate.js +31 -0
  225. package/dist/nurikabeMount.d.ts +3 -0
  226. package/dist/nurikabeMount.js +99 -0
  227. package/dist/nurikabePlay.types.d.ts +25 -0
  228. package/dist/nurikabePlay.types.js +1 -0
  229. package/dist/nurikabeSolve.d.ts +6 -0
  230. package/dist/nurikabeSolve.js +75 -0
  231. package/dist/nurikabeStrings.d.ts +70 -0
  232. package/dist/nurikabeStrings.js +5 -0
  233. package/dist/nurikabeStyle.d.ts +1 -0
  234. package/dist/nurikabeStyle.js +9 -0
  235. package/dist/play-entry.d.ts +1 -0
  236. package/dist/play-entry.js +1 -0
  237. package/dist/ripple-draw-entry.d.ts +2 -0
  238. package/dist/ripple-draw-entry.js +1 -0
  239. package/dist/ripple-entry.d.ts +12 -0
  240. package/dist/ripple-entry.js +11 -0
  241. package/dist/ripple-play-entry.d.ts +4 -0
  242. package/dist/ripple-play-entry.js +3 -0
  243. package/dist/ripple.constants.d.ts +4 -0
  244. package/dist/ripple.constants.js +4 -0
  245. package/dist/ripple.types.d.ts +34 -0
  246. package/dist/ripple.types.js +1 -0
  247. package/dist/rippleBoard.d.ts +7 -0
  248. package/dist/rippleBoard.js +99 -0
  249. package/dist/rippleDraw.d.ts +3 -0
  250. package/dist/rippleDraw.js +32 -0
  251. package/dist/rippleGame.d.ts +13 -0
  252. package/dist/rippleGame.js +85 -0
  253. package/dist/rippleGenerate.d.ts +3 -0
  254. package/dist/rippleGenerate.js +57 -0
  255. package/dist/rippleMount.d.ts +2 -0
  256. package/dist/rippleMount.js +179 -0
  257. package/dist/ripplePlay.types.d.ts +23 -0
  258. package/dist/ripplePlay.types.js +1 -0
  259. package/dist/rippleSolve.d.ts +6 -0
  260. package/dist/rippleSolve.js +83 -0
  261. package/dist/rippleStrings.d.ts +86 -0
  262. package/dist/rippleStrings.js +5 -0
  263. package/dist/rippleStyle.d.ts +1 -0
  264. package/dist/rippleStyle.js +1 -0
  265. package/dist/shikaku-draw-entry.d.ts +2 -0
  266. package/dist/shikaku-draw-entry.js +1 -0
  267. package/dist/shikaku-entry.d.ts +8 -0
  268. package/dist/shikaku-entry.js +8 -0
  269. package/dist/shikaku-play-entry.d.ts +4 -0
  270. package/dist/shikaku-play-entry.js +3 -0
  271. package/dist/shikaku.constants.d.ts +5 -0
  272. package/dist/shikaku.constants.js +5 -0
  273. package/dist/shikaku.types.d.ts +36 -0
  274. package/dist/shikaku.types.js +1 -0
  275. package/dist/shikakuBoard.d.ts +9 -0
  276. package/dist/shikakuBoard.js +59 -0
  277. package/dist/shikakuDraw.d.ts +4 -0
  278. package/dist/shikakuDraw.js +23 -0
  279. package/dist/shikakuGame.d.ts +14 -0
  280. package/dist/shikakuGame.js +61 -0
  281. package/dist/shikakuGenerate.d.ts +3 -0
  282. package/dist/shikakuGenerate.js +43 -0
  283. package/dist/shikakuMount.d.ts +3 -0
  284. package/dist/shikakuMount.js +143 -0
  285. package/dist/shikakuPacks.d.ts +27 -0
  286. package/dist/shikakuPacks.js +26 -0
  287. package/dist/shikakuPlay.types.d.ts +26 -0
  288. package/dist/shikakuPlay.types.js +1 -0
  289. package/dist/shikakuSolve.d.ts +6 -0
  290. package/dist/shikakuSolve.js +54 -0
  291. package/dist/shikakuStrings.d.ts +102 -0
  292. package/dist/shikakuStrings.js +5 -0
  293. package/dist/shikakuStyle.d.ts +1 -0
  294. package/dist/shikakuStyle.js +12 -0
  295. package/dist/shikakuWorker.d.ts +1 -0
  296. package/dist/shikakuWorker.js +10 -0
  297. package/dist/slitherlink-draw-entry.d.ts +2 -0
  298. package/dist/slitherlink-draw-entry.js +1 -0
  299. package/dist/slitherlink-entry.d.ts +11 -0
  300. package/dist/slitherlink-entry.js +9 -0
  301. package/dist/slitherlink-play-entry.d.ts +4 -0
  302. package/dist/slitherlink-play-entry.js +3 -0
  303. package/dist/slitherlink.constants.d.ts +2 -0
  304. package/dist/slitherlink.constants.js +2 -0
  305. package/dist/slitherlink.types.d.ts +34 -0
  306. package/dist/slitherlink.types.js +1 -0
  307. package/dist/slitherlinkBoard.d.ts +12 -0
  308. package/dist/slitherlinkBoard.js +132 -0
  309. package/dist/slitherlinkDraw.d.ts +4 -0
  310. package/dist/slitherlinkDraw.js +51 -0
  311. package/dist/slitherlinkGame.d.ts +8 -0
  312. package/dist/slitherlinkGame.js +63 -0
  313. package/dist/slitherlinkGenerate.d.ts +3 -0
  314. package/dist/slitherlinkGenerate.js +101 -0
  315. package/dist/slitherlinkMount.d.ts +3 -0
  316. package/dist/slitherlinkMount.js +267 -0
  317. package/dist/slitherlinkPlay.types.d.ts +23 -0
  318. package/dist/slitherlinkPlay.types.js +1 -0
  319. package/dist/slitherlinkSolve.d.ts +6 -0
  320. package/dist/slitherlinkSolve.js +127 -0
  321. package/dist/slitherlinkStrings.d.ts +86 -0
  322. package/dist/slitherlinkStrings.js +47 -0
  323. package/dist/slitherlinkStyle.d.ts +1 -0
  324. package/dist/slitherlinkStyle.js +18 -0
  325. package/dist/version.d.ts +1 -1
  326. package/dist/version.js +1 -1
  327. package/dist/yajilin-draw-entry.d.ts +2 -0
  328. package/dist/yajilin-draw-entry.js +1 -0
  329. package/dist/yajilin-entry.d.ts +6 -0
  330. package/dist/yajilin-entry.js +6 -0
  331. package/dist/yajilin-play-entry.d.ts +4 -0
  332. package/dist/yajilin-play-entry.js +3 -0
  333. package/dist/yajilin.constants.d.ts +2 -0
  334. package/dist/yajilin.constants.js +2 -0
  335. package/dist/yajilin.types.d.ts +63 -0
  336. package/dist/yajilin.types.js +1 -0
  337. package/dist/yajilinBoard.d.ts +9 -0
  338. package/dist/yajilinBoard.js +91 -0
  339. package/dist/yajilinDraw.d.ts +2 -0
  340. package/dist/yajilinDraw.js +33 -0
  341. package/dist/yajilinGame.d.ts +15 -0
  342. package/dist/yajilinGame.js +58 -0
  343. package/dist/yajilinGenerate.d.ts +3 -0
  344. package/dist/yajilinGenerate.js +224 -0
  345. package/dist/yajilinMount.d.ts +2 -0
  346. package/dist/yajilinMount.js +162 -0
  347. package/dist/yajilinPlay.types.d.ts +1 -0
  348. package/dist/yajilinPlay.types.js +1 -0
  349. package/dist/yajilinSolve.d.ts +6 -0
  350. package/dist/yajilinSolve.js +82 -0
  351. package/dist/yajilinStrings.d.ts +32 -0
  352. package/dist/yajilinStrings.js +4 -0
  353. package/dist/yajilinStyle.d.ts +1 -0
  354. package/dist/yajilinStyle.js +12 -0
  355. package/package.json +196 -3
@@ -0,0 +1,117 @@
1
+ import { KAKURO_MAX_SIDE, KAKURO_MIN_SIDE } from "./kakuro.constants.js";
2
+ export function isKakuroBoard(value) {
3
+ if (!value || typeof value !== "object")
4
+ return false;
5
+ const board = value;
6
+ if (!Number.isInteger(board.width) || board.width < KAKURO_MIN_SIDE || board.width > KAKURO_MAX_SIDE
7
+ || !Number.isInteger(board.height) || board.height < KAKURO_MIN_SIDE || board.height > KAKURO_MAX_SIDE
8
+ || !Array.isArray(board.cells) || board.cells.length !== board.width * board.height
9
+ || board.cells.some(cell => !validCell(cell)))
10
+ return false;
11
+ const runs = collectRuns(board);
12
+ if (!runs)
13
+ return false;
14
+ return runs.length > 0 && runs.every(run => {
15
+ const length = run.cells.length;
16
+ const min = length * (length + 1) / 2;
17
+ const max = length * (19 - length) / 2;
18
+ return length >= 2 && length <= 9 && run.sum >= min && run.sum <= max;
19
+ });
20
+ }
21
+ function validCell(cell) {
22
+ if (!cell || typeof cell !== "object")
23
+ return false;
24
+ if (cell.kind === "white")
25
+ return true;
26
+ return cell.kind === "black" && [cell.across, cell.down].every(value => value === null
27
+ || Number.isInteger(value) && value >= 3 && value <= 45);
28
+ }
29
+ export function kakuroRuns(board) {
30
+ if (!isKakuroBoard(board))
31
+ throw new RangeError("Invalid Kakuro board");
32
+ return collectRuns(board);
33
+ }
34
+ function collectRuns(board) {
35
+ const runs = [];
36
+ const acrossCount = Array(board.cells.length).fill(0), downCount = Array(board.cells.length).fill(0);
37
+ for (let cell = 0; cell < board.cells.length; cell += 1) {
38
+ const x = cell % board.width, y = Math.floor(cell / board.width), tile = board.cells[cell];
39
+ if (tile.kind === "white")
40
+ continue;
41
+ const rightWhite = x + 1 < board.width && board.cells[cell + 1]?.kind === "white";
42
+ const downWhite = y + 1 < board.height && board.cells[cell + board.width]?.kind === "white";
43
+ if ((tile.across !== null) !== rightWhite || (tile.down !== null) !== downWhite)
44
+ return null;
45
+ if (rightWhite) {
46
+ const cells = [];
47
+ for (let at = cell + 1; at < (y + 1) * board.width && board.cells[at]?.kind === "white"; at += 1)
48
+ cells.push(at);
49
+ if (cells.length < 2 || cells.length > 9)
50
+ return null;
51
+ cells.forEach(at => { acrossCount[at] += 1; });
52
+ runs.push({ direction: "across", clueCell: cell, cells, sum: tile.across });
53
+ }
54
+ if (downWhite) {
55
+ const cells = [];
56
+ for (let at = cell + board.width; at < board.cells.length && board.cells[at]?.kind === "white"; at += board.width)
57
+ cells.push(at);
58
+ if (cells.length < 2 || cells.length > 9)
59
+ return null;
60
+ cells.forEach(at => { downCount[at] += 1; });
61
+ runs.push({ direction: "down", clueCell: cell, cells, sum: tile.down });
62
+ }
63
+ }
64
+ for (let cell = 0; cell < board.cells.length; cell += 1) {
65
+ if (board.cells[cell].kind === "white" && (acrossCount[cell] !== 1 || downCount[cell] !== 1))
66
+ return null;
67
+ }
68
+ return runs;
69
+ }
70
+ /** Checks exact run sums and distinct digits independently of a stored answer. */
71
+ export function checkKakuro(board, values) {
72
+ if (!isKakuroBoard(board))
73
+ throw new RangeError("Invalid Kakuro board");
74
+ if (values.length !== board.cells.length)
75
+ throw new RangeError("Invalid Kakuro values");
76
+ const errors = new Set(), runs = kakuroRuns(board);
77
+ values.forEach((value, cell) => {
78
+ if (board.cells[cell].kind === "black" ? value !== 0 : !Number.isInteger(value) || value < 0 || value > 9)
79
+ errors.add(cell);
80
+ });
81
+ for (const run of runs) {
82
+ const filled = run.cells.map(cell => values[cell]).filter(value => value > 0);
83
+ const duplicate = new Set(filled.filter((value, index) => filled.indexOf(value) !== index));
84
+ const sum = filled.reduce((total, value) => total + value, 0);
85
+ const missing = run.cells.length - filled.length;
86
+ if (duplicate.size || sum > run.sum || !canReach(run.sum - sum, missing, filled))
87
+ run.cells.forEach(cell => errors.add(cell));
88
+ if (missing === 0 && sum !== run.sum)
89
+ run.cells.forEach(cell => errors.add(cell));
90
+ }
91
+ const complete = board.cells.every((cell, index) => cell.kind === "black" || values[index] > 0);
92
+ return { ok: complete && errors.size === 0, complete, errors: [...errors].sort((a, b) => a - b) };
93
+ }
94
+ /** Allows unfinished runs while flagging duplicate digits and impossible remaining totals. */
95
+ export function progressKakuro(board, values) {
96
+ const result = checkKakuro(board, values);
97
+ return { ...result, ok: result.errors.length === 0 };
98
+ }
99
+ export function canReach(target, slots, used) {
100
+ if (slots === 0)
101
+ return target === 0;
102
+ if (target < 1 || slots > 9)
103
+ return false;
104
+ const mask = used.reduce((bits, value) => bits | (1 << value), 0);
105
+ const visit = (next, remaining, count) => {
106
+ if (count === 0)
107
+ return remaining === 0;
108
+ for (let digit = next; digit <= 9; digit += 1) {
109
+ if (mask & (1 << digit))
110
+ continue;
111
+ if (visit(digit + 1, remaining - digit, count - 1))
112
+ return true;
113
+ }
114
+ return false;
115
+ };
116
+ return visit(1, target, slots);
117
+ }
@@ -0,0 +1,10 @@
1
+ import type { KakuroBoard, KakuroMaterial, KakuroPieces } from "./kakuro.types.ts";
2
+ export type KakuroDrawOptions = {
3
+ values?: readonly number[];
4
+ notes?: readonly (readonly number[])[];
5
+ selected?: number;
6
+ material?: KakuroMaterial;
7
+ pieces?: KakuroPieces;
8
+ errors?: readonly number[];
9
+ };
10
+ export declare function drawKakuro(board: KakuroBoard, options?: KakuroDrawOptions): string;
@@ -0,0 +1,21 @@
1
+ import { isKakuroBoard } from "./kakuroBoard.js";
2
+ export function drawKakuro(board, options = {}) {
3
+ if (!isKakuroBoard(board))
4
+ throw new RangeError("Invalid Kakuro board");
5
+ const size = 56, paper = options.material === "slate" ? "#262a27" : options.material === "wood" ? "#e5cda6" : "var(--kz-paper,#fbf8f1)";
6
+ const ink = options.material === "slate" ? "#ece8dc" : options.material === "wood" ? "#493e2f" : "var(--kz-ink,#1f2320)";
7
+ const grid = options.material === "slate" ? "#454a44" : "var(--kz-grid,#cfc6b2)";
8
+ const cells = board.cells.map((tile, cell) => {
9
+ const x = cell % board.width * size, y = Math.floor(cell / board.width) * size;
10
+ const white = tile.kind === "white", value = options.values?.[cell] ?? 0;
11
+ const mark = options.errors?.includes(cell) ? "var(--kz-bad,#b5452c)" : ink;
12
+ const bg = white ? options.selected === cell ? "var(--kz-selected,#e9dfca)" : paper : "#333630";
13
+ const clue = white ? "" : `<path d="M${x},${y + size} L${x + size},${y} M${x + 6},${y + size - 6} L${x + size - 6},${y + 6}" stroke="#858174"/><text x="${x + size - 7}" y="${y + 16}" text-anchor="end" fill="#fff" font-size="15" aria-label="Across ${tile.across ?? ""}">${tile.across ?? ""}</text><text x="${x + 7}" y="${y + size - 7}" fill="#fff" font-size="15" aria-label="Down ${tile.down ?? ""}">${tile.down ?? ""}</text>`;
14
+ const notes = !white || value ? "" : (options.notes?.[cell] ?? []).map((n, index) => `<text x="${x + 8 + index % 3 * 15}" y="${y + 17 + Math.floor(index / 3) * 14}" fill="${ink}" font-size="10">${n}</text>`).join("");
15
+ const piece = value && options.pieces === "tiles"
16
+ ? `<rect x="${x + 10}" y="${y + 8}" width="36" height="40" rx="8" fill="${paper}" stroke="${grid}"/>`
17
+ : "";
18
+ return `<g data-kakuro-cell="${cell}"><rect x="${x}" y="${y}" width="${size}" height="${size}" fill="${bg}" stroke="${grid}"/><title>${white ? `Cell ${Math.floor(cell / board.width) + 1}, ${cell % board.width + 1}` : `Across ${tile.across ?? "—"}; down ${tile.down ?? "—"}`}</title>${clue}${piece}${value ? `<text x="${x + size / 2}" y="${y + size / 2 + 7}" text-anchor="middle" fill="${mark}" font-size="27" font-weight="700">${value}</text>` : notes}</g>`;
19
+ }).join("");
20
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${board.width * size} ${board.height * size}" role="img" aria-label="Kakuro crossword sum grid" style="display:block;width:100%;height:auto;background:${paper}">${cells}<rect width="${board.width * size}" height="${board.height * size}" fill="none" stroke="var(--kz-frame,#a98954)" stroke-width="3"/></svg>`;
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { KakuroBoard, KakuroGame } from "./kakuro.types.ts";
2
+ export declare function newKakuro(board: KakuroBoard): KakuroGame;
3
+ export declare function enterKakuro(game: KakuroGame, cell: number, digit: number, pencil?: boolean): KakuroGame;
4
+ export declare function undoKakuro(game: KakuroGame): KakuroGame;
5
+ export declare function restartKakuro(game: KakuroGame): KakuroGame;
6
+ export declare function finishedKakuro(game: KakuroGame): boolean;
7
+ export declare function hintKakuro(game: KakuroGame): {
8
+ cell: number;
9
+ digit: number;
10
+ } | null;
11
+ export declare function encodeKakuro(game: KakuroGame): string;
12
+ export declare function decodeKakuro(code: string): KakuroGame | null;
@@ -0,0 +1,74 @@
1
+ import { checkKakuro, isKakuroBoard } from "./kakuroBoard.js";
2
+ import { solveKakuro } from "./kakuroSolve.js";
3
+ export function newKakuro(board) {
4
+ if (!isKakuroBoard(board))
5
+ throw new RangeError("Invalid Kakuro board");
6
+ return { board: { width: board.width, height: board.height, cells: board.cells.map(cell => ({ ...cell })) },
7
+ values: Array(board.cells.length).fill(0), notes: board.cells.map(() => []), history: [], helped: false };
8
+ }
9
+ export function enterKakuro(game, cell, digit, pencil = false) {
10
+ if (!Number.isInteger(cell) || cell < 0 || cell >= game.values.length || game.board.cells[cell].kind !== "white"
11
+ || !Number.isInteger(digit) || digit < 0 || digit > 9)
12
+ return game;
13
+ const values = [...game.values], notes = game.notes.map(mark => [...mark]);
14
+ if (pencil) {
15
+ if (!digit)
16
+ return game;
17
+ const index = notes[cell].indexOf(digit);
18
+ if (index < 0)
19
+ notes[cell].push(digit);
20
+ else
21
+ notes[cell].splice(index, 1);
22
+ notes[cell].sort((a, b) => a - b);
23
+ }
24
+ else {
25
+ values[cell] = digit;
26
+ notes[cell] = [];
27
+ }
28
+ return { ...game, values, notes, history: [...game.history, snapshot(game)] };
29
+ }
30
+ export function undoKakuro(game) {
31
+ const previous = game.history.at(-1);
32
+ return previous ? { ...game, ...previous, history: game.history.slice(0, -1) } : game;
33
+ }
34
+ export function restartKakuro(game) { return newKakuro(game.board); }
35
+ export function finishedKakuro(game) { return checkKakuro(game.board, game.values).ok; }
36
+ export function hintKakuro(game) {
37
+ const proof = solveKakuro(game.board, game.values, { limit: 2 });
38
+ if (!proof.complete || proof.count !== 1 || !proof.solution)
39
+ return null;
40
+ const cell = game.board.cells.findIndex((tile, at) => tile.kind === "white" && game.values[at] === 0);
41
+ return cell < 0 ? null : { cell, digit: proof.solution[cell] };
42
+ }
43
+ export function encodeKakuro(game) {
44
+ return JSON.stringify({ version: 1, board: game.board, values: game.values, notes: game.notes, helped: game.helped });
45
+ }
46
+ export function decodeKakuro(code) {
47
+ try {
48
+ if (code.length > 40000)
49
+ return null;
50
+ const value = JSON.parse(code);
51
+ if (value.version !== 1 || !isKakuroBoard(value.board) || typeof value.helped !== "boolean"
52
+ || !Array.isArray(value.values) || value.values.length !== value.board.cells.length
53
+ || !Array.isArray(value.notes) || value.notes.length !== value.board.cells.length)
54
+ return null;
55
+ let game = newKakuro(value.board);
56
+ for (let cell = 0; cell < value.values.length; cell += 1) {
57
+ const digit = value.values[cell];
58
+ if (!Number.isInteger(digit) || digit < 0 || digit > 9 || value.board.cells[cell].kind === "black" && digit !== 0)
59
+ return null;
60
+ if (digit)
61
+ game = enterKakuro(game, cell, digit);
62
+ const marks = value.notes[cell];
63
+ if (!Array.isArray(marks) || marks.some(n => !Number.isInteger(n) || n < 1 || n > 9) || new Set(marks).size !== marks.length)
64
+ return null;
65
+ for (const mark of marks)
66
+ game = enterKakuro(game, cell, mark, true);
67
+ }
68
+ return { ...game, history: [], helped: value.helped };
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function snapshot(game) { return { values: [...game.values], notes: game.notes.map(mark => [...mark]) }; }
@@ -0,0 +1,3 @@
1
+ import type { KakuroPuzzle } from "./kakuro.types.ts";
2
+ /** Makes a seeded 10×10 family of crossing runs and returns only a uniquely proved puzzle. */
3
+ export declare function generateKakuro(seed?: number): KakuroPuzzle;
@@ -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
+ /** 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 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
+ }
@@ -0,0 +1,2 @@
1
+ import type { KakuroMount, KakuroMountOptions } from "./kakuroPlay.types.ts";
2
+ export declare function mountKakuro(host: HTMLElement, initial: KakuroMountOptions): KakuroMount;
@@ -0,0 +1,109 @@
1
+ import { progressKakuro } from "./kakuroBoard.js";
2
+ import { drawKakuro } from "./kakuroDraw.js";
3
+ import { decodeKakuro, encodeKakuro, enterKakuro, finishedKakuro, hintKakuro, newKakuro, undoKakuro } from "./kakuroGame.js";
4
+ import { KAKURO_PLAY_STYLE } from "./kakuroStyle.js";
5
+ import { kakuroWords } from "./kakuroStrings.js";
6
+ export function mountKakuro(host, initial) {
7
+ const doc = host.ownerDocument;
8
+ let options = { ...initial };
9
+ const words = () => kakuroWords(options.language);
10
+ let game = initial.progress ? decodeKakuro(initial.progress) ?? newKakuro(initial.board) : newKakuro(initial.board);
11
+ let selected = game.board.cells.findIndex(cell => cell.kind === "white"), pencil = false, message = "", dead = false;
12
+ const root = doc.createElement("div"), style = doc.createElement("style"), board = doc.createElement("div"), tools = doc.createElement("div"), pad = doc.createElement("div"), status = doc.createElement("p"), help = doc.createElement("p"), grid = doc.createElement("div");
13
+ root.className = "kk-root";
14
+ style.textContent = KAKURO_PLAY_STYLE;
15
+ board.className = "kk-board";
16
+ tools.className = "kk-tools";
17
+ pad.className = "kk-pad";
18
+ pad.setAttribute("role", "group");
19
+ status.className = "kk-status";
20
+ status.setAttribute("role", "status");
21
+ help.className = "kk-help";
22
+ grid.className = "kk-cells";
23
+ board.append(doc.createElement("div"), grid);
24
+ root.append(style, board, pad, status, tools, help);
25
+ host.append(root);
26
+ const copy = () => ({ ...game, board: { ...game.board, cells: game.board.cells.map(cell => ({ ...cell })) }, values: [...game.values], notes: game.notes.map(mark => [...mark]), history: [] });
27
+ const update = (next) => { game = next; render(); options.onChange?.(copy()); if (finishedKakuro(game))
28
+ options.onFinish?.(copy()); };
29
+ const button = (label, action) => { const item = doc.createElement("button"); item.type = "button"; item.textContent = label; item.onclick = action; tools.append(item); };
30
+ const render = () => {
31
+ if (dead)
32
+ return;
33
+ const w = words(), b = game.board, focus = grid.contains(doc.activeElement);
34
+ board.firstElementChild.innerHTML = drawKakuro(b, { values: game.values, notes: game.notes, selected, material: options.material, pieces: options.pieces });
35
+ grid.style.gridTemplateColumns = `repeat(${b.width},1fr)`;
36
+ grid.replaceChildren();
37
+ b.cells.forEach((cell, index) => {
38
+ const button = doc.createElement("button");
39
+ button.type = "button";
40
+ button.className = "kk-cell";
41
+ button.disabled = cell.kind === "black";
42
+ button.tabIndex = selected === index ? 0 : -1;
43
+ const row = Math.floor(index / b.width) + 1, column = index % b.width + 1;
44
+ const tile = cell.kind === "black" ? `${w.across} ${cell.across ?? "—"}, ${w.down} ${cell.down ?? "—"}` : game.values[index] || game.notes[index].length ? `value ${game.values[index] || game.notes[index].join(",")}` : w.blank;
45
+ button.setAttribute("aria-label", `${w.row} ${row}, ${w.column} ${column}: ${tile}`);
46
+ button.onclick = () => { selected = index; render(); };
47
+ button.onkeydown = event => {
48
+ const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -b.width, ArrowDown: b.width };
49
+ if (event.key in step) {
50
+ event.preventDefault();
51
+ let next = index;
52
+ do {
53
+ next += step[event.key];
54
+ } while (next >= 0 && next < b.cells.length && b.cells[next].kind === "black");
55
+ if (next >= 0 && next < b.cells.length)
56
+ selected = next;
57
+ render();
58
+ }
59
+ else if (/^[1-9]$/.test(event.key)) {
60
+ event.preventDefault();
61
+ update(enterKakuro(game, index, Number(event.key), pencil));
62
+ }
63
+ else if (event.key === "0" || event.key === "Backspace" || event.key === "Delete") {
64
+ event.preventDefault();
65
+ update(enterKakuro(game, index, 0));
66
+ }
67
+ else if (event.key.toLowerCase() === "p") {
68
+ event.preventDefault();
69
+ pencil = !pencil;
70
+ render();
71
+ }
72
+ };
73
+ grid.append(button);
74
+ });
75
+ if (focus)
76
+ grid.querySelector(`button[aria-label^="${w.row} ${Math.floor(selected / b.width) + 1}, ${w.column} ${selected % b.width + 1}:"]`)?.focus();
77
+ tools.replaceChildren();
78
+ button(w.undo, () => update(undoKakuro(game)));
79
+ button(w.pencil, () => { pencil = !pencil; render(); });
80
+ button(w.hint, () => { const hint = hintKakuro(game); if (!hint) {
81
+ message = w.wrong;
82
+ render();
83
+ return;
84
+ } update({ ...enterKakuro(game, hint.cell, hint.digit), helped: true }); message = `${w.row} ${Math.floor(hint.cell / b.width) + 1}, ${w.column} ${hint.cell % b.width + 1}: ${hint.digit}`; render(); });
85
+ button(w.check, () => { const result = progressKakuro(game.board, game.values); message = result.errors.length ? w.wrong : finishedKakuro(game) ? game.helped ? w.helped : w.won : w.checked; render(); });
86
+ button(w.restart, () => { game = newKakuro(game.board); message = w.ready; render(); });
87
+ pad.replaceChildren();
88
+ for (let digit = 1; digit <= 9; digit += 1) {
89
+ const key = doc.createElement("button");
90
+ key.type = "button";
91
+ key.textContent = String(digit);
92
+ key.setAttribute("aria-label", `${digit}`);
93
+ key.onclick = () => { if (selected >= 0)
94
+ update(enterKakuro(game, selected, digit, pencil)); };
95
+ pad.append(key);
96
+ }
97
+ const clear = doc.createElement("button");
98
+ clear.type = "button";
99
+ clear.textContent = w.clear;
100
+ clear.setAttribute("aria-label", w.clear);
101
+ clear.onclick = () => { if (selected >= 0)
102
+ update(enterKakuro(game, selected, 0)); };
103
+ pad.append(clear);
104
+ status.textContent = finishedKakuro(game) ? game.helped ? w.helped : w.won : message || w.ready;
105
+ help.textContent = w.help;
106
+ };
107
+ render();
108
+ return { game: copy, progress: () => encodeKakuro(game), set: next => { options = { ...options, ...next }; render(); }, restart: () => { game = newKakuro(game.board); render(); }, destroy: () => { dead = true; root.remove(); } };
109
+ }
@@ -0,0 +1,20 @@
1
+ import type { KakuroBoard, KakuroGame, KakuroLanguage, KakuroMaterial, KakuroPieces } from "./kakuro.types.ts";
2
+ import type { KakuroDrawOptions } from "./kakuroDraw.ts";
3
+ export type KakuroMountOptions = KakuroDrawOptions & {
4
+ board: KakuroBoard;
5
+ language?: KakuroLanguage;
6
+ progress?: string;
7
+ onChange?: (game: KakuroGame) => void;
8
+ onFinish?: (game: KakuroGame) => void;
9
+ material?: KakuroMaterial;
10
+ pieces?: KakuroPieces;
11
+ };
12
+ export type KakuroMount = {
13
+ game: () => KakuroGame;
14
+ progress: () => string;
15
+ set: (options: KakuroDrawOptions & {
16
+ language?: KakuroLanguage;
17
+ }) => void;
18
+ restart: () => void;
19
+ destroy: () => void;
20
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { KakuroBoard, KakuroSolve } from "./kakuro.types.ts";
2
+ /** Bounded exact counter over across and down runs; an interrupted search never proves uniqueness. */
3
+ export declare function solveKakuro(board: KakuroBoard, entries?: readonly number[], options?: {
4
+ limit?: number;
5
+ nodes?: number;
6
+ }): KakuroSolve;
@@ -0,0 +1,85 @@
1
+ import { KAKURO_MAX_NODES } from "./kakuro.constants.js";
2
+ import { canReach, isKakuroBoard, kakuroRuns } from "./kakuroBoard.js";
3
+ /** Bounded exact counter over across and down runs; an interrupted search never proves uniqueness. */
4
+ export function solveKakuro(board, entries = [], options = {}) {
5
+ if (!isKakuroBoard(board))
6
+ throw new RangeError("Invalid Kakuro board");
7
+ const limit = options.limit ?? 2, budget = options.nodes ?? KAKURO_MAX_NODES;
8
+ if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1)
9
+ throw new RangeError("Invalid search bounds");
10
+ if (entries.length && entries.length !== board.cells.length)
11
+ 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];
17
+ if (board.cells[cell].kind === "black" ? value !== 0 : !Number.isInteger(value) || value < 0 || value > 9) {
18
+ return { count: 0, solution: null, complete: true, nodes: 0 };
19
+ }
20
+ }
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;
82
+ }
83
+ const slots = run.cells.filter(cell => !values[cell] && cell !== targetCell).length;
84
+ return sum <= run.sum && canReach(run.sum - sum, slots, [...used]);
85
+ }