@johnmorrisdotca/kazu 1.1.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 (323) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +379 -7
  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/fillomino-draw-entry.d.ts +2 -0
  32. package/dist/fillomino-draw-entry.js +1 -0
  33. package/dist/fillomino-entry.d.ts +8 -0
  34. package/dist/fillomino-entry.js +7 -0
  35. package/dist/fillomino-play-entry.d.ts +4 -0
  36. package/dist/fillomino-play-entry.js +3 -0
  37. package/dist/fillomino.constants.d.ts +5 -0
  38. package/dist/fillomino.constants.js +5 -0
  39. package/dist/fillomino.types.d.ts +30 -0
  40. package/dist/fillomino.types.js +1 -0
  41. package/dist/fillominoBoard.d.ts +6 -0
  42. package/dist/fillominoBoard.js +84 -0
  43. package/dist/fillominoDraw.d.ts +3 -0
  44. package/dist/fillominoDraw.js +31 -0
  45. package/dist/fillominoGame.d.ts +13 -0
  46. package/dist/fillominoGame.js +65 -0
  47. package/dist/fillominoGenerate.d.ts +3 -0
  48. package/dist/fillominoGenerate.js +125 -0
  49. package/dist/fillominoMount.d.ts +2 -0
  50. package/dist/fillominoMount.js +211 -0
  51. package/dist/fillominoPlay.types.d.ts +25 -0
  52. package/dist/fillominoPlay.types.js +1 -0
  53. package/dist/fillominoSolve.d.ts +6 -0
  54. package/dist/fillominoSolve.js +111 -0
  55. package/dist/fillominoStrings.d.ts +102 -0
  56. package/dist/fillominoStrings.js +13 -0
  57. package/dist/fillominoStyle.d.ts +1 -0
  58. package/dist/fillominoStyle.js +13 -0
  59. package/dist/fillominoWorker.d.ts +1 -0
  60. package/dist/fillominoWorker.js +10 -0
  61. package/dist/heyawake-draw-entry.d.ts +2 -0
  62. package/dist/heyawake-draw-entry.js +1 -0
  63. package/dist/heyawake-entry.d.ts +13 -0
  64. package/dist/heyawake-entry.js +11 -0
  65. package/dist/heyawake-play-entry.d.ts +2 -0
  66. package/dist/heyawake-play-entry.js +1 -0
  67. package/dist/heyawake.constants.d.ts +5 -0
  68. package/dist/heyawake.constants.js +5 -0
  69. package/dist/heyawake.types.d.ts +35 -0
  70. package/dist/heyawake.types.js +1 -0
  71. package/dist/heyawakeBoard.d.ts +7 -0
  72. package/dist/heyawakeBoard.js +116 -0
  73. package/dist/heyawakeDraw.d.ts +3 -0
  74. package/dist/heyawakeDraw.js +27 -0
  75. package/dist/heyawakeGame.d.ts +12 -0
  76. package/dist/heyawakeGame.js +51 -0
  77. package/dist/heyawakeGenerate.d.ts +2 -0
  78. package/dist/heyawakeGenerate.js +114 -0
  79. package/dist/heyawakeMount.d.ts +2 -0
  80. package/dist/heyawakeMount.js +106 -0
  81. package/dist/heyawakePlay.types.d.ts +24 -0
  82. package/dist/heyawakePlay.types.js +1 -0
  83. package/dist/heyawakeSolve.d.ts +5 -0
  84. package/dist/heyawakeSolve.js +52 -0
  85. package/dist/heyawakeStrings.d.ts +19 -0
  86. package/dist/heyawakeStrings.js +7 -0
  87. package/dist/heyawakeStyle.d.ts +1 -0
  88. package/dist/heyawakeStyle.js +2 -0
  89. package/dist/heyawakeWorker.d.ts +1 -0
  90. package/dist/heyawakeWorker.js +10 -0
  91. package/dist/hitori-draw-entry.d.ts +2 -0
  92. package/dist/hitori-draw-entry.js +1 -0
  93. package/dist/hitori-entry.d.ts +6 -0
  94. package/dist/hitori-entry.js +6 -0
  95. package/dist/hitori-play-entry.d.ts +4 -0
  96. package/dist/hitori-play-entry.js +3 -0
  97. package/dist/hitori.constants.d.ts +2 -0
  98. package/dist/hitori.constants.js +2 -0
  99. package/dist/hitori.types.d.ts +20 -0
  100. package/dist/hitori.types.js +1 -0
  101. package/dist/hitoriBoard.d.ts +8 -0
  102. package/dist/hitoriBoard.js +72 -0
  103. package/dist/hitoriDraw.d.ts +4 -0
  104. package/dist/hitoriDraw.js +25 -0
  105. package/dist/hitoriGame.d.ts +10 -0
  106. package/dist/hitoriGame.js +48 -0
  107. package/dist/hitoriGenerate.d.ts +3 -0
  108. package/dist/hitoriGenerate.js +163 -0
  109. package/dist/hitoriMount.d.ts +3 -0
  110. package/dist/hitoriMount.js +111 -0
  111. package/dist/hitoriPlay.types.d.ts +25 -0
  112. package/dist/hitoriPlay.types.js +1 -0
  113. package/dist/hitoriSolve.d.ts +6 -0
  114. package/dist/hitoriSolve.js +69 -0
  115. package/dist/hitoriStrings.d.ts +70 -0
  116. package/dist/hitoriStrings.js +17 -0
  117. package/dist/hitoriStyle.d.ts +1 -0
  118. package/dist/hitoriStyle.js +9 -0
  119. package/dist/index.d.ts +11 -0
  120. package/dist/index.js +11 -0
  121. package/dist/juosan-draw-entry.d.ts +2 -0
  122. package/dist/juosan-draw-entry.js +1 -0
  123. package/dist/juosan-entry.d.ts +11 -0
  124. package/dist/juosan-entry.js +10 -0
  125. package/dist/juosan-play-entry.d.ts +4 -0
  126. package/dist/juosan-play-entry.js +3 -0
  127. package/dist/juosan.constants.d.ts +4 -0
  128. package/dist/juosan.constants.js +4 -0
  129. package/dist/juosan.types.d.ts +33 -0
  130. package/dist/juosan.types.js +1 -0
  131. package/dist/juosanBoard.d.ts +7 -0
  132. package/dist/juosanBoard.js +102 -0
  133. package/dist/juosanDraw.d.ts +4 -0
  134. package/dist/juosanDraw.js +49 -0
  135. package/dist/juosanGame.d.ts +20 -0
  136. package/dist/juosanGame.js +86 -0
  137. package/dist/juosanGenerate.d.ts +3 -0
  138. package/dist/juosanGenerate.js +31 -0
  139. package/dist/juosanMount.d.ts +3 -0
  140. package/dist/juosanMount.js +206 -0
  141. package/dist/juosanPlay.types.d.ts +24 -0
  142. package/dist/juosanPlay.types.js +1 -0
  143. package/dist/juosanSolve.d.ts +3 -0
  144. package/dist/juosanSolve.js +82 -0
  145. package/dist/juosanStrings.d.ts +77 -0
  146. package/dist/juosanStrings.js +43 -0
  147. package/dist/juosanStyle.d.ts +1 -0
  148. package/dist/juosanStyle.js +1 -0
  149. package/dist/kakuro-draw-entry.d.ts +2 -0
  150. package/dist/kakuro-draw-entry.js +1 -0
  151. package/dist/kakuro-entry.d.ts +12 -0
  152. package/dist/kakuro-entry.js +10 -0
  153. package/dist/kakuro-play-entry.d.ts +4 -0
  154. package/dist/kakuro-play-entry.js +3 -0
  155. package/dist/kakuro.constants.d.ts +6 -0
  156. package/dist/kakuro.constants.js +6 -0
  157. package/dist/kakuro.types.d.ts +46 -0
  158. package/dist/kakuro.types.js +1 -0
  159. package/dist/kakuroBoard.d.ts +8 -0
  160. package/dist/kakuroBoard.js +117 -0
  161. package/dist/kakuroDraw.d.ts +10 -0
  162. package/dist/kakuroDraw.js +21 -0
  163. package/dist/kakuroGame.d.ts +12 -0
  164. package/dist/kakuroGame.js +74 -0
  165. package/dist/kakuroGenerate.d.ts +3 -0
  166. package/dist/kakuroGenerate.js +130 -0
  167. package/dist/kakuroMount.d.ts +2 -0
  168. package/dist/kakuroMount.js +109 -0
  169. package/dist/kakuroPlay.types.d.ts +20 -0
  170. package/dist/kakuroPlay.types.js +1 -0
  171. package/dist/kakuroSolve.d.ts +6 -0
  172. package/dist/kakuroSolve.js +85 -0
  173. package/dist/kakuroStrings.d.ts +82 -0
  174. package/dist/kakuroStrings.js +5 -0
  175. package/dist/kakuroStyle.d.ts +1 -0
  176. package/dist/kakuroStyle.js +1 -0
  177. package/dist/masyu-draw-entry.d.ts +2 -0
  178. package/dist/masyu-draw-entry.js +1 -0
  179. package/dist/masyu-entry.d.ts +6 -0
  180. package/dist/masyu-entry.js +6 -0
  181. package/dist/masyu-play-entry.d.ts +4 -0
  182. package/dist/masyu-play-entry.js +3 -0
  183. package/dist/masyu.constants.d.ts +2 -0
  184. package/dist/masyu.constants.js +2 -0
  185. package/dist/masyu.types.d.ts +46 -0
  186. package/dist/masyu.types.js +1 -0
  187. package/dist/masyuBoard.d.ts +10 -0
  188. package/dist/masyuBoard.js +88 -0
  189. package/dist/masyuDraw.d.ts +2 -0
  190. package/dist/masyuDraw.js +33 -0
  191. package/dist/masyuGame.d.ts +9 -0
  192. package/dist/masyuGame.js +46 -0
  193. package/dist/masyuGenerate.d.ts +3 -0
  194. package/dist/masyuGenerate.js +56 -0
  195. package/dist/masyuMount.d.ts +2 -0
  196. package/dist/masyuMount.js +118 -0
  197. package/dist/masyuPlay.types.d.ts +1 -0
  198. package/dist/masyuPlay.types.js +1 -0
  199. package/dist/masyuSolve.d.ts +6 -0
  200. package/dist/masyuSolve.js +88 -0
  201. package/dist/masyuStrings.d.ts +28 -0
  202. package/dist/masyuStrings.js +4 -0
  203. package/dist/masyuStyle.d.ts +1 -0
  204. package/dist/masyuStyle.js +11 -0
  205. package/dist/nurikabe-draw-entry.d.ts +2 -0
  206. package/dist/nurikabe-draw-entry.js +1 -0
  207. package/dist/nurikabe-entry.d.ts +6 -0
  208. package/dist/nurikabe-entry.js +6 -0
  209. package/dist/nurikabe-play-entry.d.ts +4 -0
  210. package/dist/nurikabe-play-entry.js +3 -0
  211. package/dist/nurikabe.constants.d.ts +2 -0
  212. package/dist/nurikabe.constants.js +2 -0
  213. package/dist/nurikabe.types.d.ts +20 -0
  214. package/dist/nurikabe.types.js +1 -0
  215. package/dist/nurikabeBoard.d.ts +8 -0
  216. package/dist/nurikabeBoard.js +72 -0
  217. package/dist/nurikabeDraw.d.ts +4 -0
  218. package/dist/nurikabeDraw.js +15 -0
  219. package/dist/nurikabeGame.d.ts +8 -0
  220. package/dist/nurikabeGame.js +40 -0
  221. package/dist/nurikabeGenerate.d.ts +3 -0
  222. package/dist/nurikabeGenerate.js +31 -0
  223. package/dist/nurikabeMount.d.ts +3 -0
  224. package/dist/nurikabeMount.js +99 -0
  225. package/dist/nurikabePlay.types.d.ts +25 -0
  226. package/dist/nurikabePlay.types.js +1 -0
  227. package/dist/nurikabeSolve.d.ts +6 -0
  228. package/dist/nurikabeSolve.js +75 -0
  229. package/dist/nurikabeStrings.d.ts +70 -0
  230. package/dist/nurikabeStrings.js +5 -0
  231. package/dist/nurikabeStyle.d.ts +1 -0
  232. package/dist/nurikabeStyle.js +9 -0
  233. package/dist/ripple-draw-entry.d.ts +2 -0
  234. package/dist/ripple-draw-entry.js +1 -0
  235. package/dist/ripple-entry.d.ts +12 -0
  236. package/dist/ripple-entry.js +11 -0
  237. package/dist/ripple-play-entry.d.ts +4 -0
  238. package/dist/ripple-play-entry.js +3 -0
  239. package/dist/ripple.constants.d.ts +4 -0
  240. package/dist/ripple.constants.js +4 -0
  241. package/dist/ripple.types.d.ts +34 -0
  242. package/dist/ripple.types.js +1 -0
  243. package/dist/rippleBoard.d.ts +7 -0
  244. package/dist/rippleBoard.js +99 -0
  245. package/dist/rippleDraw.d.ts +3 -0
  246. package/dist/rippleDraw.js +32 -0
  247. package/dist/rippleGame.d.ts +13 -0
  248. package/dist/rippleGame.js +85 -0
  249. package/dist/rippleGenerate.d.ts +3 -0
  250. package/dist/rippleGenerate.js +57 -0
  251. package/dist/rippleMount.d.ts +2 -0
  252. package/dist/rippleMount.js +179 -0
  253. package/dist/ripplePlay.types.d.ts +23 -0
  254. package/dist/ripplePlay.types.js +1 -0
  255. package/dist/rippleSolve.d.ts +6 -0
  256. package/dist/rippleSolve.js +83 -0
  257. package/dist/rippleStrings.d.ts +86 -0
  258. package/dist/rippleStrings.js +5 -0
  259. package/dist/rippleStyle.d.ts +1 -0
  260. package/dist/rippleStyle.js +1 -0
  261. package/dist/shikaku-entry.d.ts +1 -0
  262. package/dist/shikaku-entry.js +1 -0
  263. package/dist/shikakuPacks.d.ts +27 -0
  264. package/dist/shikakuPacks.js +26 -0
  265. package/dist/slitherlink-draw-entry.d.ts +2 -0
  266. package/dist/slitherlink-draw-entry.js +1 -0
  267. package/dist/slitherlink-entry.d.ts +11 -0
  268. package/dist/slitherlink-entry.js +9 -0
  269. package/dist/slitherlink-play-entry.d.ts +4 -0
  270. package/dist/slitherlink-play-entry.js +3 -0
  271. package/dist/slitherlink.constants.d.ts +2 -0
  272. package/dist/slitherlink.constants.js +2 -0
  273. package/dist/slitherlink.types.d.ts +34 -0
  274. package/dist/slitherlink.types.js +1 -0
  275. package/dist/slitherlinkBoard.d.ts +12 -0
  276. package/dist/slitherlinkBoard.js +132 -0
  277. package/dist/slitherlinkDraw.d.ts +4 -0
  278. package/dist/slitherlinkDraw.js +51 -0
  279. package/dist/slitherlinkGame.d.ts +8 -0
  280. package/dist/slitherlinkGame.js +63 -0
  281. package/dist/slitherlinkGenerate.d.ts +3 -0
  282. package/dist/slitherlinkGenerate.js +101 -0
  283. package/dist/slitherlinkMount.d.ts +3 -0
  284. package/dist/slitherlinkMount.js +267 -0
  285. package/dist/slitherlinkPlay.types.d.ts +23 -0
  286. package/dist/slitherlinkPlay.types.js +1 -0
  287. package/dist/slitherlinkSolve.d.ts +6 -0
  288. package/dist/slitherlinkSolve.js +127 -0
  289. package/dist/slitherlinkStrings.d.ts +86 -0
  290. package/dist/slitherlinkStrings.js +47 -0
  291. package/dist/slitherlinkStyle.d.ts +1 -0
  292. package/dist/slitherlinkStyle.js +18 -0
  293. package/dist/version.d.ts +1 -1
  294. package/dist/version.js +1 -1
  295. package/dist/yajilin-draw-entry.d.ts +2 -0
  296. package/dist/yajilin-draw-entry.js +1 -0
  297. package/dist/yajilin-entry.d.ts +6 -0
  298. package/dist/yajilin-entry.js +6 -0
  299. package/dist/yajilin-play-entry.d.ts +4 -0
  300. package/dist/yajilin-play-entry.js +3 -0
  301. package/dist/yajilin.constants.d.ts +2 -0
  302. package/dist/yajilin.constants.js +2 -0
  303. package/dist/yajilin.types.d.ts +63 -0
  304. package/dist/yajilin.types.js +1 -0
  305. package/dist/yajilinBoard.d.ts +9 -0
  306. package/dist/yajilinBoard.js +91 -0
  307. package/dist/yajilinDraw.d.ts +2 -0
  308. package/dist/yajilinDraw.js +33 -0
  309. package/dist/yajilinGame.d.ts +15 -0
  310. package/dist/yajilinGame.js +58 -0
  311. package/dist/yajilinGenerate.d.ts +3 -0
  312. package/dist/yajilinGenerate.js +224 -0
  313. package/dist/yajilinMount.d.ts +2 -0
  314. package/dist/yajilinMount.js +162 -0
  315. package/dist/yajilinPlay.types.d.ts +1 -0
  316. package/dist/yajilinPlay.types.js +1 -0
  317. package/dist/yajilinSolve.d.ts +6 -0
  318. package/dist/yajilinSolve.js +82 -0
  319. package/dist/yajilinStrings.d.ts +32 -0
  320. package/dist/yajilinStrings.js +4 -0
  321. package/dist/yajilinStyle.d.ts +1 -0
  322. package/dist/yajilinStyle.js +12 -0
  323. package/package.json +180 -3
