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,917 @@
1
+ /**
2
+ * The shell's switchboard: game requests in, engine calls out, the site's
3
+ * header kept in step. Pure — the windows, the engine client and the site
4
+ * bridge are handed in, so every rule here is tested without a browser.
5
+ *
6
+ * Money rules, the same as every House Original's:
7
+ * - one bet or action at a time;
8
+ * - each gets its own generation (the server's idempotency key), minted
9
+ * here — the game cannot choose or replay one;
10
+ * - a bet is the player's act (below): no click, no bet;
11
+ * - the site's header shows the debit when the bet leaves, and the
12
+ * server's balance when the game has revealed the outcome (or at
13
+ * REVEAL_DEADLINE_MS, whichever is first), so the header never spoils
14
+ * a result and never waits on a game that does not say;
15
+ * - a refused bet puts the header back; a bet whose outcome is unknown
16
+ * ends the site's round with no balance, which makes the site re-read
17
+ * the wallet (balance-reconciliation-request), as Coin Flip does.
18
+ *
19
+ * The click gate: nothing happens without the player's click. The game is
20
+ * untrusted code: a bet it sends on its own (a timer, a trigger on a date, a
21
+ * round count or a language) must not spend the player's money, and a round
22
+ * it moves on its own (a step, a cash-out) is not the player's play either.
23
+ * Browsers hand a click inside the game frame to the frame's ancestors as
24
+ * transient user activation (HTML "activation notification"), so this
25
+ * window's navigator.userActivation.isActive is true for a few seconds after
26
+ * the player clicked or pressed a key in the game. A play and every ladder
27
+ * action — an `advance` (it risks what the player could cash out) and a
28
+ * `cashout` alike (a round the player left open is theirs to come back to,
29
+ * never the game's to close for them) — is taken only while that holds, or
30
+ * inside an autoplay session the player started and on the session's own
31
+ * round. Activation alone is too wide, though: the same notification reaches
32
+ * every same-origin frame of the clicked page, and the shell is served from
33
+ * the site's own origin, so a click in the site's chat or header arms this
34
+ * window as much as one in the game; and a press on the shell's own controls
35
+ * (Stop, Cancel, the reality check's Continue, a key typed in the Provably
36
+ * Fair dialog) is a trusted interaction in this very window. So a play or an
37
+ * action needs the player's gesture (`gesture`): activation live, the focus
38
+ * inside the game frame (`focusInGame`: a click in the frame focuses it; one
39
+ * in the chat does not), that focus the player's own (`frameFocused`, below),
40
+ * and no input on the shell's own DOM — or, with the shell on the site's
41
+ * origin, on the site's page: boot.js listens on the site's document as on
42
+ * its own, and the site may report a press of its own besides (`site-input`
43
+ * on its bridge) — in the last ACTIVATION_TTL_MS (`shellInput`, the browsers'
44
+ * transient-activation lifetime, so a shell or site press's activation is
45
+ * never mistaken for a game click's). What one gesture can buy is bounded
46
+ * too: one play per CLICK_MIN_GAP_MS and CLICK_BURST_MAX per
47
+ * CLICK_BURST_WINDOW_MS; one advance per ADVANCE_MIN_GAP_MS and
48
+ * ADVANCE_BURST_MAX per window (a hidden trigger must not climb a ladder to
49
+ * its bust on one tile tap), refused as `pacing` beyond that; a cash-out is
50
+ * not paced (it only returns money, and there is one per round). A browser
51
+ * without the API cannot confirm a click, so it bets nothing: the refusal
52
+ * names the browsers that can.
53
+ *
54
+ * The focus must be the player's. A game can move the focus into its own
55
+ * frame by script (`window.focus()` from inside a cross-origin frame works
56
+ * in current browsers), and the player's next keystrokes — meant for the
57
+ * site's chat — would then land in the frame, granting it activation. So the
58
+ * page tells the router whenever the focus enters the game frame
59
+ * (`frameFocused`): the player's own focusing (a click, a tap) arrives with
60
+ * the activation it granted already live, no shell or site press within the
61
+ * activation's lifetime, and not within that lifetime of the focus leaving
62
+ * the frame (the page watches the exits too: a game sees its own window blur
63
+ * when the player tabs out of it and can take the focus straight back, with
64
+ * the activation of that Tab — a key in the game's own document — still live,
65
+ * and the shell cannot tell such a return from the player's click back; so
66
+ * for ACTIVATION_TTL_MS after leaving the frame, only a Tab the page saw
67
+ * brings the player back in) — a Tab into the frame is the player's (the
68
+ * page says when the entry followed a Tab), though the Tab itself is a shell
69
+ * or site key; a focus with no activation behind it was scripted, and no
70
+ * play or action is taken on that focus (`stolenFocus`) until the player
71
+ * focuses the frame themselves — the page also blurs a stolen focus back out
72
+ * of the frame at once, so the keystrokes land on the shell's own document
73
+ * (shell input: the shadow above) rather than in the game.
74
+ *
75
+ * Deny by default. Every request the game may make has a kind
76
+ * (`REQUEST_KIND`): a read (the round receipt, the saved state read) and a
77
+ * save (the state write: it moves no money, and the game chooses what it
78
+ * saves whether on a click or later, so a click would protect nothing — and
79
+ * BAD CAT saves its profile at boot) are always answered; a play, a ladder
80
+ * action (advance or cash-out) and the start of autoplay (they move the
81
+ * player's money, or what is on the table) need the player's click or the
82
+ * session, gated as above; anything else is refused. The kinds are an
83
+ * explicit list, so a request type added to the protocol later without a
84
+ * kind is denied, not waved through — a new money path cannot slip past the
85
+ * gate.
86
+ *
87
+ * Autoplay is the shell's, never the game's — and a run of bets is never one
88
+ * click away. One player click inside the game (on Spin) gives this window
89
+ * activation, so activation alone must not buy a hundred bets. The game's
90
+ * `autoplay.start` therefore starts nothing: it is the player's act, so it
91
+ * needs the same gesture a play does (a scripted ask with no click behind it
92
+ * is refused `needs-click`), it is refused while paused and once the session
93
+ * has ended, and it is paced — a request the player cancelled, or that
94
+ * expired, cannot be repeated for AUTOPLAY_RETRY_GAP_MS (`pacing`), so the
95
+ * confirmation cannot be reopened the instant the player closes it
96
+ * (`autoplay` with `retryAfterMs`: the player said no, they did not click too
97
+ * much, so it is not `pacing`). Then the shell fails closed unless the
98
+ * player's rules explicitly allow autoplay (`init.rg.autoplay.allowed ===
99
+ * true`; absent or false is a no), caps the terms — the per-round stake at
100
+ * the coin's max bet, at the bet's own exposure cap
101
+ * (`limits.maxStake[mode][bet]`, SPEC §3.1: the request names the bet, so
102
+ * the shell applies its figure; a bet whose figure is 0 takes nothing right
103
+ * now, and the ask is refused `unavailable`) and at the balance the shell
104
+ * last saw, when it saw one (a deposit made in the site's wallet is invisible
105
+ * here, so an unknown or empty balance caps nothing: the server refuses the
106
+ * first play and the session ends `refused`); the rounds at
107
+ * AUTOPLAY_MAX_ROUNDS; a loss limit required, defaulted
108
+ * (AUTOPLAY_DEFAULT_LOSS_MULT × the stake) when the game names none and never
109
+ * above the total the session could stake (a limit the run cannot reach
110
+ * would be a limit in name only) — and shows the player the shell's own
111
+ * confirmation with exactly those terms, the bet the run is for (`mode`,
112
+ * `bet`) among them (`site.autoplayConfirm`, shell DOM the game cannot reach,
113
+ * cover or pre-fill); while it is up, the game's `fairness.open` is dropped,
114
+ * so nothing of the shell's can be popped over the terms. The session is
115
+ * created only when the player presses Start on that confirmation
116
+ * (`confirmAutoplay`, wired to a real click on the shell's own button), never
117
+ * from the game's message; Cancel or closing refuses (`cancelAutoplay`). The
118
+ * confirmation lives AUTOPLAY_CONFIRM_MS, well under the SDK's 60 s answer
119
+ * timeout: unanswered by then it is dismissed (`site.autoplayDismiss`) and the
120
+ * game refused, so a Start pressed after the game gave up can never start a
121
+ * session the game knows nothing of; the reality check and the end of the
122
+ * session dismiss it the same way. To change the bet, the stake or the terms,
123
+ * the game asks again and the player confirms again. Once a session runs, the
124
+ * shell shows its control (rounds left, Stop — shell DOM, through
125
+ * `site.autoplay`), counts each play against it, refuses a play on another
126
+ * bet or above its stake, and ends it at the count, at a stop, on any refused
127
+ * play, on Stop (one click on the shell's control), on the reality check, when
128
+ * the session ends, after AUTOPLAY_IDLE_MS without a play of the session
129
+ * (`idle`: the granted bets cannot be saved for when the player is not
130
+ * looking) and when the page is hidden or goes (`hidden`). The game hears
131
+ * every change as an `autoplay` event. Plays inside the session need no
132
+ * click; a ladder round the last play opened keeps the session open until it
133
+ * closes, and its steps and cash-out are the run the player approved — an
134
+ * action on any other round (one resumed from an earlier page, one the player
135
+ * opened by hand) is the player's to take, gesture and all, session or not.
136
+ *
137
+ * site.roundEnd gets { corr, generation, amount, outcome, balance, payout }:
138
+ * outcome 'settled' (balance is the server's figure, or null when the
139
+ * server did not name it), 'rollback' (the stake was never taken) or
140
+ * 'unknown' (nobody knows yet; the site must re-read the wallet).
141
+ *
142
+ * The fairness dialog is the shell's, not the game's: a game's `fairness.open`
143
+ * is a request to show it on a round, taken at most once per
144
+ * FAIRNESS_MIN_GAP_MS and never while paused (the game is untrusted code,
145
+ * and the other side-effecting messages are gated the same way), and the
146
+ * dialog itself decides what an open from the game may change (page.js: never
147
+ * the player's work in it). A game's `round` request is answered with the
148
+ * settled round as the game may see it: no seed, no hash, no revealed seed
149
+ * (SPEC §2: the receipt is the dialog's).
150
+ *
151
+ * Telemetry: a game's error event is counted on the engine (rgs.telemetry
152
+ * 'error', its text only in the player's console), and a round still open
153
+ * when the page goes (unload) is counted as abandoned, once per round: the
154
+ * one the session resumed (init.activeRound) as much as one opened here.
155
+ * Counts, never text.
156
+ */
157
+ import { AUTOPLAY_MAX_ROUNDS, ERRORS, GAME_EVENTS, REQUESTS, SHELL_EVENTS, answer, event, maxStakeFor, readGameMessage, refuse } from '../../engine-sdk/protocol.js';
158
+
159
+ export const REVEAL_DEADLINE_MS = 8_000;
160
+ // A game in an error loop is counted this many times per launch, then only logged:
161
+ // the server caps a player's day anyway, and the browser is spared the requests.
162
+ export const ERROR_REPORTS_PER_LAUNCH = 20;
163
+ // A game may ask for the fairness dialog this often; the rest is dropped
164
+ // (a bundle posting it in a loop would otherwise hammer the seed and receipt
165
+ // routes under the player's session and keep the modal popping).
166
+ export const FAIRNESS_MIN_GAP_MS = 2_000;
167
+ // What one burst of the player's activation can buy without autoplay: a
168
+ // play every CLICK_MIN_GAP_MS at most, and CLICK_BURST_MAX plays in any
169
+ // CLICK_BURST_WINDOW_MS. A person cannot click faster; a game that sends more
170
+ // per click is spending on its own.
171
+ export const CLICK_MIN_GAP_MS = 600;
172
+ export const CLICK_BURST_MAX = 5;
173
+ export const CLICK_BURST_WINDOW_MS = 5_000;
174
+ // What one gesture can buy in ladder advances: a person taps a tile no faster
175
+ // than about ten times a second, and a ladder that climbs by itself (the crash
176
+ // example) steps about six times a second early on, so an honest climb runs
177
+ // through the tap's few seconds; a bundle firing steps at the server's
178
+ // round-trip (fifty to a hundred per click) is cut to a quarter of them and
179
+ // cannot take the ladder to its bust on one tap.
180
+ export const ADVANCE_MIN_GAP_MS = 100;
181
+ export const ADVANCE_BURST_MAX = 25;
182
+ // How long a browser keeps transient activation after an input (Chrome,
183
+ // Firefox and Safari: 5 s). A trusted input on the shell's own DOM arms this
184
+ // window exactly as a game click does, so for this long after one the shell
185
+ // takes no play on activation alone — it cannot tell the two apart.
186
+ export const ACTIVATION_TTL_MS = 5_000;
187
+ // When the game names no loss limit for an autoplay session, the shell
188
+ // applies one so a run always has a floor: this many times the per-round
189
+ // stake. Shown in the confirmation the player approves.
190
+ export const AUTOPLAY_DEFAULT_LOSS_MULT = 100;
191
+ // How long the shell's confirmation waits for the player. The SDK gives up on
192
+ // an unanswered request after 60 s; the confirmation goes well before that,
193
+ // so a late Start cannot start a session the game has already written off.
194
+ export const AUTOPLAY_CONFIRM_MS = 30_000;
195
+ // After the player cancelled a confirmation (or it expired), the game cannot
196
+ // ask again for this long: a Cancel must close the dialog, not reopen it.
197
+ export const AUTOPLAY_RETRY_GAP_MS = 10_000;
198
+ // A session with no play of its own for this long ends (`idle`): a run the
199
+ // player approved is played now, not held for when they have looked away.
200
+ export const AUTOPLAY_IDLE_MS = 30_000;
201
+ export { AUTOPLAY_MAX_ROUNDS };
202
+ // What a game sees of a settled round (REQUESTS.ROUND): the outcome and the
203
+ // money, never the seeds, the hash or the revealed seed. An allow-list, so a
204
+ // field the receipt gains later does not reach the bundle by default. Nor
205
+ // `forced`: a sandbox marks a round whose outcome was chosen rather than drawn
206
+ // (SPEC §2), which tells a bundle it is being tested, so neither a round nor
207
+ // a play or action answer carries it to the game (the studio's sandbox shows
208
+ // it in the shell's own fairness dialog).
209
+ export const ROUND_FIELDS = Object.freeze(['roundId', 'game', 'mathHash', 'mode', 'bet', 'kind', 'nonce', 'draws', 'multBp', 'payoutMultiplier', 'survived', 'busted', 'step', 'currency', 'amount', 'payout', 'state', 'createdAt', 'settledAt']);
210
+
211
+ /** The receipt cut to ROUND_FIELDS. */
212
+ export function roundForGame(body) {
213
+ const out = {};
214
+ for (const k of ROUND_FIELDS) if (Object.hasOwn(body, k)) out[k] = body[k];
215
+ return out;
216
+ }
217
+
218
+ /** A play or action answer as the game may see it: the same, its round without `forced`. */
219
+ export function moneyForGame(body) {
220
+ if (!body || typeof body !== 'object' || !body.round || typeof body.round !== 'object') return body;
221
+ const { forced, ...round } = body.round;
222
+ return { ...body, round };
223
+ }
224
+
225
+ /**
226
+ * What the gate does with each request type: 'read' (the round receipt, the
227
+ * state read) and 'save' (the state write — it moves no money) are always
228
+ * answered; 'money' (a play, a ladder action — advance and cash-out alike)
229
+ * needs the player's click or the session's own round and is bounded (a
230
+ * cash-out is not paced); 'autoplay-start' and
231
+ * 'autoplay-stop' are the session's own. The map is exhaustive over
232
+ * REQUESTS, so a type not listed here — a future request added to the
233
+ * protocol, say — classifies as 'deny' and is refused: no new money path
234
+ * slips past the gate by default.
235
+ */
236
+ export const REQUEST_KIND = Object.freeze({
237
+ [REQUESTS.ROUND]: 'read',
238
+ [REQUESTS.STATE_GET]: 'read',
239
+ [REQUESTS.STATE_PUT]: 'save',
240
+ [REQUESTS.PLAY]: 'money',
241
+ [REQUESTS.ACTION]: 'money',
242
+ [REQUESTS.AUTOPLAY_START]: 'autoplay-start',
243
+ [REQUESTS.AUTOPLAY_STOP]: 'autoplay-stop',
244
+ });
245
+
246
+ /** The gate's kind for a request type; 'deny' for anything not classified. */
247
+ export function requestKind(type) {
248
+ return Object.hasOwn(REQUEST_KIND, type) ? REQUEST_KIND[type] : 'deny';
249
+ }
250
+
251
+ /**
252
+ * Whether the player's activation is live in this window: true, false, or
253
+ * null when the browser has no navigator.userActivation (then no click can
254
+ * be confirmed and the shell bets nothing). The default reads the real
255
+ * window; tests hand in their own.
256
+ */
257
+ export function browserActivation(nav = globalThis.navigator) {
258
+ const ua = nav && nav.userActivation;
259
+ return ua && typeof ua.isActive === 'boolean' ? ua.isActive : null;
260
+ }
261
+
262
+ /**
263
+ * Whether the focus is inside the game frame: the shell's document has the
264
+ * focus (a descendant frame's focus counts as the document's) and its focused
265
+ * element is the frame whose window `gameWindow()` names. A click or a tap
266
+ * inside the frame focuses it; a click in the site's chat around the shell,
267
+ * or on the shell's own dialogs, does not. Outside a browser (no document,
268
+ * the tests) there is nothing to check.
269
+ */
270
+ export function browserFocusInGame(gameWindow, doc = globalThis.document) {
271
+ if (!doc) return true;
272
+ if (typeof doc.hasFocus === 'function' && !doc.hasFocus()) return false;
273
+ const el = doc.activeElement;
274
+ return !!el && !!el.contentWindow && el.contentWindow === gameWindow();
275
+ }
276
+
277
+ // The words of the refusals the gate and autoplay answer with, in English;
278
+ // boot.js hands the player's language (host/strings.js, the same keys).
279
+ export const GATE_WORDS = Object.freeze({
280
+ needsClick: 'A bet needs your click.',
281
+ browserNoClick: 'This browser cannot confirm your click, so it cannot place a bet. Use a current Chrome, Edge, Safari or Firefox.',
282
+ tooManyBets: 'Too many bets for one click.',
283
+ autoplayNotAllowed: 'Autoplay is not available on your account.',
284
+ autoplayRunning: 'Autoplay is already running. Stop it first.',
285
+ autoplayOverStake: 'This bet is above the stake autoplay was started with.',
286
+ autoplayOtherBet: 'This bet is not the one autoplay was started with.',
287
+ autoplayNoRounds: 'The autoplay session has no rounds left.',
288
+ autoplayCancelled: 'Autoplay was not started.',
289
+ // The retry gap after a cancelled or expired confirmation: the player closed
290
+ // autoplay, they did not click too much, so it is not `tooManyBets`.
291
+ autoplayAskLater: 'Autoplay was just closed. You can ask again in a moment.',
292
+ });
293
+
294
+ /**
295
+ * createRouter({ self, gameWindow, rgs, site, init, newGeneration, setTimer, clearTimer, now, activation, focusInGame, words })
296
+ * self the shell's window (receives the game's messages)
297
+ * gameWindow () => the game iframe's contentWindow, the only accepted sender
298
+ * rgs createRgs(...)
299
+ * site the host bridge: roundStart, roundEnd, ready, fairness, error, ended,
300
+ * autoplay, autoplayConfirm, autoplayDismiss
301
+ * init the authenticate answer, handed to the game as its config
302
+ * now the clock the fairness pacing and the click bound read (tests hand in their own)
303
+ * activation () => whether the player's activation is live in the shell's window
304
+ * (true/false), or null when the browser cannot say (browserActivation)
305
+ * focusInGame () => whether the focus is inside the game frame (browserFocusInGame)
306
+ * words the refusals' words in the player's language (GATE_WORDS's keys)
307
+ */
308
+ export function createRouter({ self, gameWindow, gameOrigin = '*', rgs, site, init: initialInit, newGeneration = () => crypto.randomUUID(), setTimer = setTimeout, clearTimer = clearTimeout, now = Date.now, activation = browserActivation, focusInGame = () => browserFocusInGame(gameWindow), words = GATE_WORDS }) {
309
+ let init = initialInit;
310
+ const observers = new Set();
311
+ const observe = (listener) => {
312
+ if (typeof listener !== 'function') return () => {};
313
+ observers.add(listener);
314
+ return () => observers.delete(listener);
315
+ };
316
+ const report = (observation) => {
317
+ for (const listener of observers) {
318
+ try { listener(observation); } catch { /* observers cannot affect routing */ }
319
+ }
320
+ };
321
+ let moneyBusy = false;
322
+ // While paused (the reality check), no bet and no action leaves the shell:
323
+ // a game that ignores the notice, or plays on its own, is refused here.
324
+ let paused = false;
325
+ // When the game last had the fairness dialog opened for it.
326
+ let fairnessAt = -Infinity;
327
+ // When each play, and each advance, bought by the player's gesture alone
328
+ // left (the burst bounds).
329
+ let clickPlays = [];
330
+ let clickAdvances = [];
331
+ // When the player last pressed or typed on the shell's own DOM, or on the
332
+ // site's page around it (shellInput): for ACTIVATION_TTL_MS after it,
333
+ // activation proves nothing about the game.
334
+ let lastShellInput = -Infinity;
335
+ // Whether the focus now in the game frame got there by the game's script
336
+ // (frameFocused): no play or action rides on it until the player focuses
337
+ // the frame themselves. `stolenAt` is when the game last entered with no
338
+ // activation behind it: for ACTIVATION_TTL_MS after, a focus with the
339
+ // activation live is not taken for the player's either (the game steals, a
340
+ // keystroke lands in it, it steals again on that keystroke's activation).
341
+ // An entry under a shell or site press's shadow, or within ACTIVATION_TTL_MS
342
+ // of the focus leaving the frame (the game taking it back on its own blur),
343
+ // is refused too, but opens no window of its own: the shadow ends with that
344
+ // press's activation, the exit's window with the exit's.
345
+ let stolenFocus = false;
346
+ let stolenAt = -Infinity;
347
+ // The autoplay session in force, or null:
348
+ // { mode, bet, rounds, left, played, stake, stopOnProfit, stopOnLoss, net, openRound, idle }
349
+ // `mode`/`bet` the one bet the run is for, `net` the session's result so
350
+ // far (payouts minus stakes), `openRound` a ladder round of the session
351
+ // still on the table, `idle` the timer that ends a session nothing is
352
+ // played in.
353
+ let auto = null;
354
+ // A session the game asked for and the shell is showing the player, not yet
355
+ // started: { id, terms, timer }. The session is created only when the player
356
+ // presses Start on the shell's own confirmation (confirmAutoplay), never
357
+ // from the game's message or a click inside the game frame; `timer`
358
+ // dismisses the confirmation at AUTOPLAY_CONFIRM_MS.
359
+ let pendingAuto = null;
360
+ // When a confirmation was last cancelled or dismissed: the game may not ask
361
+ // again before AUTOPLAY_RETRY_GAP_MS has passed.
362
+ let autoRefusedAt = -Infinity;
363
+ // The player's balance as last seen from the server (money answers), the
364
+ // ceiling a session's stake is capped at. Starts from the launch balance.
365
+ let lastBalance = init && typeof init.balance === 'number' && init.balance >= 0 ? init.balance : null;
366
+ const round8 = (n) => Math.round((n + Number.EPSILON) * 1e8) / 1e8;
367
+ // server round id -> { corr, amount, auto } for a round still open; `auto` is
368
+ // the session object whose play opened it, or null. The object, not a flag:
369
+ // a round of a session that has ended stays open on the table (the player
370
+ // may cash it out by hand later), and it must never be taken for a round of
371
+ // the session running then — its payout would join that session's result,
372
+ // and the loss limit the player confirmed for it would stop nothing.
373
+ const opened = new Map();
374
+ const reveals = new Map(); // server round id -> the roundEnd it is waiting to send
375
+ const abandoned = new Set(); // open rounds already counted as abandoned (a page hides more than once: bfcache)
376
+ // The round the player left open last time is open now (the server resumes it with the
377
+ // session): left again, it counts as abandoned like one opened on this page.
378
+ const resumed = init && init.activeRound;
379
+ if (resumed && typeof resumed.id === 'string' && resumed.state === 'open') opened.set(resumed.id, { corr: newGeneration(), amount: 0, auto: null });
380
+
381
+ let ended = false;
382
+ let errorsCounted = 0;
383
+ // A refusal to the game; the first one that says the session is over
384
+ // also ends the game (`ended`), and the site hears it once.
385
+ const refused = (id, e, request) => {
386
+ sendRefusal(id, e.code, e.message, e.retryAfterMs, e.round, e.maxStake, request);
387
+ if (e.code === ERRORS.SESSION && !ended) {
388
+ ended = true;
389
+ dismissAutoplay(); // a confirmation still on screen cannot start now: closed, the game refused
390
+ endAutoplay('ended');
391
+ toGame(event(SHELL_EVENTS.ENDED, {}));
392
+ report({ type: 'ended' });
393
+ site.ended();
394
+ }
395
+ };
396
+ const sendRefusal = (id, code, message, retryAfterMs, round, maxStake, request = { id }) => {
397
+ toGame(refuse(id, code, message, retryAfterMs, round, maxStake));
398
+ report({ type: 'refused', code, request });
399
+ };
400
+ const toGame = (msg) => {
401
+ const w = gameWindow();
402
+ // To the game's own origin (the bundle's, named by the descriptor) and only
403
+ // to the framed window; the shell's frame-src keeps that window on the bundle.
404
+ // (A stand with no descriptor origin posts to '*', still to that window only.)
405
+ if (w) w.postMessage(msg, gameOrigin);
406
+ };
407
+ const word = (k) => (words && typeof words[k] === 'string' ? words[k] : GATE_WORDS[k]);
408
+
409
+ function settleSite(roundId) {
410
+ const r = reveals.get(roundId);
411
+ if (!r) return;
412
+ reveals.delete(roundId);
413
+ clearTimer(r.timer);
414
+ site.roundEnd(r.end, r.round); // the round itself is for the shell's own record, not the site
415
+ if (r.end.outcome === 'settled') report({ type: 'settled', round: r.round, balance: r.end.balance });
416
+ }
417
+
418
+ // ---- autoplay -------------------------------------------------------------------
419
+ // What the game and the shell's control are told of the session.
420
+ const autoState = (reason) => {
421
+ const s = auto ? { active: true, left: auto.left, played: auto.played, stake: auto.stake } : { active: false, left: 0, played: 0 };
422
+ if (reason) s.reason = reason;
423
+ return s;
424
+ };
425
+ const tellAutoplay = (reason) => {
426
+ const s = autoState(reason);
427
+ toGame(event(SHELL_EVENTS.AUTOPLAY, s));
428
+ if (site.autoplay) site.autoplay(s);
429
+ };
430
+ // The session is over (reason: stopped, done, profit, loss, refused, paused, ended, idle, hidden).
431
+ function endAutoplay(reason) {
432
+ if (!auto) return;
433
+ const last = auto;
434
+ auto = null;
435
+ clearTimer(last.idle);
436
+ const s = { active: false, left: last.left, played: last.played, reason };
437
+ toGame(event(SHELL_EVENTS.AUTOPLAY, s));
438
+ if (site.autoplay) site.autoplay(s);
439
+ }
440
+ // The session was played in just now: it has AUTOPLAY_IDLE_MS more before it
441
+ // ends as idle. Armed at the start and after every answer of the session's.
442
+ function touchAutoplay() {
443
+ if (!auto) return;
444
+ clearTimer(auto.idle);
445
+ auto.idle = setTimer(() => endAutoplay('idle'), AUTOPLAY_IDLE_MS);
446
+ }
447
+ // After a session play settled (or a session's ladder round closed): the
448
+ // stops, and the count once nothing of the session is on the table. A
449
+ // ladder of the session still open has its stake out and its payout
450
+ // unknown, so the stops wait for it to close (onAction calls back then):
451
+ // a loss limit at the session's total would otherwise end the run the
452
+ // moment its last stake left, with the round still on the table.
453
+ function autoplaySettled() {
454
+ if (!auto) return;
455
+ if (auto.openRound) return tellAutoplay();
456
+ if (auto.stopOnProfit && auto.net >= auto.stopOnProfit) return endAutoplay('profit');
457
+ if (auto.stopOnLoss && -auto.net >= auto.stopOnLoss) return endAutoplay('loss');
458
+ if (auto.left === 0) return endAutoplay('done');
459
+ tellAutoplay();
460
+ }
461
+ // Whether the player's responsible-gambling settings allow autoplay. Fail
462
+ // closed: only an explicit `true` allows it; the backend serialises
463
+ // rg.AutoplayAllowed as init.rg.autoplay.allowed (engine.go), and anything
464
+ // else — absent rg, absent autoplay, false, a non-boolean — is a no.
465
+ const autoplayAllowed = () => init && init.rg && init.rg.autoplay && init.rg.autoplay.allowed === true;
466
+
467
+ // The game asks for a session; the shell does NOT start one. It caps the
468
+ // terms and shows the player the shell's own confirmation. One player
469
+ // click inside the game (Spin) gives this window activation, so activation
470
+ // alone must never buy a run of bets: the session is created only from the
471
+ // player's real click on the shell's Start (confirmAutoplay), a control in
472
+ // the shell document the game cannot reach, cover or pre-fill. The ask
473
+ // itself is the player's act too: it needs their gesture, is refused while
474
+ // paused or ended, and cannot follow a Cancel for AUTOPLAY_RETRY_GAP_MS —
475
+ // a modal the game could reopen the instant it closes would hold the
476
+ // player hostage until they press Start.
477
+ function onAutoplayStart(id, body) {
478
+ if (ended) return sendRefusal(id, ERRORS.SESSION, 'the session has ended');
479
+ if (paused) return sendRefusal(id, ERRORS.BUSY, 'the game is paused');
480
+ if (!autoplayAllowed()) return sendRefusal(id, ERRORS.AUTOPLAY, word('autoplayNotAllowed'));
481
+ if (auto) return sendRefusal(id, ERRORS.AUTOPLAY, word('autoplayRunning'));
482
+ // A confirmation already on screen: the game cannot queue another.
483
+ if (pendingAuto) return sendRefusal(id, ERRORS.AUTOPLAY, word('autoplayRunning'));
484
+ // Asked again too soon after a Cancel: refused as the cancel was (the
485
+ // player just said no), with when it may be asked again — not as
486
+ // `pacing`, which the panel would show as a rate limit.
487
+ const since = now() - autoRefusedAt;
488
+ if (since < AUTOPLAY_RETRY_GAP_MS) return sendRefusal(id, ERRORS.AUTOPLAY, word('autoplayAskLater'), AUTOPLAY_RETRY_GAP_MS - since);
489
+ const g = gesture();
490
+ if (g) return sendRefusal(id, g.code, g.message);
491
+ // Cap the per-round stake at the coin's max bet, at the bet's own
492
+ // exposure cap and at the balance the shell last saw; cap the rounds;
493
+ // and require a loss limit — the shell applies a default when none is
494
+ // named, and never lets it exceed the total the session could stake (a
495
+ // game naming a limit the run cannot reach would show the player a
496
+ // floor in name only). The bet's cap is limits.maxStake[mode][bet]
497
+ // (SPEC §3.1, protocol.js maxStakeFor): the request names the bet, so
498
+ // the shell applies the figure the server wrote for it; a figure of 0
499
+ // means the bet takes nothing right now (no USD rate to price the cap
500
+ // in: the server refuses every stake), so the ask is refused rather
501
+ // than confirmed at a stake of nothing; a bet the session names no
502
+ // figure for is capped by the coin's max alone, and the server is the
503
+ // last word on a play above the cap (`limits`, which ends the session
504
+ // `refused`). The balance is advisory: the shell never hears of a
505
+ // deposit made in the site's wallet, so an empty or unknown balance
506
+ // caps nothing, and the server's own refusal of the first play ends a
507
+ // session the player cannot afford (`refused`, with the server's
508
+ // words).
509
+ const limits = (init && init.limits) || {};
510
+ const betMax = maxStakeFor(limits, body.mode, body.bet);
511
+ if (betMax === 0) return sendRefusal(id, ERRORS.UNAVAILABLE, 'this bet takes no stake right now');
512
+ const caps = [body.stake];
513
+ if (limits.max > 0) caps.push(limits.max);
514
+ if (betMax > 0) caps.push(betMax);
515
+ if (lastBalance > 0) caps.push(lastBalance);
516
+ const stake = round8(Math.min(...caps));
517
+ const rounds = Math.min(Math.floor(body.rounds), AUTOPLAY_MAX_ROUNDS);
518
+ const total = round8(stake * rounds);
519
+ const stopOnProfit = body.stopOnProfit > 0 ? round8(body.stopOnProfit) : 0;
520
+ const stopOnLoss = round8(Math.min(body.stopOnLoss > 0 ? body.stopOnLoss : stake * AUTOPLAY_DEFAULT_LOSS_MULT, total));
521
+ // The bet the run is for is part of the terms: the grant is for that
522
+ // bet and no other (gate), and the player sees it on the confirmation.
523
+ const terms = { mode: body.mode, bet: body.bet, rounds, stake, total, stopOnProfit, stopOnLoss };
524
+ // Unanswered by the player in time, the confirmation goes and the game
525
+ // is refused — before the SDK gives the request up on its own.
526
+ pendingAuto = { id, terms, timer: setTimer(() => dismissAutoplay(), AUTOPLAY_CONFIRM_MS) };
527
+ // The shell shows the player exactly what they are approving. No answer
528
+ // to the game, and no session, until they press Start or cancel.
529
+ if (site.autoplayConfirm) site.autoplayConfirm({ ...terms });
530
+ }
531
+
532
+ // The player pressed Start on the shell's own confirmation (a real,
533
+ // trusted click on shell DOM outside the game frame): the session begins
534
+ // now, on exactly the terms shown.
535
+ function confirmAutoplay() {
536
+ if (!pendingAuto) return;
537
+ const { id, terms, timer } = pendingAuto;
538
+ pendingAuto = null;
539
+ clearTimer(timer);
540
+ auto = { mode: terms.mode, bet: terms.bet, rounds: terms.rounds, left: terms.rounds, played: 0, stake: terms.stake, stopOnProfit: terms.stopOnProfit, stopOnLoss: terms.stopOnLoss, net: 0, openRound: null, idle: null };
541
+ touchAutoplay();
542
+ const session = { mode: auto.mode, bet: auto.bet, rounds: auto.rounds, stake: auto.stake };
543
+ if (auto.stopOnProfit) session.stopOnProfit = auto.stopOnProfit;
544
+ if (auto.stopOnLoss) session.stopOnLoss = auto.stopOnLoss;
545
+ toGame(answer(id, { session }));
546
+ tellAutoplay();
547
+ }
548
+
549
+ // The player cancelled or closed the confirmation without starting: no
550
+ // session, and the game's request is refused so it can reset. The game
551
+ // may not ask again before AUTOPLAY_RETRY_GAP_MS.
552
+ function cancelAutoplay() {
553
+ if (!pendingAuto) return;
554
+ const { id, timer } = pendingAuto;
555
+ pendingAuto = null;
556
+ clearTimer(timer);
557
+ autoRefusedAt = now();
558
+ sendRefusal(id, ERRORS.AUTOPLAY, word('autoplayCancelled'));
559
+ }
560
+
561
+ // The shell takes the confirmation down itself (its deadline passed, the
562
+ // reality check, the session ended): the game is refused as on a cancel,
563
+ // and the dialog is closed for the player.
564
+ function dismissAutoplay() {
565
+ if (!pendingAuto) return;
566
+ cancelAutoplay();
567
+ if (site.autoplayDismiss) site.autoplayDismiss();
568
+ }
569
+
570
+ // ---- the gate ---------------------------------------------------------------------
571
+ // Whether the player's gesture is behind a request that arrived now: their
572
+ // activation is live in this window, the focus is inside the game frame
573
+ // (so the activation is not the site page's) and got there by the
574
+ // player's own act (frameFocused: not by the game's script), and nothing
575
+ // was pressed or typed on the shell's own DOM, or on the site's page
576
+ // around it, within the activation's lifetime (so it is not the shell's
577
+ // or the site's). Returns null when it is, else the refusal.
578
+ function gesture() {
579
+ const live = activation();
580
+ if (live === null) return { code: ERRORS.CLICK, message: word('browserNoClick') };
581
+ if (!live) return { code: ERRORS.CLICK, message: word('needsClick') };
582
+ if (now() - lastShellInput < ACTIVATION_TTL_MS) return { code: ERRORS.CLICK, message: word('needsClick') };
583
+ if (!focusInGame()) return { code: ERRORS.CLICK, message: word('needsClick') };
584
+ if (stolenFocus) return { code: ERRORS.CLICK, message: word('needsClick') };
585
+ return null;
586
+ }
587
+
588
+ // What one gesture may still buy: the request's own kind within its gap and
589
+ // its burst. Records the request when it may go. Returns null to allow,
590
+ // else the `pacing` refusal with when to try again.
591
+ function burst(list, minGap, max) {
592
+ const t = now();
593
+ const kept = list.filter((at) => t - at < CLICK_BURST_WINDOW_MS);
594
+ const last = kept.at(-1);
595
+ if (last !== undefined && t - last < minGap) return { kept, e: { code: ERRORS.PACING, message: word('tooManyBets'), retryAfterMs: minGap - (t - last) } };
596
+ if (kept.length >= max) return { kept, e: { code: ERRORS.PACING, message: word('tooManyBets'), retryAfterMs: kept[0] + CLICK_BURST_WINDOW_MS - t } };
597
+ kept.push(t);
598
+ return { kept, e: null };
599
+ }
600
+
601
+ // Whether a play or a ladder action may go now: the player's gesture, or
602
+ // the session — a session play on the granted bet within the stake and
603
+ // the count, or an action on the session's own open round. Returns null
604
+ // to allow, else the refusal.
605
+ function gate(type, body) {
606
+ if (auto) {
607
+ if (type === REQUESTS.PLAY) {
608
+ if (body.mode !== auto.mode || body.bet !== auto.bet) return { code: ERRORS.AUTOPLAY, message: word('autoplayOtherBet') };
609
+ if (body.amount > auto.stake) return { code: ERRORS.AUTOPLAY, message: word('autoplayOverStake') };
610
+ if (auto.left === 0) return { code: ERRORS.AUTOPLAY, message: word('autoplayNoRounds') };
611
+ return null;
612
+ }
613
+ // A step or the cash-out of the ladder the session opened: the run
614
+ // the player approved. Any other round — one resumed from an earlier
615
+ // page, one the player opened by hand before the session — is the
616
+ // player's to move, with their gesture, below.
617
+ if (auto.openRound && body.roundId === auto.openRound) return null;
618
+ }
619
+ const g = gesture();
620
+ if (g) return g;
621
+ if (type === REQUESTS.PLAY) {
622
+ const r = burst(clickPlays, CLICK_MIN_GAP_MS, CLICK_BURST_MAX);
623
+ clickPlays = r.kept;
624
+ return r.e;
625
+ }
626
+ // A cash-out on the player's gesture is not paced: it only returns
627
+ // money, and a round has one.
628
+ if (body.action !== 'advance') return null;
629
+ // An advance moves nothing out of the wallet, but it risks what is on
630
+ // the table, so one gesture buys a bounded run of steps, not the ladder.
631
+ const r = burst(clickAdvances, ADVANCE_MIN_GAP_MS, ADVANCE_BURST_MAX);
632
+ clickAdvances = r.kept;
633
+ return r.e;
634
+ }
635
+
636
+ // After a money answer: is the round over, and what does the header say?
637
+ // `playSession` is the session the play was sent in (null for an action,
638
+ // and for a play outside a session).
639
+ function afterMoney(corr, generation, amount, body, playSession) {
640
+ const round = body.round;
641
+ const known = typeof body.balance === 'number' && body.balance >= 0 ? body.balance : null;
642
+ if (known !== null) lastBalance = known; // the stake cap of the next session follows the balance
643
+ if (!round || typeof round.id !== 'string') return;
644
+ const was = opened.get(round.id);
645
+ // The round is the running session's when that session's own play
646
+ // opened it, or when it carries that session's mark from its opening
647
+ // (a step is not a session play, but the round keeps its mark, or the
648
+ // ladder's close would never reach the session's result and the session
649
+ // would never end `done`). The session is matched by identity: a play
650
+ // whose answer arrives after its session ended, and a round of an
651
+ // ended session cashed out while a later one runs, belong to no
652
+ // session — their payout joins no result, and they hold no stop back.
653
+ const session = auto !== null && (playSession === auto || !!(was && was.auto === auto)) ? auto : null;
654
+ if (round.state === 'open') {
655
+ opened.set(round.id, { corr, amount, auto: session });
656
+ if (session) session.openRound = round.id;
657
+ report({ type: 'opened', round });
658
+ return;
659
+ }
660
+ opened.delete(round.id);
661
+ const payout = typeof round.payout === 'number' ? round.payout : 0;
662
+ // A session's round closed: its payout joins the session's result
663
+ // (the stake left it when the play went).
664
+ if (session) {
665
+ session.net += payout;
666
+ if (session.openRound === round.id) session.openRound = null;
667
+ }
668
+ const end = { corr, generation, amount, outcome: 'settled', balance: known, payout, roundId: round.id };
669
+ reveals.set(round.id, { end, round, timer: setTimer(() => settleSite(round.id), REVEAL_DEADLINE_MS) });
670
+ }
671
+
672
+ async function onPlay(id, body) {
673
+ // No free round: a stake of nothing is refused here as the server
674
+ // refuses it (code 4, `limits`), whatever the server's version. The
675
+ // protocol gate (readGameMessage) drops such a play before it gets
676
+ // here; this is the money path's own word on it.
677
+ if (!(body.amount > 0)) return sendRefusal(id, ERRORS.LIMITS, 'a stake is required: a round is never played for free', undefined, undefined, undefined, { id, type: REQUESTS.PLAY, body });
678
+ // The session the play goes in, if any. Held by identity: the answer
679
+ // may arrive after this session ended, or after a later one began, and
680
+ // then it touches, settles or ends nothing of the session running.
681
+ const session = auto;
682
+ if (session) {
683
+ session.left--;
684
+ session.played++;
685
+ session.net -= body.amount;
686
+ }
687
+ const generation = newGeneration();
688
+ const corr = generation;
689
+ site.roundStart({ corr, generation, amount: body.amount });
690
+ const r = await rgs.play(generation, body);
691
+ const inSession = session !== null && auto === session;
692
+ if (r.ok) {
693
+ afterMoney(corr, generation, body.amount, r.body, session);
694
+ if (inSession) touchAutoplay(); // the session was played in: its idle clock starts over
695
+ toGame(answer(id, moneyForGame(r.body)));
696
+ // The answer first, then the session's state: a game reads the
697
+ // round before it learns the session moved on.
698
+ if (inSession) autoplaySettled();
699
+ return;
700
+ }
701
+ // Refused: nothing was charged, the header goes back. Unknown: the
702
+ // site re-reads the wallet.
703
+ site.roundEnd({ corr, generation, amount: body.amount, outcome: r.unknown ? 'unknown' : 'rollback', balance: null, payout: 0 });
704
+ refused(id, r.error, { id, type: REQUESTS.PLAY, body });
705
+ // A session does not run through refusals (no balance, limits, an
706
+ // outage): it stops, as the Originals' autobet stops.
707
+ if (inSession) endAutoplay('refused');
708
+ }
709
+
710
+ async function onAction(id, body) {
711
+ const open = opened.get(body.roundId);
712
+ const generation = newGeneration();
713
+ const corr = open ? open.corr : generation;
714
+ const amount = open ? open.amount : 0;
715
+ const r = await rgs.action(generation, body);
716
+ if (r.ok) {
717
+ afterMoney(corr, generation, amount, r.body, null);
718
+ // The round is the running session's own (marked with that very
719
+ // session): a step of its ladder is the session being played, and
720
+ // the ladder's close may end the session. A round of a session that
721
+ // has ended, moved by the player's hand, is nobody's.
722
+ const ofSession = auto !== null && !!open && open.auto === auto;
723
+ if (ofSession) touchAutoplay();
724
+ toGame(answer(id, moneyForGame(r.body)));
725
+ if (ofSession && !opened.has(body.roundId)) autoplaySettled();
726
+ return;
727
+ }
728
+ // A refused action leaves the round as it was; only an unknown one
729
+ // needs the site to look at the wallet.
730
+ if (r.unknown) site.roundEnd({ corr, generation, amount, outcome: 'unknown', balance: null, payout: 0 });
731
+ refused(id, r.error, { id, type: REQUESTS.ACTION, body });
732
+ }
733
+
734
+ // A play or a ladder action: serialised (one in flight), refused while
735
+ // paused, and gated — every one of them needs the player's click or the
736
+ // session (a cash-out too: a round the player left open is theirs to come
737
+ // back to — the server holds it with the session — never the game's to
738
+ // close for them at a moment of its choosing).
739
+ async function onMoney(id, type, body) {
740
+ const request = { id, type, body };
741
+ if (paused) return sendRefusal(id, ERRORS.BUSY, 'the game is paused', undefined, undefined, undefined, request);
742
+ if (moneyBusy) return sendRefusal(id, ERRORS.BUSY, 'another bet is still with the server', undefined, undefined, undefined, request);
743
+ const e = gate(type, body);
744
+ if (e) return sendRefusal(id, e.code, e.message, e.retryAfterMs, undefined, undefined, request);
745
+ moneyBusy = true;
746
+ try {
747
+ await (type === REQUESTS.PLAY ? onPlay(id, body) : onAction(id, body));
748
+ } finally {
749
+ moneyBusy = false;
750
+ }
751
+ }
752
+
753
+ async function onRequest(msg) {
754
+ const { id, type, body } = msg;
755
+ switch (requestKind(type)) {
756
+ case 'autoplay-start':
757
+ return onAutoplayStart(id, body);
758
+ case 'autoplay-stop':
759
+ // Always taken: stopping is never refused, whoever asks.
760
+ endAutoplay('stopped');
761
+ return toGame(answer(id, { stopped: true }));
762
+ case 'money':
763
+ return onMoney(id, type, body);
764
+ case 'save': {
765
+ // A state write moves no money and the game chooses its content
766
+ // whenever it saves, so a click would protect nothing; an honest
767
+ // game saves at boot (BAD CAT migrates its profile) and after
768
+ // the reveal that followed the click, long after it. Answered
769
+ // without a click; the server paces and sizes it (409 stale,
770
+ // 413 too large).
771
+ const r = await rgs.putState(body);
772
+ return r.ok ? toGame(answer(id, r.body)) : refused(id, r.error, { id, type, body });
773
+ }
774
+ case 'read': {
775
+ // The game never sees seed material (SPEC §2); the receipt is the dialog's.
776
+ const r = type === REQUESTS.ROUND ? await rgs.round(body.roundId) : await rgs.getState();
777
+ return r.ok ? toGame(answer(id, type === REQUESTS.ROUND ? roundForGame(r.body) : r.body)) : refused(id, r.error, { id, type, body });
778
+ }
779
+ default:
780
+ // Deny by default: a type that is not a read, a save, a money
781
+ // path or the session's own is refused. readGameMessage already
782
+ // drops a type the protocol does not know; this is the shell's
783
+ // own last word, so a new request type is never run ungated.
784
+ return sendRefusal(id, ERRORS.INVALID, 'unknown request');
785
+ }
786
+ }
787
+
788
+ function onEvent(msg) {
789
+ const b = msg.body;
790
+ switch (msg.type) {
791
+ // The game's page asks for its config (again on every page it
792
+ // loads, and until answered).
793
+ case GAME_EVENTS.HELLO: return toGame(event(SHELL_EVENTS.INIT, init));
794
+ case GAME_EVENTS.READY: return site.ready();
795
+ case GAME_EVENTS.REVEALED: return settleSite(b.roundId);
796
+ case GAME_EVENTS.FAIRNESS:
797
+ // Dropped while paused, like a bet; while the autoplay
798
+ // confirmation is up (nothing of the shell's is popped over the
799
+ // terms the player is reading); and paced, since the game is
800
+ // untrusted and the dialog is the player's.
801
+ if (paused || pendingAuto || now() - fairnessAt < FAIRNESS_MIN_GAP_MS) return;
802
+ fairnessAt = now();
803
+ return site.fairness(b.roundId);
804
+ case GAME_EVENTS.LOADING: return site.loading(b.group, b.loaded, b.total);
805
+ case GAME_EVENTS.ERROR:
806
+ if (errorsCounted++ < ERROR_REPORTS_PER_LAUNCH) rgs.telemetry('error');
807
+ return site.error(b.message);
808
+ }
809
+ }
810
+
811
+ function onMessage(e) {
812
+ if (e.source !== gameWindow()) return;
813
+ if (gameOrigin !== '*' && e.origin !== gameOrigin) return;
814
+ const msg = readGameMessage(e.data);
815
+ if (!msg) return;
816
+ if (msg.kind === 'req') onRequest(msg).catch(() => sendRefusal(msg.id, ERRORS.SERVER, 'the shell failed', undefined, undefined, undefined, { id: msg.id, type: msg.type, body: msg.body }));
817
+ else onEvent(msg);
818
+ }
819
+
820
+ self.addEventListener('message', onMessage);
821
+ return {
822
+ /** Read-only router observations for shell tools such as sandbox scenarios. */
823
+ observe,
824
+ /** The current authenticate answer, including refreshed limits and balance. */
825
+ init: () => init,
826
+ /** Whether a round is open now (a ladder in play, the one the session resumed,
827
+ * or a bet still with the server): the fairness dialog holds the seed rotation,
828
+ * which the server refuses meanwhile, until it ends. */
829
+ roundOpen: () => moneyBusy || opened.size > 0,
830
+ /** The autoplay session as the shell's control shows it. */
831
+ autoplay: () => autoState(),
832
+ /** The player pressed Start on the shell's confirmation: the session begins now. Wire
833
+ * this to a real click on the shell's own control, never to a message from the game. */
834
+ confirmAutoplay: () => confirmAutoplay(),
835
+ /** The player cancelled or closed the confirmation: no session. */
836
+ cancelAutoplay: () => cancelAutoplay(),
837
+ /** The shell's Stop (its own control, outside the game frame): ends the session at once. */
838
+ autoplayStop: () => endAutoplay('stopped'),
839
+ /** A trusted input the player made outside the game frame: on the shell's own DOM, or on
840
+ * the site's page around the shell (page.js listens on both documents when the shell is a
841
+ * same-origin frame of the site; the site may also report a press of its own as `site-input`
842
+ * on its bridge, boot.js). For ACTIVATION_TTL_MS the window's activation is that press's,
843
+ * not the game's, and no play or action is taken on it — so a bundle that stole the focus
844
+ * back by script rides nothing from a click or a key in the chat or header. */
845
+ shellInput: () => { lastShellInput = now(); },
846
+ /** The focus entered the game frame (the page reads it off document.activeElement). The
847
+ * player's own focusing — a click or a tap in the frame — comes with the activation it
848
+ * granted already live, no shell or site press within the activation's lifetime, no
849
+ * scripted focus of the game's within it either, and not within the lifetime of the focus
850
+ * last leaving the frame (`sinceExitMs`, as the page saw it: a bundle sees its own window
851
+ * blur and can take the focus straight back with the activation of the player's last key
852
+ * in the frame — the Tab that took them out — still live, and the shell cannot tell that
853
+ * from a click back; so a re-entry within ACTIVATION_TTL_MS of leaving is the player's
854
+ * only via a Tab the page saw). A Tab into the frame (`viaTab`: the page saw the player's
855
+ * Tab key just before the entry) is the player's though the Tab itself was a key on the
856
+ * shell's or the site's document, and whatever the time since leaving — it still needs
857
+ * the activation live and no scripted entry within the lifetime. Anything else is the
858
+ * game's script (`window.focus()` from inside the frame): no play or action rides on that
859
+ * focus until the player focuses the frame themselves. An entry with no activation opens
860
+ * the ACTIVATION_TTL_MS window in which no entry is the player's; one merely shadowed by
861
+ * a press, or merely quick after an exit, does not (its cost ends with the press's
862
+ * activation, or ACTIVATION_TTL_MS after the exit). Fail closed on the time: only a number
863
+ * of milliseconds at or past the lifetime says the exit is long enough ago; anything else
864
+ * (NaN, a string) is a quick entry. No time at all (the default) is no exit yet: the frame
865
+ * was never seen focused before. Returns whether the focus was the player's; the page
866
+ * blurs a stolen one straight back out of the frame. */
867
+ frameFocused: (viaTab = false, sinceExitMs = Infinity) => {
868
+ const noActivation = activation() !== true;
869
+ const shadowed = viaTab !== true && now() - lastShellInput < ACTIVATION_TTL_MS;
870
+ const quick = viaTab !== true && !(typeof sinceExitMs === 'number' && sinceExitMs >= ACTIVATION_TTL_MS);
871
+ if (noActivation) stolenAt = now();
872
+ stolenFocus = noActivation || shadowed || quick || now() - stolenAt < ACTIVATION_TTL_MS;
873
+ return !stolenFocus;
874
+ },
875
+ /** The page is hidden (visibilitychange): a session is for a player who is looking,
876
+ * so it ends; a confirmation nobody can see is taken down. */
877
+ hidden: () => {
878
+ endAutoplay('hidden');
879
+ dismissAutoplay();
880
+ },
881
+ /** Site → game notices (sound, speed, rules, pause, resume, ended). */
882
+ tell: (type, body) => {
883
+ if (type === SHELL_EVENTS.PAUSE) {
884
+ paused = true;
885
+ // The reality check is the player's moment to stop: autoplay does not
886
+ // outlive it, and no confirmation stays up over it.
887
+ endAutoplay('paused');
888
+ dismissAutoplay();
889
+ }
890
+ if (type === SHELL_EVENTS.RESUME) paused = false;
891
+ toGame(event(type, body));
892
+ },
893
+ balanceChanged: (balance) => {
894
+ if (typeof balance === 'number' && Number.isFinite(balance) && balance >= 0) lastBalance = balance;
895
+ toGame(event(SHELL_EVENTS.BALANCE, { balance }));
896
+ },
897
+ refreshInit: (nextInit) => {
898
+ init = nextInit;
899
+ if (typeof nextInit?.balance === 'number' && Number.isFinite(nextInit.balance) && nextInit.balance >= 0) lastBalance = nextInit.balance;
900
+ },
901
+ stop: () => {
902
+ self.removeEventListener('message', onMessage);
903
+ for (const id of [...reveals.keys()]) settleSite(id);
904
+ },
905
+ /** The page is going (pagehide): the session ends as hidden, and a round still open
906
+ * is counted as abandoned, once. A page that hides, comes back from the cache and
907
+ * hides again on the same round counts it once; a round opened in between counts. */
908
+ unload: () => {
909
+ endAutoplay('hidden');
910
+ dismissAutoplay();
911
+ const fresh = [...opened.keys()].filter((id) => !abandoned.has(id));
912
+ if (fresh.length === 0) return;
913
+ for (const id of fresh) abandoned.add(id);
914
+ rgs.telemetry('abandon');
915
+ },
916
+ };
917
+ }