hbengine 0.2.3

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 (561) hide show
  1. package/README.md +20 -0
  2. package/engine-cli/connect.mjs +73 -0
  3. package/engine-cli/hbengine.mjs +565 -0
  4. package/engine-cli/package.json +1 -0
  5. package/engine-cli/templates.mjs +14 -0
  6. package/engine-conformance/analyse-worker.mjs +12 -0
  7. package/engine-conformance/analyse.mjs +4458 -0
  8. package/engine-conformance/bundle.mjs +208 -0
  9. package/engine-conformance/clock.mjs +294 -0
  10. package/engine-conformance/csp.go +56 -0
  11. package/engine-conformance/csp.json +6 -0
  12. package/engine-conformance/csp.mjs +20 -0
  13. package/engine-conformance/gzip-sizes.mjs +9 -0
  14. package/engine-conformance/package.json +1 -0
  15. package/engine-conformance/picture.mjs +55 -0
  16. package/engine-conformance/report.go +349 -0
  17. package/engine-conformance/run.mjs +1217 -0
  18. package/engine-conformance/scan.mjs +476 -0
  19. package/engine-conformance/testdata/clock-overlay/bundle/assets.json +1 -0
  20. package/engine-conformance/testdata/clock-overlay/bundle/game.css +17 -0
  21. package/engine-conformance/testdata/clock-overlay/bundle/game.js +83 -0
  22. package/engine-conformance/testdata/clock-overlay/bundle/hbtest.json +4 -0
  23. package/engine-conformance/testdata/clock-overlay/bundle/index.html +34 -0
  24. package/engine-conformance/testdata/clock-probe/bundle/assets.json +1 -0
  25. package/engine-conformance/testdata/clock-probe/bundle/game.css +17 -0
  26. package/engine-conformance/testdata/clock-probe/bundle/game.js +65 -0
  27. package/engine-conformance/testdata/clock-probe/bundle/hbtest.json +4 -0
  28. package/engine-conformance/testdata/clock-probe/bundle/index.html +35 -0
  29. package/engine-conformance/testdata/clock-probe/bundle/probe-worker.js +46 -0
  30. package/engine-conformance/testdata/clock-probe/bundle/probe.js +210 -0
  31. package/engine-conformance/testdata/clock-probe/math/dice.tsv +100 -0
  32. package/engine-conformance/testdata/clock-probe/math/manifest.json +2925 -0
  33. package/engine-conformance/testdata/clock-worker/bundle/assets.json +1 -0
  34. package/engine-conformance/testdata/clock-worker/bundle/clock-worker.js +14 -0
  35. package/engine-conformance/testdata/clock-worker/bundle/game.css +17 -0
  36. package/engine-conformance/testdata/clock-worker/bundle/game.js +61 -0
  37. package/engine-conformance/testdata/clock-worker/bundle/hbtest.json +4 -0
  38. package/engine-conformance/testdata/clock-worker/bundle/index.html +35 -0
  39. package/engine-conformance/testdata/clock-worker/math/dice.tsv +100 -0
  40. package/engine-conformance/testdata/clock-worker/math/manifest.json +2925 -0
  41. package/engine-conformance/travel.mjs +1209 -0
  42. package/engine-conformance/vendor/acorn/LICENSE +21 -0
  43. package/engine-conformance/vendor/acorn/acorn.mjs +6266 -0
  44. package/engine-conformance/wasm-releases.json +168 -0
  45. package/engine-examples/README.md +23 -0
  46. package/engine-examples/coin/AGENTS.md +3 -0
  47. package/engine-examples/coin/README.md +7 -0
  48. package/engine-examples/coin/bundle/assets.json +1 -0
  49. package/engine-examples/coin/bundle/game.css +5 -0
  50. package/engine-examples/coin/bundle/game.js +19 -0
  51. package/engine-examples/coin/bundle/hbtest.json +5 -0
  52. package/engine-examples/coin/bundle/index.html +16 -0
  53. package/engine-examples/coin/math/coin.tsv +2 -0
  54. package/engine-examples/coin/math/manifest.json +17 -0
  55. package/engine-examples/crash/AGENTS.md +3 -0
  56. package/engine-examples/crash/README.md +14 -0
  57. package/engine-examples/crash/bundle/assets.json +1 -0
  58. package/engine-examples/crash/bundle/game.css +8 -0
  59. package/engine-examples/crash/bundle/game.js +90 -0
  60. package/engine-examples/crash/bundle/hbtest.json +5 -0
  61. package/engine-examples/crash/bundle/index.html +20 -0
  62. package/engine-examples/crash/math/crash-01.tsv +2 -0
  63. package/engine-examples/crash/math/crash-02.tsv +2 -0
  64. package/engine-examples/crash/math/crash-03.tsv +2 -0
  65. package/engine-examples/crash/math/crash-04.tsv +2 -0
  66. package/engine-examples/crash/math/crash-05.tsv +2 -0
  67. package/engine-examples/crash/math/crash-06.tsv +2 -0
  68. package/engine-examples/crash/math/crash-07.tsv +2 -0
  69. package/engine-examples/crash/math/crash-08.tsv +2 -0
  70. package/engine-examples/crash/math/crash-09.tsv +2 -0
  71. package/engine-examples/crash/math/crash-10.tsv +2 -0
  72. package/engine-examples/crash/math/crash-11.tsv +2 -0
  73. package/engine-examples/crash/math/crash-12.tsv +2 -0
  74. package/engine-examples/crash/math/crash-13.tsv +2 -0
  75. package/engine-examples/crash/math/crash-14.tsv +2 -0
  76. package/engine-examples/crash/math/crash-15.tsv +2 -0
  77. package/engine-examples/crash/math/crash-16.tsv +2 -0
  78. package/engine-examples/crash/math/crash-17.tsv +2 -0
  79. package/engine-examples/crash/math/crash-18.tsv +2 -0
  80. package/engine-examples/crash/math/crash-19.tsv +2 -0
  81. package/engine-examples/crash/math/crash-20.tsv +2 -0
  82. package/engine-examples/crash/math/crash-21.tsv +2 -0
  83. package/engine-examples/crash/math/crash-22.tsv +2 -0
  84. package/engine-examples/crash/math/crash-23.tsv +2 -0
  85. package/engine-examples/crash/math/crash-24.tsv +2 -0
  86. package/engine-examples/crash/math/crash-25.tsv +2 -0
  87. package/engine-examples/crash/math/crash-26.tsv +2 -0
  88. package/engine-examples/crash/math/crash-27.tsv +2 -0
  89. package/engine-examples/crash/math/crash-28.tsv +2 -0
  90. package/engine-examples/crash/math/crash-29.tsv +2 -0
  91. package/engine-examples/crash/math/crash-30.tsv +2 -0
  92. package/engine-examples/crash/math/crash-31.tsv +2 -0
  93. package/engine-examples/crash/math/crash-32.tsv +2 -0
  94. package/engine-examples/crash/math/crash-33.tsv +2 -0
  95. package/engine-examples/crash/math/crash-34.tsv +2 -0
  96. package/engine-examples/crash/math/crash-35.tsv +2 -0
  97. package/engine-examples/crash/math/crash-36.tsv +2 -0
  98. package/engine-examples/crash/math/crash-37.tsv +2 -0
  99. package/engine-examples/crash/math/crash-38.tsv +2 -0
  100. package/engine-examples/crash/math/crash-39.tsv +2 -0
  101. package/engine-examples/crash/math/crash-40.tsv +2 -0
  102. package/engine-examples/crash/math/crash-41.tsv +2 -0
  103. package/engine-examples/crash/math/crash-42.tsv +2 -0
  104. package/engine-examples/crash/math/crash-43.tsv +2 -0
  105. package/engine-examples/crash/math/crash-44.tsv +2 -0
  106. package/engine-examples/crash/math/crash-45.tsv +2 -0
  107. package/engine-examples/crash/math/crash-46.tsv +2 -0
  108. package/engine-examples/crash/math/crash-47.tsv +2 -0
  109. package/engine-examples/crash/math/crash-48.tsv +2 -0
  110. package/engine-examples/crash/math/crash-49.tsv +2 -0
  111. package/engine-examples/crash/math/crash-50.tsv +2 -0
  112. package/engine-examples/crash/math/crash-51.tsv +2 -0
  113. package/engine-examples/crash/math/crash-52.tsv +2 -0
  114. package/engine-examples/crash/math/crash-53.tsv +2 -0
  115. package/engine-examples/crash/math/crash-54.tsv +2 -0
  116. package/engine-examples/crash/math/crash-55.tsv +2 -0
  117. package/engine-examples/crash/math/crash-56.tsv +2 -0
  118. package/engine-examples/crash/math/crash-57.tsv +2 -0
  119. package/engine-examples/crash/math/crash-58.tsv +2 -0
  120. package/engine-examples/crash/math/crash-59.tsv +2 -0
  121. package/engine-examples/crash/math/crash-60.tsv +2 -0
  122. package/engine-examples/crash/math/crash-61.tsv +2 -0
  123. package/engine-examples/crash/math/crash-62.tsv +2 -0
  124. package/engine-examples/crash/math/crash-63.tsv +2 -0
  125. package/engine-examples/crash/math/crash-64.tsv +2 -0
  126. package/engine-examples/crash/math/gen.mjs +31 -0
  127. package/engine-examples/crash/math/manifest.json +1 -0
  128. package/engine-examples/dice/AGENTS.md +3 -0
  129. package/engine-examples/dice/README.md +25 -0
  130. package/engine-examples/dice/bundle/assets.json +1 -0
  131. package/engine-examples/dice/bundle/game.css +17 -0
  132. package/engine-examples/dice/bundle/game.js +61 -0
  133. package/engine-examples/dice/bundle/hbtest.json +4 -0
  134. package/engine-examples/dice/bundle/index.html +34 -0
  135. package/engine-examples/dice/math/dice.tsv +100 -0
  136. package/engine-examples/dice/math/manifest.json +2925 -0
  137. package/engine-examples/keno/AGENTS.md +3 -0
  138. package/engine-examples/keno/README.md +11 -0
  139. package/engine-examples/keno/bundle/assets.json +1 -0
  140. package/engine-examples/keno/bundle/game.css +14 -0
  141. package/engine-examples/keno/bundle/game.js +86 -0
  142. package/engine-examples/keno/bundle/hbtest.json +4 -0
  143. package/engine-examples/keno/bundle/i18n.json +342 -0
  144. package/engine-examples/keno/bundle/index.html +20 -0
  145. package/engine-examples/keno/bundle/model.json +1 -0
  146. package/engine-examples/keno/math/gen.mjs +35 -0
  147. package/engine-examples/keno/math/keno-picks-1.tsv +2 -0
  148. package/engine-examples/keno/math/keno-picks-10.tsv +11 -0
  149. package/engine-examples/keno/math/keno-picks-2.tsv +3 -0
  150. package/engine-examples/keno/math/keno-picks-3.tsv +4 -0
  151. package/engine-examples/keno/math/keno-picks-4.tsv +5 -0
  152. package/engine-examples/keno/math/keno-picks-5.tsv +6 -0
  153. package/engine-examples/keno/math/keno-picks-6.tsv +7 -0
  154. package/engine-examples/keno/math/keno-picks-7.tsv +8 -0
  155. package/engine-examples/keno/math/keno-picks-8.tsv +9 -0
  156. package/engine-examples/keno/math/keno-picks-9.tsv +10 -0
  157. package/engine-examples/keno/math/manifest.json +1 -0
  158. package/engine-examples/keno/math-kit/pack.mjs +165 -0
  159. package/engine-examples/keno/math-kit/package.json +8 -0
  160. package/engine-examples/keno/math-kit/slot.mjs +94 -0
  161. package/engine-examples/keno/math-kit/tumble.mjs +94 -0
  162. package/engine-examples/lines/AGENTS.md +3 -0
  163. package/engine-examples/lines/README.md +20 -0
  164. package/engine-examples/lines/bundle/assets.json +1 -0
  165. package/engine-examples/lines/bundle/game.css +16 -0
  166. package/engine-examples/lines/bundle/game.js +125 -0
  167. package/engine-examples/lines/bundle/hbtest.json +4 -0
  168. package/engine-examples/lines/bundle/i18n.json +242 -0
  169. package/engine-examples/lines/bundle/index.html +23 -0
  170. package/engine-examples/lines/bundle/model.json +1 -0
  171. package/engine-examples/lines/math/gen.mjs +54 -0
  172. package/engine-examples/lines/math-kit/pack.mjs +165 -0
  173. package/engine-examples/lines/math-kit/package.json +8 -0
  174. package/engine-examples/lines/math-kit/slot.mjs +94 -0
  175. package/engine-examples/lines/math-kit/tumble.mjs +94 -0
  176. package/engine-examples/mines/AGENTS.md +3 -0
  177. package/engine-examples/mines/README.md +13 -0
  178. package/engine-examples/mines/bundle/assets.json +1 -0
  179. package/engine-examples/mines/bundle/game.css +12 -0
  180. package/engine-examples/mines/bundle/game.js +58 -0
  181. package/engine-examples/mines/bundle/hbtest.json +8 -0
  182. package/engine-examples/mines/bundle/index.html +16 -0
  183. package/engine-examples/mines/math/gen.mjs +40 -0
  184. package/engine-examples/mines/math/manifest.json +1 -0
  185. package/engine-examples/mines/math/mines-1-1.tsv +2 -0
  186. package/engine-examples/mines/math/mines-1-2.tsv +2 -0
  187. package/engine-examples/mines/math/mines-1-22.tsv +2 -0
  188. package/engine-examples/mines/math/mines-1-23.tsv +2 -0
  189. package/engine-examples/mines/math/mines-1-24.tsv +2 -0
  190. package/engine-examples/mines/math/mines-1-3.tsv +2 -0
  191. package/engine-examples/mines/math/mines-10-1.tsv +2 -0
  192. package/engine-examples/mines/math/mines-10-10.tsv +2 -0
  193. package/engine-examples/mines/math/mines-10-11.tsv +2 -0
  194. package/engine-examples/mines/math/mines-10-12.tsv +2 -0
  195. package/engine-examples/mines/math/mines-10-13.tsv +2 -0
  196. package/engine-examples/mines/math/mines-10-14.tsv +2 -0
  197. package/engine-examples/mines/math/mines-10-15.tsv +2 -0
  198. package/engine-examples/mines/math/mines-10-2.tsv +2 -0
  199. package/engine-examples/mines/math/mines-10-3.tsv +2 -0
  200. package/engine-examples/mines/math/mines-10-4.tsv +2 -0
  201. package/engine-examples/mines/math/mines-10-5.tsv +2 -0
  202. package/engine-examples/mines/math/mines-10-6.tsv +2 -0
  203. package/engine-examples/mines/math/mines-10-7.tsv +2 -0
  204. package/engine-examples/mines/math/mines-10-8.tsv +2 -0
  205. package/engine-examples/mines/math/mines-10-9.tsv +2 -0
  206. package/engine-examples/mines/math/mines-11-1.tsv +2 -0
  207. package/engine-examples/mines/math/mines-11-10.tsv +2 -0
  208. package/engine-examples/mines/math/mines-11-11.tsv +2 -0
  209. package/engine-examples/mines/math/mines-11-12.tsv +2 -0
  210. package/engine-examples/mines/math/mines-11-13.tsv +2 -0
  211. package/engine-examples/mines/math/mines-11-14.tsv +2 -0
  212. package/engine-examples/mines/math/mines-11-2.tsv +2 -0
  213. package/engine-examples/mines/math/mines-11-3.tsv +2 -0
  214. package/engine-examples/mines/math/mines-11-4.tsv +2 -0
  215. package/engine-examples/mines/math/mines-11-5.tsv +2 -0
  216. package/engine-examples/mines/math/mines-11-6.tsv +2 -0
  217. package/engine-examples/mines/math/mines-11-7.tsv +2 -0
  218. package/engine-examples/mines/math/mines-11-8.tsv +2 -0
  219. package/engine-examples/mines/math/mines-11-9.tsv +2 -0
  220. package/engine-examples/mines/math/mines-12-1.tsv +2 -0
  221. package/engine-examples/mines/math/mines-12-10.tsv +2 -0
  222. package/engine-examples/mines/math/mines-12-11.tsv +2 -0
  223. package/engine-examples/mines/math/mines-12-12.tsv +2 -0
  224. package/engine-examples/mines/math/mines-12-13.tsv +2 -0
  225. package/engine-examples/mines/math/mines-12-2.tsv +2 -0
  226. package/engine-examples/mines/math/mines-12-3.tsv +2 -0
  227. package/engine-examples/mines/math/mines-12-4.tsv +2 -0
  228. package/engine-examples/mines/math/mines-12-5.tsv +2 -0
  229. package/engine-examples/mines/math/mines-12-6.tsv +2 -0
  230. package/engine-examples/mines/math/mines-12-7.tsv +2 -0
  231. package/engine-examples/mines/math/mines-12-8.tsv +2 -0
  232. package/engine-examples/mines/math/mines-12-9.tsv +2 -0
  233. package/engine-examples/mines/math/mines-13-1.tsv +2 -0
  234. package/engine-examples/mines/math/mines-13-10.tsv +2 -0
  235. package/engine-examples/mines/math/mines-13-11.tsv +2 -0
  236. package/engine-examples/mines/math/mines-13-12.tsv +2 -0
  237. package/engine-examples/mines/math/mines-13-2.tsv +2 -0
  238. package/engine-examples/mines/math/mines-13-3.tsv +2 -0
  239. package/engine-examples/mines/math/mines-13-4.tsv +2 -0
  240. package/engine-examples/mines/math/mines-13-5.tsv +2 -0
  241. package/engine-examples/mines/math/mines-13-6.tsv +2 -0
  242. package/engine-examples/mines/math/mines-13-7.tsv +2 -0
  243. package/engine-examples/mines/math/mines-13-8.tsv +2 -0
  244. package/engine-examples/mines/math/mines-13-9.tsv +2 -0
  245. package/engine-examples/mines/math/mines-14-1.tsv +2 -0
  246. package/engine-examples/mines/math/mines-14-10.tsv +2 -0
  247. package/engine-examples/mines/math/mines-14-11.tsv +2 -0
  248. package/engine-examples/mines/math/mines-14-2.tsv +2 -0
  249. package/engine-examples/mines/math/mines-14-3.tsv +2 -0
  250. package/engine-examples/mines/math/mines-14-4.tsv +2 -0
  251. package/engine-examples/mines/math/mines-14-5.tsv +2 -0
  252. package/engine-examples/mines/math/mines-14-6.tsv +2 -0
  253. package/engine-examples/mines/math/mines-14-7.tsv +2 -0
  254. package/engine-examples/mines/math/mines-14-8.tsv +2 -0
  255. package/engine-examples/mines/math/mines-14-9.tsv +2 -0
  256. package/engine-examples/mines/math/mines-15-1.tsv +2 -0
  257. package/engine-examples/mines/math/mines-15-10.tsv +2 -0
  258. package/engine-examples/mines/math/mines-15-2.tsv +2 -0
  259. package/engine-examples/mines/math/mines-15-3.tsv +2 -0
  260. package/engine-examples/mines/math/mines-15-4.tsv +2 -0
  261. package/engine-examples/mines/math/mines-15-5.tsv +2 -0
  262. package/engine-examples/mines/math/mines-15-6.tsv +2 -0
  263. package/engine-examples/mines/math/mines-15-7.tsv +2 -0
  264. package/engine-examples/mines/math/mines-15-8.tsv +2 -0
  265. package/engine-examples/mines/math/mines-15-9.tsv +2 -0
  266. package/engine-examples/mines/math/mines-16-1.tsv +2 -0
  267. package/engine-examples/mines/math/mines-16-2.tsv +2 -0
  268. package/engine-examples/mines/math/mines-16-3.tsv +2 -0
  269. package/engine-examples/mines/math/mines-16-4.tsv +2 -0
  270. package/engine-examples/mines/math/mines-16-5.tsv +2 -0
  271. package/engine-examples/mines/math/mines-16-6.tsv +2 -0
  272. package/engine-examples/mines/math/mines-16-7.tsv +2 -0
  273. package/engine-examples/mines/math/mines-16-8.tsv +2 -0
  274. package/engine-examples/mines/math/mines-16-9.tsv +2 -0
  275. package/engine-examples/mines/math/mines-17-1.tsv +2 -0
  276. package/engine-examples/mines/math/mines-17-2.tsv +2 -0
  277. package/engine-examples/mines/math/mines-17-3.tsv +2 -0
  278. package/engine-examples/mines/math/mines-17-4.tsv +2 -0
  279. package/engine-examples/mines/math/mines-17-5.tsv +2 -0
  280. package/engine-examples/mines/math/mines-17-6.tsv +2 -0
  281. package/engine-examples/mines/math/mines-17-7.tsv +2 -0
  282. package/engine-examples/mines/math/mines-17-8.tsv +2 -0
  283. package/engine-examples/mines/math/mines-18-1.tsv +2 -0
  284. package/engine-examples/mines/math/mines-18-2.tsv +2 -0
  285. package/engine-examples/mines/math/mines-18-3.tsv +2 -0
  286. package/engine-examples/mines/math/mines-18-4.tsv +2 -0
  287. package/engine-examples/mines/math/mines-18-5.tsv +2 -0
  288. package/engine-examples/mines/math/mines-18-6.tsv +2 -0
  289. package/engine-examples/mines/math/mines-18-7.tsv +2 -0
  290. package/engine-examples/mines/math/mines-19-1.tsv +2 -0
  291. package/engine-examples/mines/math/mines-19-2.tsv +2 -0
  292. package/engine-examples/mines/math/mines-19-3.tsv +2 -0
  293. package/engine-examples/mines/math/mines-19-4.tsv +2 -0
  294. package/engine-examples/mines/math/mines-19-5.tsv +2 -0
  295. package/engine-examples/mines/math/mines-19-6.tsv +2 -0
  296. package/engine-examples/mines/math/mines-2-1.tsv +2 -0
  297. package/engine-examples/mines/math/mines-2-19.tsv +2 -0
  298. package/engine-examples/mines/math/mines-2-2.tsv +2 -0
  299. package/engine-examples/mines/math/mines-2-20.tsv +2 -0
  300. package/engine-examples/mines/math/mines-2-21.tsv +2 -0
  301. package/engine-examples/mines/math/mines-2-22.tsv +2 -0
  302. package/engine-examples/mines/math/mines-2-23.tsv +2 -0
  303. package/engine-examples/mines/math/mines-2-3.tsv +2 -0
  304. package/engine-examples/mines/math/mines-2-4.tsv +2 -0
  305. package/engine-examples/mines/math/mines-2-5.tsv +2 -0
  306. package/engine-examples/mines/math/mines-20-1.tsv +2 -0
  307. package/engine-examples/mines/math/mines-20-2.tsv +2 -0
  308. package/engine-examples/mines/math/mines-20-3.tsv +2 -0
  309. package/engine-examples/mines/math/mines-20-4.tsv +2 -0
  310. package/engine-examples/mines/math/mines-20-5.tsv +2 -0
  311. package/engine-examples/mines/math/mines-21-1.tsv +2 -0
  312. package/engine-examples/mines/math/mines-21-2.tsv +2 -0
  313. package/engine-examples/mines/math/mines-21-3.tsv +2 -0
  314. package/engine-examples/mines/math/mines-21-4.tsv +2 -0
  315. package/engine-examples/mines/math/mines-22-1.tsv +2 -0
  316. package/engine-examples/mines/math/mines-22-2.tsv +2 -0
  317. package/engine-examples/mines/math/mines-22-3.tsv +2 -0
  318. package/engine-examples/mines/math/mines-23-1.tsv +2 -0
  319. package/engine-examples/mines/math/mines-23-2.tsv +2 -0
  320. package/engine-examples/mines/math/mines-24-1.tsv +2 -0
  321. package/engine-examples/mines/math/mines-3-1.tsv +2 -0
  322. package/engine-examples/mines/math/mines-3-17.tsv +2 -0
  323. package/engine-examples/mines/math/mines-3-18.tsv +2 -0
  324. package/engine-examples/mines/math/mines-3-19.tsv +2 -0
  325. package/engine-examples/mines/math/mines-3-2.tsv +2 -0
  326. package/engine-examples/mines/math/mines-3-20.tsv +2 -0
  327. package/engine-examples/mines/math/mines-3-21.tsv +2 -0
  328. package/engine-examples/mines/math/mines-3-22.tsv +2 -0
  329. package/engine-examples/mines/math/mines-3-3.tsv +2 -0
  330. package/engine-examples/mines/math/mines-3-4.tsv +2 -0
  331. package/engine-examples/mines/math/mines-3-5.tsv +2 -0
  332. package/engine-examples/mines/math/mines-3-6.tsv +2 -0
  333. package/engine-examples/mines/math/mines-4-1.tsv +2 -0
  334. package/engine-examples/mines/math/mines-4-13.tsv +2 -0
  335. package/engine-examples/mines/math/mines-4-14.tsv +2 -0
  336. package/engine-examples/mines/math/mines-4-15.tsv +2 -0
  337. package/engine-examples/mines/math/mines-4-16.tsv +2 -0
  338. package/engine-examples/mines/math/mines-4-17.tsv +2 -0
  339. package/engine-examples/mines/math/mines-4-18.tsv +2 -0
  340. package/engine-examples/mines/math/mines-4-19.tsv +2 -0
  341. package/engine-examples/mines/math/mines-4-2.tsv +2 -0
  342. package/engine-examples/mines/math/mines-4-20.tsv +2 -0
  343. package/engine-examples/mines/math/mines-4-21.tsv +2 -0
  344. package/engine-examples/mines/math/mines-4-3.tsv +2 -0
  345. package/engine-examples/mines/math/mines-4-4.tsv +2 -0
  346. package/engine-examples/mines/math/mines-4-5.tsv +2 -0
  347. package/engine-examples/mines/math/mines-4-6.tsv +2 -0
  348. package/engine-examples/mines/math/mines-4-7.tsv +2 -0
  349. package/engine-examples/mines/math/mines-4-8.tsv +2 -0
  350. package/engine-examples/mines/math/mines-4-9.tsv +2 -0
  351. package/engine-examples/mines/math/mines-5-1.tsv +2 -0
  352. package/engine-examples/mines/math/mines-5-10.tsv +2 -0
  353. package/engine-examples/mines/math/mines-5-11.tsv +2 -0
  354. package/engine-examples/mines/math/mines-5-12.tsv +2 -0
  355. package/engine-examples/mines/math/mines-5-13.tsv +2 -0
  356. package/engine-examples/mines/math/mines-5-14.tsv +2 -0
  357. package/engine-examples/mines/math/mines-5-15.tsv +2 -0
  358. package/engine-examples/mines/math/mines-5-16.tsv +2 -0
  359. package/engine-examples/mines/math/mines-5-17.tsv +2 -0
  360. package/engine-examples/mines/math/mines-5-18.tsv +2 -0
  361. package/engine-examples/mines/math/mines-5-19.tsv +2 -0
  362. package/engine-examples/mines/math/mines-5-2.tsv +2 -0
  363. package/engine-examples/mines/math/mines-5-20.tsv +2 -0
  364. package/engine-examples/mines/math/mines-5-3.tsv +2 -0
  365. package/engine-examples/mines/math/mines-5-4.tsv +2 -0
  366. package/engine-examples/mines/math/mines-5-5.tsv +2 -0
  367. package/engine-examples/mines/math/mines-5-6.tsv +2 -0
  368. package/engine-examples/mines/math/mines-5-7.tsv +2 -0
  369. package/engine-examples/mines/math/mines-5-8.tsv +2 -0
  370. package/engine-examples/mines/math/mines-5-9.tsv +2 -0
  371. package/engine-examples/mines/math/mines-6-1.tsv +2 -0
  372. package/engine-examples/mines/math/mines-6-10.tsv +2 -0
  373. package/engine-examples/mines/math/mines-6-11.tsv +2 -0
  374. package/engine-examples/mines/math/mines-6-12.tsv +2 -0
  375. package/engine-examples/mines/math/mines-6-13.tsv +2 -0
  376. package/engine-examples/mines/math/mines-6-14.tsv +2 -0
  377. package/engine-examples/mines/math/mines-6-15.tsv +2 -0
  378. package/engine-examples/mines/math/mines-6-16.tsv +2 -0
  379. package/engine-examples/mines/math/mines-6-17.tsv +2 -0
  380. package/engine-examples/mines/math/mines-6-18.tsv +2 -0
  381. package/engine-examples/mines/math/mines-6-19.tsv +2 -0
  382. package/engine-examples/mines/math/mines-6-2.tsv +2 -0
  383. package/engine-examples/mines/math/mines-6-3.tsv +2 -0
  384. package/engine-examples/mines/math/mines-6-4.tsv +2 -0
  385. package/engine-examples/mines/math/mines-6-5.tsv +2 -0
  386. package/engine-examples/mines/math/mines-6-6.tsv +2 -0
  387. package/engine-examples/mines/math/mines-6-7.tsv +2 -0
  388. package/engine-examples/mines/math/mines-6-8.tsv +2 -0
  389. package/engine-examples/mines/math/mines-6-9.tsv +2 -0
  390. package/engine-examples/mines/math/mines-7-1.tsv +2 -0
  391. package/engine-examples/mines/math/mines-7-10.tsv +2 -0
  392. package/engine-examples/mines/math/mines-7-11.tsv +2 -0
  393. package/engine-examples/mines/math/mines-7-12.tsv +2 -0
  394. package/engine-examples/mines/math/mines-7-13.tsv +2 -0
  395. package/engine-examples/mines/math/mines-7-14.tsv +2 -0
  396. package/engine-examples/mines/math/mines-7-15.tsv +2 -0
  397. package/engine-examples/mines/math/mines-7-16.tsv +2 -0
  398. package/engine-examples/mines/math/mines-7-17.tsv +2 -0
  399. package/engine-examples/mines/math/mines-7-18.tsv +2 -0
  400. package/engine-examples/mines/math/mines-7-2.tsv +2 -0
  401. package/engine-examples/mines/math/mines-7-3.tsv +2 -0
  402. package/engine-examples/mines/math/mines-7-4.tsv +2 -0
  403. package/engine-examples/mines/math/mines-7-5.tsv +2 -0
  404. package/engine-examples/mines/math/mines-7-6.tsv +2 -0
  405. package/engine-examples/mines/math/mines-7-7.tsv +2 -0
  406. package/engine-examples/mines/math/mines-7-8.tsv +2 -0
  407. package/engine-examples/mines/math/mines-7-9.tsv +2 -0
  408. package/engine-examples/mines/math/mines-8-1.tsv +2 -0
  409. package/engine-examples/mines/math/mines-8-10.tsv +2 -0
  410. package/engine-examples/mines/math/mines-8-11.tsv +2 -0
  411. package/engine-examples/mines/math/mines-8-12.tsv +2 -0
  412. package/engine-examples/mines/math/mines-8-13.tsv +2 -0
  413. package/engine-examples/mines/math/mines-8-14.tsv +2 -0
  414. package/engine-examples/mines/math/mines-8-15.tsv +2 -0
  415. package/engine-examples/mines/math/mines-8-16.tsv +2 -0
  416. package/engine-examples/mines/math/mines-8-17.tsv +2 -0
  417. package/engine-examples/mines/math/mines-8-2.tsv +2 -0
  418. package/engine-examples/mines/math/mines-8-3.tsv +2 -0
  419. package/engine-examples/mines/math/mines-8-4.tsv +2 -0
  420. package/engine-examples/mines/math/mines-8-5.tsv +2 -0
  421. package/engine-examples/mines/math/mines-8-6.tsv +2 -0
  422. package/engine-examples/mines/math/mines-8-7.tsv +2 -0
  423. package/engine-examples/mines/math/mines-8-8.tsv +2 -0
  424. package/engine-examples/mines/math/mines-8-9.tsv +2 -0
  425. package/engine-examples/mines/math/mines-9-1.tsv +2 -0
  426. package/engine-examples/mines/math/mines-9-10.tsv +2 -0
  427. package/engine-examples/mines/math/mines-9-11.tsv +2 -0
  428. package/engine-examples/mines/math/mines-9-12.tsv +2 -0
  429. package/engine-examples/mines/math/mines-9-13.tsv +2 -0
  430. package/engine-examples/mines/math/mines-9-14.tsv +2 -0
  431. package/engine-examples/mines/math/mines-9-15.tsv +2 -0
  432. package/engine-examples/mines/math/mines-9-16.tsv +2 -0
  433. package/engine-examples/mines/math/mines-9-2.tsv +2 -0
  434. package/engine-examples/mines/math/mines-9-3.tsv +2 -0
  435. package/engine-examples/mines/math/mines-9-4.tsv +2 -0
  436. package/engine-examples/mines/math/mines-9-5.tsv +2 -0
  437. package/engine-examples/mines/math/mines-9-6.tsv +2 -0
  438. package/engine-examples/mines/math/mines-9-7.tsv +2 -0
  439. package/engine-examples/mines/math/mines-9-8.tsv +2 -0
  440. package/engine-examples/mines/math/mines-9-9.tsv +2 -0
  441. package/engine-examples/plinko/AGENTS.md +3 -0
  442. package/engine-examples/plinko/README.md +9 -0
  443. package/engine-examples/plinko/bundle/assets.json +1 -0
  444. package/engine-examples/plinko/bundle/game.css +5 -0
  445. package/engine-examples/plinko/bundle/game.js +92 -0
  446. package/engine-examples/plinko/bundle/hbtest.json +4 -0
  447. package/engine-examples/plinko/bundle/index.html +16 -0
  448. package/engine-examples/plinko/bundle/model.json +1 -0
  449. package/engine-examples/plinko/math/gen.mjs +40 -0
  450. package/engine-examples/plinko/math/manifest.json +1 -0
  451. package/engine-examples/plinko/math/plinko-12-high.tsv +13 -0
  452. package/engine-examples/plinko/math/plinko-12-low.tsv +13 -0
  453. package/engine-examples/plinko/math/plinko-12-medium.tsv +13 -0
  454. package/engine-examples/plinko/math/plinko-16-high.tsv +17 -0
  455. package/engine-examples/plinko/math/plinko-16-low.tsv +17 -0
  456. package/engine-examples/plinko/math/plinko-16-medium.tsv +17 -0
  457. package/engine-examples/plinko/math/plinko-8-high.tsv +9 -0
  458. package/engine-examples/plinko/math/plinko-8-low.tsv +9 -0
  459. package/engine-examples/plinko/math/plinko-8-medium.tsv +9 -0
  460. package/engine-examples/plinko/math-kit/pack.mjs +165 -0
  461. package/engine-examples/plinko/math-kit/package.json +8 -0
  462. package/engine-examples/plinko/math-kit/slot.mjs +94 -0
  463. package/engine-examples/plinko/math-kit/tumble.mjs +94 -0
  464. package/engine-examples/slot/AGENTS.md +3 -0
  465. package/engine-examples/slot/README.md +26 -0
  466. package/engine-examples/slot/bundle/assets.json +1 -0
  467. package/engine-examples/slot/bundle/game.css +19 -0
  468. package/engine-examples/slot/bundle/game.js +42 -0
  469. package/engine-examples/slot/bundle/hbtest.json +4 -0
  470. package/engine-examples/slot/bundle/index.html +23 -0
  471. package/engine-examples/slot/math/gen.mjs +79 -0
  472. package/engine-examples/slot/math/manifest.json +1 -0
  473. package/engine-examples/slot/math/slot.tsv +110592 -0
  474. package/engine-examples/tower/AGENTS.md +3 -0
  475. package/engine-examples/tower/README.md +21 -0
  476. package/engine-examples/tower/bundle/assets.json +1 -0
  477. package/engine-examples/tower/bundle/game.css +8 -0
  478. package/engine-examples/tower/bundle/game.js +37 -0
  479. package/engine-examples/tower/bundle/hbtest.json +5 -0
  480. package/engine-examples/tower/bundle/index.html +16 -0
  481. package/engine-examples/tower/math/floor.tsv +4 -0
  482. package/engine-examples/tower/math/manifest.json +61 -0
  483. package/engine-examples/tumble/AGENTS.md +3 -0
  484. package/engine-examples/tumble/README.md +18 -0
  485. package/engine-examples/tumble/bundle/assets.json +1 -0
  486. package/engine-examples/tumble/bundle/game.css +13 -0
  487. package/engine-examples/tumble/bundle/game.js +119 -0
  488. package/engine-examples/tumble/bundle/hbtest.json +4 -0
  489. package/engine-examples/tumble/bundle/i18n.json +122 -0
  490. package/engine-examples/tumble/bundle/index.html +20 -0
  491. package/engine-examples/tumble/bundle/model.json +1 -0
  492. package/engine-examples/tumble/math/gen.mjs +38 -0
  493. package/engine-examples/tumble/math-kit/pack.mjs +165 -0
  494. package/engine-examples/tumble/math-kit/package.json +8 -0
  495. package/engine-examples/tumble/math-kit/slot.mjs +94 -0
  496. package/engine-examples/tumble/math-kit/tumble.mjs +94 -0
  497. package/engine-examples/wheel/README.md +14 -0
  498. package/engine-examples/wheel/bundle/assets.json +1 -0
  499. package/engine-examples/wheel/bundle/game.css +7 -0
  500. package/engine-examples/wheel/bundle/game.js +51 -0
  501. package/engine-examples/wheel/bundle/hbtest.json +4 -0
  502. package/engine-examples/wheel/bundle/index.html +19 -0
  503. package/engine-examples/wheel/math/high.tsv +24 -0
  504. package/engine-examples/wheel/math/low.tsv +24 -0
  505. package/engine-examples/wheel/math/manifest.json +48 -0
  506. package/engine-examples/wheel/math/medium.tsv +24 -0
  507. package/engine-math/pack.mjs +165 -0
  508. package/engine-math/package.json +1 -0
  509. package/engine-math/slot.mjs +94 -0
  510. package/engine-math/tumble.mjs +94 -0
  511. package/engine-mcp/package.json +1 -0
  512. package/engine-mcp/server.mjs +151 -0
  513. package/engine-sdk/GUIDE.md +855 -0
  514. package/engine-sdk/assets.js +92 -0
  515. package/engine-sdk/autoplay.js +82 -0
  516. package/engine-sdk/bet-field.js +391 -0
  517. package/engine-sdk/kit/fiat-coins.js +51 -0
  518. package/engine-sdk/kit/numeric-input.js +116 -0
  519. package/engine-sdk/kit/wallet-format.js +423 -0
  520. package/engine-sdk/marks.js +22 -0
  521. package/engine-sdk/messages.js +289 -0
  522. package/engine-sdk/money.js +89 -0
  523. package/engine-sdk/package.json +1 -0
  524. package/engine-sdk/protocol.js +263 -0
  525. package/engine-sdk/sdk.d.ts +138 -0
  526. package/engine-sdk/sdk.js +330 -0
  527. package/engine-sdk/stake.js +68 -0
  528. package/engine-sdk/ui.d.ts +36 -0
  529. package/engine-sdk/ui.js +340 -0
  530. package/engine-shell/core/faults.js +127 -0
  531. package/engine-shell/core/rgs.js +193 -0
  532. package/engine-shell/core/router.js +917 -0
  533. package/engine-shell/host/boot.js +408 -0
  534. package/engine-shell/host/engine-v1.js +438 -0
  535. package/engine-shell/host/fair.js +285 -0
  536. package/engine-shell/host/fonts/SOURCE.json +11 -0
  537. package/engine-shell/host/fonts/figtree-400.woff2 +0 -0
  538. package/engine-shell/host/fonts/figtree-500.woff2 +0 -0
  539. package/engine-shell/host/fonts/figtree-600.woff2 +0 -0
  540. package/engine-shell/host/fonts/red-hat-display-700.woff2 +0 -0
  541. package/engine-shell/host/index.html +230 -0
  542. package/engine-shell/host/page.js +809 -0
  543. package/engine-shell/host/sandbox-panel.js +681 -0
  544. package/engine-shell/host/sandbox-scenarios.js +554 -0
  545. package/engine-shell/host/site-bridge.js +87 -0
  546. package/engine-shell/host/strings.js +916 -0
  547. package/engine-shell/package.json +1 -0
  548. package/engine-shell/verifiers/badcat/coin.tsv +2 -0
  549. package/engine-shell/verifiers/badcat/dice-2.tsv +11 -0
  550. package/engine-shell/verifiers/badcat/dice-5.tsv +26 -0
  551. package/engine-shell/verifiers/badcat/dice-9.tsv +46 -0
  552. package/engine-shell/verifiers/badcat/manifest.json +3344 -0
  553. package/engine-shell/verifiers/badcat/rps.tsv +3 -0
  554. package/engine-shell/verifiers/badcat/triple.tsv +66 -0
  555. package/engine-shell/verifiers/engine-verify.js +358 -0
  556. package/engine-shell/verifiers/package.json +5 -0
  557. package/engine-shell/verifiers/parity/manifest.json +27 -0
  558. package/engine-shell/verifiers/parity/wide.tsv +5 -0
  559. package/engine-shell/verifiers/parity-vectors.json +244 -0
  560. package/engine-shell/verifiers/vectors.json +100 -0
  561. package/package.json +10 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The maths kit: a game's outcomes as weighted rows, its RTP exactly, and the
