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,565 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hbengine — a studio's whole workflow in six words.
4
+ *
5
+ * hbengine init <dir> [--template coin|dice|slot|tower|wheel] [--slug name]
6
+ * hbengine verify [dir] the math, by the engine's own verifier: RTP per bet, the hash
7
+ * hbengine lint [dir] the bundle: budgets, staged loading, forbidden code
8
+ * hbengine dev [dir] play the bundle inside the real shell against the sandbox
9
+ * hbengine test [dir] offline: the upload's checks on desktop and phone sizes (Chrome)
10
+ * hbengine pack [dir] verify + lint, then the .tgz an upload takes (math/, bundle/) to hand over
11
+ * hbengine upload [dir] verify + lint, then the version goes to the portal, which
12
+ * registers it with the sandbox and plays the conformance run
13
+ * hbengine status [dir] every version of the game and its state
14
+ * hbengine stats [dir] rounds, turnover, GGR and RTP by coin, last 30 days, and the shell's counts
15
+ * hbengine store [dir] the store page from store.json (and tile.png/.webp, 3:4) to the dashboard
16
+ * hbengine submit [dir] the newest version that passed every check, to review
17
+ * hbengine live [dir] the newest approved version, live (an owner's key)
18
+ * hbengine preflight [dir] the review checklist items a machine can check, on the newest version
19
+ * hbengine review [dir] the review: every thread Housebets raised, open must-fix first
20
+ * hbengine reply [dir] --thread <id> --message "<what you changed>" [--fixed] (4000 characters at most)
21
+ * hbengine feedback [dir] --message "<what happened>" [--kind bug|question|idea|feedback] [--subject "<one line>"]
22
+ * a ticket to Housebets (the game in dir, if it is a project, goes with it);
23
+ * --ticket <id> [--close] adds to one instead (8000 characters at most)
24
+ * hbengine tickets the studio's tickets and Housebets' answers
25
+ * hbengine connect [url] [--studio <slug>]
26
+ * approve in the browser, confirm the studio, keep the key, add the MCP tools to Claude Code, Cursor, Codex
27
+ * hbengine login [url] keep a studio API key (from the dashboard: Developer)
28
+ * hbengine mcp the MCP server (what connect adds to the agents)
29
+ * hbengine version this hbengine's version (a newer one on npm is said after any command)
30
+ * hbengine templates what init can start from
31
+ *
32
+ * A project is a directory with math/, bundle/ and hbengine.json
33
+ * ({slug, portal, rgs}). Nothing here decides anything: verify asks the
34
+ * engine, lint runs the upload's scan, upload asks the portal. What passes
35
+ * here passes there, because it is the same code.
36
+ *
37
+ * With a key (hbengine login, or HBENGINE_KEY and HBENGINE_PORTAL) the
38
+ * portal is the studio platform's API; without one, the local pipeline.
39
+ * Offline (the kit), verify and dev start the kit's engine-dev: the
40
+ * engine's own maths and round rules on this machine, play money.
41
+ */
42
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
43
+ import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
44
+ import { createHash } from 'node:crypto';
45
+ import { homedir } from 'node:os';
46
+ import { dirname, join, resolve } from 'node:path';
47
+ import { createInterface } from 'node:readline/promises';
48
+ import { handshake } from './connect.mjs';
49
+ import { TEMPLATES } from './templates.mjs';
50
+
51
+ const PKG = new URL('..', import.meta.url).pathname;
52
+ const [cmd, ...rest] = process.argv.slice(2);
53
+ // Flags that stand alone; every other --flag takes the word after it as its value.
54
+ const BOOLEAN = new Set(['--fixed', '--close']);
55
+ const takesValue = (i) => i >= 0 && rest[i].startsWith('--') && !BOOLEAN.has(rest[i]);
56
+ const opt = (k, d) => { const i = rest.findIndex((a, j) => a === '--' + k && !takesValue(j - 1)); return i >= 0 ? rest[i + 1] : d; };
57
+ const flag = (k) => rest.some((a, j) => a === '--' + k && !takesValue(j - 1));
58
+ // The directory is the one word that is neither a flag nor a flag's value.
59
+ const dir = resolve(rest.filter((a, i) => !a.startsWith('--') && !takesValue(i - 1))[0] || '.');
60
+ const DEFAULTS = { portal: 'http://127.0.0.1:8860', rgs: 'http://127.0.0.1:8090', site: process.env.ENGINE_SITE || 'http://localhost:3000' };
61
+ const CREDENTIALS = join(homedir(), '.hbengine', 'credentials');
62
+ // The studio platform when neither the command nor HBENGINE_PORTAL names one (studio/render.go cliPortal).
63
+ // The studio platform: House Studio, unless HBENGINE_PORTAL or a URL on the
64
+ // command names another (Housebets' own testing).
65
+ const PORTAL = process.env.HBENGINE_PORTAL || 'https://housestudio.io';
66
+ // macOS tar otherwise adds AppleDouble `._*` entries; the portal intentionally
67
+ // rejects those metadata files at the upload boundary.
68
+ const TAR_ENV = { ...process.env, COPYFILE_DISABLE: '1' };
69
+ // This hbengine's version: the published package's (npm's hbengine/package.json, one level up);
70
+ // a checkout is "dev", which neither names a release to the portal nor looks for a newer one.
71
+ const VERSION = (() => {
72
+ try {
73
+ const p = JSON.parse(readFileSync(join(PKG, 'package.json'), 'utf8'));
74
+ if (p.name === 'hbengine' && typeof p.version === 'string' && p.version) return p.version;
75
+ } catch { /* a checkout */ }
76
+ return 'dev';
77
+ })();
78
+ // Every call to the portal names this hbengine, so the dashboard can say when a studio's tools are behind.
79
+ const UA = { 'User-Agent': `hbengine/${VERSION} (${process.env.HBENGINE_VIA === 'mcp' ? 'mcp' : 'cli'}; node ${process.version})` };
80
+ // A newer hbengine on npm, looked for at most once a day (~/.hbengine/update.json) and said when
81
+ // the command ends: in the terminal, and through the MCP tools to the agent, which tells its user.
82
+ const UPDATE = join(homedir(), '.hbengine', 'update.json');
83
+ const newerThan = (a, b) => {
84
+ const x = a.split('.').map(Number), y = b.split('.').map(Number);
85
+ for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) > (y[i] || 0);
86
+ return false;
87
+ };
88
+ let newer = null;
89
+ if (VERSION !== 'dev' && !process.env.HBENGINE_NO_UPDATE_CHECK && cmd !== 'mcp' && cmd !== 'version') {
90
+ (async () => {
91
+ let seen = {};
92
+ try { seen = JSON.parse(readFileSync(UPDATE, 'utf8')); } catch { /* first look */ }
93
+ let latest = seen.latest;
94
+ if (typeof latest !== 'string' || !(Date.now() - (seen.at || 0) < 24 * 3600 * 1000)) {
95
+ const r = await fetch('https://registry.npmjs.org/hbengine/latest', { signal: AbortSignal.timeout(2000) }).catch(() => null);
96
+ const j = r && r.ok ? await r.json().catch(() => null) : null;
97
+ if (j && typeof j.version === 'string' && /^\d+\.\d+\.\d+$/.test(j.version)) {
98
+ latest = j.version;
99
+ try { mkdirSync(dirname(UPDATE), { recursive: true, mode: 0o700 }); writeFileSync(UPDATE, JSON.stringify({ latest, at: Date.now() }) + '\n'); } catch { /* a read-only home: look again next time */ }
100
+ }
101
+ }
102
+ if (typeof latest === 'string' && newerThan(latest, VERSION)) newer = latest;
103
+ })().catch(() => {});
104
+ process.on('exit', () => {
105
+ if (newer) console.error(`\nhbengine ${newer} is out (you have ${VERSION}): restart your agent (it takes the newest when it starts), or npm i -g hbengine@latest if you installed it.`);
106
+ });
107
+ }
108
+ const login = () => {
109
+ if (process.env.HBENGINE_KEY) return { key: process.env.HBENGINE_KEY, portal: PORTAL };
110
+ return existsSync(CREDENTIALS) ? JSON.parse(readFileSync(CREDENTIALS, 'utf8')) : null;
111
+ };
112
+
113
+ const fail = (msg) => { console.error(msg); process.exit(1); };
114
+ // A project is hbengine.json beside math/ and bundle/; a bare pack (an
115
+ // example) is a project too, named by its manifest.
116
+ // A project may come from someone else: its slug names files, so it is checked.
117
+ const SLUG = /^[a-z0-9][a-z0-9-]{1,23}$/;
118
+ const project = () => {
119
+ const f = join(dir, 'hbengine.json');
120
+ const manifest = join(dir, 'math', 'manifest.json');
121
+ if (!existsSync(f) && !existsSync(manifest)) fail(`${dir} has no hbengine.json and no math/manifest.json (hbengine init makes a project)`);
122
+ const p = existsSync(f) ? { ...DEFAULTS, ...JSON.parse(readFileSync(f, 'utf8')) } : { ...DEFAULTS, slug: JSON.parse(readFileSync(manifest, 'utf8')).game };
123
+ if (typeof p.slug !== 'string' || !SLUG.test(p.slug)) fail(`slug ${JSON.stringify(p.slug)}: lowercase letters, digits and dashes, 2–24`);
124
+ return p;
125
+ };
126
+ // The games API: the studio platform's with a key, else the local pipeline's.
127
+ const games = (p) => {
128
+ const l = login();
129
+ return l ? { base: l.portal.replace(/\/$/, '') + '/engine/portal/v1/games', auth: { Authorization: 'Bearer ' + l.key } } : { base: p.portal + '/api/games', auth: {} };
130
+ };
131
+ const get = async (url, auth = {}) => {
132
+ const r = await fetch(url, { headers: { ...UA, ...auth } }).catch((e) => fail(`${url}: ${e.message} — is it running?`));
133
+ const j = await r.json().catch(() => fail(`${url}: HTTP ${r.status}`));
134
+ if (!r.ok) fail(j.error || `${url}: HTTP ${r.status}`);
135
+ return j;
136
+ };
137
+ // A 429 or 503 with Retry-After is the studio saying its pipeline is busy
138
+ // (another pack in the verifier, the play-tests full), not that the pack is
139
+ // bad: the call is made again after that wait, three times at most.
140
+ const post = async (url, body, type = 'application/json', auth = {}, tries = 3) => {
141
+ const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': type, ...UA, ...auth }, body }).catch((e) => fail(`${url}: ${e.message} — is it running?`));
142
+ const text = await r.text();
143
+ let j;
144
+ try { j = JSON.parse(text); } catch { fail(`${url}: HTTP ${r.status} ${text.slice(0, 120)}`); }
145
+ const after = Number(r.headers.get('retry-after'));
146
+ if ((r.status === 429 || r.status === 503) && after > 0 && tries > 1) {
147
+ console.error(`${j.error || `${url}: HTTP ${r.status}`} — trying again in ${after} s`);
148
+ await new Promise((resolve) => setTimeout(resolve, Math.min(after, 60) * 1000));
149
+ return post(url, body, type, auth, tries - 1);
150
+ }
151
+ if (!r.ok) fail(j.error || `${url}: HTTP ${r.status}`);
152
+ return j;
153
+ };
154
+
155
+ async function init() {
156
+ const template = opt('template', 'coin');
157
+ if (!TEMPLATES[template]) fail(`--template: one of ${Object.keys(TEMPLATES).join(', ')}`);
158
+ const slug = opt('slug', template);
159
+ if (!SLUG.test(slug)) fail(`--slug ${slug}: lowercase letters, digits and dashes, 2–24`);
160
+ if (existsSync(dir)) fail(`${dir} exists`);
161
+ const from = join(PKG, 'engine-examples', template);
162
+ if (!existsSync(from)) fail(`no template ${template}`);
163
+ cpSync(from, dir, { recursive: true });
164
+ // math-kit/ is a link in the repo; a project gets its own copy (no tests).
165
+ const kit = join(dir, 'math-kit');
166
+ if (existsSync(kit) && lstatSync(kit).isSymbolicLink()) {
167
+ unlinkSync(kit);
168
+ cpSync(join(PKG, 'engine-math'), kit, { recursive: true, filter: (f) => !f.endsWith('.test.mjs') });
169
+ }
170
+ writeFileSync(join(dir, 'hbengine.json'), JSON.stringify({ slug, ...DEFAULTS }, null, 2) + '\n');
171
+ // The store page, to fill in and send with hbengine store.
172
+ if (!existsSync(join(dir, 'store.json'))) writeFileSync(join(dir, 'store.json'), JSON.stringify({ title: slug, category: 'other', description: '', features: [], tags: [], languages: ['en'], rules: { en: '' } }, null, 2) + '\n');
173
+ // A template whose tables are generated (the maths kit) makes them for this slug now.
174
+ if (!existsSync(join(dir, 'math', 'manifest.json'))) {
175
+ console.log('making the maths (math/gen.mjs)…');
176
+ const g = spawnSync('node', [join('math', 'gen.mjs'), slug], { cwd: dir, stdio: 'inherit' });
177
+ if (g.status !== 0) fail('math/gen.mjs failed');
178
+ } else {
179
+ // The pack is the game's: its slug, and its tables named after it.
180
+ const manifest = join(dir, 'math', 'manifest.json');
181
+ const m = JSON.parse(readFileSync(manifest, 'utf8'));
182
+ m.game = slug;
183
+ const rename = (t) => (t && t.startsWith(template) ? slug + t.slice(template.length) : t);
184
+ for (const f of readdirSync(join(dir, 'math'))) if (f.endsWith('.tsv') && rename(f) !== f) renameSync(join(dir, 'math', f), join(dir, 'math', rename(f)));
185
+ for (const mode of m.modes) {
186
+ mode.table = rename(mode.table);
187
+ for (const step of mode.steps || []) step.table = rename(step.table);
188
+ }
189
+ writeFileSync(manifest, JSON.stringify(m, null, 2) + '\n');
190
+ }
191
+ console.log(`${dir}: a ${template} game as "${slug}".\nNext: edit math/ and bundle/, then hbengine verify and hbengine dev; hbengine pack makes the file to hand over (or hbengine upload with a studio key).`);
192
+ }
193
+
194
+ // The kit's offline sandbox: when nothing answers at rgs, start engine-dev
195
+ // (bin/engine-dev-<os>-<arch>) on this project's math. True when the sandbox
196
+ // is engine-dev; its process, if this command started it.
197
+ const ENGINE_DEV = join(PKG, '..', 'bin', `engine-dev-${process.platform}-${process.arch}${process.platform === 'win32' ? '.exe' : ''}`);
198
+ const answers = (url) => fetch(url, { method: 'HEAD' }).then((r) => r.status).catch(() => 0);
199
+ async function sandbox(rgs) {
200
+ const at = await answers(`${rgs}/dev/launch`);
201
+ if (at) return { offline: at === 200 };
202
+ if (!existsSync(ENGINE_DEV)) return { offline: false };
203
+ // engine-dev listens on this machine only, whatever a project's hbengine.json says.
204
+ const host = new URL(rgs).hostname;
205
+ if (host !== '127.0.0.1' && host !== 'localhost') fail(`rgs ${rgs}: engine-dev runs on 127.0.0.1 only`);
206
+ const child = spawn(ENGINE_DEV, ['--math', join(dir, 'math'), '--addr', new URL(rgs).host], { stdio: 'ignore' });
207
+ for (let i = 0; i < 50 && !(await answers(`${rgs}/dev/launch`)); i++) await new Promise((r) => setTimeout(r, 100));
208
+ return { offline: true, child };
209
+ }
210
+
211
+ async function verify() {
212
+ const { rgs } = project();
213
+ const box = login() ? {} : await sandbox(rgs);
214
+ process.on('exit', () => box.child?.kill());
215
+ const mathDir = join(dir, 'math');
216
+ const files = Object.fromEntries(readdirSync(mathDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => [e.name, readFileSync(join(mathDir, e.name), 'utf8')]));
217
+ // With a key the studio platform asks the engine (CI has no sandbox); without, the local sandbox.
218
+ const l = login();
219
+ const r = l ? await post(`${l.portal.replace(/\/$/, '')}/engine/portal/v1/verify`, JSON.stringify({ files }), 'application/json', { Authorization: 'Bearer ' + l.key })
220
+ : await post(`${rgs}/engine/v1/verify`, JSON.stringify({ files }));
221
+ if (!r.ok) fail('math does not verify:\n ' + r.errors.join('\n '));
222
+ console.log(`math ok hash ${r.hash}`);
223
+ const pct = (bp) => (Number(bp) / 100).toFixed(4) + '%';
224
+ for (const m of r.report.modes) for (const b of m.bets) console.log(` ${m.id}/${b.id}: RTP ${pct(b.rtp)} hit ${pct(b.hitRate)} max ${b.maxMult}×${b.stops ? ' stops ' + b.stops.map(pct).join(' ') : ''}`);
225
+ for (const w of r.report.warnings || []) console.log(' warning: ' + w);
226
+ box.child?.kill();
227
+ }
228
+
229
+ function lint() {
230
+ const r = spawnSync('node', [join(PKG, 'engine-conformance', 'scan.mjs'), join(dir, 'bundle')], { encoding: 'utf8' });
231
+ process.stdout.write(r.stdout + r.stderr);
232
+ if (r.status !== 0) process.exit(1);
233
+ }
234
+
235
+ async function dev() {
236
+ const { slug, portal, rgs } = project();
237
+ const box = await sandbox(rgs);
238
+ if (box.offline) {
239
+ // engine-dev reads math/ at every launch: edit either, reload the page.
240
+ return spawn('node', [join(PKG, 'engine-conformance', 'run.mjs'), join(dir, 'bundle'), '--slug', slug, '--game-id', '0', '--rgs', rgs, '--dev'], { stdio: 'inherit' });
241
+ }
242
+ // engine-dev ships with the offline kit, not with npm: on the platform the upload is the sandbox.
243
+ if (login() && !(await answers(`${rgs}/dev/launch`))) fail('hbengine dev needs a sandbox on this machine (the offline kit). With a studio key, hbengine upload plays the game in the platform\'s sandbox and prints the link.');
244
+ // The sandbox plays your local bundle on the math it already has: the
245
+ // first `hbengine upload` registers the game and its pack there.
246
+ if (!(await fetch(`${rgs}/engine/v1/math/${slug}`).then((r) => r.ok).catch(() => fail(`${rgs}: the sandbox RGS is not reachable`)))) fail(`the sandbox does not know "${slug}" yet: run hbengine upload once, then hbengine dev plays your local bundle`);
247
+ const g = await post(`${portal}/api/games`, JSON.stringify({ slug })); // the local pipeline: dev plays locally
248
+ spawn('node', [join(PKG, 'engine-conformance', 'run.mjs'), join(dir, 'bundle'), '--slug', slug, '--game-id', String(g.id), '--rgs', rgs, '--dev'], { stdio: 'inherit' });
249
+ }
250
+
251
+ async function upload() {
252
+ await verify();
253
+ lint();
254
+ const p = project();
255
+ const { slug, site } = p;
256
+ const { base, auth } = games(p);
257
+ const g = await post(base, JSON.stringify({ slug, title: p.title || slug }), 'application/json', auth);
258
+ const tgz = execFileSync('tar', ['-czf', '-', 'math', 'bundle'], { cwd: dir, env: TAR_ENV, maxBuffer: 200 * 1024 * 1024 });
259
+ let v = await post(`${base}/${slug}/uploads`, tgz, 'application/gzip', auth);
260
+ console.log(`version ${v.n} ${v.state}${v.hash ? ' math ' + v.hash.slice(0, 12) : ''}`);
261
+ if (v.state === 'failed') fail(report(v));
262
+ console.log('the sandbox plays it now on desktop and phone (about two minutes; hbengine status shows the state meanwhile)');
263
+ process.stdout.write('conformance run ');
264
+ for (let waited = 0; v.run && v.run.detail === 'running'; waited += 2) {
265
+ if (waited > 600) fail('\nthe run is still going after ten minutes: hbengine status');
266
+ await new Promise((r) => setTimeout(r, 2000));
267
+ process.stdout.write('.');
268
+ v = await get(`${base}/${slug}/uploads/${v.n}`, auth);
269
+ }
270
+ console.log('\n' + report(v));
271
+ if (v.state !== 'sandbox') process.exit(1);
272
+ // The local site's address means nothing to a studio on the platform: there, the game's own link.
273
+ const play = login() ? g.play : `${site}/games/housebets-engine-${slug}`;
274
+ if (play) console.log(`play it: ${play} (id ${g.id})`);
275
+ if (g.dashboard) console.log(`submit it for review: ${g.dashboard}?tab=versions`);
276
+ }
277
+
278
+ function report(v) {
279
+ const lines = [];
280
+ if (v.verify && !v.verify.ok) lines.push(...v.verify.errors.map((e) => 'math: ' + e));
281
+ if (v.scan) for (const f of v.scan.findings) lines.push(`${f.level.toUpperCase()} ${f.rule}${f.file ? ' ' + f.file : ''}: ${f.detail}`);
282
+ if (v.run) for (const c of v.run.checks || []) lines.push(`${c.status === 'skipped' ? 'skip' : !c.ok ? 'FAIL' : c.status === 'warning' ? 'warn' : 'ok '} ${c.size.padEnd(8)} ${c.rule.padEnd(8)} ${c.detail}`);
283
+ if (v.run && v.run.detail && v.run.detail !== 'running' && !v.run.checks?.length) lines.push('run: ' + v.run.detail);
284
+ return `${v.state}\n` + lines.map((l) => ' ' + l).join('\n');
285
+ }
286
+
287
+ async function status() {
288
+ const p = project();
289
+ const { base, auth } = games(p);
290
+ const g = await get(`${base}/${p.slug}`, auth);
291
+ console.log(`${p.slug} id ${g.id}${g.state ? ' ' + g.state : ''}${g.play ? ' play ' + g.play : ''}${g.dashboard ? '\n ' + g.dashboard : ''}`);
292
+ const review = { pending: 'in review', approved: 'approved', returned: 'returned with notes' };
293
+ for (const v of g.uploads) console.log(` v${v.n} ${v.at} ${v.state}${v.hash ? ' math ' + v.hash.slice(0, 12) : ''}${review[v.review] ? ' ' + review[v.review] : ''}${g.liveVersion === v.n ? ' LIVE' : ''}`);
294
+ const approved = [...g.uploads].reverse().find((v) => v.review === 'approved');
295
+ if (approved && approved.n !== g.liveVersion && approved.n > (g.liveVersion || 0)) console.log(`v${approved.n} is approved and not live yet: hbengine live puts it live.`);
296
+ if (g.liveVersion && g.onSite === false) console.log('Live on the engine; Housebets opens it on the site next and emails you when players can find it.');
297
+ }
298
+
299
+ async function stats() {
300
+ const p = project();
301
+ const l = login() || fail('hbengine stats reads the studio platform: hbengine login first');
302
+ const r = await get(`${games(p).base}/${p.slug}/stats`, games(p).auth);
303
+ console.log(`${p.slug} ${r.From} … ${r.To}`);
304
+ // The shell's counts (never text): a launch per open, an error per engine.reportError or uncaught
305
+ // error, a round left open per page closed on one. Before the rounds: a game that fails at every
306
+ // launch has launches and errors and no round, and that is when the counts matter.
307
+ const s = r.Telemetry;
308
+ if (s && (s.Boot || s.Error || s.Abandon || s.Slow)) console.log(` shell launches ${s.Boot} errors ${s.Error} rounds left open ${s.Abandon}${s.Slow ? ` slow starts ${s.Slow}` : ''}`);
309
+ if (!r.Totals || !r.Totals.length) return console.log(' no rounds');
310
+ // Percentages cut, never rounded up; the band only where it means something (as the dashboard).
311
+ const pct = (x) => (Math.floor(x * 100 + 1e-9) / 100).toFixed(2) + '%';
312
+ for (const t of r.Totals) console.log(` ${t.Currency.padEnd(5)} rounds ${t.Rounds} turnover ${t.Turnover} GGR ${t.GGR.toFixed(8)} RTP ${pct(t.RTP)}${t.Expected ? ` (expected ${pct(t.Expected)}${t.Band < 10 ? ` ± ${pct(t.Band)}` : ', too few rounds to compare yet'})` : ''}`);
313
+ }
314
+
315
+ async function loginCmd() {
316
+ const portal = (rest.find((a) => /^https?:\/\//.test(a)) || PORTAL).replace(/\/$/, '');
317
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
318
+ const key = (await rl.question(`API key for ${portal} (dashboard → Developer): `)).trim();
319
+ rl.close();
320
+ if (!/^hbk_[\w-]{20,}$/.test(key)) fail('that is not an API key (hbk_…)');
321
+ const list = await get(`${portal}/engine/portal/v1/games`, { Authorization: 'Bearer ' + key });
322
+ mkdirSync(join(homedir(), '.hbengine'), { recursive: true, mode: 0o700 });
323
+ writeFileSync(CREDENTIALS, JSON.stringify({ portal, key }) + '\n', { mode: 0o600 });
324
+ chmodSync(CREDENTIALS, 0o600);
325
+ console.log(`signed in to ${portal}: ${list.length} game(s). The key is in ${CREDENTIALS}.`);
326
+ }
327
+
328
+ // The browser handshake (connect.mjs): a code, an Approve in the dashboard, the key, and
329
+ // the approving studio confirmed before anything is written. Then every agent on this
330
+ // machine gets the MCP tools, which read the key from the credentials file.
331
+ async function connect() {
332
+ const portal = (rest.find((a) => /^https?:\/\//.test(a)) || PORTAL).replace(/\/$/, '');
333
+ const studio = opt('studio', '');
334
+ // An agent runs this with no terminal to answer the question in: it names the studio
335
+ // instead, and hears so now, before a code is made and someone approves it.
336
+ if (!studio && !process.stdin.isTTY) fail('No terminal to confirm the studio in: run hbengine connect --studio <slug> (Organisation, Profile shows the slug).');
337
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
338
+ // The terminal closing under the question (Ctrl-D) would leave it unanswered for ever and
339
+ // the process would just end: it is an error instead, and the handshake declines on it.
340
+ const prompt = (q) => new Promise((resolve, reject) => {
341
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
342
+ rl.once('close', () => reject(new Error('the terminal closed before an answer')));
343
+ rl.question(q).then(resolve, reject).finally(() => rl.close());
344
+ });
345
+ const got = await handshake({ portal, studio, prompt, open: (url) => spawn(opener, [url], { stdio: 'ignore', detached: true }).on('error', () => {}).unref() })
346
+ .catch((e) => fail(e.message));
347
+ mkdirSync(join(homedir(), '.hbengine'), { recursive: true, mode: 0o700 });
348
+ writeFileSync(CREDENTIALS, JSON.stringify({ portal: got.portal, key: got.key }) + '\n', { mode: 0o600 });
349
+ chmodSync(CREDENTIALS, 0o600);
350
+ // From a checkout, the server beside this file; from npm, the published one.
351
+ const local = !PKG.includes('node_modules');
352
+ const server = local ? [process.execPath, join(PKG, 'engine-mcp', 'server.mjs')] : ['npx', '-y', 'hbengine', 'mcp'];
353
+ const added = [];
354
+ const has = (bin) => spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { stdio: 'ignore' }).status === 0;
355
+ if (has('claude')) {
356
+ spawnSync('claude', ['mcp', 'remove', 'house-engine', '-s', 'user'], { stdio: 'ignore' });
357
+ if (spawnSync('claude', ['mcp', 'add', 'house-engine', '-s', 'user', '--', ...server], { stdio: 'ignore' }).status === 0) added.push('Claude Code');
358
+ }
359
+ if (has('codex')) {
360
+ spawnSync('codex', ['mcp', 'remove', 'house-engine'], { stdio: 'ignore' });
361
+ if (spawnSync('codex', ['mcp', 'add', 'house-engine', '--', ...server], { stdio: 'ignore' }).status === 0) added.push('Codex');
362
+ }
363
+ const cursor = join(homedir(), '.cursor');
364
+ if (existsSync(cursor)) {
365
+ const f = join(cursor, 'mcp.json');
366
+ let cfg = {};
367
+ let readable = true;
368
+ if (existsSync(f)) try { cfg = JSON.parse(readFileSync(f, 'utf8')); } catch { readable = false; }
369
+ if (readable) {
370
+ cfg.mcpServers = { ...cfg.mcpServers, 'house-engine': { command: server[0], args: server.slice(1) } };
371
+ writeFileSync(f, JSON.stringify(cfg, null, 2) + '\n');
372
+ added.push('Cursor');
373
+ } else {
374
+ console.log(`${f} is not plain JSON (comments or a trailing comma?), so it was left alone. Add this under "mcpServers":\n "house-engine": ${JSON.stringify({ command: server[0], args: server.slice(1) })}\n`);
375
+ }
376
+ }
377
+ console.log(`Connected to ${got.studio} (${got.slug}).`);
378
+ console.log(added.length ? `House Engine tools added to ${added.join(', ')}. Restart it, then ask: "Make me a coin flip game and put it in the sandbox."`
379
+ : `No agent found. Add this MCP server to yours: ${server.join(' ')}`);
380
+ }
381
+
382
+ async function storeCmd() {
383
+ const p = project();
384
+ login() || fail('hbengine store sends to the studio platform: hbengine login first');
385
+ const { base, auth } = games(p);
386
+ const f = join(dir, 'store.json');
387
+ if (!existsSync(f)) fail(`${f}: {title, category, description, features, tags, languages, rules{en}}`);
388
+ const r = await fetch(`${base}/${p.slug}/store`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...UA, ...auth }, body: readFileSync(f) });
389
+ const j = await r.json().catch(() => ({}));
390
+ if (r.status === 404) fail(`${p.slug} is not on the studio platform yet: hbengine upload creates it`);
391
+ if (!r.ok) fail(j.error || `HTTP ${r.status}`);
392
+ const tile = ['tile.png', 'tile.webp'].map((t) => join(dir, t)).find(existsSync);
393
+ let kept = ''; // why the tile stayed as it was
394
+ if (tile) {
395
+ const t = await fetch(`${base}/${p.slug}/tile`, { method: 'PUT', headers: { ...UA, ...auth }, body: readFileSync(tile) });
396
+ const why = t.ok ? '' : (await t.json().catch(() => ({}))).error || `HTTP ${t.status}`;
397
+ // 409: the game is live and its tile is the one review saw (until Housebets reset it).
398
+ // The page is saved; the tile staying is said, not an error, or every store of a live game fails.
399
+ if (t.status === 409) kept = why;
400
+ else if (!t.ok) fail('tile: ' + why);
401
+ }
402
+ const missing = tile ? (j.missing || []).filter((m) => m !== 'a tile') : j.missing || [];
403
+ console.log(`store page saved${tile && !kept ? ' with the tile' : ''}.${kept ? ' Tile unchanged: ' + kept : ''}${missing.length ? ' Before review it needs: ' + missing.join(', ') + '.' : ' Ready for review: hbengine submit.'}`);
404
+ }
405
+
406
+ async function submit() {
407
+ const p = project();
408
+ login() || fail('hbengine submit sends to the studio platform: hbengine login first');
409
+ const { base, auth } = games(p);
410
+ const g = await get(`${base}/${p.slug}`, auth);
411
+ const v = [...(g.uploads || [])].reverse().find((u) => u.state === 'sandbox') || fail('no version has passed every check yet: hbengine upload');
412
+ const r = await post(`${base}/${p.slug}/uploads/${v.n}/submit`, '{}', 'application/json', auth);
413
+ console.log(`version ${v.n} is in review. ${r.dashboard}\nHousebets emails you the verdict; once it is approved, hbengine live puts it live.`);
414
+ }
415
+
416
+ // The newest approved version, live: the round server and the front end switch to it within
417
+ // seconds. Going live is an owner's, so the key must be one an owner made.
418
+ async function live() {
419
+ const p = project();
420
+ login() || fail('hbengine live talks to the studio platform: hbengine connect first');
421
+ const { base, auth } = games(p);
422
+ const g = await get(`${base}/${p.slug}`, auth);
423
+ const ups = g.uploads || [];
424
+ const v = [...ups].reverse().find((u) => u.review === 'approved');
425
+ if (!v) {
426
+ const waiting = [...ups].reverse().find((u) => u.review === 'pending');
427
+ fail(waiting ? `v${waiting.n} is still in review: Housebets emails you when it is approved, then hbengine live` : 'no version is approved yet: hbengine submit sends the newest that passed every check to review');
428
+ }
429
+ if (g.liveVersion && v.n <= g.liveVersion) return console.log(`v${g.liveVersion} is live; no newer version is approved.`);
430
+ const r = await post(`${base}/${p.slug}/uploads/${v.n}/live`, '{}', 'application/json', auth);
431
+ console.log(`${p.slug} v${r.version} is live. ${r.dashboard}` + (g.liveVersion ? '' : '\nPlayers find it on Housebets once Housebets switches its catalogue page on; you get an email when it is.'));
432
+ }
433
+
434
+ // The review, as Housebets left it: threads (must-fix first), what each asks, the replies so far.
435
+ async function review() {
436
+ const p = project();
437
+ login() || fail('hbengine review reads the studio platform: hbengine login first');
438
+ const { base, auth } = games(p);
439
+ const r = await get(`${base}/${p.slug}/review`, auth);
440
+ const ts = r.threads || [];
441
+ const rank = (t) => (t.severity === 'must' && t.state === 'open' ? 0 : t.state === 'open' ? 1 : t.state === 'fixed' ? 2 : 3);
442
+ console.log(`${p.slug} review: ${r.mustFixOpen} must-fix not resolved (${r.markedFixed} marked fixed), ${r.suggestionsOpen} suggestions open\n ${r.dashboard}`);
443
+ if (!ts.length) console.log(' no threads');
444
+ for (const t of [...ts].sort((a, b) => rank(a) - rank(b))) {
445
+ console.log(`\n#${t.id} ${t.severity === 'must' ? 'MUST FIX' : 'suggestion'} ${t.area} raised on v${t.version} ${t.state}${t.fixedIn ? ' (fixed in v' + t.fixedIn + ')' : ''}`);
446
+ for (const c of t.comments || []) console.log(` ${c.from === 'housebets' ? 'Housebets' : 'Studio'} (${c.name}): ${c.body.replace(/\n/g, '\n ')}`);
447
+ }
448
+ const open = ts.filter((t) => t.severity === 'must' && t.state === 'open');
449
+ if (open.length) console.log(`\nFix each open must-fix thread in the project, then hbengine upload and hbengine submit: the submitted version is marked as fixing them, and Housebets checks each one. To ask the reviewer something: hbengine reply --thread <id> --message "…".`);
450
+ const failed = (r.checklist || []).filter((c) => c.result === 'fail');
451
+ if (failed.length) console.log(`\nChecklist items that failed on v${r.checklistVersion}:\n` + failed.map((c) => ` ${c.area}: ${c.check}`).join('\n'));
452
+ }
453
+
454
+ // What a reviewer would send back that a machine can see already: run it before hbengine submit.
455
+ async function preflight() {
456
+ const p = project();
457
+ login() || fail('hbengine preflight reads the studio platform: hbengine login first');
458
+ const { base, auth } = games(p);
459
+ const r = await get(`${base}/${p.slug}/preflight`, auth);
460
+ console.log(`${p.slug} v${r.version} pre-flight: ${r.passed} passed, ${r.failed} failed`);
461
+ for (const i of r.items) {
462
+ const mark = { pass: 'ok ', fail: 'FAIL', wait: '... ' }[i.state] || ' ';
463
+ console.log(` ${mark} ${i.area.padEnd(10)} ${i.state === 'reviewer' ? i.check + ' (the reviewer checks)' : i.detail}`);
464
+ }
465
+ if (r.rulesFacts) console.log(`\nThe rules must quote (from the verified maths):\n ${r.rulesFacts.replace(/\n/g, '\n ')}`);
466
+ if (r.failed) process.exitCode = 1;
467
+ }
468
+
469
+ async function reply() {
470
+ const p = project();
471
+ login() || fail('hbengine reply writes to the studio platform: hbengine login first');
472
+ const thread = opt('thread') || fail('--thread <id>: the number hbengine review shows');
473
+ const message = opt('message', '');
474
+ const fixed = flag('fixed');
475
+ if (!message && !fixed) fail('--message "what you changed", and --fixed when the project has the change');
476
+ // The platform refuses more; said here with the count, before anything is sent.
477
+ if (message.length > 4000) fail(`the reply is over 4000 characters (${message.length}): say less, or split it over two replies`);
478
+ const { base, auth } = games(p);
479
+ await post(`${base}/${p.slug}/review/threads/${encodeURIComponent(thread)}`, JSON.stringify({ body: message, fixed }), 'application/json', auth);
480
+ console.log(`replied on #${thread}${fixed ? ', marked fixed: upload the version that has it, then hbengine submit' : ''}.`);
481
+ }
482
+
483
+ // Feedback and support: a ticket to Housebets (a bug, a question, an idea, what you think), or a
484
+ // message on one. What goes with it: this hbengine's version, node, the OS and the project's game,
485
+ // never a key (the platform removes any it finds in the message too).
486
+ const TICKET_MAX = 8000;
487
+ const KINDS = ['bug', 'question', 'idea', 'feedback'];
488
+ const support = () => {
489
+ const l = login() || fail('hbengine feedback talks to the studio platform: hbengine connect first');
490
+ return { base: l.portal.replace(/\/$/, '') + '/engine/portal/v1/support', auth: { Authorization: 'Bearer ' + l.key } };
491
+ };
492
+
493
+ async function feedback() {
494
+ const { base, auth } = support();
495
+ const message = opt('message', '');
496
+ const ticket = opt('ticket');
497
+ const close = flag('close');
498
+ if (!message && !(ticket && close)) fail('--message "what you did, what happened, what you expected, the exact error"');
499
+ if (message.length > TICKET_MAX) fail(`the message is over ${TICKET_MAX} characters (${message.length}): put the essentials first, or send the rest with --ticket <id>`);
500
+ if (ticket) {
501
+ if (!/^\d+$/.test(ticket)) fail('--ticket <id>: the number hbengine tickets shows');
502
+ const r = await post(`${base}/${ticket}`, JSON.stringify({ body: message, close }), 'application/json', auth);
503
+ return console.log(`ticket #${ticket}: ${r.state === 'closed' ? 'closed' : 'sent; Housebets answers by email and in hbengine tickets'}.`);
504
+ }
505
+ const kind = opt('kind', 'feedback');
506
+ if (!KINDS.includes(kind)) fail(`--kind: one of ${KINDS.join(', ')}`);
507
+ // The game, when the directory is a project.
508
+ let game = '';
509
+ try {
510
+ const s = JSON.parse(readFileSync(join(dir, 'hbengine.json'), 'utf8')).slug;
511
+ if (typeof s === 'string' && SLUG.test(s)) game = s;
512
+ } catch { /* not a project: a ticket about no game */ }
513
+ const context = { hbengine: VERSION, node: process.version, os: `${process.platform} ${process.arch}`, ...(game ? { game } : {}) };
514
+ const r = await post(base, JSON.stringify({ kind, subject: opt('subject', ''), body: message, game, source: process.env.HBENGINE_VIA === 'mcp' ? 'mcp' : 'cli', context }), 'application/json', auth);
515
+ console.log(`ticket #${r.id} sent to Housebets${game ? ` (about ${game})` : ''}. The answer comes by email and in hbengine tickets.\n ${r.url}`);
516
+ }
517
+
518
+ async function tickets() {
519
+ const { base, auth } = support();
520
+ const r = await get(base, auth);
521
+ const ts = r.tickets || [];
522
+ if (!ts.length) return console.log('no tickets. hbengine feedback --message "…" opens one.');
523
+ const word = { open: 'waiting for Housebets', answered: 'ANSWERED', closed: 'closed' };
524
+ for (const t of ts) {
525
+ console.log(`\n#${t.id} ${t.kind} ${word[t.state] || t.state}${t.game ? ' ' + t.game : ''} ${t.subject}`);
526
+ for (const m of t.messages || []) console.log(` ${m.from === 'housebets' ? 'Housebets' : 'Studio'} (${m.name}): ${m.body.replace(/\n/g, '\n ')}`);
527
+ }
528
+ console.log(`\nTo answer: hbengine feedback --ticket <id> --message "…" (--close when it is solved).\n${r.dashboard}`);
529
+ }
530
+
531
+ // The upload's conformance run, offline against engine-dev (needs Chrome
532
+ // and playwright-core: npm install playwright-core).
533
+ async function test() {
534
+ const { slug, rgs } = project();
535
+ const box = await sandbox(rgs);
536
+ if (!box.offline) fail(`${rgs} is not engine-dev: with a sandbox, hbengine upload runs these checks`);
537
+ process.on('exit', () => box.child?.kill());
538
+ const r = spawnSync('node', [join(PKG, 'engine-conformance', 'run.mjs'), join(dir, 'bundle'), '--slug', slug, '--game-id', '0', '--rgs', rgs], { stdio: 'inherit' });
539
+ process.exit(r.status ?? 1);
540
+ }
541
+
542
+ // One file to hand over, after the same checks an upload makes: exactly
543
+ // what an upload accepts (math/ and bundle/); the sha256 lets the receiver
544
+ // check it. The store page travels on its own (hbengine store, or by hand).
545
+ async function pack() {
546
+ const p = project();
547
+ await verify();
548
+ lint();
549
+ mkdirSync(join(dir, 'dist'), { recursive: true });
550
+ const out = join(dir, 'dist', `${p.slug}-${new Date().toISOString().slice(0, 10)}.tgz`);
551
+ execFileSync('tar', ['-czf', out, '-C', dir, 'math', 'bundle'], { env: TAR_ENV });
552
+ const sum = createHash('sha256').update(readFileSync(out)).digest('hex');
553
+ const store = ['store.json', 'tile.png', 'tile.webp'].filter((f) => existsSync(join(dir, f)));
554
+ console.log(`${out}\nsha256 ${sum}\nsend both; the receiver uploads the .tgz in the dashboard (the game's page, New version).` + (store.length ? `\nthe store page is not in it: send ${store.join(' and ')} too.` : ''));
555
+ }
556
+
557
+ function templates() {
558
+ for (const [t, about] of Object.entries(TEMPLATES)) console.log(`${t.padEnd(7)} ${about}`);
559
+ }
560
+
561
+ const mcp = async () => (await import('../engine-mcp/server.mjs')).serve();
562
+ const version = () => console.log(VERSION);
563
+ const commands = { init, verify, lint, dev, test, pack, upload, status, stats, store: storeCmd, submit, live, preflight, review, reply, feedback, tickets, login: loginCmd, connect, mcp, templates, version };
564
+ if (!commands[cmd]) fail('usage: hbengine init|verify|lint|dev|test|pack|upload|status|stats|store|submit|live|preflight|review|reply|feedback|tickets|connect|login|mcp|templates|version [dir]');
565
+ await commands[cmd]();
@@ -0,0 +1 @@
1
+ { "type": "module" }
@@ -0,0 +1,14 @@
1
+ // What `hbengine init` can start from (packages/engine-examples), and what each is.
2
+ export const TEMPLATES = {
3
+ coin: 'one bet, two outcomes: the smallest complete game',
4
+ dice: 'a target per mode, exact RTP on each',
5
+ slot: 'three reels, every stop combination in the table',
6
+ lines: 'five reels, paylines, scatters and free spins, built by the maths kit',
7
+ tumble: 'a 6 × 5 scatter-pays grid with tumbles, free spins with multipliers (the maths kit)',
8
+ plinko: 'rows and risk levels, each bucket weighted by the binomial',
9
+ keno: '40 numbers, 10 drawn, 1–10 picks; exact hypergeometric weights',
10
+ crash: 'a ladder that climbs by itself: cash out before it busts, 99% at every level',
11
+ mines: 'a 5 × 5 grid, 1–24 mines: reveal tiles, cash out; exact weights per reveal',
12
+ tower: 'a ladder: climb, cash out, or lose the stake',
13
+ wheel: 'weighted segments, risk levels as modes',
14
+ };
@@ -0,0 +1,12 @@
1
+ // The code analysis of every script in a bundle, for scan.mjs: parsed and
2
+ // walked on a worker thread (bundle.mjs does the work), so a file built to
3
+ // keep a parser busy never holds the event loop of the process that asked (the
4
+ // pipeline serves an API while it scans), and scan.mjs can stop the thread at
5
+ // its deadline. One message back: the findings per file, with what scan.mjs
6
+ // decides at bundle level (minified files against their source maps,
7
+ // WebAssembly against its sources). scan.mjs says what each file is (its
8
+ // `kind`: 'xml', 'html', 'data' or 'script'); the bundle is read as one.
9
+ import { parentPort, workerData } from 'node:worker_threads';
10
+ import { analyseBundle } from './bundle.mjs';
11
+
12
+ parentPort.postMessage(analyseBundle(workerData.files));