@johnmorrisdotca/kazu 1.1.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 (372) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +424 -10
  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 +13 -0
  6. package/dist/akari-entry.js +11 -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 +7 -0
  10. package/dist/akari.constants.js +7 -0
  11. package/dist/akari.types.d.ts +52 -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 +10 -0
  20. package/dist/akariGenerate.js +101 -0
  21. package/dist/akariLogic.d.ts +10 -0
  22. package/dist/akariLogic.js +84 -0
  23. package/dist/akariMount.d.ts +3 -0
  24. package/dist/akariMount.js +214 -0
  25. package/dist/akariPlay.types.d.ts +25 -0
  26. package/dist/akariPlay.types.js +1 -0
  27. package/dist/akariRate.d.ts +6 -0
  28. package/dist/akariRate.js +34 -0
  29. package/dist/akariSolve.d.ts +6 -0
  30. package/dist/akariSolve.js +18 -0
  31. package/dist/akariStrings.d.ts +98 -0
  32. package/dist/akariStrings.js +53 -0
  33. package/dist/akariStyle.d.ts +1 -0
  34. package/dist/akariStyle.js +9 -0
  35. package/dist/akariTemplate.d.ts +7 -0
  36. package/dist/akariTemplate.js +78 -0
  37. package/dist/csp.d.ts +63 -0
  38. package/dist/csp.js +162 -0
  39. package/dist/fillomino-draw-entry.d.ts +2 -0
  40. package/dist/fillomino-draw-entry.js +1 -0
  41. package/dist/fillomino-entry.d.ts +9 -0
  42. package/dist/fillomino-entry.js +8 -0
  43. package/dist/fillomino-play-entry.d.ts +4 -0
  44. package/dist/fillomino-play-entry.js +3 -0
  45. package/dist/fillomino.constants.d.ts +9 -0
  46. package/dist/fillomino.constants.js +9 -0
  47. package/dist/fillomino.types.d.ts +47 -0
  48. package/dist/fillomino.types.js +1 -0
  49. package/dist/fillominoBoard.d.ts +6 -0
  50. package/dist/fillominoBoard.js +84 -0
  51. package/dist/fillominoBuild.d.ts +10 -0
  52. package/dist/fillominoBuild.js +78 -0
  53. package/dist/fillominoDraw.d.ts +3 -0
  54. package/dist/fillominoDraw.js +31 -0
  55. package/dist/fillominoGame.d.ts +13 -0
  56. package/dist/fillominoGame.js +65 -0
  57. package/dist/fillominoGenerate.d.ts +9 -0
  58. package/dist/fillominoGenerate.js +93 -0
  59. package/dist/fillominoLogic.d.ts +14 -0
  60. package/dist/fillominoLogic.js +177 -0
  61. package/dist/fillominoMount.d.ts +2 -0
  62. package/dist/fillominoMount.js +214 -0
  63. package/dist/fillominoPlay.types.d.ts +25 -0
  64. package/dist/fillominoPlay.types.js +1 -0
  65. package/dist/fillominoRate.d.ts +3 -0
  66. package/dist/fillominoRate.js +57 -0
  67. package/dist/fillominoSolve.d.ts +6 -0
  68. package/dist/fillominoSolve.js +38 -0
  69. package/dist/fillominoStrings.d.ts +102 -0
  70. package/dist/fillominoStrings.js +13 -0
  71. package/dist/fillominoStyle.d.ts +1 -0
  72. package/dist/fillominoStyle.js +13 -0
  73. package/dist/fillominoWorker.d.ts +1 -0
  74. package/dist/fillominoWorker.js +10 -0
  75. package/dist/heyawake-draw-entry.d.ts +2 -0
  76. package/dist/heyawake-draw-entry.js +1 -0
  77. package/dist/heyawake-entry.d.ts +13 -0
  78. package/dist/heyawake-entry.js +11 -0
  79. package/dist/heyawake-play-entry.d.ts +2 -0
  80. package/dist/heyawake-play-entry.js +1 -0
  81. package/dist/heyawake.constants.d.ts +5 -0
  82. package/dist/heyawake.constants.js +5 -0
  83. package/dist/heyawake.types.d.ts +35 -0
  84. package/dist/heyawake.types.js +1 -0
  85. package/dist/heyawakeBoard.d.ts +7 -0
  86. package/dist/heyawakeBoard.js +116 -0
  87. package/dist/heyawakeDraw.d.ts +3 -0
  88. package/dist/heyawakeDraw.js +27 -0
  89. package/dist/heyawakeGame.d.ts +12 -0
  90. package/dist/heyawakeGame.js +51 -0
  91. package/dist/heyawakeGenerate.d.ts +2 -0
  92. package/dist/heyawakeGenerate.js +114 -0
  93. package/dist/heyawakeMount.d.ts +2 -0
  94. package/dist/heyawakeMount.js +106 -0
  95. package/dist/heyawakePlay.types.d.ts +24 -0
  96. package/dist/heyawakePlay.types.js +1 -0
  97. package/dist/heyawakeSolve.d.ts +5 -0
  98. package/dist/heyawakeSolve.js +52 -0
  99. package/dist/heyawakeStrings.d.ts +19 -0
  100. package/dist/heyawakeStrings.js +7 -0
  101. package/dist/heyawakeStyle.d.ts +1 -0
  102. package/dist/heyawakeStyle.js +2 -0
  103. package/dist/heyawakeWorker.d.ts +1 -0
  104. package/dist/heyawakeWorker.js +10 -0
  105. package/dist/hitori-draw-entry.d.ts +2 -0
  106. package/dist/hitori-draw-entry.js +1 -0
  107. package/dist/hitori-entry.d.ts +7 -0
  108. package/dist/hitori-entry.js +7 -0
  109. package/dist/hitori-play-entry.d.ts +4 -0
  110. package/dist/hitori-play-entry.js +3 -0
  111. package/dist/hitori.constants.d.ts +8 -0
  112. package/dist/hitori.constants.js +8 -0
  113. package/dist/hitori.types.d.ts +37 -0
  114. package/dist/hitori.types.js +1 -0
  115. package/dist/hitoriBoard.d.ts +8 -0
  116. package/dist/hitoriBoard.js +73 -0
  117. package/dist/hitoriBuild.d.ts +12 -0
  118. package/dist/hitoriBuild.js +117 -0
  119. package/dist/hitoriDraw.d.ts +4 -0
  120. package/dist/hitoriDraw.js +25 -0
  121. package/dist/hitoriGame.d.ts +10 -0
  122. package/dist/hitoriGame.js +48 -0
  123. package/dist/hitoriGenerate.d.ts +10 -0
  124. package/dist/hitoriGenerate.js +65 -0
  125. package/dist/hitoriLogic.d.ts +12 -0
  126. package/dist/hitoriLogic.js +189 -0
  127. package/dist/hitoriMount.d.ts +3 -0
  128. package/dist/hitoriMount.js +111 -0
  129. package/dist/hitoriPlay.types.d.ts +25 -0
  130. package/dist/hitoriPlay.types.js +1 -0
  131. package/dist/hitoriRate.d.ts +3 -0
  132. package/dist/hitoriRate.js +32 -0
  133. package/dist/hitoriSolve.d.ts +9 -0
  134. package/dist/hitoriSolve.js +18 -0
  135. package/dist/hitoriStrings.d.ts +70 -0
  136. package/dist/hitoriStrings.js +17 -0
  137. package/dist/hitoriStyle.d.ts +1 -0
  138. package/dist/hitoriStyle.js +9 -0
  139. package/dist/index.d.ts +11 -0
  140. package/dist/index.js +11 -0
  141. package/dist/juosan-draw-entry.d.ts +2 -0
  142. package/dist/juosan-draw-entry.js +1 -0
  143. package/dist/juosan-entry.d.ts +11 -0
  144. package/dist/juosan-entry.js +10 -0
  145. package/dist/juosan-play-entry.d.ts +4 -0
  146. package/dist/juosan-play-entry.js +3 -0
  147. package/dist/juosan.constants.d.ts +4 -0
  148. package/dist/juosan.constants.js +4 -0
  149. package/dist/juosan.types.d.ts +33 -0
  150. package/dist/juosan.types.js +1 -0
  151. package/dist/juosanBoard.d.ts +7 -0
  152. package/dist/juosanBoard.js +102 -0
  153. package/dist/juosanDraw.d.ts +4 -0
  154. package/dist/juosanDraw.js +49 -0
  155. package/dist/juosanGame.d.ts +20 -0
  156. package/dist/juosanGame.js +86 -0
  157. package/dist/juosanGenerate.d.ts +3 -0
  158. package/dist/juosanGenerate.js +31 -0
  159. package/dist/juosanMount.d.ts +3 -0
  160. package/dist/juosanMount.js +206 -0
  161. package/dist/juosanPlay.types.d.ts +24 -0
  162. package/dist/juosanPlay.types.js +1 -0
  163. package/dist/juosanSolve.d.ts +3 -0
  164. package/dist/juosanSolve.js +82 -0
  165. package/dist/juosanStrings.d.ts +77 -0
  166. package/dist/juosanStrings.js +43 -0
  167. package/dist/juosanStyle.d.ts +1 -0
  168. package/dist/juosanStyle.js +1 -0
  169. package/dist/kakuro-draw-entry.d.ts +2 -0
  170. package/dist/kakuro-draw-entry.js +1 -0
  171. package/dist/kakuro-entry.d.ts +13 -0
  172. package/dist/kakuro-entry.js +11 -0
  173. package/dist/kakuro-play-entry.d.ts +4 -0
  174. package/dist/kakuro-play-entry.js +3 -0
  175. package/dist/kakuro.constants.d.ts +11 -0
  176. package/dist/kakuro.constants.js +11 -0
  177. package/dist/kakuro.types.d.ts +65 -0
  178. package/dist/kakuro.types.js +1 -0
  179. package/dist/kakuroBoard.d.ts +8 -0
  180. package/dist/kakuroBoard.js +117 -0
  181. package/dist/kakuroBuild.d.ts +30 -0
  182. package/dist/kakuroBuild.js +337 -0
  183. package/dist/kakuroDraw.d.ts +10 -0
  184. package/dist/kakuroDraw.js +21 -0
  185. package/dist/kakuroGame.d.ts +12 -0
  186. package/dist/kakuroGame.js +74 -0
  187. package/dist/kakuroGenerate.d.ts +10 -0
  188. package/dist/kakuroGenerate.js +62 -0
  189. package/dist/kakuroLogic.d.ts +13 -0
  190. package/dist/kakuroLogic.js +153 -0
  191. package/dist/kakuroMount.d.ts +2 -0
  192. package/dist/kakuroMount.js +109 -0
  193. package/dist/kakuroPlay.types.d.ts +20 -0
  194. package/dist/kakuroPlay.types.js +1 -0
  195. package/dist/kakuroRate.d.ts +3 -0
  196. package/dist/kakuroRate.js +40 -0
  197. package/dist/kakuroSolve.d.ts +6 -0
  198. package/dist/kakuroSolve.js +33 -0
  199. package/dist/kakuroStrings.d.ts +82 -0
  200. package/dist/kakuroStrings.js +5 -0
  201. package/dist/kakuroStyle.d.ts +1 -0
  202. package/dist/kakuroStyle.js +1 -0
  203. package/dist/kakuroTemplate.d.ts +3 -0
  204. package/dist/kakuroTemplate.js +130 -0
  205. package/dist/masyu-draw-entry.d.ts +2 -0
  206. package/dist/masyu-draw-entry.js +1 -0
  207. package/dist/masyu-entry.d.ts +6 -0
  208. package/dist/masyu-entry.js +6 -0
  209. package/dist/masyu-play-entry.d.ts +4 -0
  210. package/dist/masyu-play-entry.js +3 -0
  211. package/dist/masyu.constants.d.ts +2 -0
  212. package/dist/masyu.constants.js +2 -0
  213. package/dist/masyu.types.d.ts +46 -0
  214. package/dist/masyu.types.js +1 -0
  215. package/dist/masyuBoard.d.ts +10 -0
  216. package/dist/masyuBoard.js +88 -0
  217. package/dist/masyuDraw.d.ts +2 -0
  218. package/dist/masyuDraw.js +33 -0
  219. package/dist/masyuGame.d.ts +9 -0
  220. package/dist/masyuGame.js +46 -0
  221. package/dist/masyuGenerate.d.ts +3 -0
  222. package/dist/masyuGenerate.js +56 -0
  223. package/dist/masyuMount.d.ts +2 -0
  224. package/dist/masyuMount.js +118 -0
  225. package/dist/masyuPlay.types.d.ts +1 -0
  226. package/dist/masyuPlay.types.js +1 -0
  227. package/dist/masyuSolve.d.ts +6 -0
  228. package/dist/masyuSolve.js +88 -0
  229. package/dist/masyuStrings.d.ts +28 -0
  230. package/dist/masyuStrings.js +4 -0
  231. package/dist/masyuStyle.d.ts +1 -0
  232. package/dist/masyuStyle.js +11 -0
  233. package/dist/nurikabe-draw-entry.d.ts +2 -0
  234. package/dist/nurikabe-draw-entry.js +1 -0
  235. package/dist/nurikabe-entry.d.ts +6 -0
  236. package/dist/nurikabe-entry.js +6 -0
  237. package/dist/nurikabe-play-entry.d.ts +4 -0
  238. package/dist/nurikabe-play-entry.js +3 -0
  239. package/dist/nurikabe.constants.d.ts +2 -0
  240. package/dist/nurikabe.constants.js +2 -0
  241. package/dist/nurikabe.types.d.ts +20 -0
  242. package/dist/nurikabe.types.js +1 -0
  243. package/dist/nurikabeBoard.d.ts +8 -0
  244. package/dist/nurikabeBoard.js +72 -0
  245. package/dist/nurikabeDraw.d.ts +4 -0
  246. package/dist/nurikabeDraw.js +15 -0
  247. package/dist/nurikabeGame.d.ts +8 -0
  248. package/dist/nurikabeGame.js +40 -0
  249. package/dist/nurikabeGenerate.d.ts +3 -0
  250. package/dist/nurikabeGenerate.js +31 -0
  251. package/dist/nurikabeMount.d.ts +3 -0
  252. package/dist/nurikabeMount.js +99 -0
  253. package/dist/nurikabePlay.types.d.ts +25 -0
  254. package/dist/nurikabePlay.types.js +1 -0
  255. package/dist/nurikabeSolve.d.ts +6 -0
  256. package/dist/nurikabeSolve.js +75 -0
  257. package/dist/nurikabeStrings.d.ts +70 -0
  258. package/dist/nurikabeStrings.js +5 -0
  259. package/dist/nurikabeStyle.d.ts +1 -0
  260. package/dist/nurikabeStyle.js +9 -0
  261. package/dist/ripple-draw-entry.d.ts +2 -0
  262. package/dist/ripple-draw-entry.js +1 -0
  263. package/dist/ripple-entry.d.ts +12 -0
  264. package/dist/ripple-entry.js +11 -0
  265. package/dist/ripple-play-entry.d.ts +4 -0
  266. package/dist/ripple-play-entry.js +3 -0
  267. package/dist/ripple.constants.d.ts +4 -0
  268. package/dist/ripple.constants.js +4 -0
  269. package/dist/ripple.types.d.ts +34 -0
  270. package/dist/ripple.types.js +1 -0
  271. package/dist/rippleBoard.d.ts +7 -0
  272. package/dist/rippleBoard.js +99 -0
  273. package/dist/rippleDraw.d.ts +3 -0
  274. package/dist/rippleDraw.js +32 -0
  275. package/dist/rippleGame.d.ts +13 -0
  276. package/dist/rippleGame.js +85 -0
  277. package/dist/rippleGenerate.d.ts +3 -0
  278. package/dist/rippleGenerate.js +57 -0
  279. package/dist/rippleMount.d.ts +2 -0
  280. package/dist/rippleMount.js +179 -0
  281. package/dist/ripplePlay.types.d.ts +23 -0
  282. package/dist/ripplePlay.types.js +1 -0
  283. package/dist/rippleSolve.d.ts +6 -0
  284. package/dist/rippleSolve.js +83 -0
  285. package/dist/rippleStrings.d.ts +86 -0
  286. package/dist/rippleStrings.js +5 -0
  287. package/dist/rippleStyle.d.ts +1 -0
  288. package/dist/rippleStyle.js +1 -0
  289. package/dist/shikaku-entry.d.ts +2 -0
  290. package/dist/shikaku-entry.js +2 -0
  291. package/dist/shikaku.constants.d.ts +5 -2
  292. package/dist/shikaku.constants.js +5 -2
  293. package/dist/shikaku.types.d.ts +18 -1
  294. package/dist/shikakuBuild.d.ts +21 -0
  295. package/dist/shikakuBuild.js +135 -0
  296. package/dist/shikakuGenerate.d.ts +6 -1
  297. package/dist/shikakuGenerate.js +50 -35
  298. package/dist/shikakuLogic.d.ts +13 -0
  299. package/dist/shikakuLogic.js +84 -0
  300. package/dist/shikakuPacks.d.ts +27 -0
  301. package/dist/shikakuPacks.js +26 -0
  302. package/dist/shikakuRate.d.ts +3 -0
  303. package/dist/shikakuRate.js +27 -0
  304. package/dist/shikakuSolve.d.ts +1 -1
  305. package/dist/shikakuSolve.js +23 -44
  306. package/dist/shikakuTemplate.d.ts +3 -0
  307. package/dist/shikakuTemplate.js +43 -0
  308. package/dist/slitherlink-draw-entry.d.ts +2 -0
  309. package/dist/slitherlink-draw-entry.js +1 -0
  310. package/dist/slitherlink-entry.d.ts +12 -0
  311. package/dist/slitherlink-entry.js +10 -0
  312. package/dist/slitherlink-play-entry.d.ts +4 -0
  313. package/dist/slitherlink-play-entry.js +3 -0
  314. package/dist/slitherlink.constants.d.ts +7 -0
  315. package/dist/slitherlink.constants.js +7 -0
  316. package/dist/slitherlink.types.d.ts +52 -0
  317. package/dist/slitherlink.types.js +1 -0
  318. package/dist/slitherlinkBoard.d.ts +12 -0
  319. package/dist/slitherlinkBoard.js +132 -0
  320. package/dist/slitherlinkDraw.d.ts +4 -0
  321. package/dist/slitherlinkDraw.js +51 -0
  322. package/dist/slitherlinkGame.d.ts +8 -0
  323. package/dist/slitherlinkGame.js +63 -0
  324. package/dist/slitherlinkGenerate.d.ts +10 -0
  325. package/dist/slitherlinkGenerate.js +182 -0
  326. package/dist/slitherlinkLogic.d.ts +10 -0
  327. package/dist/slitherlinkLogic.js +267 -0
  328. package/dist/slitherlinkMount.d.ts +3 -0
  329. package/dist/slitherlinkMount.js +267 -0
  330. package/dist/slitherlinkPlay.types.d.ts +23 -0
  331. package/dist/slitherlinkPlay.types.js +1 -0
  332. package/dist/slitherlinkRate.d.ts +3 -0
  333. package/dist/slitherlinkRate.js +26 -0
  334. package/dist/slitherlinkSolve.d.ts +6 -0
  335. package/dist/slitherlinkSolve.js +19 -0
  336. package/dist/slitherlinkStrings.d.ts +86 -0
  337. package/dist/slitherlinkStrings.js +47 -0
  338. package/dist/slitherlinkStyle.d.ts +1 -0
  339. package/dist/slitherlinkStyle.js +18 -0
  340. package/dist/slitherlinkTemplate.d.ts +3 -0
  341. package/dist/slitherlinkTemplate.js +101 -0
  342. package/dist/version.d.ts +1 -1
  343. package/dist/version.js +1 -1
  344. package/dist/yajilin-draw-entry.d.ts +2 -0
  345. package/dist/yajilin-draw-entry.js +1 -0
  346. package/dist/yajilin-entry.d.ts +6 -0
  347. package/dist/yajilin-entry.js +6 -0
  348. package/dist/yajilin-play-entry.d.ts +4 -0
  349. package/dist/yajilin-play-entry.js +3 -0
  350. package/dist/yajilin.constants.d.ts +2 -0
  351. package/dist/yajilin.constants.js +2 -0
  352. package/dist/yajilin.types.d.ts +63 -0
  353. package/dist/yajilin.types.js +1 -0
  354. package/dist/yajilinBoard.d.ts +9 -0
  355. package/dist/yajilinBoard.js +91 -0
  356. package/dist/yajilinDraw.d.ts +2 -0
  357. package/dist/yajilinDraw.js +33 -0
  358. package/dist/yajilinGame.d.ts +15 -0
  359. package/dist/yajilinGame.js +58 -0
  360. package/dist/yajilinGenerate.d.ts +3 -0
  361. package/dist/yajilinGenerate.js +224 -0
  362. package/dist/yajilinMount.d.ts +2 -0
  363. package/dist/yajilinMount.js +162 -0
  364. package/dist/yajilinPlay.types.d.ts +1 -0
  365. package/dist/yajilinPlay.types.js +1 -0
  366. package/dist/yajilinSolve.d.ts +6 -0
  367. package/dist/yajilinSolve.js +82 -0
  368. package/dist/yajilinStrings.d.ts +32 -0
  369. package/dist/yajilinStrings.js +4 -0
  370. package/dist/yajilinStyle.d.ts +1 -0
  371. package/dist/yajilinStyle.js +12 -0
  372. package/package.json +180 -3