@@ -0,0 +1,6 @@
1
+ import type { SlitherlinkBoard, SlitherlinkSolve } from "./slitherlink.types.ts";
2
+ /** Counts loop solutions with clue and vertex propagation under explicit work bounds. */
3
+ export declare function solveSlitherlink(board: SlitherlinkBoard, options?: {
4
+ limit?: number;
5
+ nodes?: number;
6
+ }): SlitherlinkSolve;
@@ -0,0 +1,127 @@
1
+ import { SLITHERLINK_MOST_NODES } from "./slitherlink.constants.js";
2
+ import { checkSlitherlink, edgeVertices, isSlitherlinkBoard, slitherlinkCellEdges, slitherlinkEdgeCount, slitherlinkVertexEdges, } from "./slitherlinkBoard.js";
3
+ /** Counts loop solutions with clue and vertex propagation under explicit work bounds. */
4
+ export function solveSlitherlink(board, options = {}) {
5
+ if (!isSlitherlinkBoard(board))
6
+ throw new RangeError("Invalid Slitherlink board");
7
+ const limit = options.limit ?? 2;
8
+ const budget = options.nodes ?? SLITHERLINK_MOST_NODES;
9
+ if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(budget) || budget < 1) {
10
+ throw new RangeError("Invalid search bounds");
11
+ }
12
+ const edgeCount = slitherlinkEdgeCount(board);
13
+ const vertices = Array.from({ length: (board.width + 1) * (board.height + 1) }, (_, vertex) => slitherlinkVertexEdges(board, vertex));
14
+ const cells = board.clues.flatMap((clue, cell) => clue === null ? [] : [slitherlinkCellEdges(board, cell)]);
15
+ let count = 0;
16
+ let nodes = 0;
17
+ let complete = true;
18
+ let solution = null;
19
+ const search = (start) => {
20
+ if (!complete)
21
+ return;
22
+ nodes += 1;
23
+ if (nodes > budget) {
24
+ complete = false;
25
+ return;
26
+ }
27
+ const state = start.slice();
28
+ let changed = true;
29
+ while (changed) {
30
+ changed = false;
31
+ for (const group of vertices) {
32
+ const on = group.filter(edge => state[edge] === 1).length;
33
+ const unknown = group.filter(edge => state[edge] < 0);
34
+ if (on > 2 || on === 1 && !unknown.length)
35
+ return;
36
+ if (on === 2) {
37
+ for (const edge of unknown) {
38
+ state[edge] = 0;
39
+ changed = true;
40
+ }
41
+ }
42
+ else if (on === 1 && unknown.length === 1) {
43
+ state[unknown[0]] = 1;
44
+ changed = true;
45
+ }
46
+ else if (on === 0 && unknown.length === 1) {
47
+ state[unknown[0]] = 0;
48
+ changed = true;
49
+ }
50
+ }
51
+ let clueIndex = 0;
52
+ for (const clue of board.clues) {
53
+ if (clue === null)
54
+ continue;
55
+ const group = cells[clueIndex++];
56
+ const on = group.filter(edge => state[edge] === 1).length;
57
+ const unknown = group.filter(edge => state[edge] < 0);
58
+ if (on > clue || on + unknown.length < clue)
59
+ return;
60
+ if (on === clue) {
61
+ for (const edge of unknown) {
62
+ state[edge] = 0;
63
+ changed = true;
64
+ }
65
+ }
66
+ else if (on + unknown.length === clue) {
67
+ for (const edge of unknown) {
68
+ state[edge] = 1;
69
+ changed = true;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ const selected = Array.from(state).flatMap((value, edge) => value === 1 ? [edge] : []);
75
+ const closed = closedComponents(board, selected);
76
+ if (closed.length > 1)
77
+ return;
78
+ if (closed.length === 1 && closed[0].length !== selected.length)
79
+ return;
80
+ const branch = state.findIndex(value => value < 0);
81
+ if (branch < 0) {
82
+ if (!checkSlitherlink(board, selected).ok)
83
+ return;
84
+ count += 1;
85
+ solution ?? (solution = selected);
86
+ if (count >= limit)
87
+ complete = false;
88
+ return;
89
+ }
90
+ for (const value of [1, 0]) {
91
+ const next = state.slice();
92
+ next[branch] = value;
93
+ search(next);
94
+ if (!complete)
95
+ return;
96
+ }
97
+ };
98
+ search(new Int8Array(edgeCount).fill(-1));
99
+ return { count, solution, complete, nodes };
100
+ }
101
+ function closedComponents(board, selected) {
102
+ const unseen = new Set(selected);
103
+ const components = [];
104
+ while (unseen.size) {
105
+ const first = unseen.values().next().value;
106
+ const component = [first];
107
+ const queue = [first];
108
+ unseen.delete(first);
109
+ while (queue.length) {
110
+ const edge = queue.pop();
111
+ for (const vertex of edgeVertices(board, edge)) {
112
+ for (const adjacent of slitherlinkVertexEdges(board, vertex)) {
113
+ if (!unseen.has(adjacent))
114
+ continue;
115
+ unseen.delete(adjacent);
116
+ component.push(adjacent);
117
+ queue.push(adjacent);
118
+ }
119
+ }
120
+ }
121
+ const vertices = new Set(component.flatMap(edge => edgeVertices(board, edge)));
122
+ if ([...vertices].every(vertex => slitherlinkVertexEdges(board, vertex)
123
+ .filter(edge => component.includes(edge)).length === 2))
124
+ components.push(component);
125
+ }
126
+ return components;
127
+ }
@@ -0,0 +1,86 @@
1
+ import type { SlitherlinkLanguage } from "./slitherlinkPlay.types.ts";
2
+ export declare const SLITHERLINK_STRINGS: {
3
+ readonly en: {
4
+ readonly title: "Slitherlink";
5
+ readonly help: "Connect adjacent dots with horizontal and vertical edges to make one closed loop. Each number says how many of its four bordering edges are in the loop. Empty cells may have any number of edges. The loop cannot branch or split into separate loops. Click or tap an edge, or focus it and press Enter or Space. Arrow keys move between edges.";
6
+ readonly undo: "Undo";
7
+ readonly hint: "Hint";
8
+ readonly check: "Check";
9
+ readonly restart: "Restart";
10
+ readonly expand: "Just the board";
11
+ readonly close: "Close";
12
+ readonly won: "One closed loop. Solved!";
13
+ readonly helped: "Solved with help.";
14
+ readonly checked: "No rule conflicts so far. Keep drawing the loop.";
15
+ readonly wrong: "Check the numbered cells, branches and loop connections.";
16
+ readonly hintWhy: "This edge is part of the only proved loop.";
17
+ readonly noHint: "No proved hint is available.";
18
+ readonly row: "Row";
19
+ readonly column: "column";
20
+ readonly clue: "number";
21
+ readonly blank: "no number";
22
+ readonly edge: "edge";
23
+ };
24
+ readonly ja: {
25
+ readonly title: "スリザーリンク";
26
+ readonly help: "点を縦横の線でつなぎ、一つの閉じた輪を作ります。数字はそのマスを囲む四辺のうち、輪になる辺の数です。数字のないマスは何辺でも構いません。輪を分岐させたり、別々の輪に分けたりしません。線をタップ、クリック、または選んでEnterかSpaceを押します。矢印キーで辺を移動します。";
27
+ readonly undo: "戻す";
28
+ readonly hint: "ヒント";
29
+ readonly check: "確かめる";
30
+ readonly restart: "やり直す";
31
+ readonly expand: "盤だけ";
32
+ readonly close: "閉じる";
33
+ readonly won: "一つの輪ができました。完成!";
34
+ readonly helped: "ヒントを使って完成!";
35
+ readonly checked: "ここまでルールに合っています。輪を続けましょう。";
36
+ readonly wrong: "数字のマス、分岐、輪のつながりを確かめましょう。";
37
+ readonly hintWhy: "唯一の解答に含まれる辺です。";
38
+ readonly noHint: "確かなヒントがありません。";
39
+ readonly row: "行";
40
+ readonly column: "列";
41
+ readonly clue: "数字";
42
+ readonly blank: "数字なし";
43
+ readonly edge: "辺";
44
+ };
45
+ };
46
+ export declare function slitherlinkWords(language?: SlitherlinkLanguage): {
47
+ readonly title: "Slitherlink";
48
+ readonly help: "Connect adjacent dots with horizontal and vertical edges to make one closed loop. Each number says how many of its four bordering edges are in the loop. Empty cells may have any number of edges. The loop cannot branch or split into separate loops. Click or tap an edge, or focus it and press Enter or Space. Arrow keys move between edges.";
49
+ readonly undo: "Undo";
50
+ readonly hint: "Hint";
51
+ readonly check: "Check";
52
+ readonly restart: "Restart";
53
+ readonly expand: "Just the board";
54
+ readonly close: "Close";
55
+ readonly won: "One closed loop. Solved!";
56
+ readonly helped: "Solved with help.";
57
+ readonly checked: "No rule conflicts so far. Keep drawing the loop.";
58
+ readonly wrong: "Check the numbered cells, branches and loop connections.";
59
+ readonly hintWhy: "This edge is part of the only proved loop.";
60
+ readonly noHint: "No proved hint is available.";
61
+ readonly row: "Row";
62
+ readonly column: "column";
63
+ readonly clue: "number";
64
+ readonly blank: "no number";
65
+ readonly edge: "edge";
66
+ } | {
67
+ readonly title: "スリザーリンク";
68
+ readonly help: "点を縦横の線でつなぎ、一つの閉じた輪を作ります。数字はそのマスを囲む四辺のうち、輪になる辺の数です。数字のないマスは何辺でも構いません。輪を分岐させたり、別々の輪に分けたりしません。線をタップ、クリック、または選んでEnterかSpaceを押します。矢印キーで辺を移動します。";
69
+ readonly undo: "戻す";
70
+ readonly hint: "ヒント";
71
+ readonly check: "確かめる";
72
+ readonly restart: "やり直す";
73
+ readonly expand: "盤だけ";
74
+ readonly close: "閉じる";
75
+ readonly won: "一つの輪ができました。完成!";
76
+ readonly helped: "ヒントを使って完成!";
77
+ readonly checked: "ここまでルールに合っています。輪を続けましょう。";
78
+ readonly wrong: "数字のマス、分岐、輪のつながりを確かめましょう。";
79
+ readonly hintWhy: "唯一の解答に含まれる辺です。";
80
+ readonly noHint: "確かなヒントがありません。";
81
+ readonly row: "行";
82
+ readonly column: "列";
83
+ readonly clue: "数字";
84
+ readonly blank: "数字なし";
85
+ readonly edge: "辺";
86
+ };
@@ -0,0 +1,47 @@
1
+ export const SLITHERLINK_STRINGS = {
2
+ en: {
3
+ title: "Slitherlink",
4
+ help: "Connect adjacent dots with horizontal and vertical edges to make one closed loop. Each number says how many of its four bordering edges are in the loop. Empty cells may have any number of edges. The loop cannot branch or split into separate loops. Click or tap an edge, or focus it and press Enter or Space. Arrow keys move between edges.",
5
+ undo: "Undo",
6
+ hint: "Hint",
7
+ check: "Check",
8
+ restart: "Restart",
9
+ expand: "Just the board",
10
+ close: "Close",
11
+ won: "One closed loop. Solved!",
12
+ helped: "Solved with help.",
13
+ checked: "No rule conflicts so far. Keep drawing the loop.",
14
+ wrong: "Check the numbered cells, branches and loop connections.",
15
+ hintWhy: "This edge is part of the only proved loop.",
16
+ noHint: "No proved hint is available.",
17
+ row: "Row",
18
+ column: "column",
19
+ clue: "number",
20
+ blank: "no number",
21
+ edge: "edge",
22
+ },
23
+ ja: {
24
+ title: "スリザーリンク",
25
+ help: "点を縦横の線でつなぎ、一つの閉じた輪を作ります。数字はそのマスを囲む四辺のうち、輪になる辺の数です。数字のないマスは何辺でも構いません。輪を分岐させたり、別々の輪に分けたりしません。線をタップ、クリック、または選んでEnterかSpaceを押します。矢印キーで辺を移動します。",
26
+ undo: "戻す",
27
+ hint: "ヒント",
28
+ check: "確かめる",
29
+ restart: "やり直す",
30
+ expand: "盤だけ",
31
+ close: "閉じる",
32
+ won: "一つの輪ができました。完成!",
33
+ helped: "ヒントを使って完成!",
34
+ checked: "ここまでルールに合っています。輪を続けましょう。",
35
+ wrong: "数字のマス、分岐、輪のつながりを確かめましょう。",
36
+ hintWhy: "唯一の解答に含まれる辺です。",
37
+ noHint: "確かなヒントがありません。",
38
+ row: "行",
39
+ column: "列",
40
+ clue: "数字",
41
+ blank: "数字なし",
42
+ edge: "辺",
43
+ },
44
+ };
45
+ export function slitherlinkWords(language = "en") {
46
+ return SLITHERLINK_STRINGS[language];
47
+ }
@@ -0,0 +1 @@
1
+ export declare const SLITHERLINK_PLAY_STYLE = "\n.sl-root{color:var(--sl-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:760px;margin:auto}\n.sl-board{display:grid;position:relative;gap:0;width:100%;min-width:240px;touch-action:manipulation;user-select:none}\n.sl-cell{position:relative;grid-column:span 2;grid-row:span 2;display:grid;place-items:center;min-width:0;min-height:0;border:0;background:transparent;padding:0;color:var(--sl-ink,#1f2320);font:700 clamp(14px,2vw,22px)/1 system-ui,sans-serif}\n.sl-dot{z-index:2;grid-column:span 1;grid-row:span 1;justify-self:center;align-self:center;width:8px;height:8px;border-radius:50%;background:var(--sl-ink,#1f2320);pointer-events:none}\n.sl-edge{position:relative;z-index:1;grid-column:span 2;grid-row:span 1;min-width:0;min-height:0;border:0;padding:0;background:transparent;cursor:pointer;touch-action:manipulation}\n.sl-edge.vertical{grid-column:span 1;grid-row:span 2}\n.sl-edge::after{content:\"\";position:absolute;background:var(--kz-grid,#cfc6b2);transition:background .12s ease}\n.sl-edge.horizontal::after{left:0;right:0;top:50%;height:5px;transform:translateY(-50%)}\n.sl-edge.vertical::after{top:0;bottom:0;left:50%;width:5px;transform:translateX(-50%)}\n.sl-edge.on::after{background:var(--sl-ink,#1f2320)}\n.sl-edge.error::after{background:var(--kz-bad,#b5452c)}\n.sl-edge:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-2px;border-radius:4px}\n.sl-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}\n.sl-tools button,.sl-close{font:inherit;border:1px solid var(--sl-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--sl-surface,#fbf8f1);color:var(--sl-ink,#1f2320);cursor:pointer}\n.sl-tools button:disabled{opacity:.45;cursor:default}.sl-status{min-height:1.5em}.sl-help{font-size:14px;opacity:.8}\n.sl-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,820px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--sl-ink:var(--ink,#1f2320);--sl-surface:var(--surface,#fbf8f1);--sl-rule:var(--rule,#ddd6c6)}.sl-dialog::backdrop{background:#172c28bb}.sl-close{float:right;margin-bottom:10px}.sl-dialog .sl-root{clear:both}\n";
@@ -0,0 +1,18 @@
1
+ export const SLITHERLINK_PLAY_STYLE = `
2
+ .sl-root{color:var(--sl-ink,#1f2320);font:16px/1.5 var(--font,system-ui,sans-serif);max-width:760px;margin:auto}
3
+ .sl-board{display:grid;position:relative;gap:0;width:100%;min-width:240px;touch-action:manipulation;user-select:none}
4
+ .sl-cell{position:relative;grid-column:span 2;grid-row:span 2;display:grid;place-items:center;min-width:0;min-height:0;border:0;background:transparent;padding:0;color:var(--sl-ink,#1f2320);font:700 clamp(14px,2vw,22px)/1 system-ui,sans-serif}
5
+ .sl-dot{z-index:2;grid-column:span 1;grid-row:span 1;justify-self:center;align-self:center;width:8px;height:8px;border-radius:50%;background:var(--sl-ink,#1f2320);pointer-events:none}
6
+ .sl-edge{position:relative;z-index:1;grid-column:span 2;grid-row:span 1;min-width:0;min-height:0;border:0;padding:0;background:transparent;cursor:pointer;touch-action:manipulation}
7
+ .sl-edge.vertical{grid-column:span 1;grid-row:span 2}
8
+ .sl-edge::after{content:"";position:absolute;background:var(--kz-grid,#cfc6b2);transition:background .12s ease}
9
+ .sl-edge.horizontal::after{left:0;right:0;top:50%;height:5px;transform:translateY(-50%)}
10
+ .sl-edge.vertical::after{top:0;bottom:0;left:50%;width:5px;transform:translateX(-50%)}
11
+ .sl-edge.on::after{background:var(--sl-ink,#1f2320)}
12
+ .sl-edge.error::after{background:var(--kz-bad,#b5452c)}
13
+ .sl-edge:focus-visible{outline:3px solid var(--accent,#b5452c);outline-offset:-2px;border-radius:4px}
14
+ .sl-tools{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}
15
+ .sl-tools button,.sl-close{font:inherit;border:1px solid var(--sl-rule,#ddd6c6);border-radius:999px;min-height:44px;padding:0 14px;background:var(--sl-surface,#fbf8f1);color:var(--sl-ink,#1f2320);cursor:pointer}
16
+ .sl-tools button:disabled{opacity:.45;cursor:default}.sl-status{min-height:1.5em}.sl-help{font-size:14px;opacity:.8}
17
+ .sl-dialog{border:0;border-radius:14px;padding:24px;width:min(86vw,820px);max-height:90vh;overflow:auto;background:var(--surface,#fbf8f1);--sl-ink:var(--ink,#1f2320);--sl-surface:var(--surface,#fbf8f1);--sl-rule:var(--rule,#ddd6c6)}.sl-dialog::backdrop{background:#172c28bb}.sl-close{float:right;margin-bottom:10px}.sl-dialog .sl-root{clear:both}
18
+ `;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version. */
2
- export declare const VERSION = "1.1.0";
2
+ export declare const VERSION = "1.2.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version. */
2
- export const VERSION = "1.1.0";
2
+ export const VERSION = "1.2.0";
@@ -0,0 +1,2 @@
1
+ export { drawYajilin } from "./yajilinDraw.ts";
2
+ export type { YajilinDrawOptions } from "./yajilin.types.ts";
@@ -0,0 +1 @@
1
+ export { drawYajilin } from "./yajilinDraw.js";
@@ -0,0 +1,6 @@
1
+ export * from "./yajilin.types.ts";
2
+ export * from "./yajilin.constants.ts";
3
+ export * from "./yajilinBoard.ts";
4
+ export * from "./yajilinSolve.ts";
5
+ export * from "./yajilinGenerate.ts";
6
+ export * from "./yajilinGame.ts";
@@ -0,0 +1,6 @@
1
+ export * from "./yajilin.types.js";
2
+ export * from "./yajilin.constants.js";
3
+ export * from "./yajilinBoard.js";
4
+ export * from "./yajilinSolve.js";
5
+ export * from "./yajilinGenerate.js";
6
+ export * from "./yajilinGame.js";
@@ -0,0 +1,4 @@
1
+ export { mountYajilin } from "./yajilinMount.ts";
2
+ export { YAJILIN_PLAY_STYLE } from "./yajilinStyle.ts";
3
+ export { YAJILIN_STRINGS } from "./yajilinStrings.ts";
4
+ export type * from "./yajilinPlay.types.ts";
@@ -0,0 +1,3 @@
1
+ export { mountYajilin } from "./yajilinMount.js";
2
+ export { YAJILIN_PLAY_STYLE } from "./yajilinStyle.js";
3
+ export { YAJILIN_STRINGS } from "./yajilinStrings.js";
@@ -0,0 +1,2 @@
1
+ export declare const YAJILIN_SIZES: readonly [5, 7];
2
+ export declare const YAJILIN_NODE_BUDGET = 250000;
@@ -0,0 +1,2 @@
1
+ export const YAJILIN_SIZES = [5, 7];
2
+ export const YAJILIN_NODE_BUDGET = 250000;
@@ -0,0 +1,63 @@
1
+ export type YajilinDirection = "up" | "right" | "down" | "left";
2
+ export type YajilinClue = {
3
+ direction: YajilinDirection;
4
+ count: number;
5
+ };
6
+ export type YajilinBoard = {
7
+ width: number;
8
+ height: number;
9
+ clues: readonly (YajilinClue | null)[];
10
+ };
11
+ export type YajilinPuzzle = YajilinBoard & {
12
+ seed: number;
13
+ solution: {
14
+ shaded: readonly boolean[];
15
+ edges: readonly number[];
16
+ };
17
+ };
18
+ export type YajilinCheck = {
19
+ ok: boolean;
20
+ errors: readonly number[];
21
+ };
22
+ export type YajilinSolution = {
23
+ shaded: readonly boolean[];
24
+ edges: readonly number[];
25
+ };
26
+ export type YajilinSolve = {
27
+ count: number;
28
+ solution: YajilinSolution | null;
29
+ complete: boolean;
30
+ nodes: number;
31
+ };
32
+ export type YajilinGame = {
33
+ board: YajilinBoard;
34
+ shaded: readonly boolean[];
35
+ edges: readonly number[];
36
+ history: readonly {
37
+ shaded: readonly boolean[];
38
+ edges: readonly number[];
39
+ }[];
40
+ helped: boolean;
41
+ };
42
+ export type YajilinDrawOptions = {
43
+ shaded?: readonly boolean[];
44
+ edges?: readonly number[];
45
+ selected?: number;
46
+ errors?: readonly number[];
47
+ material?: "ivory" | "wood" | "slate";
48
+ pieces?: "plain" | "tiles";
49
+ language?: "en" | "ja";
50
+ };
51
+ export type YajilinMountOptions = YajilinDrawOptions & {
52
+ board: YajilinBoard;
53
+ progress?: string;
54
+ onChange?: (game: YajilinGame) => void;
55
+ onFinish?: (game: YajilinGame) => void;
56
+ };
57
+ export type YajilinMount = {
58
+ game: () => YajilinGame;
59
+ progress: () => string;
60
+ set: (options: YajilinDrawOptions) => void;
61
+ restart: () => void;
62
+ destroy: () => void;
63
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { YajilinBoard, YajilinCheck, YajilinDirection } from "./yajilin.types.ts";
2
+ export declare const yajilinEdgeCount: (board: YajilinBoard) => number;
3
+ export declare function isYajilinBoard(value: unknown): value is YajilinBoard;
4
+ export declare function yajilinNeighbors(board: YajilinBoard, cell: number): {
5
+ cell: number;
6
+ edge: number;
7
+ }[];
8
+ export declare function checkYajilin(board: YajilinBoard, shaded: readonly boolean[], lines: readonly number[]): YajilinCheck;
9
+ export declare function yajilinRay(board: YajilinBoard, cell: number, direction: YajilinDirection): number[];
@@ -0,0 +1,91 @@
1
+ import { YAJILIN_SIZES } from "./yajilin.constants.js";
2
+ const directionDelta = { up: [0, -1], right: [1, 0], down: [0, 1], left: [-1, 0] };
3
+ export const yajilinEdgeCount = (board) => board.height * (board.width - 1) + board.width * (board.height - 1);
4
+ export function isYajilinBoard(value) {
5
+ if (!value || typeof value !== "object")
6
+ return false;
7
+ const board = value;
8
+ if (!YAJILIN_SIZES.includes(board.width) || board.width !== board.height
9
+ || !Array.isArray(board.clues) || board.clues.length !== board.width * board.height)
10
+ return false;
11
+ return board.clues.every(clue => clue === null || (!!clue && typeof clue === "object"
12
+ && Object.hasOwn(directionDelta, clue.direction) && Number.isInteger(clue.count) && clue.count >= 0 && clue.count < board.width));
13
+ }
14
+ export function yajilinNeighbors(board, cell) {
15
+ const x = cell % board.width, y = Math.floor(cell / board.width), horizontal = board.height * (board.width - 1), result = [];
16
+ if (y > 0)
17
+ result.push({ cell: cell - board.width, edge: horizontal + (y - 1) * board.width + x });
18
+ if (x < board.width - 1)
19
+ result.push({ cell: cell + 1, edge: y * (board.width - 1) + x });
20
+ if (y < board.height - 1)
21
+ result.push({ cell: cell + board.width, edge: horizontal + y * board.width + x });
22
+ if (x > 0)
23
+ result.push({ cell: cell - 1, edge: y * (board.width - 1) + x - 1 });
24
+ return result;
25
+ }
26
+ function ray(board, cell, direction) {
27
+ const [dx, dy] = directionDelta[direction], cells = [];
28
+ let x = cell % board.width + dx, y = Math.floor(cell / board.width) + dy;
29
+ while (x >= 0 && x < board.width && y >= 0 && y < board.height) {
30
+ cells.push(y * board.width + x);
31
+ x += dx;
32
+ y += dy;
33
+ }
34
+ return cells;
35
+ }
36
+ export function checkYajilin(board, shaded, lines) {
37
+ if (!isYajilinBoard(board))
38
+ throw new RangeError("Invalid Yajilin board");
39
+ const size = board.width, total = size * size, edges = new Set(lines), errors = new Set();
40
+ if (shaded.length !== total || !shaded.every(value => typeof value === "boolean"))
41
+ return { ok: false, errors: [] };
42
+ if (edges.size !== lines.length || lines.some(edge => !Number.isInteger(edge) || edge < 0 || edge >= yajilinEdgeCount(board)))
43
+ return { ok: false, errors: [] };
44
+ for (let cell = 0; cell < total; cell += 1) {
45
+ const clue = board.clues[cell];
46
+ if (clue && shaded[cell])
47
+ errors.add(cell);
48
+ if (shaded[cell]) {
49
+ if (cell % size < size - 1 && shaded[cell + 1]) {
50
+ errors.add(cell);
51
+ errors.add(cell + 1);
52
+ }
53
+ if (cell + size < total && shaded[cell + size]) {
54
+ errors.add(cell);
55
+ errors.add(cell + size);
56
+ }
57
+ }
58
+ if (clue && ray(board, cell, clue.direction).filter(next => shaded[next]).length !== clue.count)
59
+ errors.add(cell);
60
+ }
61
+ const degrees = board.clues.map((clue, cell) => {
62
+ const degree = yajilinNeighbors(board, cell).filter(next => edges.has(next.edge)).length;
63
+ if ((clue || shaded[cell]) && degree > 0)
64
+ errors.add(cell);
65
+ if (!clue && !shaded[cell] && degree !== 2)
66
+ errors.add(cell);
67
+ return degree;
68
+ });
69
+ if (edges.size) {
70
+ const first = degrees.findIndex(degree => degree > 0), reached = new Set([first]), stack = [first];
71
+ while (stack.length) {
72
+ const cell = stack.pop();
73
+ for (const next of yajilinNeighbors(board, cell))
74
+ if (edges.has(next.edge) && !reached.has(next.cell)) {
75
+ reached.add(next.cell);
76
+ stack.push(next.cell);
77
+ }
78
+ }
79
+ const loopCells = degrees.filter(degree => degree > 0).length;
80
+ if (reached.size !== loopCells || edges.size !== loopCells)
81
+ for (const cell of reached)
82
+ errors.add(cell);
83
+ }
84
+ const playable = board.clues.filter((clue, cell) => !clue && !shaded[cell]).length;
85
+ const ok = playable >= 4 && edges.size === playable && errors.size === 0
86
+ && degrees.every((degree, cell) => board.clues[cell] || shaded[cell] ? degree === 0 : degree === 2);
87
+ return { ok, errors: [...errors] };
88
+ }
89
+ export function yajilinRay(board, cell, direction) {
90
+ return ray(board, cell, direction);
91
+ }
@@ -0,0 +1,2 @@
1
+ import type { YajilinBoard, YajilinDrawOptions } from "./yajilin.types.ts";
2
+ export declare function drawYajilin(board: YajilinBoard, options?: YajilinDrawOptions): string;
@@ -0,0 +1,33 @@
1
+ import { isYajilinBoard, yajilinNeighbors } from "./yajilinBoard.js";
2
+ import { YAJILIN_STRINGS } from "./yajilinStrings.js";
3
+ const arrows = { up: "↑", right: "→", down: "↓", left: "←" };
4
+ export function drawYajilin(board, options = {}) {
5
+ if (!isYajilinBoard(board))
6
+ throw new RangeError("Invalid Yajilin board");
7
+ const words = YAJILIN_STRINGS[options.language ?? "en"], size = 52, pad = 14;
8
+ const palette = { ivory: ["var(--kz-paper,#fbf8f1)", "var(--kz-ink,#1f2320)", "var(--kz-grid,#cfc6b2)"], wood: ["#e5cda6", "#493e2f", "#bca582"], slate: ["#262a27", "#ece8dc", "#454a44"] };
9
+ const [paper, ink, grid] = palette[options.material ?? "ivory"], edges = new Set(options.edges ?? []), parts = [];
10
+ for (let cell = 0; cell < board.clues.length; cell += 1) {
11
+ const x = pad + cell % board.width * size, y = pad + Math.floor(cell / board.width) * size, clue = board.clues[cell];
12
+ const bad = options.errors?.includes(cell), active = options.selected === cell;
13
+ parts.push(`<rect x="${x - size / 2}" y="${y - size / 2}" width="${size}" height="${size}" fill="${active ? ink : "transparent"}" fill-opacity="${active ? ".08" : "1"}" stroke="${grid}" stroke-width="1"/>`);
14
+ if (options.shaded?.[cell])
15
+ parts.push(`<rect x="${x - 17}" y="${y - 17}" width="34" height="34" rx="2" fill="${bad ? "var(--kz-bad,#b5452c)" : ink}"/>`);
16
+ if (clue) {
17
+ if (options.pieces === "tiles")
18
+ parts.push(`<rect x="${x - 19}" y="${y - 19}" width="38" height="38" rx="5" fill="${paper}" stroke="${grid}"/>`);
19
+ parts.push(`<text x="${x - 2}" y="${y + 1}" text-anchor="middle" dominant-baseline="middle" fill="${bad ? "var(--kz-bad,#b5452c)" : ink}" font-size="20" font-weight="700" font-family="system-ui,sans-serif">${arrows[clue.direction]}</text>`);
20
+ parts.push(`<text x="${x + 11}" y="${y + 14}" text-anchor="middle" dominant-baseline="middle" fill="${ink}" font-size="12" font-weight="700" font-family="system-ui,sans-serif">${clue.count}</text>`);
21
+ }
22
+ for (const next of yajilinNeighbors(board, cell))
23
+ if (cell < next.cell && edges.has(next.edge)) {
24
+ const nx = pad + next.cell % board.width * size, ny = pad + Math.floor(next.cell / board.width) * size;
25
+ parts.push(`<line x1="${x}" y1="${y}" x2="${nx}" y2="${ny}" stroke="${bad ? "var(--kz-bad,#b5452c)" : ink}" stroke-width="5" stroke-linecap="round"/>`);
26
+ }
27
+ }
28
+ for (let y = 0; y < board.height; y += 1)
29
+ for (let x = 0; x < board.width; x += 1) {
30
+ parts.push(`<circle cx="${pad + x * size}" cy="${pad + y * size}" r="2" fill="${grid}"/>`);
31
+ }
32
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${pad * 2 + (board.width - 1) * size} ${pad * 2 + (board.height - 1) * size}" role="img" aria-label="${words.title}" style="display:block;width:100%;height:auto;background:${paper}">${parts.join("")}</svg>`;
33
+ }
@@ -0,0 +1,15 @@
1
+ import type { YajilinBoard, YajilinGame } from "./yajilin.types.ts";
2
+ export declare function newYajilin(board: YajilinBoard): YajilinGame;
3
+ export declare function toggleYajilinShade(game: YajilinGame, cell: number): YajilinGame;
4
+ export declare function toggleYajilinEdge(game: YajilinGame, edge: number): YajilinGame;
5
+ export declare function undoYajilin(game: YajilinGame): YajilinGame;
6
+ export declare function restartYajilin(game: YajilinGame): YajilinGame;
7
+ export declare function hintYajilin(game: YajilinGame): {
8
+ type: "shade";
9
+ cell: number;
10
+ } | {
11
+ type: "edge";
12
+ edge: number;
13
+ } | null;
14
+ export declare function encodeYajilin(game: YajilinGame): string;
15
+ export declare function decodeYajilin(code: string): YajilinGame | null;
@@ -0,0 +1,58 @@
1
+ import { isYajilinBoard, yajilinEdgeCount } from "./yajilinBoard.js";
2
+ import { solveYajilin } from "./yajilinSolve.js";
3
+ export function newYajilin(board) {
4
+ if (!isYajilinBoard(board))
5
+ throw new RangeError("Invalid Yajilin board");
6
+ return { board: { width: board.width, height: board.height, clues: board.clues.map(clue => clue ? { ...clue } : null) }, shaded: Array(board.width * board.height).fill(false), edges: [], history: [], helped: false };
7
+ }
8
+ function snapshot(game) { return { shaded: [...game.shaded], edges: [...game.edges] }; }
9
+ export function toggleYajilinShade(game, cell) {
10
+ if (!Number.isInteger(cell) || cell < 0 || cell >= game.shaded.length || game.board.clues[cell])
11
+ return game;
12
+ const shaded = [...game.shaded];
13
+ shaded[cell] = !shaded[cell];
14
+ return { ...game, shaded, history: [...game.history, snapshot(game)] };
15
+ }
16
+ export function toggleYajilinEdge(game, edge) {
17
+ if (!Number.isInteger(edge) || edge < 0 || edge >= yajilinEdgeCount(game.board))
18
+ return game;
19
+ const edges = game.edges.includes(edge) ? game.edges.filter(value => value !== edge) : [...game.edges, edge].sort((a, b) => a - b);
20
+ return { ...game, edges, history: [...game.history, snapshot(game)] };
21
+ }
22
+ export function undoYajilin(game) {
23
+ const previous = game.history.at(-1);
24
+ return previous ? { ...game, shaded: [...previous.shaded], edges: [...previous.edges], history: game.history.slice(0, -1) } : game;
25
+ }
26
+ export function restartYajilin(game) { return { ...newYajilin(game.board), helped: false }; }
27
+ export function hintYajilin(game) {
28
+ const result = solveYajilin(game.board);
29
+ if (!result.complete || result.count !== 1)
30
+ return null;
31
+ const cell = result.solution.shaded.findIndex((value, index) => value !== game.shaded[index]);
32
+ if (cell >= 0)
33
+ return { type: "shade", cell };
34
+ const edge = result.solution.edges.find(value => !game.edges.includes(value));
35
+ return edge === undefined ? null : { type: "edge", edge };
36
+ }
37
+ export function encodeYajilin(game) {
38
+ return JSON.stringify({ version: 1, board: game.board, shaded: game.shaded, edges: game.edges, helped: game.helped });
39
+ }
40
+ export function decodeYajilin(code) {
41
+ try {
42
+ if (code.length > 40000)
43
+ return null;
44
+ const value = JSON.parse(code);
45
+ if (value.version !== 1 || !isYajilinBoard(value.board) || !Array.isArray(value.shaded)
46
+ || value.shaded.length !== value.board.width * value.board.height || !value.shaded.every((cell) => typeof cell === "boolean")
47
+ || !Array.isArray(value.edges) || !value.edges.every((edge) => Number.isInteger(edge))
48
+ || typeof value.helped !== "boolean")
49
+ return null;
50
+ const edges = [...new Set(value.edges)].sort((a, b) => a - b);
51
+ if (edges.length !== value.edges.length || edges.some(edge => edge < 0 || edge >= yajilinEdgeCount(value.board)))
52
+ return null;
53
+ return { ...newYajilin(value.board), shaded: [...value.shaded], edges, helped: value.helped };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
@@ -0,0 +1,3 @@
1
+ import type { YajilinPuzzle } from "./yajilin.types.ts";
2
+ /** Seeded original 5×5 families, accepted only after a completed uniqueness proof. */
3
+ export declare function generateYajilin(size?: 5, seed?: number): YajilinPuzzle;