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,855 @@
1
+ # Build a House Engine game
2
+
3
+ A House Engine game is two things: a **math pack** (what can happen and
4
+ what it pays, as data) and a **bundle** (static files that draw it). The
5
+ engine draws every outcome on our server with our provably-fair stream,
6
+ moves the money on the player's Housebets wallet, and your bundle shows what
7
+ the server decided. You never compute money, randomness or balances.
8
+
9
+ ```
10
+ my-game/
11
+ hbengine.json {slug, portal, rgs}
12
+ math/manifest.json modes, bets, payouts (§2)
13
+ math/<table>.tsv weighted outcomes, one per line
14
+ bundle/index.html your game (§3)
15
+ bundle/assets.json what loads first (§3.2)
16
+ bundle/hbtest.json how the pipeline plays one bet (§3.3)
17
+ ```
18
+
19
+ ```
20
+ npx hbengine templates # coin dice slot lines tumble keno crash mines plinko tower wheel
21
+ npx hbengine init my-game --template lines --slug my-game
22
+ cd my-game
23
+ npx hbengine connect # a key from House Studio, one click (or hbengine login <portal> with a key)
24
+ npx hbengine verify # the engine's own verifier: exact RTP per bet, the mathHash
25
+ npx hbengine dev # play your bundle inside the real shell, against the sandbox (after a first upload)
26
+ npx hbengine upload # verify + lint + upload; the sandbox plays it; the dashboard has the checks
27
+ npx hbengine store # store.json (+ tile.png) → the store page
28
+ npx hbengine submit # the newest version that passed, to review
29
+ npx hbengine stats # rounds, GGR and RTP by coin once it is played
30
+ ```
31
+
32
+ The dashboard (sign up at the same address) does the rest: upload by
33
+ dragging the folder in, the store page and tile, the sandbox in any coin
34
+ and language with a forced first round, submit for review, go live,
35
+ reports, statements and payouts.
36
+
37
+ `hbengine connect` opens the approve page in your browser; an owner or
38
+ developer approves there, and the terminal shows which studio did and asks
39
+ `Connect to <studio> (<slug>)? [y/N]` before it keeps the key (a no revokes
40
+ it; so does closing the terminal at the question). An agent, or a script with
41
+ no terminal, passes `--studio <slug>` instead (Organisation → Profile shows
42
+ the slug): the portal then lets only that studio approve; without it and
43
+ without a terminal, `hbengine connect` stops before asking for a code. A
44
+ gateway hiccup while waiting is retried a few times.
45
+
46
+ With an agent: `npx hbengine connect` adds the MCP server (`npx hbengine mcp`)
47
+ to Claude Code, Cursor and Codex, and the same steps are tools (`engine_guide`,
48
+ `engine_templates`, `engine_init`, `engine_verify`, `engine_lint`,
49
+ `engine_upload`, `engine_store`, `engine_submit`, `engine_live`,
50
+ `engine_status`, `engine_stats`): an agent takes a game from a template to
51
+ review and, once Housebets approves it, live.
52
+
53
+ Keep hbengine current: after any command it says when npm has a newer
54
+ version (restart your agent to take it, or `npm i -g hbengine@latest` if you
55
+ installed it; `hbengine version` says which you have), and the dashboard shows
56
+ a notice while your studio's tools are behind.
57
+
58
+ Stuck on something of House Engine itself (a bug in the engine, the SDK or
59
+ these tools, a question this guide does not answer), or have an idea? Tell
60
+ us: `hbengine feedback --kind bug --message "…"`, the dashboard's Support
61
+ page, or ask your agent to send Housebets feedback (`engine_feedback`). Say
62
+ what you did, what happened and the exact error; hbengine adds its version,
63
+ node, the OS and the game. We answer by email and in `hbengine tickets`
64
+ (`engine_tickets`).
65
+
66
+ ## 1. How it runs
67
+
68
+ ```
69
+ housebets.com page ─ frame ─▶ our shell (same origin as the site)
70
+ · mints the player's session, talks to the engine
71
+ · header balance, round lifecycle, fairness dialog
72
+ └─ frame ─▶ your bundle, on its own origin
73
+ sandboxed: no network but its own files,
74
+ talks only to the shell (postMessage)
75
+ ```
76
+
77
+ Your page imports the SDK from its own origin — the platform serves it
78
+ there, your bundle must not carry a copy:
79
+
80
+ ```html
81
+ <script type="module" src="/engine-sdk/bet-field.js"></script>
82
+ <script type="module" src="./game.js"></script>
83
+ ```
84
+
85
+ Two ways to build the frontend; both ship through the same pipeline:
86
+
87
+ - **The engine panel** (`/engine-sdk/ui.js`): you draw only the game; the
88
+ Originals' panel (stake in the player's currency, Bet, Auto with the
89
+ Originals' rules — count, on win / on loss reset or increase by a
90
+ percent, stop on profit / loss — mode and bet pickers from your math,
91
+ Next / Cash out on a ladder, refusals in the player's language) is ours,
92
+ and its Auto runs as a shell autoplay session (§3.1).
93
+ `engine-examples/coin` is 30 lines.
94
+ - **Your own frontend**: any static web tech (plain, Pixi, Phaser, your
95
+ framework's build output). You use the SDK's `connect()` and show the
96
+ rounds it returns, and the money fields (`<hb-bet-amount>`) so currencies
97
+ are right. BAD CAT (four games, a lobby, skins, music) is the reference.
98
+ Build it as one page with views inside it, not as pages that frame other
99
+ pages of the bundle (the upload's scan names such pages, `nested-pages`).
100
+
101
+ ## 2. The math pack
102
+
103
+ `math/manifest.json`:
104
+
105
+ ```json
106
+ { "format": "house-engine-math/1", "game": "coin", "rtpBp": 9900,
107
+ "modes": [ { "id": "coin", "label": "Coin", "kind": "table", "table": "coin.tsv",
108
+ "bets": [ { "id": "heads", "label": "Heads", "tiers": [ { "outcomes": [[0, 0]], "multBp": 19800 } ] },
109
+ { "id": "tails", "label": "Tails", "tiers": [ { "outcomes": [[1, 1]], "multBp": 19800 } ] } ] } ] }
110
+ ```
111
+
112
+ `math/coin.tsv` — one outcome per line, `weight<TAB>json`; the JSON is
113
+ what your frontend gets back as `round.data[i]` (the dice faces, the reel
114
+ stops, the trap tile), the server never reads it:
115
+
116
+ ```
117
+ 1 {"side":"heads"}
118
+ 1 {"side":"tails"}
119
+ ```
120
+
121
+ - **table** mode: one draw settles the bet. Each bet names payout tiers
122
+ over outcome index ranges; `multBp` is basis points of the stake,
123
+ stake included (19800 = 1.98×). A dice with 97 targets is 97 bets on one
124
+ 100-row table; a 3-reel slot with 48-stop reels is a 110,592-row table whose tiers group the
125
+ paying combinations. **100,000 different rounds in every mode is
126
+ recommended for a slot** (each bonus buy too): under it pre-flight warns
127
+ and the reviewer decides (fine for a classic 3-reel, not for a video
128
+ slot). Rows count once when another has the same data and pays the same;
129
+ the verifier reports `rounds` per mode. Originals have no recommendation.
130
+ - **ladder** mode (`"kind": "ladder"`): every step is drawn at bet from its
131
+ table; `bets[].survive` are the rows that let the player go on;
132
+ `ladderBp[k-1]` is the cumulative payout after `k` survived steps;
133
+ `cashout: true` lets the player stop after any step; `autoAdvance: true`
134
+ plays all steps at once (all or nothing). Tower, chicken road, mines-like
135
+ games are ladders.
136
+ - Slug `^[a-z0-9][a-z0-9-]{1,23}$`; ids `^[a-z0-9][a-z0-9-]{0,31}$`;
137
+ weights are integers, Σ ≤ 2⁶³, ≤ 1,000,000 rows, row JSON ≤ 4 KB;
138
+ ≤ 64 steps. Unknown fields are refused.
139
+ - **RTP** is computed exactly (integer arithmetic) for every bet:
140
+ `9000 ≤ RTP ≤ rtpBp ≤ 9900`, every bet must be able to pay. A ladder's
141
+ RTP is its best stop. An outcome no bet pays on (a push, a tie, a zero
142
+ segment) is fine — the verifier only names it. `hbengine verify` prints RTP, hit rate, max
143
+ multiplier and (ladders) the RTP at every stop; what it accepts the upload
144
+ accepts, because it is the same code (`POST /engine/v1/verify`).
145
+ - A ladder round's **outcome** is `round.state`: `open`, `cashed_out` or
146
+ `busted` (`settled` for a table). `round.busted` and `round.survived`
147
+ describe the drawn path and arrive when the round closes — after a
148
+ cash-out too, when the path would have busted later. Show the player's
149
+ result from `round.state` and `round.step`.
150
+ - The pack's **mathHash** names the version. A session is pinned to one
151
+ hash; a new upload is a new version; players on an open ladder finish on
152
+ the version they started.
153
+
154
+ ### 2.1 The maths kit — slots, plinko, anything with features
155
+
156
+ `packages/engine-math` (copied into a project as `math-kit/` by the
157
+ `lines`, `tumble` and `plinko` templates) writes the pack from rows you
158
+ describe — `slot.mjs` for reels and lines, `tumble.mjs` for scatter pays
159
+ with tumbles:
160
+
161
+ ```js
162
+ import { fit, rng, writePack } from '../math-kit/pack.mjs';
163
+ import { books, slot } from '../math-kit/slot.mjs';
164
+ const game = slot({ reels, rows: 3, lines, pays, wild: 'W', scatter: { symbol: 'S', pays, spins, retrigger, multiplier: 2, cap: 60 }, maxWinBp });
165
+ const rows = fit(books(game, rng(SEED), 400_000), 9600); // every row a whole round, RTP exactly 96.00%
166
+ writePack('math', { game: 'my-slot', rtpBp: 9600, modes: [{ id: 'base', rows, bets: ['spin'] }] });
167
+ ```
168
+
169
+ - A **row is a whole round** (a "book"): the base spin and every free spin
170
+ it starts, with what it pays in total. Features, cascades, multipliers,
171
+ pick bonuses — anything you can play in code — become rows the same way;
172
+ the page plays back what the row holds.
173
+ - **`fit(rows, targetBp)`** makes the RTP exactly the target: the paying
174
+ rows keep their proportions, the losing rows share the weight left.
175
+ Integer arithmetic throughout; the engine's verifier then agrees to the
176
+ fourth decimal.
177
+ - **`balance(rows, targetBp)`** does the same for a table where every row
178
+ pays (a bonus buy, plinko): the rows above and below the target are each
179
+ scaled by one integer. The `lines` template's **bonus buy** mode is the
180
+ free spins alone, priced at what they return on average (27 spins),
181
+ balanced to exactly 96%.
182
+ - **`writePack`** sorts rows so each pay tier is one range, checks the
183
+ limits (rows, Σ weights, row size) and prints RTP, hit rate and max win.
184
+ - Tune the model until RTP *before* the fit is near the target (the gen
185
+ prints it): the fit should nudge, not reshape, the hit rate.
186
+ - Exact games (plinko, wheels, dice) need no fit: weights are exact
187
+ (`binomial(n, k)`), and you solve one multiplier for the RTP — round it
188
+ to what the page shows, so the board shows exactly what pays.
189
+ - Write the model the page needs (reels, lines, pays) to `bundle/` from
190
+ the same script, so the page and the table can never disagree.
191
+
192
+ The studio dashboard shows every bet's RTP, hit rate, largest win and
193
+ volatility (σ of one round's multiplier: low < 2, medium < 5, high < 15,
194
+ very high above) from the verified pack; players see the same on the
195
+ game's page.
196
+
197
+ ## 3. The bundle
198
+
199
+ `bundle/index.html` at the root, everything relative. Budgets: boot group
200
+ ≤ 2 MB gzip (its JavaScript ≤ 250 KB gzip), any other group ≤ 25 MB gzip,
201
+ ≤ 300 MB and ≤ 2,500 files in all, one file ≤ 8 MB (code ≤ 4 MB), plain
202
+ files only.
203
+
204
+ ### 3.1 The SDK
205
+
206
+ ```js
207
+ import { connect } from '/engine-sdk/sdk.js';
208
+ const engine = await connect(); // waits for the shell's init
209
+ engine.config // game, mathHash, env, currency, limits, modes, balance, locale, wallet, state, activeRound?
210
+ const { round, balance } = await engine.play({ mode: 'coin', bet: 'heads', amount: field.amount });
211
+ round.data[0] // the row JSON you wrote in the table
212
+ engine.revealed(round.id); // the outcome is on screen: the header balance moves now
213
+ // round: { id, mode, bet, kind, amount (wallet units), state 'open'|'settled'|'cashed_out'|'busted',
214
+ // step, payout, multBp, data: [row JSON per draw], draws: [{index, raw, rejected}], nonce, mathHash }
215
+ // engine.config.limits: { min, max, maxWin, maxStake: { <mode>: { <bet>: "12.50" } } } in the wallet coin
216
+ // (may be empty in the sandbox); maxStake is the most each bet takes, §4.1
217
+ // a ladder draws every step at the bet; while it is open you see only the steps played,
218
+ // once it ends (bust, cash-out, autoAdvance) `data` has every step — show up to the first miss
219
+ // a push (a tier paying 1.00×) is `payout === amount`; a loss is `payout === 0`
220
+ await engine.action(round, 'advance' | 'cashout'); // ladders; pass the round as you last had it; both from the player's click
221
+ engine.on('balance' | 'control' | 'pause' | 'resume' | 'ended' | 'autoplay', fn)
222
+ // pause/resume: the shell's reality check (engine.config.rg.realityCheckSec); ended: the session is over
223
+ // engine.config.rg: { autoplay: { allowed }, realityCheckSec } — the jurisdiction's rules; the panel obeys them
224
+ // autoplay { active, left, played, stake?, reason? }: the shell's session changed (started, a play, or ended)
225
+ await engine.autoplay.start({ mode, bet, rounds, stake, stopOnProfit?, stopOnLoss? }) // the player's grant for one bet: the shell shows the terms, they press Start
226
+ engine.autoplay.stop() // end it; the shell also shows its own Stop over your game
227
+ engine.state.get() / engine.state.put(revision, data) // the player's own saved data, ≤ 256 KB, any device
228
+ // both resolve { revision, data }; put with the revision you read
229
+ // (0 the first time); a stale revision rejects with `conflict`
230
+ // save when the player changes something, not per frame: over 10 writes
231
+ // at once and then one a second rejects with `pacing` (retryAfterMs)
232
+ // keep a `version` field in it: a new release of your game reads old data
233
+ engine.money // §4
234
+ engine.openFairness(round.id); engine.reportError(msg);
235
+ ```
236
+
237
+ One `play`/`action` at a time; a second is refused with `busy`. Every
238
+ refusal is an `EngineError` with a `code` (`invalid busy
239
+ insufficient-balance limits pacing pending unavailable session-ended round
240
+ conflict server needs-click autoplay`), a message to show, and for `round`
241
+ the round as it now stands (redraw it). `pending` means the wallet has not
242
+ answered: the shell and the server settle it; show the message, do not retry
243
+ on your own.
244
+
245
+ **Nothing happens without the player's click.** A `play` and every ladder
246
+ `action` — an `advance` and a `cashout` alike — are taken only when the
247
+ player has just clicked or pressed a key in your page — the shell reads the
248
+ real browser activation a click gives it, which lasts a few seconds, and
249
+ checks that the focus is inside your frame and got there by the player's own
250
+ act (a click in your page puts it there; a click elsewhere on the site page
251
+ does not, and a `focus()` your script calls is undone and buys nothing) — or
252
+ inside an autoplay session the player started (its plays, and the steps and
253
+ cash-out of the ladder it opened). Call them from the handler of the
254
+ player's gesture, not from a timer, a load, a date or a round count, or the
255
+ shell refuses them with `needs-click` ("A bet needs your click", in the
256
+ player's language). An auto cash-out target is such a timer: the shell
257
+ refuses the cash-out the moment the click lapses, so put a target in your
258
+ maths (its own stop) rather than in your page; a round the player leaves open
259
+ stays open for them to come back to (the server resumes it with the session),
260
+ and is never your page's to close. One click
261
+ buys one bet: a second `play` within 600 ms, or a sixth within 5 s, is
262
+ refused with `pacing` and `retryAfterMs`; one tap buys at most one `advance`
263
+ per 100 ms and 25 per 5 s, refused the same way; a cash-out is not paced. For
264
+ 5 s after the player pressed one of the shell's own controls (Stop on the
265
+ autoplay pill, Cancel on its confirmation, Continue on the reality check, a
266
+ key in the Provably Fair dialog) — or, in production, anything on the site's
267
+ page around your game (the chat, the header: the shell is a frame of that
268
+ page and a press there arms it as a click in your page does) — the shell
269
+ takes no bet on activation alone either, so a bet pressed within those 5 s
270
+ is refused with `needs-click` — every one of them, until 5 s have passed
271
+ since the last press outside your frame; show the message and let the player
272
+ press again. The same 5 s follow the player tabbing out of your page by
273
+ keyboard: a click back into it within them is refused `needs-click` and the
274
+ focus is put back outside (the shell cannot tell that click from a
275
+ `focus()` your script might call on its own `blur`, so neither gets in); a
276
+ Tab back into your page is let in, and so is a click 5 s after they left.
277
+ The read-only `round` and `state.get`, and `state.put` (a save
278
+ moves no money: save at boot or after a reveal as you always did) need no
279
+ click. A browser without `navigator.userActivation` cannot confirm a click,
280
+ so the shell bets nothing there and the refusal names the browsers that can
281
+ (current Chrome, Edge, Safari and Firefox all have it). The upload's
282
+ conformance run presses your Bet with a real click, so a game that bets from
283
+ the click handler passes as before.
284
+
285
+ A ladder your page climbs itself (a crash) advances on the Bet click's
286
+ activation for those few seconds and within the tap's run of steps, then the
287
+ shell refuses the next step: on `pacing` wait `retryAfterMs` and climb on;
288
+ on anything else (`null` from `game.advance()` — read `game.refusal` for why
289
+ — or `needs-click` from `engine.action`) stop climbing, keep the round and
290
+ Cash out on screen, and climb on at the player's next tap. Take that tap on
291
+ `click` (or `pointerup`), not `pointerdown`: a touch grants the shell its
292
+ activation only when the finger lifts, so a step sent from `pointerdown`
293
+ can be refused again on a phone. Cash out is the player's press, never the
294
+ page's (no auto cash-out on a timer). `engine-examples/crash` does exactly
295
+ this. A tile or a Next button pressed per step (mines, tower) is a click per
296
+ step and needs nothing more.
297
+
298
+ **Autoplay is the player's grant through the shell:**
299
+
300
+ ```js
301
+ const { session } = await engine.autoplay.start({ mode: game.mode, bet: game.bet, rounds: 50, stake: field.amount, stopOnProfit: 20, stopOnLoss: 10 });
302
+ // Call it from the player's click on your Auto button (a start with no click behind it is refused
303
+ // `needs-click`, like a bet). The run is for one bet (`mode`, `bet`: the player approves that bet, and
304
+ // a play on another is refused `autoplay`); rounds ≤ 1000; no play in the session may stake more than
305
+ // `stake`; the stops are optional, in wallet units, on the session's net result (a loss limit is
306
+ // always applied: yours, or 100× the stake, and never above the total the run could stake). The shell
307
+ // shows the player the terms — the bet, capped at the coin's max bet, the bet's own ceiling
308
+ // (engine.config.limits.maxStake[mode][bet], §4.1: the request names the bet, so the shell applies it;
309
+ // a bet whose figure is 0 takes nothing right now, and the start rejects `unavailable`) and the
310
+ // balance it last saw — in its own dialog over your frame and resolves this only when they press
311
+ // Start there (its Start is dead for the dialog's first second); rejects with `autoplay` on Cancel,
312
+ // after 30 s unanswered, on the reality check, when the player's rules forbid autoplay
313
+ // (engine.config.rg.autoplay.allowed), while one is already running, and (with `retryAfterMs`;
314
+ // "Autoplay was just closed. You can ask again in a moment.", in the player's language) for 10 s
315
+ // after a Cancel — a cancelled request cannot be repeated at once, so do not retry it from your
316
+ // rejection handler; with `busy` while paused. `session` carries the terms as granted: keep your run
317
+ // to them.
318
+ engine.on('autoplay', ({ active, left, played, reason }) => …); // on start, after each play, at the end
319
+ await engine.autoplay.stop(); // always taken
320
+ ```
321
+
322
+ The shell counts the rounds, refuses a play on another bet, above the stake
323
+ or past the count, ends the session at the count (once a ladder round it
324
+ opened has closed; the stops wait for it too), at a stop, on any refused play, on `stop()`, on the reality check,
325
+ after 30 s without a play of the session (`idle`: play the granted rounds
326
+ now, not later), when the page is hidden (`hidden`: another tab, the phone
327
+ locked) and when the player presses **Stop** on the shell's own control — a
328
+ small pill over your frame showing the rounds left, which your page cannot
329
+ cover or press — and tells you of every change through the `autoplay` event
330
+ (`reason`: `stopped done profit loss refused paused ended idle hidden`).
331
+ Plays you send while a session is live need no further click. Build no
332
+ autoplay engine of your own that bets without the player: a click carries
333
+ only the first few seconds, so the shell refuses such a loop's bets with
334
+ `needs-click` once the player's activation lapses, and with `pacing` beyond
335
+ five plays in 5 s — a three-spin loop passing your test proves nothing. The
336
+ engine panel's Auto (`ui.js`) already runs through the shell; a custom front
337
+ end starts the session from its own Auto button (the player then confirms in
338
+ the shell's dialog) and calls `stop()` from its own Stop. A bundle already
339
+ live with an autoplay loop of its own (Kraken's autopick run, BAD CAT's
340
+ auto-bets) is cut off the same way from the day this shell deploys — its
341
+ Auto plays a few rounds after the click and then stops with `needs-click` —
342
+ until it is republished on `engine.autoplay`; the shell goes first, by
343
+ decision, because a game betting on its own is exactly what it exists to
344
+ stop.
345
+
346
+ `engine.reportError(msg)`, and an uncaught error or unhandled promise
347
+ rejection in your page (the SDK listens for both on `window`), is logged in
348
+ the player's console and counted on the engine as one error for the game's
349
+ day; the shell also counts a launch per open and a round left open when the
350
+ page closes on one. Your Reports page and `hbengine stats` show the counts
351
+ per day — counts only, never the text: nothing a game sends reaches the
352
+ studio as text.
353
+
354
+ With the engine panel instead:
355
+
356
+ ```js
357
+ import { mount } from '/engine-sdk/ui.js';
358
+ const game = await mount(document.querySelector('hb-game'), {
359
+ reveal: async (round, zone) => { /* animate round.data[0]; resolve when on screen */ },
360
+ step: async (round, zone) => { /* a ladder round moved */ },
361
+ });
362
+ game.mode = 'hard'; game.bet = 'over-50'; // when your zone picks (a mode with > 12 bets)
363
+ el.addEventListener('select', (e) => draw(e.detail.mode)); // the panel's pick reached your zone
364
+ // engine.config.modes carries ids, labels, kinds and each bet's maxMultBp — never the
365
+ // tables: what your zone draws (a wheel's segments, a paytable) is a constant of yours.
366
+ ```
367
+
368
+ ### 3.2 Loading in stages — `assets.json`
369
+
370
+ ```json
371
+ { "groups": { "boot": { "files": ["index.html", "game.js", "game.css"] },
372
+ "music": { "files": ["music/a.mp3"], "load": "idle" },
373
+ "skins": { "files": ["skins/tabby.webp"], "load": "demand" } } }
374
+ ```
375
+
376
+ `boot` is what the first frame needs; `idle` groups load after
377
+ `engine.ready()`; `demand` groups when you ask (`createAssetLoader` in
378
+ `/engine-sdk/assets.js`, or your own loader — the manifest is the contract).
379
+ Every file in exactly one group.
380
+
381
+ ### 3.3 Fit — the frame, and what the site's bar does
382
+
383
+ The game is the frame, as a House Original is: the site gives it the
384
+ column's width × **720 px** (phone: ~364 × 720), wider in theatre mode and
385
+ the whole screen in fullscreen (the site's buttons). Lay the game out for
386
+ the box it is given — `height: 100%`, the board taking what is left — and
387
+ re-fit when it changes. The frame never grows to the page and never
388
+ scrolls: the pipeline plays your game at 1194×720 and 364×720, then
389
+ fullscreen at 1920×1080 and 390×844, and every control must be on screen
390
+ at each — and no text, image or canvas may be cut by the frame's edge (a
391
+ reel that runs off a phone fails). Decoration meant to bleed off the edge
392
+ is marked `aria-hidden="true"`; a clip inside the frame (a reel window, a
393
+ ticker) is yours and fine.
394
+
395
+ The site already shows the title, the balance and a bar under the frame —
396
+ draw none of them. Showing the balance in the game is allowed (slots do),
397
+ from `engine.balance` through `engine.money`. The bar's buttons reach the
398
+ game as `engine.on('control', ({ action }) => …)`: `sound` (toggle your
399
+ sound), `settings`; **Provably Fair** opens the shell's own dialog, the
400
+ House Originals' modal in the game window: a Seeds tab (the player's client
401
+ seed, the server seed hash before play, the next hash, the nonce, Rotate
402
+ seed pair, the revealed pairs) and a Verify tab (server seed, client seed,
403
+ nonce and mode in, the recomputed result out, matched against the round when
404
+ opened on one). It is the same for every engine game and it is the shell's,
405
+ so build no fairness UI of your own — no seeds view, no rotate button, no
406
+ verifier; `engine.openFairness(round.id)` opens the shell's dialog on a
407
+ round from inside your game, filled and verified, when you want a button
408
+ there (call it on the player's click: the shell takes it at most once every
409
+ two seconds, not while paused, and never over what the player has typed in
410
+ the dialog). `engine.round(id)` gives you the settled round without its
411
+ seeds. Every round is a bet: send `engine.play` a stake of at least
412
+ `engine.config.limits.min` and never offer a free or zero-stake spin — the
413
+ server refuses a stake under the minimum and a stake of 0 (a round that
414
+ costs nothing would still consume a nonce of the player's seed pair), demo
415
+ or otherwise.
416
+
417
+ ### 3.4 `hbtest.json` — how the pipeline plays one bet
418
+
419
+ ```json
420
+ { "open": [{ "page": "index.html", "selector": "a[href='./coin.html']" }],
421
+ "stake": { "page": "index.html", "selector": "hb-game hb-bet-amount input", "value": "1.00" },
422
+ "bet": { "page": "index.html", "selector": "hb-game [data-main]" } }
423
+ ```
424
+
425
+ Selectors reach into shadow roots; `page` is the page's file name. The run
426
+ types the stake, presses the bet, and expects `engine.play` and then
427
+ `revealed`. A ladder's bet opens a round: add `"then": [{ "page": "index.html", "selector":
428
+ "hb-game [data-next]" }, { "page": "index.html", "selector": "hb-game [data-cashout]" }]`
429
+ so the run closes it. Each `then` step is pressed only if its control is
430
+ visible and enabled — a bust can end the round before Cash out exists.
431
+ `"shows": { "page": "index.html", "selector": "#face", "path": "data.0.side" }`
432
+ makes the run check that the element shows the value the server drew
433
+ (`path` walks the round: `data.0.side` = `round.data[0].side`).
434
+ A game with several screens (a lobby of games) lists the others in
435
+ `"visit"`: clicks made before `open`, each screen and back, e.g.
436
+ `[{ "page": "index.html", "selector": "[data-card='dice']" }, { "page": "index.html", "selector": "#back" }]`.
437
+ An error or a scroll on any of them fails the run like one on the bet's screen.
438
+ A custom frontend names its own elements: any selector works
439
+ (`#play`, `.stake input`); give the Bet button and the stake input stable
440
+ ids and use those.
441
+ The engine panel's selectors are a contract: `hb-game hb-bet-amount input`
442
+ (stake), `hb-game [data-main]` (Bet), `[data-next]`, `[data-cashout]`,
443
+ `[data-modes] button[data-id="<mode>"]`, `[data-bets] button[data-id="<bet>"]`.
444
+ The typed stake is parsed in every currency of the run (a fiat at 2
445
+ decimals, the coin at 8): keep it representable in both, `1.00` is.
446
+
447
+ ### 3.5 Libraries (Pixi, Howler, GSAP …)
448
+
449
+ Ship them in the bundle under `bundle/vendor/` — nothing loads from a CDN at
450
+ run time. Libraries often contain code paths the player's browser will
451
+ refuse here (`new Function`, default CDN URLs). The scan reads a file under
452
+ `vendor/` as a library when its file name and the banner comment in its
453
+ head match a library we know (PixiJS and its `unsafe-eval` module, howler.js,
454
+ GSAP, three.js, Phaser, Babylon.js, Matter.js, Planck.js, anime.js, jQuery,
455
+ lodash, Spine, Tone.js, Hammer.js, Tween.js, p5.js, CreateJS, PlayCanvas,
456
+ cannon.js, Box2D, Rapier — `KNOWN_LIBRARIES` in `engine-conformance/scan.mjs`;
457
+ ask us to add one): what it would refuse in your own code it only names for
458
+ the reviewer, and the library may be minified as released. Anything else
459
+ under `vendor/` is read as your code (§3.7 applies, a source map and all).
460
+ The conformance run fails a game that trips the policy even once — configure
461
+ the library not to take those paths. Ship the release file as it is: never
462
+ edit a library file to get past the scan.
463
+
464
+ **PixiJS v8**: load its no-eval module right after it — the policy has no
465
+ `unsafe-eval`, and without this Pixi's renderer cannot start:
466
+
467
+ ```html
468
+ <script src="./vendor/pixi.min.js"></script> <!-- pixi.js@8 dist/pixi.min.js -->
469
+ <script src="./vendor/unsafe-eval.min.js"></script> <!-- pixi.js@8 dist/packages/unsafe-eval.min.js -->
470
+ ```
471
+
472
+ `ticker.add(fn)` returns the ticker, not `fn`: keep `fn` to remove it when a
473
+ screen closes, or it runs on destroyed sprites (an error the run catches).
474
+
475
+ Pixi is ~185 KB gzip and fits the boot budget; put your textures in
476
+ staged groups.
477
+
478
+ ### 3.6 Compressed textures and WebAssembly (KTX2 / Basis)
479
+
480
+ The policy allows WebAssembly (`'wasm-unsafe-eval'`) and workers started
481
+ from `blob:` (`worker-src 'self' blob:`), so a KTX2/Basis transcoder runs —
482
+ GPU-compressed textures keep phone memory down. But a `.wasm` is a program
483
+ nobody at Housebets can read, so the scan accepts WebAssembly on one footing
484
+ only: **a known library's released module, run by that library's own code.**
485
+
486
+ - The module's bytes must be a release we know: the scan hashes every
487
+ WebAssembly module in the bundle (by its first bytes, whatever the file is
488
+ called) and looks the SHA-256 up in
489
+ `packages/engine-conformance/wasm-releases.json` — three.js's
490
+ `basis_transcoder.wasm` and `draco_decoder.wasm`, Rapier's
491
+ `rapier_wasm2d_bg.wasm`/`rapier_wasm3d_bg.wasm`, Box2D's `Box2D.wasm`,
492
+ at the versions listed. Ship the file exactly as the package publishes it.
493
+ A module whose hash is not there is refused, whatever its name and
494
+ wherever it sits; a name like `rapier_bg.wasm` is no tie to anything.
495
+ Need another library or version? Ask us to add its hash (we take it from
496
+ the published tarball, never from a file in an upload).
497
+ - Only the library's own glue may reach `WebAssembly`: its `.js` under
498
+ `vendor/`, named and bannered as released (§3.5: `basis_transcoder.js`,
499
+ `draco_decoder.js`, `rapier*.js`, `Box2D*.js` are known). Your own code
500
+ reaching `WebAssembly` in any way — `WebAssembly.instantiate(…)`, a name
501
+ bound to it, `this.wa = WebAssembly`, a module embedded as base64 — is
502
+ refused. `typeof WebAssembly === 'object'` is a feature check, not a use.
503
+ - Its JS glue must not call `new Function`/`eval`. Emscripten's embind does
504
+ by default; the published three.js builds do not. The conformance run
505
+ fails a game whose transcoder trips the policy, so `hbengine test` tells
506
+ you at once.
507
+ - three.js: `new KTX2Loader().setTranscoderPath('./vendor/basis/')
508
+ .detectSupport(renderer)`. In Pixi or another engine, point its KTX2
509
+ transcoder path at the same `./vendor/` folder.
510
+ - Put `.ktx2` textures in staged `assets.json` groups like any other art;
511
+ keep a PNG/WebP fallback only if a target device lacks every supported
512
+ GPU format.
513
+ - A transcoder or physics engine you built yourself cannot ship as
514
+ WebAssembly: there is no source we could tie to the bytes. Ship it as
515
+ JavaScript (readable, or with its source map, §3.7).
516
+
517
+ ### 3.7 What a bundle must not do — and why the scan reads your code
518
+
519
+ The upload's scan parses every script of the bundle (every `.js`, every
520
+ inline `<script>`) and reads what it reaches from the syntax, with names
521
+ resolved through scopes — not by matching patterns. A rule is refused in
522
+ every spelling: `window['ev' + 'al']` is `eval`, `(0, eval)` is `eval`,
523
+ `fn.constructor('…')` is the Function constructor. Write plainly; a reader
524
+ at Housebets has to see what your game does before a player does.
525
+
526
+ **Refused** (the upload fails, naming file and line):
527
+
528
+ - `eval` and the Function constructor in any form: `Function(…)`,
529
+ `new Function`, `x.constructor(…)` on anything (a function's
530
+ `.constructor` *is* `Function`, and the scan cannot tell a function from
531
+ a class instance). Reading a property off it is fine — `.constructor.name`,
532
+ `this.constructor.DEFAULTS`, `this.constructor.create(opts)`,
533
+ `new this.constructor()` with no arguments, `x.constructor === Array` —
534
+ but calling it, `new o.constructor(a, b)` with arguments, keeping it in a
535
+ name (`const C = o.constructor`, `const { constructor } = o`) or reaching
536
+ `.call`/`.apply`/`.bind` on it is refused: name the class instead.
537
+ - The global object by a computed name — `window[name]`, `globalThis[k]`,
538
+ `self[k]`, `top[k]`, `parent[k]`, `frames[i]`, `document[k]`,
539
+ `Reflect.get(window, k)`, `Object.keys(window)`, `for (k in window)`,
540
+ `{ ...window }`, `const { ...rest } = window`, `new Proxy(window, …)` —
541
+ also through a name bound to it: `const w = window`, `let w; w = window`,
542
+ `const { self: w } = window`, a `(function (root) { … })(window)` wrapper.
543
+ `window.name`, `window['name']` and `const { name } = window` are fine:
544
+ they say what they reach, and are judged as that name (`const { eval } =
545
+ window` is `eval`).
546
+ - A property name built at run time that spells an API: `obj['ev' + 'al']`,
547
+ `obj[atob('…')]`, `obj[String.fromCharCode(…)]`, `obj['\x65val']`,
548
+ `'lave'.split('').reverse().join('')`. Ordinary dynamic keys
549
+ (`sounds['win_' + level]`, `obj[key]`, `dict[lang.toLowerCase()]`) are fine.
550
+ - A string handed to `setTimeout`/`setInterval`; `with`; `document.write`
551
+ (on any document: `frame.contentDocument.write`, `node.ownerDocument.write`);
552
+ `createElement('script')` or a `<script>` tag in a string (its text would
553
+ run as code nobody read — load scripts with `<script src>` or `import()` of
554
+ a path); code in attributes (`onclick="…"`, `href="javascript:…"`,
555
+ `setAttribute('onclick', …)` however the name is spelled — `'on' + 'click'`,
556
+ reversed, decoded, each name of a list a loop runs over — `setAttributeNS`,
557
+ `toggleAttribute` and `createAttribute` too, and a handler inside markup you
558
+ build from a string: `innerHTML = '<img onerror=…>'`, `insertAdjacentHTML`,
559
+ `outerHTML`, `createContextualFragment`, `setHTMLUnsafe`,
560
+ `DOMParser.parseFromString`, also when the markup is split across pieces,
561
+ `'<img on' + 'error=…>'`) — use `addEventListener`.
562
+ - An attribute name the scan cannot read: `setAttribute(name, …)`,
563
+ `toggleAttribute(name)`, `createAttribute(name)` with `name` computed.
564
+ Write the name down. A `for (const key of ['title', 'alt'])` or
565
+ `['title', 'alt'].forEach((key) => …)` over written names is read, and so
566
+ is a written prefix: `'data-' + key` and `` `aria-${name}` `` are fine,
567
+ `'on' + type` is a handler, `prefix + '-id'` is not readable.
568
+ `createElement(name)` with a computed tag stays a review warning (a
569
+ helper's `el(tag, …)` is the common case).
570
+ - Markup built from text the scan cannot read exactly: `innerHTML =
571
+ atob(…)`, `innerHTML = decodeURIComponent(…)`, `new TextDecoder().decode(…)`,
572
+ a written markup string rewritten by a pattern (`TPL.replace(/{{x}}/g,
573
+ …)`, `.sort`, `.map` over its characters); text from outside the code at
574
+ a markup or URL sink: a response body (`.text()`, `.json()`), storage, a
575
+ cookie, a field's value, a message's data (`addEventListener('message',
576
+ …)` in any spelling, `onmessage`), typed text, `window.name`, the address
577
+ (`location.hash`, `location.search`, `document.URL`, `URLSearchParams`:
578
+ the game can write the address and read it back decoded) — through any
579
+ name, list, `.map`, `.then`, destructuring or property the text passes.
580
+ - Markup or a URL of which nothing is written down: `innerHTML = x`,
581
+ `el.href = url`, `img.src = asset(id)` with nothing the scan can read
582
+ behind `x`. The scan follows a value to where it is written: through the
583
+ names it was bound to, objects and lists you wrote (`ART[sym]`, `P[0] +
584
+ P[1]`), your own functions (what they return, their parameters from where
585
+ you call them, in any file), properties and `dataset` attributes you wrote
586
+ and read back, `Map.set`/`get`, imports (`import * as ART from './art.js'`,
587
+ a JSON module), globals another script set on `window`, and the page's own
588
+ elements (`document.getElementById('f').dataset.src` is the attribute as
589
+ written in the page). What it cannot place anywhere is refused; one
590
+ alternative written beside one it cannot place (`ART[sym] || sym`) is
591
+ named for review. As a URL, the written start fixes the scheme: `'./x/' +
592
+ id` and `location.origin + path` are fine, `base + '/a.png'` with `base`
593
+ unknown, `'java' + x`, `'data:' + x` are refused; an `Image` or an
594
+ `<img>` you create loads a resource and may take any address. A computed
595
+ part inside written markup (`` `<div class="${c}">${text}</div>` ``,
596
+ `'<b>' + name + '</b>'`) stands in as a word; where a tag or an attribute
597
+ name would go, or as an unquoted attribute value (`` `<${tag}>` ``, ``
598
+ `<img ${attrs}>` ``, `` `<img src=${u}>` ``), it is named for review —
599
+ quote values, write tags and attributes down.
600
+ - A constant spelled across pieces anywhere in the code (`'java' +
601
+ 'script:x'`, `['<img o', 'nerror=x>'].join('')`, `atob('PGltZy…')` in a
602
+ helper) is judged where it stands, as a literal is.
603
+ - A method that sets attributes or builds markup or elements kept in a name,
604
+ bound or passed on instead of called: `const set = el.setAttribute`,
605
+ `el.setAttribute.bind(el)`, `document.createElement.bind(document)`,
606
+ `fn(el.insertAdjacentHTML)`, and its property descriptor taken
607
+ (`Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML')` kept, or
608
+ of a name not written down). Call it where you use it (`.call`, `.apply`,
609
+ `Reflect.apply`, `Function.prototype.call.call` and `desc.set.call(el, v)`
610
+ / `desc.value.call(el, …)` are read through to the call; `if
611
+ (el.setAttribute)` and `typeof el.setAttribute` are checks, not references).
612
+ `a.protocol = 'javascript'` turns a link into a `javascript:` URL and is
613
+ refused like one; `navigator[k]`, `location[k]`, `screen[k]`, `crypto[k]`,
614
+ `performance[k]`, `Reflect.get(navigator, k)` with `k` computed are refused
615
+ (write the name, or a list of names a loop runs over).
616
+ - A `Blob`, `File` or `Response` of JavaScript or of a document whose text
617
+ is not written down (`new Blob([code], { type: 'text/javascript' })` with
618
+ `code` computed, `new Response(t, { headers: { 'content-type': 'text/html'
619
+ } })`, headers not written down): put a worker in a `.js` file. One written
620
+ down is read as code (its rules apply), as a page (`text/html`) or as XML
621
+ (`application/xhtml+xml`, `image/svg+xml`, any `+xml` type: prefixes
622
+ dropped, a DTD subset refused, as a framed document is).
623
+ - A `data:` URL that is a page (`data:text/html,…`), an SVG `data:` URL that
624
+ holds a script or a handler, a `data:` script, or a `data:` body the scan
625
+ cannot decode. Inline SVG icons as `data:image/svg+xml` are fine.
626
+ - A handler attribute, a `<script>`, a `javascript:` URL or a `data:` page in
627
+ a `.json` or `.txt` file's text, or in a page's data block (`<script
628
+ type="application/json">`, `<xmp>`, `<textarea>`): what the game fetches
629
+ or reads back may reach the page as markup, so data files and data blocks
630
+ are read as the code's strings are (`<b>` in a translation is fine). A
631
+ `.json` that does not parse, or a data file of more than a million values,
632
+ is refused: the scan reads nothing it cannot read to the end.
633
+ - `import()` of a URL (`https:`, `data:`, `blob:`) or of a computed address
634
+ (`import('./views/' + name + '.js')` — import each path literally); a `<script
635
+ type="importmap">`; a `<script src="data:…">`; an SVG or XML document
636
+ (`.svg`, `.xml`, `.xhtml`, `.xsl`) with a `<script>` (under any namespace
637
+ prefix: `<svg:script>` is one) or a handler attribute — a frame runs them as
638
+ a page. The scan reads such a document as the browser renders it, prefixes
639
+ dropped (`svg:onload` is `onload`), and refuses outright what it would have
640
+ to emulate the parser to read: an internal DTD subset (`<!DOCTYPE svg [ …
641
+ ]>`), any `<!ENTITY>` or `<!ATTLIST>` declaration (an entity becomes markup
642
+ where it is referenced, an attribute default — an `onload` among them — is
643
+ added to every such element), and an `<?xml-stylesheet?>`. Export art as
644
+ plain SVG without a DTD (Illustrator: *Export As → SVG*, not the SVG 1.1
645
+ DTD profile; an external `<!DOCTYPE svg PUBLIC …>` with no `[ … ]` is
646
+ fine, browsers do not fetch it).
647
+ - `WebSocket`, WebRTC (`RTCPeerConnection`), `navigator.serviceWorker`,
648
+ `fetch('/api/…')` of a server of your own (saved data goes through
649
+ `engine.state`), the string `/csp-report`, any URL outside the bundle.
650
+ - Own CSP, `<base>`, meta refresh, `<object>`/`<embed>`, `srcdoc`, a copy of
651
+ `engine-sdk/`.
652
+ - No bet of its own: a play with no click of the player's behind it is
653
+ refused, and an autoplay loop that is not a shell session runs only for the
654
+ click's few seconds, then is refused (§3.1).
655
+ - WebAssembly reached by your own code, and any WebAssembly module whose
656
+ bytes are not a known library release (§3.6).
657
+ - **Minified or obfuscated code without a source map.** The measure: a file
658
+ with twenty names or more, nine in ten of them one or two letters, is
659
+ minified (every minifier's output is at 98–100%; the densest hand-written
660
+ game we have is at 72%). Names of the form `_0x1a2b` or ASCII text written
661
+ in `\x`/`\u` escapes are an obfuscator's and refused outright. Ship readable
662
+ code, or ship the build with its source map in the bundle — the `.map`
663
+ beside the file or named by its `//# sourceMappingURL=` comment (an inline
664
+ `data:` map works too), version 3, with `sourcesContent` (Vite: `build:
665
+ { sourcemap: true }`; esbuild: `--sourcemap`). A minified inline script
666
+ has nowhere for a map: put built code in a `.js` file. The reviewer reads
667
+ the sources; the play-tests play the real bytes.
668
+ - A file that does not parse as JavaScript (as a module or as a classic
669
+ script): a browser would refuse it too.
670
+
671
+ **Named for the reviewer** (the upload passes; each is listed with file and
672
+ line, so the reviewer knows where to look — keep them to what the game
673
+ needs): reading the date or time (`Date`, `Temporal`,
674
+ `performance.timeOrigin`); the time zone or locale formatting
675
+ (`getTimezoneOffset`, `Intl.DateTimeFormat`, `toLocale…`); the language
676
+ outside the SDK (`navigator.language`, `Intl` — `engine.config.locale` is the
677
+ player's language); the browser's identity (`userAgent`, `platform`,
678
+ `hardwareConcurrency`, `deviceMemory`, `screen.colorDepth`,
679
+ `navigator.webdriver`); a timer past 60 s or with a computed delay; state
680
+ kept on the device (`localStorage`, `sessionStorage`, `indexedDB`, cookies —
681
+ per device only, and anything a player keeps across devices goes through
682
+ `engine.state`); a text field or form (`<input>`, `<textarea>`, `<form>`,
683
+ `createElement('input')`: a game takes nothing typed but the stake) and
684
+ words like "password" or "sign in" in its text; browser randomness
685
+ (`Math.random`, `crypto.getRandomValues` — for visuals only, every outcome
686
+ is the server's); a `fetch` of a computed address, `XMLHttpRequest`,
687
+ `sendBeacon`; a Worker on a built URL or `URL.createObjectURL`; reaching the
688
+ parent or top frame; `location` changes and `window.open`.
689
+
690
+ ## 4. Currencies — never compute money
691
+
692
+ The player plays in any Housebets coin and may see it as a fiat (EUR, JPY,
693
+ …) at the platform's rate. `engine.money` is the Originals' own money code
694
+ bound to the session:
695
+
696
+ ```js
697
+ const m = engine.money;
698
+ m.unit, m.sign, m.decimals // 'EUR', '€', 2 — or 'BTC', '', 8
699
+ m.format(amount) // a wallet amount, as the header shows it
700
+ m.caption(amount) // '0.00019230 BTC' under a fiat field
701
+ m.parse(text) // what the player typed → wallet amount, truncated down
702
+ m.mark // our picture of the currency (data: URI)
703
+ ```
704
+
705
+ Stakes go to `engine.play` in **wallet units** at the wallet's precision;
706
+ `<hb-bet-amount>` does all of it (mark, sign, precision, ½ 2× max, limits,
707
+ remembered stake):
708
+
709
+ ```html
710
+ <hb-bet-amount id="stake" max-button></hb-bet-amount>
711
+ <script type="module">
712
+ const field = document.getElementById('stake').connect(engine);
713
+ field.bet = { mode: 'coin', bet: 'heads' }; // the bet the stake is for: its own ceiling (§4.1)
714
+ field.amount // wallet units for engine.play
715
+ field.reason // {code, message} when the stake cannot be played, else null
716
+ </script>
717
+ ```
718
+
719
+ Balances and payouts come from the server; show them with `m.format`.
720
+
721
+ **Bet levels, as a slot shows them.** A game with its own bet buttons (a
722
+ bet up/down, a bet menu) offers `m.levels(limits.min, limits.max)`: round
723
+ stakes in the player's display unit — €0.10, €0.20, €0.50, €1 … or 0.1, 1,
724
+ 10 USDT — each with the wallet `amount` to send, the header's `text` and a
725
+ `label` for the button (`€2.00`, `2 USDT`). Never show a raw wallet amount
726
+ or a limit converted from dollars ("2,000.62019200 USDT"): the limits are
727
+ set in dollars and converted to the coin at the live rate, so only the
728
+ levels are round. Decimals never change the maths: a payout is the stake ×
729
+ the multiplier, cut to the wallet's precision (10⁻⁸ for a coin).
730
+ Bet up / down moves one level — never by a fixed amount such as the
731
+ minimum bet or `m.step` (the wallet's smallest unit, 0.00000001): a button
732
+ that adds the minimum takes a thousand clicks to go from 0.10 to 100. A
733
+ stake is never below `engine.config.limits.min` and never 0: there is no
734
+ free spin (§3.3), so a play button stays disabled until the field holds a
735
+ playable stake (`field.reason === null` and `field.amount > 0`; an empty
736
+ field's `reason` is `min`, and `engine.play` itself refuses a stake of 0
737
+ with `limits` before anything is sent). The engine panel (`ui.js`) does
738
+ exactly this.
739
+
740
+ ### 4.1 Maximum win per round
741
+
742
+ No single round of any engine game pays more than the platform's **maximum
743
+ win per round** (1,000,000 USD unless Housebets sets another figure; the
744
+ same for every game), or the player's coin's own maximum win when that is
745
+ less. The cap is enforced **before the bet**, never by cutting a win: a win
746
+ reduced after the draw would change your RTP and break the round's
747
+ provably-fair receipt. So the server refuses a stake whose largest possible
748
+ win — the stake times the bet's top multiplier (`maxMultBp` of a table bet,
749
+ the last `ladderBp` of a ladder) — would pass the cap, with a `limits`
750
+ refusal that names the most the bet takes.
751
+
752
+ What this means for your game: **a high-multiplier bet has a low maximum
753
+ stake.** A bet that can pay 5,000× takes at most 1,000,000 / 5,000 = 200
754
+ USD a round; one that can pay 1,000,000× takes at most a dollar. Your Maths
755
+ card in House Studio (the game's page, once the maths are verified) shows
756
+ the figure per bet (`Max stake`) and marks one under a dollar `under $1`.
757
+ Choose your multipliers with that in mind: a 2× coin flip is held by the
758
+ coin's max bet alone.
759
+
760
+ The session says it per bet, in the wallet coin at its precision, rounded
761
+ down:
762
+
763
+ ```js
764
+ engine.config.limits.maxWin // the most one round pays, in the wallet coin
765
+ engine.config.limits.maxStake.coin.heads // "12.50": the most this bet takes
766
+ ```
767
+
768
+ Nothing to compute: tell the field which bet the stake is for
769
+ (`field.bet = { mode, bet }`; the engine panel does it on every
770
+ selection) and it holds the stake to that bet's figure, turns red and says
771
+ the most the bet takes when the player types more (`field.reason.code ===
772
+ 'maxWin'`; of the coin's max bet and the bet's own figure, the lower is the
773
+ one named), and its max chip stops there. `engine.play` refuses a stake over
774
+ the figure before anything is sent, with `limits` and the same message; a
775
+ `limits` refusal from the server over the cap carries the figure too
776
+ (`err.maxStake`, a decimal string in the wallet coin), so you can set the
777
+ field to it (`field.value = err.maxStake`).
778
+
779
+ A session that names `"0"` for a bet (and `maxWin` 0) means nothing can be
780
+ staked on it until the coin's USD rate is back: the field says so (the
781
+ Originals' "Something went wrong" line) and hides its max chip, a play
782
+ button on `field.reason` stays disabled, and `engine.play` refuses with
783
+ `unavailable`, as the server would. A session from an older server names no
784
+ `maxStake` at all: then the coin's max bet alone holds in the SDK, and the
785
+ server is the last word anyway.
786
+
787
+ ## 5. Languages
788
+
789
+ `engine.config.locale` is the player's language (the site's). The panel,
790
+ the field and every refusal are already in it (13 languages). Your own
791
+ strings: ship them as `bundle/i18n.json` — `{ "en": {…}, "ru": {…} }` —
792
+ and pick by `engine.config.locale`, falling back to `en` (required). The
793
+ engine panel names modes and bets from your math's labels; give it names
794
+ by language with `mount(el, { labels: { de: { base: 'Drehen' } } })`
795
+ (the lines and keno templates do). The
796
+ upload tells you which of the site's languages (`ar da de en es fi fr hi hu
797
+ id it ja ko pl pt ru th tr vi zh`) you lack. Numbers and money: always
798
+ through `engine.money`.
799
+
800
+ ## 6. What the upload checks — the whole list
801
+
802
+ `hbengine upload` = `verify` + `lint` + the portal's conformance run (about
803
+ two minutes on real desktop and phone frames; `hbengine status` shows the
804
+ state meanwhile). A version is `sandbox` only when every line is `ok`:
805
+
806
+ | check | what passes |
807
+ |---|---|
808
+ | math | the pack verifies (§2): format, RTP band, every bet pays |
809
+ | budgets | §3 sizes; `assets.json` well formed, `boot` present, every file in one group |
810
+ | code | every script parsed: nothing of §3.7's refused list, in any spelling (libraries under `vendor/` we know: named, §3.5); what is named for review is listed with file and line |
811
+ | readable | no minified or obfuscated code without its source map; WebAssembly only as a known library's released module, run by the library (§3.6, §3.7) |
812
+ | engine use | the code connects, calls `play`, calls `revealed` |
813
+ | boot (desktop, phone) | `hello` answered and `ready()` within 25 s |
814
+ | idle | left untouched for 15 s, the game places no bet: a bet is always the player's act (and the shell refuses one with no click behind it, §3.1) |
815
+ | bet | one bet through your UI (`hbtest.json`), pressed with a real click, reaches `engine.play`, then `revealed` |
816
+ | shows | with `shows` in `hbtest.json`: the element shows the value of the round the server drew |
817
+ | draws | after the round, the play area shows something: its visible content covers at least 2% of the frame (a board with no size fails) |
818
+ | picture | the screenshot of the frame is not one flat colour (a warning for the reviewer for now): a canvas that drew nothing, or WebGL that could not start, shows a black frame. The run draws WebGL on the CPU (SwiftShader), so a WebGL game must start without a GPU |
819
+ | currency | under a fiat display (desktop) and a bare coin (phone): the stake sent equals what the Originals send for the typed amount; the screen shows that currency |
820
+ | outage | the next bet meets an ambiguous wallet answer: the game keeps running, the site is asked to re-read the wallet |
821
+ | fit | the game fits the frame it is given — 1194×720 and 364×720, then fullscreen 1920×1080 and 390×844: no page scrolls, every visible control is inside the frame, no text (or more than a tenth of a picture) is cut by its edge |
822
+ | errors | no uncaught error in any page |
823
+ | network | no request leaves the bundle's origin, and the policy refuses nothing (a blocked worker, script or style fails the run) |
824
+
825
+ Then a reviewer plays it (`sandbox → review → live`), with a kill switch at
826
+ every level. Money, seeds, limits and refunds are the platform's and the
827
+ same as for every House Original: you never handle them.
828
+
829
+ ## 7. Examples
830
+
831
+ `engine-examples/`: **coin** (table, engine panel, 30 lines), **dice**
832
+ (97 bets, the zone picks), **slot** (110,592-row table, reels from
833
+ `round.data`), **lines** (a video slot from the maths kit: 5×3, ten lines,
834
+ wild, scatter, free spins ×2 with retriggers, 96.00% exactly), **tumble**
835
+ (a 6 × 5 scatter-pays grid with tumbles and free-spin multipliers, 96.00%
836
+ exactly), **keno** (40 numbers, 1–10 picks, hypergeometric weights; the
837
+ picks choose the mode), **crash** (a 64-level ladder with cash-out that
838
+ the page climbs itself with `game.advance()`; 99% at every level), **mines** (a ladder of reveals, 1–24 mines, exact
839
+ weights per reveal), **plinko**
840
+ (8/12/16 rows × three risks, binomial weights), **tower** (8-step ladder
841
+ with cash-out), **wheel** (three modes). BAD CAT (`badcat-engine/`) is a full custom frontend ported as a
842
+ studio would.
843
+
844
+ ## 8. Review
845
+
846
+ Every version a player can see is reviewed by a person at Housebets first. Review works like a pull request.
847
+
848
+ 1. Run `npx hbengine preflight` (or `engine_preflight`) first: on the newest version that passed every check (the one submit sends), it checks what a machine can of the review checklist — the RTP and max win your rules quote against the verified maths, rules in every language, the store page, phone and desktop fit, errors, load and network — and prints the facts the rules must quote. A pre-flight fail is pre-ticked on the reviewer's checklist; a pass is a note to them, not a tick, since each item asks more than a machine sees. Then `npx hbengine submit` (or `engine_submit`, or the Review tab) sends the newest version that passed every check.
849
+ 2. The reviewer plays it on desktop and phone and goes through the **review checklist** (the game's Review tab, and `checklist` in `engine_review`): the maths against the rules and the screen, the rules in every language, phone and desktop play, sound, recovery from errors, content you own, nothing aimed at children, no outcome logic or outside servers in the bundle, an honest store page. Go through it yourself before you submit.
850
+ 3. Anything to change is a **thread** on the version: a **must fix** blocks going live, a **suggestion** does not. Each thread names its area and says what, where and why.
851
+ 4. Fix every must-fix thread in the project, upload the new version and submit it. Submitting marks the open must-fix threads as fixed in that version (on the Review tab you can untick any it does not fix). To ask the reviewer something instead, reply on the thread (`npx hbengine reply --thread 12 --message "…"`, `engine_reply`, or the Review tab), in at most 4,000 characters.
852
+ 5. The reviewer checks each fix and resolves the thread, or reopens it with a reply.
853
+ 6. With no must-fix left, the version is approved: the studio's owners and its uploaders get an email. An owner puts it live: **Make live** on the Versions tab, `npx hbengine live` in the project, or `engine_live` (with a key an owner made). The round server and the front end switch within seconds, and everyone gets an email that it is live. The first time, players find the game on Housebets once Housebets switches its catalogue page on; we email you when it is. Players keep the live version until then.
854
+
855
+ With an agent: "fix the review" is `engine_review` → change the project → `engine_upload` → `engine_preflight` → `engine_submit`; "put it live" is `engine_status` (is it approved?) → `engine_live`.