@@ -0,0 +1,99 @@
1
+ import { RIPPLE_MAX_SIDE, RIPPLE_MIN_SIDE } from "./ripple.constants.js";
2
+ export function isRippleBoard(value) {
3
+ if (!value || typeof value !== "object")
4
+ return false;
5
+ const board = value;
6
+ if (!Number.isInteger(board.width) || board.width < RIPPLE_MIN_SIDE || board.width > RIPPLE_MAX_SIDE
7
+ || !Number.isInteger(board.height) || board.height < RIPPLE_MIN_SIDE || board.height > RIPPLE_MAX_SIDE
8
+ || !Array.isArray(board.rooms) || !Array.isArray(board.clues)
9
+ || board.rooms.length !== board.width * board.height || board.clues.length !== board.rooms.length
10
+ || board.rooms.some(id => !Number.isInteger(id) || id < 0))
11
+ return false;
12
+ const members = new Map();
13
+ board.rooms.forEach((id, cell) => members.set(id, [...(members.get(id) ?? []), cell]));
14
+ const ids = [...members.keys()].sort((a, b) => a - b);
15
+ if (ids.some((id, index) => id !== index))
16
+ return false;
17
+ const sizes = new Map([...members].map(([id, cells]) => [id, cells.length]));
18
+ if (board.clues.some((clue, cell) => clue !== null && (!Number.isInteger(clue) || clue < 1 || clue > sizes.get(board.rooms[cell]))))
19
+ return false;
20
+ return [...members].every(([id, cells]) => connected(board, id, cells));
21
+ }
22
+ function connected(board, room, cells) {
23
+ const reached = new Set([cells[0]]);
24
+ const queue = [cells[0]];
25
+ while (queue.length) {
26
+ const cell = queue.pop();
27
+ for (const next of [cell - board.width, cell + board.width, cell - 1, cell + 1]) {
28
+ if (next < 0 || next >= board.rooms.length || (Math.abs(next - cell) === 1 && Math.floor(next / board.width) !== Math.floor(cell / board.width)))
29
+ continue;
30
+ if (board.rooms[next] === room && !reached.has(next)) {
31
+ reached.add(next);
32
+ queue.push(next);
33
+ }
34
+ }
35
+ }
36
+ return reached.size === cells.length;
37
+ }
38
+ export function rippleRoomSizes(board) {
39
+ const sizes = [];
40
+ board.rooms.forEach(id => { sizes[id] = (sizes[id] ?? 0) + 1; });
41
+ return sizes;
42
+ }
43
+ /** Checks room permutations and the at-least-N-cell gap between equal N clues in a line. */
44
+ export function checkRipple(board, values) {
45
+ if (!isRippleBoard(board))
46
+ throw new RangeError("Invalid Ripple Effect board");
47
+ if (values.length !== board.rooms.length)
48
+ throw new RangeError("Invalid Ripple Effect values");
49
+ const errors = new Set();
50
+ const sizes = rippleRoomSizes(board);
51
+ const seen = new Map();
52
+ values.forEach((value, cell) => {
53
+ const clue = board.clues[cell];
54
+ if (!Number.isInteger(value) || value < 0 || value > sizes[board.rooms[cell]]
55
+ || clue !== null && value !== clue) {
56
+ errors.add(cell);
57
+ return;
58
+ }
59
+ if (value > 0)
60
+ seen.set(board.rooms[cell], [...(seen.get(board.rooms[cell]) ?? []), value]);
61
+ });
62
+ for (const room of seen.keys()) {
63
+ const cells = values.flatMap((value, cell) => board.rooms[cell] === room && value > 0 ? [value] : []);
64
+ const duplicates = new Set(cells.filter((value, index) => cells.indexOf(value) !== index));
65
+ if (duplicates.size)
66
+ values.forEach((value, cell) => { if (board.rooms[cell] === room && duplicates.has(value))
67
+ errors.add(cell); });
68
+ }
69
+ const lines = [...Array.from({ length: board.height }, (_, y) => Array.from({ length: board.width }, (_, x) => y * board.width + x)),
70
+ ...Array.from({ length: board.width }, (_, x) => Array.from({ length: board.height }, (_, y) => y * board.width + x))];
71
+ for (const line of lines)
72
+ for (let a = 0; a < line.length; a += 1) {
73
+ const cell = line[a], value = values[cell];
74
+ if (!value)
75
+ continue;
76
+ for (let b = a + 1; b < line.length; b += 1) {
77
+ const other = line[b];
78
+ if (values[other] === value && b - a <= value) {
79
+ errors.add(cell);
80
+ errors.add(other);
81
+ }
82
+ }
83
+ }
84
+ const complete = values.every(value => value > 0);
85
+ if (complete) {
86
+ for (let room = 0; room < sizes.length; room += 1) {
87
+ const list = values.flatMap((value, cell) => board.rooms[cell] === room ? [value] : []).sort((a, b) => a - b);
88
+ if (list.some((value, index) => value !== index + 1))
89
+ board.rooms.forEach((id, cell) => { if (id === room)
90
+ errors.add(cell); });
91
+ }
92
+ }
93
+ return { ok: errors.size === 0 && complete, errors: [...errors].sort((a, b) => a - b), complete };
94
+ }
95
+ /** Partial progress check; missing entries are allowed, but impossible room/ripple pairs are flagged. */
96
+ export function progressRipple(board, values) {
97
+ const result = checkRipple(board, values);
98
+ return { ...result, ok: result.errors.length === 0 };
99
+ }
@@ -0,0 +1,3 @@
1
+ import type { RippleBoard } from "./ripple.types.ts";
2
+ import type { RippleDrawOptions } from "./ripplePlay.types.ts";
3
+ export declare function drawRipple(board: RippleBoard, options?: RippleDrawOptions): string;
@@ -0,0 +1,32 @@
1
+ import { rippleRoomSizes } from "./rippleBoard.js";
2
+ export function drawRipple(board, options = {}) {
3
+ const size = 56, margin = 8, width = board.width * size, height = board.height * size;
4
+ const ink = options.material === "slate" ? "#f1eee5" : options.material === "wood" ? "#382d20" : "#252824";
5
+ const line = options.material === "slate" ? "#899697" : options.material === "wood" ? "#80613c" : "#8b8578";
6
+ const surface = options.material === "slate" ? "#354247" : options.material === "wood" ? "#dfc18f" : "#faf7ef";
7
+ const values = options.values ?? board.clues.map(clue => clue ?? 0);
8
+ const notes = options.notes ?? board.rooms.map(() => []);
9
+ const sizes = rippleRoomSizes(board);
10
+ const cells = Array.from({ length: board.rooms.length }, (_, cell) => {
11
+ const x = cell % board.width * size, y = Math.floor(cell / board.width) * size;
12
+ const room = board.rooms[cell];
13
+ const clue = board.clues[cell];
14
+ const value = values[cell] ?? 0;
15
+ const text = clue !== null ? `<text class="rp-clue" x="${x + size / 2}" y="${y + size * .64}">${clue}</text>`
16
+ : value ? `<text class="rp-value" x="${x + size / 2}" y="${y + size * .64}">${value}</text>`
17
+ : (notes[cell] ?? []).map((note, i) => `<text class="rp-note" x="${x + 14 + i % 3 * 14}" y="${y + 18 + Math.floor(i / 3) * 14}">${note}</text>`).join("");
18
+ const selected = options.selected === cell;
19
+ const piece = options.pieces === "tiles" ? `<rect class="rp-piece" x="${x + 9}" y="${y + 9}" width="${size - 18}" height="${size - 18}" rx="6"/>` : "";
20
+ const error = options.errors?.includes(cell) ?? false;
21
+ return `<rect x="${x}" y="${y}" width="${size}" height="${size}" fill="${selected ? "#dce9c9" : surface}" stroke="${error ? "#c34b3e" : line}" stroke-width="1"/>${piece}${text}<title>${Math.floor(cell / board.width) + 1}, ${cell % board.width + 1}: ${clue !== null ? `clue ${clue}` : value ? `entry ${value}` : `room of ${sizes[room]}`}</title>`;
22
+ }).join("");
23
+ const walls = board.rooms.flatMap((room, cell) => {
24
+ const x = cell % board.width, y = Math.floor(cell / board.width), edges = [];
25
+ if (x + 1 < board.width && board.rooms[cell + 1] !== room)
26
+ edges.push(`<path d="M${(x + 1) * size} ${y * size}v${size}"/>`);
27
+ if (y + 1 < board.height && board.rooms[cell + board.width] !== room)
28
+ edges.push(`<path d="M${x * size} ${(y + 1) * size}h${size}"/>`);
29
+ return edges;
30
+ }).join("");
31
+ return `<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Ripple Effect ${board.width} by ${board.height}" viewBox="-${margin} -${margin} ${width + margin * 2} ${height + margin * 2}" style="color:${ink}"><style>.rp-clue,.rp-value{font:500 27px system-ui;text-anchor:middle;fill:${ink}}.rp-clue{font-weight:700}.rp-note{font:12px system-ui;text-anchor:middle;fill:${ink}}.rp-piece{fill:none;stroke:${line};stroke-width:2}</style>${cells}<g fill="none" stroke="${ink}" stroke-width="3">${walls}</g></svg>`;
32
+ }
@@ -0,0 +1,13 @@
1
+ import type { RippleBoard, RippleGame } from "./ripple.types.ts";
2
+ export declare function newRipple(board: RippleBoard): RippleGame;
3
+ export declare function setRippleValue(game: RippleGame, cell: number, value: number): RippleGame;
4
+ export declare function toggleRippleNote(game: RippleGame, cell: number, value: number): RippleGame;
5
+ export declare function undoRipple(game: RippleGame): RippleGame;
6
+ export declare function rippleFinished(game: RippleGame): boolean;
7
+ /** Offers a cell only when the current public entries leave one proved completion. */
8
+ export declare function hintRipple(game: RippleGame): {
9
+ cell: number;
10
+ value: number;
11
+ } | null;
12
+ export declare function encodeRipple(game: RippleGame): string;
13
+ export declare function decodeRipple(code: string): RippleGame | null;
@@ -0,0 +1,85 @@
1
+ import { isRippleBoard, checkRipple, rippleRoomSizes } from "./rippleBoard.js";
2
+ import { solveRipple } from "./rippleSolve.js";
3
+ export function newRipple(board) {
4
+ if (!isRippleBoard(board))
5
+ throw new RangeError("Invalid Ripple Effect board");
6
+ return { board: { width: board.width, height: board.height, rooms: [...board.rooms], clues: [...board.clues] },
7
+ values: board.clues.map(clue => clue ?? 0), notes: board.rooms.map(() => []), history: [], helped: false };
8
+ }
9
+ export function setRippleValue(game, cell, value) {
10
+ if (!Number.isInteger(cell) || cell < 0 || cell >= game.values.length || game.board.clues[cell] !== null
11
+ || !Number.isInteger(value) || value < 0 || value > rippleRoomSizes(game.board)[game.board.rooms[cell]])
12
+ return game;
13
+ if (game.values[cell] === value)
14
+ return game;
15
+ const values = [...game.values];
16
+ values[cell] = value;
17
+ const notes = game.notes.map((list, index) => index === cell ? [] : [...list]);
18
+ return { ...game, values, notes, history: [...game.history, snapshot(game)] };
19
+ }
20
+ export function toggleRippleNote(game, cell, value) {
21
+ if (!Number.isInteger(cell) || cell < 0 || cell >= game.values.length || game.board.clues[cell] !== null || game.values[cell] !== 0
22
+ || !Number.isInteger(value) || value < 1 || value > rippleRoomSizes(game.board)[game.board.rooms[cell]])
23
+ return game;
24
+ const notes = game.notes.map(list => [...list]);
25
+ const current = new Set(notes[cell]);
26
+ if (current.has(value))
27
+ current.delete(value);
28
+ else
29
+ current.add(value);
30
+ notes[cell] = [...current].sort((a, b) => a - b);
31
+ return { ...game, notes, history: [...game.history, snapshot(game)] };
32
+ }
33
+ export function undoRipple(game) {
34
+ const previous = game.history.at(-1);
35
+ return previous ? { ...game, values: [...previous.values], notes: previous.notes.map(list => [...list]), history: game.history.slice(0, -1) } : game;
36
+ }
37
+ export function rippleFinished(game) { return checkRipple(game.board, game.values).ok; }
38
+ /** Offers a cell only when the current public entries leave one proved completion. */
39
+ export function hintRipple(game) {
40
+ const clues = game.values.map((value, cell) => game.board.clues[cell] ?? (value || null));
41
+ const solved = solveRipple({ ...game.board, clues });
42
+ if (!solved.complete || solved.count !== 1 || !solved.solution)
43
+ return null;
44
+ const cell = solved.solution.findIndex((value, index) => game.values[index] === 0 && value > 0);
45
+ return cell < 0 ? null : { cell, value: solved.solution[cell] };
46
+ }
47
+ export function encodeRipple(game) {
48
+ return JSON.stringify({ version: 1, board: game.board, values: game.values, notes: game.notes, helped: game.helped });
49
+ }
50
+ export function decodeRipple(code) {
51
+ try {
52
+ if (code.length > 40000)
53
+ return null;
54
+ const data = JSON.parse(code);
55
+ if (data.version !== 1 || !isRippleBoard(data.board) || !Array.isArray(data.values)
56
+ || data.values.length !== data.board.rooms.length || !Array.isArray(data.notes)
57
+ || data.notes.length !== data.board.rooms.length || typeof data.helped !== "boolean")
58
+ return null;
59
+ let game = newRipple(data.board);
60
+ for (let cell = 0; cell < data.values.length; cell += 1) {
61
+ const value = data.values[cell];
62
+ if (game.board.clues[cell] !== null && value !== game.board.clues[cell])
63
+ return null;
64
+ if (game.board.clues[cell] === null && value) {
65
+ const next = setRippleValue(game, cell, value);
66
+ if (next === game)
67
+ return null;
68
+ game = next;
69
+ }
70
+ const list = data.notes[cell];
71
+ if (!Array.isArray(list) || new Set(list).size !== list.length
72
+ || list.some(note => !Number.isInteger(note) || note < 1 || note > rippleRoomSizes(game.board)[game.board.rooms[cell]]))
73
+ return null;
74
+ for (const note of list)
75
+ game = toggleRippleNote(game, cell, note);
76
+ }
77
+ return { ...game, history: [], helped: data.helped };
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ function snapshot(game) {
84
+ return { values: [...game.values], notes: game.notes.map(list => [...list]) };
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { RipplePuzzle } from "./ripple.types.ts";
2
+ /** Builds a seeded block-room family and accepts it only after a bounded uniqueness proof. */
3
+ export declare function generateRipple(width?: number, height?: number, seed?: number): RipplePuzzle;
@@ -0,0 +1,57 @@
1
+ import { RIPPLE_MAX_ATTEMPTS } from "./ripple.constants.js";
2
+ import { checkRipple } from "./rippleBoard.js";
3
+ import { solveRipple } from "./rippleSolve.js";
4
+ import { isKazuSeed, seededRandom } from "./random.js";
5
+ /** Builds a seeded block-room family and accepts it only after a bounded uniqueness proof. */
6
+ export function generateRipple(width = 9, height = width, seed = 1) {
7
+ if (width !== 9 || height !== 9 || !isKazuSeed(seed)) {
8
+ throw new RangeError("Ripple Effect currently supports original 9×9 boards and valid integer seeds.");
9
+ }
10
+ const random = seededRandom(seed);
11
+ for (let attempt = 0; attempt < RIPPLE_MAX_ATTEMPTS; attempt += 1) {
12
+ const rooms = Array.from({ length: 81 }, (_, cell) => Math.floor(Math.floor(cell / 9) / 3) * 3 + Math.floor(cell % 9 / 3));
13
+ const solution = blockSolution(random);
14
+ const clues = [...solution];
15
+ const order = Array.from({ length: clues.length }, (_, cell) => cell);
16
+ order.splice(0, order.length, ...shuffled(order, random));
17
+ const target = 42 + Math.floor(random() * 7);
18
+ let clueCount = clues.length;
19
+ for (const cell of order) {
20
+ if (clueCount <= target)
21
+ break;
22
+ const previous = clues[cell];
23
+ clues[cell] = null;
24
+ const board = { width, height, rooms, clues };
25
+ const proof = solveRipple(board, { limit: 2 });
26
+ if (!proof.complete || proof.count !== 1) {
27
+ clues[cell] = previous;
28
+ continue;
29
+ }
30
+ clueCount -= 1;
31
+ }
32
+ if (clueCount > 58 || clueCount >= clues.length)
33
+ continue;
34
+ const board = { width, height, rooms, clues };
35
+ const proof = solveRipple(board, { limit: 2 });
36
+ if (proof.complete && proof.count === 1 && proof.solution && checkRipple(board, proof.solution).ok) {
37
+ return { ...board, seed, solution: proof.solution };
38
+ }
39
+ }
40
+ throw new Error("No uniquely solvable Ripple Effect board was proved within the generation budget.");
41
+ }
42
+ function blockSolution(random) {
43
+ const bands = shuffled([0, 1, 2], random);
44
+ const stacks = shuffled([0, 1, 2], random);
45
+ const rows = bands.flatMap(band => shuffled([0, 1, 2], random).map(row => band * 3 + row));
46
+ const columns = stacks.flatMap(stack => shuffled([0, 1, 2], random).map(column => stack * 3 + column));
47
+ const digits = shuffled([1, 2, 3, 4, 5, 6, 7, 8, 9], random);
48
+ return rows.flatMap(row => columns.map(column => digits[(row * 3 + Math.floor(row / 3) + column) % 9]));
49
+ }
50
+ function shuffled(items, random) {
51
+ const result = [...items];
52
+ for (let i = result.length - 1; i > 0; i -= 1) {
53
+ const j = Math.floor(random() * (i + 1));
54
+ [result[i], result[j]] = [result[j], result[i]];
55
+ }
56
+ return result;
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { RippleMount, RippleMountOptions } from "./ripplePlay.types.ts";
2
+ export declare function mountRipple(host: HTMLElement, initial: RippleMountOptions): RippleMount;
@@ -0,0 +1,179 @@
1
+ import { KAZU_STYLE } from "./style.js";
2
+ import { checkRipple, progressRipple, rippleRoomSizes } from "./rippleBoard.js";
3
+ import { decodeRipple, encodeRipple, hintRipple, newRipple, rippleFinished, setRippleValue, toggleRippleNote, undoRipple } from "./rippleGame.js";
4
+ import { RIPPLE_PLAY_STYLE } from "./rippleStyle.js";
5
+ import { rippleWords } from "./rippleStrings.js";
6
+ export function mountRipple(host, initial) {
7
+ const doc = host.ownerDocument;
8
+ let options = { ...initial };
9
+ let game = initial.progress ? decodeRipple(initial.progress) : null;
10
+ game ?? (game = newRipple(initial.board));
11
+ let selected = game.values.findIndex((value, cell) => game.board.clues[cell] === null);
12
+ if (selected < 0)
13
+ selected = 0;
14
+ let pencil = false, errors = [], message = "", dead = false;
15
+ const root = doc.createElement("section");
16
+ root.className = "rp-root";
17
+ const style = doc.createElement("style");
18
+ style.textContent = KAZU_STYLE.replace(/\.kazu/g, ".rp-palette") + RIPPLE_PLAY_STYLE;
19
+ const board = doc.createElement("div");
20
+ board.className = "rp-board";
21
+ board.setAttribute("role", "grid");
22
+ const status = doc.createElement("p");
23
+ status.className = "rp-status";
24
+ status.setAttribute("role", "status");
25
+ const help = doc.createElement("p");
26
+ help.className = "rp-help";
27
+ const controls = doc.createElement("div");
28
+ controls.className = "rp-controls";
29
+ const numpad = doc.createElement("div");
30
+ numpad.className = "rp-numpad";
31
+ numpad.setAttribute("aria-label", "Numbers");
32
+ root.append(style, board, status, help, numpad, controls);
33
+ host.append(root);
34
+ const words = () => rippleWords(options.language);
35
+ const copy = () => ({ ...game, board: { ...game.board, rooms: [...game.board.rooms], clues: [...game.board.clues] }, values: [...game.values], notes: game.notes.map(list => [...list]), history: game.history.map(turn => ({ values: [...turn.values], notes: turn.notes.map(list => [...list]) })) });
36
+ const changed = () => {
37
+ errors = [];
38
+ message = "";
39
+ render();
40
+ const snapshot = copy();
41
+ options.onChange?.(snapshot);
42
+ root.dispatchEvent(new CustomEvent("ripple-change", { detail: snapshot, bubbles: true }));
43
+ if (rippleFinished(game)) {
44
+ options.onFinish?.(snapshot);
45
+ root.dispatchEvent(new CustomEvent("ripple-finish", { detail: snapshot, bubbles: true }));
46
+ }
47
+ };
48
+ const enter = (value) => {
49
+ if (game.board.clues[selected] !== null)
50
+ return;
51
+ game = pencil ? toggleRippleNote(game, selected, value) : setRippleValue(game, selected, value);
52
+ changed();
53
+ };
54
+ const button = (label, action, disabled = false) => {
55
+ const item = doc.createElement("button");
56
+ item.type = "button";
57
+ item.className = "rp-tool";
58
+ item.textContent = label;
59
+ item.disabled = disabled;
60
+ item.onclick = action;
61
+ controls.append(item);
62
+ return item;
63
+ };
64
+ const render = () => {
65
+ if (dead)
66
+ return;
67
+ const focused = board.contains(doc.activeElement);
68
+ const w = words(), current = game.board, sizes = rippleRoomSizes(current), validation = progressRipple(current, game.values);
69
+ root.dataset.material = options.material ?? "ivory";
70
+ root.dataset.pieces = options.pieces ?? "ink";
71
+ root.classList.toggle("rp-tiles", options.pieces === "tiles");
72
+ root.style.setProperty("--rp-width", String(current.width));
73
+ board.setAttribute("aria-label", w.title);
74
+ board.replaceChildren();
75
+ for (let cell = 0; cell < current.rooms.length; cell += 1) {
76
+ const x = cell % current.width, y = Math.floor(cell / current.width), room = current.rooms[cell];
77
+ const control = doc.createElement("button");
78
+ control.type = "button";
79
+ control.className = "rp-cell";
80
+ if (x + 1 < current.width && current.rooms[cell + 1] !== room)
81
+ control.classList.add("rp-room-right");
82
+ if (y + 1 < current.height && current.rooms[cell + current.width] !== room)
83
+ control.classList.add("rp-room-bottom");
84
+ const clue = current.clues[cell] !== null, value = game.values[cell];
85
+ control.dataset.cell = String(cell);
86
+ control.dataset.clue = String(clue);
87
+ control.dataset.error = String(errors.includes(cell));
88
+ control.dataset.selected = String(cell === selected);
89
+ control.setAttribute("aria-disabled", String(clue));
90
+ control.tabIndex = cell === selected ? 0 : -1;
91
+ control.setAttribute("role", "gridcell");
92
+ control.setAttribute("aria-label", `${w.cell.replace("{row}", String(y + 1)).replace("{column}", String(x + 1))}, ${clue ? w.clue.replace("{value}", String(current.clues[cell])) : value ? w.entry.replace("{value}", String(value)) : game.notes[cell].length ? w.note.replace("{values}", game.notes[cell].join(", ")) : w.empty}`);
93
+ if (clue)
94
+ control.textContent = String(current.clues[cell]);
95
+ else if (value)
96
+ control.textContent = String(value);
97
+ else if (game.notes[cell].length) {
98
+ const mini = doc.createElement("span");
99
+ mini.className = "rp-notes";
100
+ for (let digit = 1; digit <= sizes[room]; digit += 1) {
101
+ const span = doc.createElement("span");
102
+ span.textContent = game.notes[cell].includes(digit) ? String(digit) : "";
103
+ mini.append(span);
104
+ }
105
+ control.append(mini);
106
+ }
107
+ control.onclick = () => { selected = cell; render(); };
108
+ control.onkeydown = event => {
109
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) {
110
+ event.preventDefault();
111
+ move(cell, event.key);
112
+ }
113
+ else if (/^[1-9]$/.test(event.key)) {
114
+ event.preventDefault();
115
+ enter(Number(event.key));
116
+ }
117
+ else if (event.key === "Backspace" || event.key === "Delete") {
118
+ event.preventDefault();
119
+ enter(0);
120
+ }
121
+ else if (event.key.toLowerCase() === "p") {
122
+ event.preventDefault();
123
+ pencil = !pencil;
124
+ render();
125
+ }
126
+ };
127
+ board.append(control);
128
+ }
129
+ if (focused)
130
+ board.querySelector(`[data-cell="${selected}"]`)?.focus({ preventScroll: true });
131
+ numpad.replaceChildren();
132
+ for (let digit = 1; digit <= Math.max(...sizes); digit += 1) {
133
+ const key = doc.createElement("button");
134
+ key.type = "button";
135
+ key.className = "rp-key";
136
+ key.textContent = String(digit);
137
+ key.disabled = digit > sizes[current.rooms[selected]];
138
+ key.onclick = () => enter(digit);
139
+ numpad.append(key);
140
+ }
141
+ status.textContent = rippleFinished(game) ? (game.helped ? `${w.won} · ${w.hint}` : w.won) : message || (validation.ok ? w.won : w.checked);
142
+ help.textContent = w.help;
143
+ controls.replaceChildren();
144
+ button(w.undo, () => { game = undoRipple(game); changed(); }, !game.history.length);
145
+ button(w.check, () => { errors = [...checkRipple(current, game.values).errors]; message = checkRipple(current, game.values).ok ? w.won : w.wrong; render(); });
146
+ button(w.hint, () => { const hint = hintRipple(game); if (!hint) {
147
+ message = w.noHint;
148
+ render();
149
+ return;
150
+ } game = { ...setRippleValue(game, hint.cell, hint.value), helped: true }; selected = hint.cell; changed(); message = w.hintWhy; render(); });
151
+ button(pencil ? `${w.pencil} ✓` : w.pencil, () => { pencil = !pencil; render(); });
152
+ button(w.restart, restart);
153
+ button(w.view, expand);
154
+ };
155
+ function move(cell, direction) {
156
+ const dx = direction === "ArrowLeft" ? -1 : direction === "ArrowRight" ? 1 : 0;
157
+ const dy = direction === "ArrowUp" ? -1 : direction === "ArrowDown" ? 1 : 0;
158
+ const x = cell % game.board.width, y = Math.floor(cell / game.board.width);
159
+ const nx = Math.min(game.board.width - 1, Math.max(0, x + dx)), ny = Math.min(game.board.height - 1, Math.max(0, y + dy));
160
+ selected = ny * game.board.width + nx;
161
+ render();
162
+ board.querySelector(`[data-cell="${selected}"]`)?.focus();
163
+ }
164
+ function restart() { game = newRipple(game.board); selected = game.values.findIndex((value, cell) => game.board.clues[cell] === null); changed(); }
165
+ function expand() {
166
+ const dialog = doc.createElement("dialog"), close = doc.createElement("button");
167
+ dialog.className = "rp-dialog";
168
+ close.type = "button";
169
+ close.className = "rp-tool rp-close";
170
+ close.textContent = words().close;
171
+ close.onclick = () => dialog.close();
172
+ dialog.append(close, root);
173
+ host.append(dialog);
174
+ dialog.onclose = () => { host.append(root); dialog.remove(); };
175
+ dialog.showModal();
176
+ }
177
+ render();
178
+ return { game: copy, progress: () => encodeRipple(game), restart, set: (next) => { options = { ...options, ...next }; render(); }, destroy: () => { dead = true; root.remove(); root.closest("dialog")?.remove(); } };
179
+ }
@@ -0,0 +1,23 @@
1
+ import type { RippleBoard, RippleGame, RippleLanguage, RippleMaterial, RipplePieces } from "./ripple.types.ts";
2
+ export type RippleDrawOptions = {
3
+ values?: readonly number[];
4
+ notes?: readonly (readonly number[])[];
5
+ errors?: readonly number[];
6
+ selected?: number | null;
7
+ material?: RippleMaterial;
8
+ pieces?: RipplePieces;
9
+ language?: RippleLanguage;
10
+ };
11
+ export type RippleMountOptions = RippleDrawOptions & {
12
+ board: RippleBoard;
13
+ progress?: string;
14
+ onChange?: (game: RippleGame) => void;
15
+ onFinish?: (game: RippleGame) => void;
16
+ };
17
+ export type RippleMount = {
18
+ game: () => RippleGame;
19
+ progress: () => string;
20
+ set: (options: RippleDrawOptions) => void;
21
+ restart: () => void;
22
+ destroy: () => void;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { RippleBoard, RippleSolve } from "./ripple.types.ts";
2
+ /** Counts rule-valid fillings within an explicit node budget. */
3
+ export declare function solveRipple(board: RippleBoard, options?: {
4
+ limit?: number;
5
+ nodes?: number;
6
+ }): RippleSolve;
@@ -0,0 +1,83 @@
1
+ import { RIPPLE_MAX_NODES } from "./ripple.constants.js";
2
+ import { isRippleBoard, rippleRoomSizes } from "./rippleBoard.js";
3
+ /** Counts rule-valid fillings within an explicit node budget. */
4
+ export function solveRipple(board, options = {}) {
5
+ if (!isRippleBoard(board))
6
+ throw new RangeError("Invalid Ripple Effect board");
7
+ const limit = options.limit ?? 2, budget = options.nodes ?? RIPPLE_MAX_NODES;
8
+ if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1)
9
+ throw new RangeError("Invalid search bounds");
10
+ const sizes = rippleRoomSizes(board);
11
+ const values = board.clues.map(clue => clue ?? 0);
12
+ const roomUsed = sizes.map(() => new Set());
13
+ let count = 0, nodes = 0, complete = true, solution = null;
14
+ for (let cell = 0; cell < values.length; cell += 1) {
15
+ const value = values[cell];
16
+ if (!value)
17
+ continue;
18
+ const room = board.rooms[cell];
19
+ if (roomUsed[room].has(value) || conflicts(cell, value, values, board)) {
20
+ return { count: 0, solution: null, complete: true, nodes: 0 };
21
+ }
22
+ roomUsed[room].add(value);
23
+ }
24
+ const visit = () => {
25
+ if (count >= limit || !complete) {
26
+ complete = false;
27
+ return;
28
+ }
29
+ if (++nodes > budget) {
30
+ complete = false;
31
+ return;
32
+ }
33
+ let bestCell = -1, bestValues = [];
34
+ for (let cell = 0; cell < values.length; cell += 1) {
35
+ if (values[cell])
36
+ continue;
37
+ const room = board.rooms[cell];
38
+ const candidates = [];
39
+ for (let value = 1; value <= sizes[room]; value += 1) {
40
+ if (!roomUsed[room].has(value) && !conflicts(cell, value, values, board))
41
+ candidates.push(value);
42
+ }
43
+ if (!candidates.length)
44
+ return;
45
+ if (bestCell < 0 || candidates.length < bestValues.length) {
46
+ bestCell = cell;
47
+ bestValues = candidates;
48
+ if (candidates.length === 1)
49
+ break;
50
+ }
51
+ }
52
+ if (bestCell < 0) {
53
+ count += 1;
54
+ solution ?? (solution = [...values]);
55
+ return;
56
+ }
57
+ const room = board.rooms[bestCell];
58
+ for (const value of bestValues) {
59
+ values[bestCell] = value;
60
+ roomUsed[room].add(value);
61
+ visit();
62
+ roomUsed[room].delete(value);
63
+ values[bestCell] = 0;
64
+ if (!complete || count >= limit) {
65
+ complete = false;
66
+ return;
67
+ }
68
+ }
69
+ };
70
+ visit();
71
+ return { count, solution, complete, nodes };
72
+ }
73
+ function conflicts(cell, value, values, board) {
74
+ const x = cell % board.width, y = Math.floor(cell / board.width);
75
+ for (let other = 0; other < values.length; other += 1) {
76
+ if (values[other] !== value || other === cell)
77
+ continue;
78
+ const ox = other % board.width, oy = Math.floor(other / board.width);
79
+ if ((oy === y && Math.abs(ox - x) <= value) || (ox === x && Math.abs(oy - y) <= value))
80
+ return true;
81
+ }
82
+ return false;
83
+ }