3
+ * pack the engine verifies (SPEC §1). A row is { weight, data, pay } where
4
+ * pay is { betId: multBp } (10000 = 1×, the total stake back) and data is
5
+ * what the bundle receives to show the round.
6
+ *
7
+ * import { writePack, fit } from './pack.mjs';
8
+ * const rows = fit(simulated, 9600); // exact 96.00% (balance() when no row pays nothing)
9
+ * writePack('math', { game: 'my-slot', rtpBp: 9600, modes: [{ id: 'base', rows, bets: ['spin'] }] });
10
+ *
11
+ * Every number here is an integer: weights are BigInt, RTP is a fraction.
12
+ */
13
+ import { mkdirSync, writeFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+
16
+ const MAX_SUM = 1n << 63n;
17
+
18
+ // A small seeded generator for building tables (never for play: the engine draws).
19
+ export function rng(seed) {
20
+ let a = seed >>> 0;
21
+ const next = () => {
22
+ a = (a + 0x6d2b79f5) >>> 0;
23
+ let t = a;
24
+ t = Math.imul(t ^ (t >>> 15), t | 1);
25
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
26
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
27
+ };
28
+ next.int = (n) => Math.floor(next() * n);
29
+ return next;
30
+ }
31
+
32
+ export function binomial(n, k) {
33
+ let r = 1n;
34
+ for (let i = 1n; i <= BigInt(k); i++) r = (r * BigInt(n - k) + r * i) / i;
35
+ return r;
36
+ }
37
+
38
+ const gcd = (a, b) => { while (b) [a, b] = [b, a % b]; return a < 0n ? -a : a; };
39
+
40
+ /** RTP of one bet over rows, exactly: { num, den } in basis points, and a 4-decimal string. */
41
+ export function rtp(rows, bet) {
42
+ let num = 0n, den = 0n;
43
+ for (const r of rows) { den += r.weight; num += r.weight * BigInt(r.pay[bet] || 0); }
44
+ return { num, den, bp: bp4(num, den) };
45
+ }
46
+
47
+ export const bp4 = (num, den) => {
48
+ const x = (num * 10000n) / den; // bp × 10^4, truncated as the verifier reports it
49
+ return `${x / 10000n}.${String(x % 10000n).padStart(4, '0')}`;
50
+ };
51
+
52
+ /**
53
+ * Weights so that one bet's RTP is exactly targetBp: the paying rows keep
54
+ * their proportions (all multiplied by one integer), the rows that pay
55
+ * nothing share what is left in their own proportions. Needs rows that pay
56
+ * nothing, and paying rows that alone return more than the target.
57
+ */
58
+ export function fit(rows, targetBp, bet = Object.keys(rows[0].pay)[0]) {
59
+ const T = BigInt(targetBp);
60
+ const wins = rows.filter((r) => r.pay[bet] > 0), zeros = rows.filter((r) => !(r.pay[bet] > 0));
61
+ if (!zeros.length) throw new Error('fit: no row pays nothing');
62
+ const S = wins.reduce((s, r) => s + r.weight * (BigInt(r.pay[bet]) - T), 0n); // what the wins return above T
63
+ if (S <= 0n) throw new Error(`fit: the paying rows alone return less than ${targetBp / 100}%`);
64
+ const k = T / gcd(S, T); // so that k·S is a multiple of T
65
+ const Z = (k * S) / T; // the weight the zero rows share
66
+ const zw = zeros.reduce((s, r) => s + r.weight, 0n);
67
+ let given = 0n;
68
+ const out = rows.map((r) => {
69
+ if (r.pay[bet] > 0) return { ...r, weight: r.weight * k };
70
+ const w = (Z * r.weight) / zw;
71
+ given += w;
72
+ return { ...r, weight: w };
73
+ });
74
+ // The rounding remainder goes to the first zero row; a zero weight is dropped.
75
+ const first = out.find((r) => !(r.pay[bet] > 0));
76
+ first.weight += Z - given;
77
+ const kept = out.filter((r) => r.weight > 0n);
78
+ const check = rtp(kept, bet);
79
+ if (check.num !== T * check.den) throw new Error('fit: not exact (a bug)');
80
+ return kept;
81
+ }
82
+
83
+ /**
84
+ * Weights so that one bet's RTP is exactly targetBp when every row pays
85
+ * something (a bonus buy, plinko): rows paying more than the target are
86
+ * all multiplied by one integer, rows paying less by another, so each
87
+ * group keeps its own proportions and the two balance at the target.
88
+ */
89
+ export function balance(rows, targetBp, bet = Object.keys(rows[0].pay)[0]) {
90
+ const T = BigInt(targetBp);
91
+ const over = rows.reduce((s, r) => s + (BigInt(r.pay[bet] || 0) > T ? r.weight * (BigInt(r.pay[bet]) - T) : 0n), 0n);
92
+ const under = rows.reduce((s, r) => s + (BigInt(r.pay[bet] || 0) < T ? r.weight * (T - BigInt(r.pay[bet] || 0)) : 0n), 0n);
93
+ if (!over || !under) throw new Error('balance: needs rows on both sides of the target');
94
+ const g = gcd(over, under);
95
+ const out = rows.map((r) => ({ ...r, weight: r.weight * (BigInt(r.pay[bet] || 0) > T ? under / g : BigInt(r.pay[bet] || 0) < T ? over / g : 1n) }));
96
+ const check = rtp(out, bet);
97
+ if (check.num !== T * check.den) throw new Error('balance: not exact (a bug)');
98
+ return out;
99
+ }
100
+
101
+ /** Rows with the same data are one row with the sum of their weights. */
102
+ export function merge(rows) {
103
+ const by = new Map();
104
+ for (const r of rows) {
105
+ const key = JSON.stringify(r.data);
106
+ const x = by.get(key);
107
+ if (x) x.weight += r.weight;
108
+ else by.set(key, { ...r, weight: r.weight });
109
+ }
110
+ return [...by.values()];
111
+ }
112
+
113
+ // Inclusive [lo, hi] ranges of sorted indexes.
114
+ function ranges(ids) {
115
+ const out = [];
116
+ for (const i of ids) {
117
+ const last = out.at(-1);
118
+ if (last && last[1] === i - 1) last[1] = i;
119
+ else out.push([i, i]);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /**
125
+ * Writes manifest.json and one table per mode. A mode is { id, label?, rows,
126
+ * bets: [id | {id, label}] }; its rows are sorted by what the first bet pays,
127
+ * so each pay tier is one range. Prints every bet's exact RTP, hit rate and
128
+ * largest multiplier; returns them.
129
+ */
130
+ export function writePack(dir, { game, rtpBp, modes }) {
131
+ mkdirSync(dir, { recursive: true });
132
+ const manifest = { format: 'house-engine-math/1', game, rtpBp, modes: [] };
133
+ const report = [];
134
+ for (const mode of modes) {
135
+ const bets = mode.bets.map((b) => (typeof b === 'string' ? { id: b } : b));
136
+ const first = bets[0].id;
137
+ const rows = [...mode.rows].sort((a, b) => (a.pay[first] || 0) - (b.pay[first] || 0));
138
+ if (rows.length < 2 || rows.length > 1_000_000) throw new Error(`${mode.id}: 2 to 1,000,000 rows, not ${rows.length}`);
139
+ const sum = rows.reduce((s, r) => s + r.weight, 0n);
140
+ if (sum > MAX_SUM) throw new Error(`${mode.id}: weights sum to more than 2^63`);
141
+ const lines = rows.map((r) => {
142
+ if (r.weight <= 0n) throw new Error(`${mode.id}: a weight is not positive`);
143
+ const json = r.data === undefined ? '' : JSON.stringify(r.data);
144
+ if (json.length > 4096) throw new Error(`${mode.id}: a row's data is over 4096 bytes`);
145
+ return json ? `${r.weight}\t${json}` : String(r.weight);
146
+ });
147
+ const table = `${game}-${mode.id}.tsv`;
148
+ writeFileSync(join(dir, table), lines.join('\n') + '\n');
149
+ manifest.modes.push({
150
+ id: mode.id, ...(mode.label ? { label: mode.label } : {}), kind: 'table', table,
151
+ bets: bets.map((b) => {
152
+ const tiers = new Map();
153
+ rows.forEach((r, i) => { const m = r.pay[b.id] || 0; if (m > 0) (tiers.get(m) || tiers.set(m, []).get(m)).push(i); });
154
+ const { num, den, bp } = rtp(rows, b.id);
155
+ const hit = rows.reduce((s, r) => s + (r.pay[b.id] > 0 ? r.weight : 0n), 0n);
156
+ const max = Math.max(...tiers.keys());
157
+ report.push({ mode: mode.id, bet: b.id, rtp: bp, hit: bp4(hit * 10000n, den), max: max / 10000, rows: rows.length, exact: num % den === 0n });
158
+ return { id: b.id, ...(b.label ? { label: b.label } : {}), tiers: [...tiers].map(([multBp, ids]) => ({ outcomes: ranges(ids), multBp })) };
159
+ }),
160
+ });
161
+ }
162
+ writeFileSync(join(dir, 'manifest.json'), JSON.stringify(manifest) + '\n');
163
+ for (const r of report) console.log(`${r.mode}/${r.bet}: RTP ${r.rtp} bp hit ${r.hit} bp max ${r.max}× rows ${r.rows}`);
164
+ return report;
165
+ }
@@ -0,0 +1 @@
1
+ { "type": "module" }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A video slot for the maths kit: reels, paylines, a wild, a scatter that
3
+ * starts free spins (with a multiplier, retriggers), and a win cap. It plays
4
+ * rounds with a seeded generator to make the table's rows ("books"): every
5
+ * row is a whole round, base spin and free spins, with what it pays. fit()
6
+ * then makes the table's RTP exact; the table is the game.
7
+ *
8
+ * Pays are in basis points of the total stake (10000 = 1×) for 3, 4, 5 of a
9
+ * kind on a line, left to right; scatters pay anywhere, times the stake.
10
+ *
11
+ * A row's data, for the bundle: { b: spin, f: [spin…], x: free multiplier }
12
+ * where a spin is [stop0…stopN, winBp, lineMask, scatterCount].
13
+ */
14
+ export function slot(cfg) {
15
+ const { reels, freeReels = reels, rows, lines, pays, wild, scatter, maxWinBp } = cfg;
16
+ const n = reels.length;
17
+
18
+ const grid = (strips, stops) => stops.map((s, r) => Array.from({ length: rows }, (_, y) => strips[r][(s + y) % strips[r].length]));
19
+
20
+ // One spin's wins: each line pays its best run from the left (wilds stand in).
21
+ function evaluate(strips, stops) {
22
+ const g = grid(strips, stops);
23
+ let win = 0, mask = 0;
24
+ lines.forEach((line, li) => {
25
+ const cells = line.map((y, r) => g[r][y]);
26
+ const sym = cells.find((c) => c !== wild) ?? wild;
27
+ if (sym === scatter.symbol) return;
28
+ let count = 0;
29
+ while (count < n && (cells[count] === sym || cells[count] === wild)) count++;
30
+ const pay = (pays[sym] || {})[count] || 0;
31
+ // A run of wilds alone pays as wilds when that is more.
32
+ let wc = 0;
33
+ while (wc < n && cells[wc] === wild) wc++;
34
+ const best = Math.max(pay, (pays[wild] || {})[wc] || 0);
35
+ if (best > 0) { win += best; mask |= 1 << li; }
36
+ });
37
+ const scatters = g.flat().filter((c) => c === scatter.symbol).length;
38
+ win += (scatter.pays[scatters] || 0);
39
+ return { win, mask, scatters };
40
+ }
41
+
42
+ const spin = (strips, next) => strips.map((s) => next.int(s.length));
43
+
44
+ /** One whole round: the row's data and what it pays (bp of the stake).
45
+ * A bought round is the free spins alone, as three scatters start them. */
46
+ function round(next, buy = false) {
47
+ const stops = buy ? null : spin(reels, next);
48
+ const base = buy ? { win: 0, scatters: 3 } : evaluate(reels, stops);
49
+ const data = buy ? { buy: 1 } : { b: [...stops, base.win, base.mask, base.scatters] };
50
+ let total = base.win;
51
+ let left = scatter.spins[base.scatters] || 0;
52
+ if (left) {
53
+ data.f = [];
54
+ data.x = scatter.multiplier;
55
+ while (left > 0 && data.f.length < scatter.cap) {
56
+ left--;
57
+ const s = spin(freeReels, next);
58
+ const e = evaluate(freeReels, s);
59
+ data.f.push([...s, e.win * scatter.multiplier, e.mask, e.scatters]);
60
+ total += e.win * scatter.multiplier;
61
+ left += scatter.retrigger[e.scatters] || 0;
62
+ }
63
+ }
64
+ if (total > maxWinBp) { total = maxWinBp; data.cap = 1; }
65
+ return { data, pay: total };
66
+ }
67
+
68
+ return { evaluate, round, grid };
69
+ }
70
+
71
+ /**
72
+ * Plays `count` rounds (bought ones with `buy`) and keeps them as rows: every paying round, and up to
73
+ * `losing` different losing rounds that share the weight of every losing
74
+ * round played (so the rows keep the played RTP and hit rate before fit).
75
+ */
76
+ export function books(game, next, count, losing = 4000, buy = false) {
77
+ const wins = new Map(), lose = new Map();
78
+ let lost = 0n;
79
+ for (let i = 0; i < count; i++) {
80
+ const { data, pay } = game.round(next, buy);
81
+ if (pay <= 0) {
82
+ lost++;
83
+ if (lose.size < losing) lose.set(JSON.stringify(data), { weight: 0n, data, pay: { spin: 0 } });
84
+ continue;
85
+ }
86
+ const key = JSON.stringify(data);
87
+ const r = wins.get(key);
88
+ if (r) r.weight += 1n;
89
+ else wins.set(key, { weight: 1n, data, pay: { spin: pay } });
90
+ }
91
+ const zeros = [...lose.values()];
92
+ zeros.forEach((r, i) => { r.weight = lost / BigInt(zeros.length) + (BigInt(i) < lost % BigInt(zeros.length) ? 1n : 0n); });
93
+ return [...zeros.filter((r) => r.weight > 0n), ...wins.values()];
94
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A tumbling, scatter-pays slot for the maths kit: a cols × rows grid, every
3
+ * cell drawn by symbol weight; a symbol pays when enough of it is anywhere
4
+ * on the grid; winners vanish, the rest fall, new symbols drop in, until
5
+ * nothing pays. Scatters start free spins, where multiplier symbols land
6
+ * and multiply the spin's win. Like slot.mjs, it plays whole rounds into
7
+ * rows; fit() then makes the RTP exact.
8
+ *
9
+ * Symbols are single characters. Pays are basis points of the stake by
10
+ * count: { A: [[8, 2000], [10, 5000], [12, 10000]] } pays the highest band
11
+ * the count reaches.
12
+ *
13
+ * A row's data, for the page:
14
+ * { b: spin, f: [spin…] } spin = [grid, [tumble…], multipliers?]
15
+ * grid the first grid, column by column, top to bottom ("ABCDE…")
16
+ * tumble [winners, winBp, fill0, fill1, …]: the symbols that paid, what
17
+ * they paid, and per column the new symbols that drop in (top first)
18
+ * multipliers free spins only: the values of the multiplier symbols on the
19
+ * last grid, summed onto the spin's win when it won
20
+ */
21
+ export function tumble(cfg) {
22
+ const { cols, rows, weights, freeWeights = weights, pays, scatter, bomb, maxWinBp } = cfg;
23
+ const pick = (w, next) => {
24
+ const total = w.reduce((s, [, n]) => s + n, 0);
25
+ let x = next() * total;
26
+ for (const [s, n] of w) if ((x -= n) < 0) return s;
27
+ return w[w.length - 1][0];
28
+ };
29
+ const table = (o) => Object.entries(o);
30
+ const base = table(weights), free = table(freeWeights);
31
+ const counts = Object.keys(scatter.spins).map(Number), least = Math.min(...counts), most = Math.max(...counts);
32
+
33
+ // One spin: the grid, its tumbles, and (free spins) its multipliers.
34
+ function spin(next, inFree) {
35
+ const w = inFree ? free : base;
36
+ const draw = () => {
37
+ const s = pick(w, next);
38
+ return s === bomb?.symbol && !inFree ? pick(base.filter(([x]) => x !== bomb.symbol), next) : s;
39
+ };
40
+ let grid = Array.from({ length: cols }, () => Array.from({ length: rows }, draw));
41
+ const first = grid.map((c) => c.join('')).join('');
42
+ const tumbles = [];
43
+ let win = 0;
44
+ for (;;) {
45
+ const counts = {};
46
+ for (const c of grid) for (const s of c) counts[s] = (counts[s] || 0) + 1;
47
+ let paid = '', bp = 0;
48
+ for (const [s, bands] of Object.entries(pays)) {
49
+ const band = bands.filter(([n]) => counts[s] >= n).at(-1);
50
+ if (band) { paid += s; bp += band[1]; }
51
+ }
52
+ if (!paid) break;
53
+ win += bp;
54
+ const fills = grid.map((c) => {
55
+ const kept = c.filter((s) => !paid.includes(s));
56
+ const add = Array.from({ length: rows - kept.length }, draw);
57
+ c.splice(0, rows, ...add, ...kept); // new symbols on top
58
+ return add.join('');
59
+ });
60
+ tumbles.push([paid, bp, ...fills]);
61
+ }
62
+ const out = [first, tumbles];
63
+ const scatters = grid.flat().filter((s) => s === scatter.symbol).length; // on the last grid
64
+ let total = win;
65
+ if (inFree && bomb) {
66
+ const bombs = grid.flat().filter((s) => s === bomb.symbol).map(() => pick(bomb.values, next));
67
+ if (bombs.length) out.push(bombs);
68
+ if (win && bombs.length) total = win * bombs.reduce((a, b) => a + b, 0);
69
+ }
70
+ return { out, total, scatters };
71
+ }
72
+
73
+ /** One whole round: the row's data and what it pays (bp of the stake). */
74
+ function round(next) {
75
+ const b = spin(next, false);
76
+ const data = { b: b.out };
77
+ let total = b.total + (scatter.pays[Math.min(b.scatters, most)] || 0);
78
+ let left = b.scatters >= least ? scatter.spins[Math.min(b.scatters, most)] : 0;
79
+ if (left) {
80
+ data.f = [];
81
+ while (left > 0 && data.f.length < scatter.cap) {
82
+ left--;
83
+ const f = spin(next, true);
84
+ data.f.push(f.out);
85
+ total += f.total;
86
+ if (f.scatters >= scatter.retrigger.count) left += scatter.retrigger.spins;
87
+ }
88
+ }
89
+ if (total > maxWinBp) { total = maxWinBp; data.cap = 1; }
90
+ return { data, pay: total };
91
+ }
92
+
93
+ return { round };
94
+ }
@@ -0,0 +1 @@
1
+ { "type": "module" }
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The House Engine MCP server: hbengine's commands as tools, so an agent
4
+ * (Claude Code, Cursor, …) builds, verifies and ships a studio game without
5
+ * leaving its editor. Stdio JSON-RPC, the Model Context Protocol 2024-11-05
6
+ * subset every client speaks: initialize, tools/list, tools/call. No
7
+ * dependencies: each tool runs hbengine and returns what it printed.
8
+ *
9
+ * { "mcpServers": { "house-engine": { "command": "node", "args": ["…/engine-mcp/server.mjs"],
10
+ * "env": { "HBENGINE_KEY": "hbk_…" } } } }
11
+ *
12
+ * With HBENGINE_KEY the uploads and numbers go through the studio platform
13
+ * (the key from the dashboard's Developer page); without it, the local pipeline.
14
+ *
15
+ * Run as a file it serves; imported (hbengine mcp, the tests) it exports
16
+ * run, handle and serve.
17
+ */
18
+ import { spawnSync } from 'node:child_process';
19
+ import { readFileSync } from 'node:fs';
20
+ import { createInterface } from 'node:readline';
21
+ import { pathToFileURL } from 'node:url';
22
+ import { TEMPLATES } from '../engine-cli/templates.mjs';
23
+
24
+
25
+ // The version a client sees in serverInfo: the published package's
26
+ // (hbengine/package.json, one level up in the npm layout), else this
27
+ // package's own, so a client never reads a number the build did not set.
28
+ const VERSION = (() => {
29
+ for (const rel of ['../package.json', './package.json']) {
30
+ try {
31
+ const v = JSON.parse(readFileSync(new URL(rel, import.meta.url), 'utf8')).version;
32
+ if (typeof v === 'string' && v) return v;
33
+ } catch { /* the next candidate */ }
34
+ }
35
+ return 'dev';
36
+ })();
37
+ const HBENGINE = new URL('../engine-cli/hbengine.mjs', import.meta.url).pathname;
38
+ const GUIDE = new URL('../engine-sdk/GUIDE.md', import.meta.url).pathname;
39
+ const DIR = { type: 'string', description: 'the project directory (math/, bundle/, hbengine.json)' };
40
+ // The most a reply to the reviewer may say (the studio platform refuses more).
41
+ export const REPLY_MAX = 4000;
42
+ // The most one support message may say (the same).
43
+ export const TICKET_MAX = 8000;
44
+
45
+ export const TOOLS = [
46
+ { name: 'engine_connect', description: 'Link this machine to the developer\'s studio on House Studio: opens the studio dashboard in their browser, they press Approve, and the key is kept on this machine for every other tool. Ask the developer for their studio slug (dashboard, Organisation, Profile) first. Waits up to ten minutes for the approval.', inputSchema: { type: 'object', properties: { studio: { type: 'string', description: 'the studio slug, as the dashboard shows it' } }, required: ['studio'] } },
47
+ { name: 'engine_guide', description: 'The guide to building a House Engine game: math pack, bundle, SDK, panel, currencies, languages, what the upload checks. Read it first.', inputSchema: { type: 'object', properties: {} } },
48
+ { name: 'engine_templates', description: 'The templates a game can start from, with what each one is.', inputSchema: { type: 'object', properties: {} } },
49
+ { name: 'engine_init', description: 'Start a game from a template: math/, bundle/, hbengine.json in a new directory.', inputSchema: { type: 'object', properties: { dir: { type: 'string', description: 'new directory' }, template: { type: 'string', enum: Object.keys(TEMPLATES) }, slug: { type: 'string', description: 'the game slug, a-z 0-9 -, 2–24 chars' } }, required: ['dir', 'slug'] } },
50
+ { name: 'engine_verify', description: 'Verify the math pack with the engine’s own verifier: exact RTP, hit rate and max multiplier per bet, the mathHash; or the errors.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
51
+ { name: 'engine_lint', description: 'Scan the bundle as the upload will: budgets, assets.json, forbidden code, outside URLs, engine use.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
52
+ { name: 'engine_test', description: 'Offline: the upload\'s checks (boot, idle, fit at desktop and phone sizes and fullscreen, a bet through the game\'s own UI, currency, errors, network) in Chrome against engine-dev, the engine on this machine. Needs Chrome and playwright-core. About two minutes.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
53
+ { name: 'engine_pack', description: 'Verify and lint, then write dist/<slug>-<date>.tgz (math, bundle, store page, tile) and its sha256: the file to hand to Housebets.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
54
+ { name: 'engine_upload', description: 'Ship a version: verify + lint, upload to the portal, which registers it with the sandbox and plays the conformance run on desktop and phone (bet, currency, outage, fit, errors, network). Returns the report and, on success, the link to play it inside the site. Takes about two minutes.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
55
+ { name: 'engine_status', description: 'Every version of the game and its state (uploaded, verified, sandbox, failed), its review (in review, approved, returned) and which is live, and the game\'s state on the studio platform.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
56
+ { name: 'engine_store', description: 'Send the store page from store.json in the project (title, category, description, features, tags, languages, rules per language; tile.png or tile.webp, 3:4, if present). Answers what review still needs. On a live game the tile stays the reviewed one until Housebets reset it; the page is saved all the same. Needs HBENGINE_KEY.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
57
+ { name: 'engine_submit', description: 'Submit the newest version that passed every check for review by Housebets. Needs the studio agreement (accepted by an owner in the dashboard), a complete store page and HBENGINE_KEY. Housebets emails the verdict; once approved, engine_live puts it live.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
58
+ { name: 'engine_live', description: 'Put the newest approved version live, as an owner pressing Make live on the game\'s page: the round server and the front end switch to it within seconds. Only after Housebets approved it (engine_status shows "approved"; the studio gets an email). Going live is an owner\'s: the key must be one an owner of the studio made. The first time, players find the game on Housebets once Housebets switches its catalogue page on. Needs HBENGINE_KEY.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
59
+ { name: 'engine_preflight', description: 'Before engine_submit: the review checklist items a machine can check on the newest version (the RTP and max win the rules quote against the verified maths, rules in every store language, the store page, fit on phone and desktop, recovery from errors, load, the network and the scan), each pass / fail with the reason, and the exact facts the rules must quote. Fix every failure first; a reviewer would send it back. Needs HBENGINE_KEY.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
60
+ { name: 'engine_review', description: 'Housebets\' review of the game, like a pull request: every thread (must-fix or suggestion, the area, the version it was raised on, open / fixed / resolved) with the conversation, and the review checklist results. Fix every open must-fix thread in the project, then engine_upload and engine_submit: submitting marks the open must-fix threads fixed in that version, and Housebets checks each. Use engine_reply only to ask the reviewer a question or explain a choice. Needs HBENGINE_KEY.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
61
+ { name: 'engine_reply', description: `Ask the reviewer a question on a review thread, or explain a change or a choice, in at most ${REPLY_MAX} characters. Fixes themselves go in by uploading and submitting (engine_upload, engine_submit). Needs HBENGINE_KEY.`, inputSchema: { type: 'object', properties: { dir: DIR, thread: { type: 'integer', description: 'the thread id from engine_review' }, message: { type: 'string', description: `your question, or what you changed and why (at most ${REPLY_MAX} characters)` }, fixed: { type: 'boolean', description: 'only when the change is already in a version you uploaded but have not submitted yet; engine_submit marks fixes itself' } }, required: ['dir', 'thread', 'message'] } },
62
+ { name: 'engine_stats', description: 'The game\'s play in the last 30 days by coin: rounds, turnover, GGR, actual RTP against the maths\' expected band, and the shell\'s counts of launches, errors and rounds left open. Needs HBENGINE_KEY.', inputSchema: { type: 'object', properties: { dir: DIR }, required: ['dir'] } },
63
+ { name: 'engine_feedback', description: `Send Housebets a support ticket or feedback: a bug in the engine, the SDK, hbengine or these tools; a question the guide does not answer; an idea; or what the developer thinks of House Engine. Use it when something of House Engine (not the developer's own game) is broken or unclear after reading engine_guide, or when the developer asks to send feedback. Say what you did, what happened, what you expected, and the exact error text; never include a key or a password. hbengine's version, node, the OS and the game in dir go with it. Housebets answers by email and in engine_tickets. With ticket, adds to that ticket instead (close: true when it is solved). Needs HBENGINE_KEY.`, inputSchema: { type: 'object', properties: { message: { type: 'string', description: `what happened, what you expected, the exact error (at most ${TICKET_MAX} characters)` }, kind: { type: 'string', enum: ['bug', 'question', 'idea', 'feedback'], description: 'what it is (default feedback)' }, subject: { type: 'string', description: 'one line; empty, the message\'s first line' }, dir: { type: 'string', description: 'the project directory, when it is about a game' }, ticket: { type: 'integer', description: 'a ticket id from engine_tickets, to add to it' }, close: { type: 'boolean', description: 'with ticket: close it, it is solved' } }, required: ['message'] } },
64
+ { name: 'engine_tickets', description: 'The studio\'s support tickets and Housebets\' answers: each open one with its whole conversation. Answer one with engine_feedback and its ticket id. Needs HBENGINE_KEY.', inputSchema: { type: 'object', properties: {} } },
65
+ ];
66
+
67
+ export function run(tool, a) {
68
+ if (tool === 'engine_templates') return { text: Object.entries(TEMPLATES).map(([t, about]) => `${t}: ${about}`).join('\n'), status: 0 };
69
+ if (tool === 'engine_connect') {
70
+ if (typeof a.studio !== 'string' || !/^[a-z0-9][a-z0-9-]{1,47}$/.test(a.studio)) return { text: 'studio is required: the slug the dashboard shows (Organisation, Profile)', status: 1 };
71
+ const r = spawnSync('node', [HBENGINE, 'connect', '--studio', a.studio], { encoding: 'utf8', timeout: 11 * 60 * 1000, stdio: ['ignore', 'pipe', 'pipe'] });
72
+ if (r.error) return { text: `hbengine did not run: ${r.error.message}`, status: 1 };
73
+ return { text: ((r.stdout || '') + (r.stderr || '')).trim() || (r.status === 0 ? 'connected' : `exit ${r.status ?? 1}`), status: r.status ?? 1 };
74
+ }
75
+ if (tool === 'engine_feedback' || tool === 'engine_tickets') return support(tool, a);
76
+ if (typeof a.dir !== 'string' && tool !== 'engine_guide') return { text: 'dir is required', status: 1 };
77
+ if (tool === 'engine_init' && typeof a.slug !== 'string') return { text: 'slug is required', status: 1 };
78
+ if (tool === 'engine_init' && a.template && !TEMPLATES[a.template]) return { text: `template: one of ${Object.keys(TEMPLATES).join(', ')}`, status: 1 };
79
+ // Refused here, before hbengine and the platform see it, with the count the agent can act on.
80
+ if (tool === 'engine_reply' && typeof a.message === 'string' && a.message.length > REPLY_MAX) return { text: `the reply is over ${REPLY_MAX} characters (${a.message.length}): say less, or split it over two replies`, status: 1 };
81
+ if (tool === 'engine_guide') {
82
+ return { text: readFileSync(GUIDE, 'utf8'), status: 0 };
83
+ }
84
+ const args = tool === 'engine_init' ? ['init', a.dir, '--template', a.template || 'coin', '--slug', a.slug]
85
+ : tool === 'engine_reply' ? ['reply', a.dir, '--thread', String(a.thread), '--message', String(a.message || ''), ...(a.fixed ? ['--fixed'] : [])]
86
+ : [tool.slice('engine_'.length), a.dir];
87
+ const r = spawnSync('node', [HBENGINE, ...args], { encoding: 'utf8', timeout: 10 * 60 * 1000 });
88
+ // hbengine did not run at all (not found, killed at the timeout): say so, not "nullnull".
89
+ if (r.error) return { text: `hbengine did not run: ${r.error.message}`, status: 1 };
90
+ return { text: ((r.stdout || '') + (r.stderr || '')).trim() || (r.status === 0 ? 'ok' : `exit ${r.status ?? 1}`), status: r.status ?? 1 };
91
+ }
92
+
93
+ // Support: hbengine feedback / tickets, told it is an agent asking (the ticket says so).
94
+ function support(tool, a) {
95
+ let args = ['tickets'];
96
+ if (tool === 'engine_feedback') {
97
+ const message = typeof a.message === 'string' ? a.message : '';
98
+ if (!message.trim() && !(a.ticket && a.close)) return { text: 'message is required: what happened, what you expected, the exact error', status: 1 };
99
+ if (message.length > TICKET_MAX) return { text: `the message is over ${TICKET_MAX} characters (${message.length}): put the essentials first, or send the rest on the ticket`, status: 1 };
100
+ if (a.kind !== undefined && !['bug', 'question', 'idea', 'feedback'].includes(a.kind)) return { text: 'kind: one of bug, question, idea, feedback', status: 1 };
101
+ if (a.ticket !== undefined && !Number.isInteger(a.ticket)) return { text: 'ticket: the number engine_tickets shows', status: 1 };
102
+ args = ['feedback', ...(typeof a.dir === 'string' ? [a.dir] : []), '--message', message];
103
+ if (a.ticket !== undefined) args.push('--ticket', String(a.ticket), ...(a.close ? ['--close'] : []));
104
+ else args.push('--kind', a.kind || 'feedback', ...(typeof a.subject === 'string' && a.subject ? ['--subject', a.subject] : []));
105
+ }
106
+ const r = spawnSync('node', [HBENGINE, ...args], { encoding: 'utf8', timeout: 2 * 60 * 1000, env: { ...process.env, HBENGINE_VIA: 'mcp' } });
107
+ if (r.error) return { text: `hbengine did not run: ${r.error.message}`, status: 1 };
108
+ return { text: ((r.stdout || '') + (r.stderr || '')).trim() || (r.status === 0 ? 'ok' : `exit ${r.status ?? 1}`), status: r.status ?? 1 };
109
+ }
110
+
111
+ const error = (text) => ({ content: [{ type: 'text', text }], isError: true });
112
+
113
+ /** One request's result (null for a notification, or for a line that is not a request), the same whatever the transport. */
114
+ export function handle(msg) {
115
+ if (!msg || typeof msg !== 'object') return null;
116
+ const { id, method } = msg;
117
+ // `params` may be missing, null or a scalar from a loose client: an empty one, never a crash.
118
+ const params = msg.params && typeof msg.params === 'object' ? msg.params : {};
119
+ if (method === 'initialize') return { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'house-engine', version: VERSION } };
120
+ if (method === 'tools/list') return { tools: TOOLS };
121
+ if (method === 'tools/call') {
122
+ const tool = TOOLS.find((t) => t.name === params.name);
123
+ if (!tool) return error(`no tool ${params.name}`);
124
+ // A tool that throws answers its own call; the server stays up for the next.
125
+ try {
126
+ const result = run(tool.name, params.arguments && typeof params.arguments === 'object' ? params.arguments : {});
127
+ return { content: [{ type: 'text', text: result.text }], isError: result.status !== 0 };
128
+ } catch (e) {
129
+ return error(`${params.name} failed: ${e && e.message ? e.message : String(e)}`);
130
+ }
131
+ }
132
+ return id !== undefined ? {} : null;
133
+ }
134
+
135
+ /** Serves JSON-RPC over a pair of streams (stdio by default). No line can end the server: a
136
+ * request it cannot handle is answered with a JSON-RPC error, anything else is dropped. */
137
+ export function serve({ input = process.stdin, output = process.stdout } = {}) {
138
+ const send = (body) => output.write(JSON.stringify({ jsonrpc: '2.0', ...body }) + '\n');
139
+ createInterface({ input }).on('line', (line) => {
140
+ let msg;
141
+ try { msg = JSON.parse(line); } catch { return; }
142
+ try {
143
+ const result = handle(msg);
144
+ if (result !== null) send({ id: msg.id, result });
145
+ } catch (e) {
146
+ if (msg && typeof msg === 'object' && msg.id !== undefined) send({ id: msg.id, error: { code: -32603, message: e && e.message ? e.message : String(e) } });
147
+ }
148
+ });
149
+ }
150
+
151
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) serve();