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,809 @@
1
+ // The shell page's chrome: the frame, the loader, the failure panel, the
2
+ // reality check and the Provably Fair dialog. A file of its own so the
3
+ // shell's policy allows no inline script at all.
4
+ import { API, boot } from './boot.js';
5
+ import { CLIENT_SEED_MAX, betFor, chars, compare, drawLines, expectation, outcome, rotationDifferences, roundLine, withRevealed } from './fair.js';
6
+ import { loadPack, play, sha256Hex } from './engine-v1.js';
7
+ import { STRINGS, fill, wordsFor } from './strings.js';
8
+ const $ = (id) => document.getElementById(id);
9
+ const frame = $('game');
10
+ const groups = new Map();
11
+ // How long after the autoplay confirmation opens its Start stays dead. The
12
+ // game chose the moment the dialog appeared and knows where Start renders, so
13
+ // a player already tapping Spin where Start will be must not land the next tap
14
+ // on it: a press counts only once it began after this arming — the input
15
+ // protection browsers give their own permission prompts.
16
+ export const AUTOPLAY_CONFIRM_ARM_MS = 1000;
17
+ // How often the page reads where the focus is (watchGameFocus): browsers fire
18
+ // no event in this document when the focus moves into the game frame, so the
19
+ // entry is read off document.activeElement — at once after anything that
20
+ // moves the focus, and at this pace besides.
21
+ export const FOCUS_POLL_MS = 50;
22
+ // How long after the player's Tab key (on this document or the site's) a focus
23
+ // entering the game frame counts as that Tab's: sequential focus navigation
24
+ // moves the focus in the key's own task, so the entry is read within a tick
25
+ // of it; a scripted move of the game's lands at a time of the game's.
26
+ export const TAB_ENTRY_MS = 100;
27
+ // When the player last pressed Tab outside the game frame (onShellInput).
28
+ let lastTabAt = -Infinity;
29
+ // The trusted input events that are the player's press or key on a document
30
+ // (onShellInput, watchGameFocus): the ones that grant activation.
31
+ const INPUT_TYPES = Object.freeze(['pointerdown', 'pointerup', 'mousedown', 'keydown', 'touchend']);
32
+ // The confirmation's arming: when Start became live, and when the press on it
33
+ // began (a pointer down or the key that activates a button).
34
+ const ap = { armedAt: Infinity, pressAt: -Infinity };
35
+ const NONCE = /^(0|[1-9]\d{0,19})$/;
36
+ const HEX64 = /^[0-9a-f]{64}$/;
37
+ const VERIFY_FIELDS = ['fair-v-server', 'fair-v-client', 'fair-v-nonce'];
38
+ const el = (tag, text, cls) => { const e = document.createElement(tag); e.textContent = text; if (cls) e.className = cls; return e; };
39
+
40
+ /**
41
+ * The Provably Fair dialog: the House Originals' modal, one window in the
42
+ * game page with two tabs, in the player's language (strings.js).
43
+ *
44
+ * Seeds: the client seed (it follows the server's until the player types;
45
+ * then it is theirs and goes with the next rotation), the active and the
46
+ * next server seed hashes, the nonce, Rotate seed pair, and the pairs
47
+ * revealed so far (the server's last retired pair, then each rotation's),
48
+ * each one a step into the Verify tab. The bets go on behind the dialog, as
49
+ * behind the Originals' modal: Rotate follows the round and the pair is read
50
+ * again when one ends, so the nonce shown is the last used.
51
+ *
52
+ * Verify: the revealed server seed, the client seed, the nonce and (when the
53
+ * game has more than one) the mode in; the result computed here, in the
54
+ * browser (engine-v1.js), against the maths pack fetched from this origin,
55
+ * whose recomputed hash must be the version asked for or nothing is shown.
56
+ * Opened on a round it fills the fields from the receipt and verifies at
57
+ * once; a round whose pair is still active offers the rotation that reveals
58
+ * it. The game is the shell's and the version is the receipt's, else the one
59
+ * a revealed pair played on when the server names it, else the one the shell
60
+ * is running — named on the result line; neither is typed.
61
+ *
62
+ * The commitment is checked, not assumed: a revealed seed must hash to the
63
+ * hash the receipt (or the Seeds tab) showed for it, and a rotation must
64
+ * promote the next hash shown and reveal a seed that hashes to the hash
65
+ * shown — the hashes held here from before the rotation, which the server
66
+ * cannot rewrite. A receipt that cannot be read gives no verdict either way.
67
+ *
68
+ * Every call goes through the engine client under the session the shell
69
+ * holds: the player's site token is never on this page, and the game frame
70
+ * never touches the dialog — its `fairness.open` may show a round, never
71
+ * reset what the player typed or started here. Each rotation goes to the
72
+ * server under a generation of its own; an answer lost on the way is read
73
+ * back from the pair (a new hash means it committed) and otherwise keeps
74
+ * that generation for the next press, which the server answers as it
75
+ * answered the first, never rotating twice; a client seed typed meanwhile
76
+ * stays in the field and goes with the press after that. Nothing from a
77
+ * receipt or a pack is inserted as HTML.
78
+ */
79
+ const fair = {
80
+ rgs: null, ctx: null, roundId: null, receipt: null, seeds: null, revealed: [], packs: new Map(),
81
+ busy: false, tab: 'seeds', seedTouched: false, roundOpen: false,
82
+ words: STRINGS.en,
83
+ // `token` cancels what an earlier opening of the dialog was still doing;
84
+ // `check` cancels an earlier receipt or verification once the player edits;
85
+ // `seedsGen` cancels a read of the pair that a rotation has overtaken;
86
+ // `seedsReady` is whether a pair the server answered since this opening
87
+ // began is on screen — the opening's read, or a rotation's answer that
88
+ // overtook it (Rotate waits for it, so a late answer can never show a
89
+ // retired hash as the active one); `verifying` counts the receipts and
90
+ // verifications in flight.
91
+ token: 0, check: 0, seedsGen: 0, seedsReady: false, verifying: 0,
92
+ // The rotation waiting for the server, or whose answer was lost: { clientSeed, generation }.
93
+ pending: null,
94
+ // The bet history's request this opening answers, to be cancelled by name.
95
+ requestId: null,
96
+ // A revealed pair carried into Verify: the hash it was listed under and
97
+ // the maths version its rounds were drawn on (when the server named one).
98
+ commitment: null, version: null,
99
+ // The rotation chain found broken by the last rotation, if any (a word key).
100
+ chain: null,
101
+
102
+ /** The page's language: every word of the dialog is set from it. */
103
+ setLocale(lang) {
104
+ const w = this.words = wordsFor(lang);
105
+ const text = (id, t) => { $(id).textContent = t; };
106
+ text('fair-title', w.provablyFair);
107
+ text('fair-open', w.provablyFair);
108
+ $('fair-tabs').setAttribute('aria-label', w.provablyFair);
109
+ $('fair-x').setAttribute('aria-label', w.close);
110
+ text('fair-tab-seeds', w.seedsTab);
111
+ text('fair-tab-verify', w.verifyTab);
112
+ text('fair-l-client', w.clientSeed);
113
+ text('fair-l-hash', w.serverSeedHashed);
114
+ text('fair-l-next', w.nextServerSeedHashed);
115
+ text('fair-l-nonce', w.nonce);
116
+ text('fair-rotate', w.rotateSeedPair);
117
+ text('fair-reveal', w.rotateSeedPair);
118
+ text('fair-l-v-server', w.serverSeedRevealed);
119
+ text('fair-l-v-client', w.clientSeed);
120
+ text('fair-l-v-nonce', w.nonce);
121
+ text('fair-l-v-mode', w.mode);
122
+ text('fair-verify-run', w.verifyRun);
123
+ // The autoplay control's Stop (the shell's, over the game) speaks the same language.
124
+ text('auto-stop', w.stop);
125
+ this.setRoundOpen(this.roundOpen);
126
+ this.showRevealed();
127
+ },
128
+
129
+ /**
130
+ * Opens the dialog on the player's seeds, or on a round's receipt (the
131
+ * Verify tab). A player's gesture (the site's bar, the bet history, the
132
+ * shell's button) opens it afresh. The game's `fairness.open`
133
+ * (ctx.fromGame) on a dialog already open, or busy rotating, may at most
134
+ * show its round — and only when the player has typed nothing and
135
+ * nothing is in flight; otherwise it is ignored.
136
+ */
137
+ async open(rgs, roundId, ctx) {
138
+ this.rgs = rgs;
139
+ this.ctx = ctx;
140
+ const dlg = $('fair');
141
+ if (ctx.fromGame && (dlg.open || this.busy)) {
142
+ if (!roundId || this.seedTouched || this.verifying > 0 || VERIFY_FIELDS.some((id) => $(id).value !== '')) return;
143
+ this.roundId = roundId;
144
+ this.show('verify');
145
+ return this.loadRound(this.token);
146
+ }
147
+ this.roundId = roundId || null;
148
+ this.requestId = ctx.requestId || null;
149
+ this.receipt = null;
150
+ this.chain = null;
151
+ if (!ctx.fromGame) this.seedTouched = false;
152
+ const token = ++this.token;
153
+ this.note('');
154
+ this.resetVerify();
155
+ this.show(this.roundId ? 'verify' : 'seeds');
156
+ // The pair is read afresh on every opening: until it answers, nothing
157
+ // of an earlier opening shows and Rotate waits.
158
+ this.seeds = null;
159
+ this.seedsReady = false;
160
+ this.clearSeeds();
161
+ this.setRoundOpen(ctx.roundOpen());
162
+ if (!dlg.open) {
163
+ dlg.showModal();
164
+ // The window takes the focus, as the Originals' modal does: no
165
+ // control lights its ring before the player reaches for one.
166
+ dlg.focus();
167
+ }
168
+ await Promise.all([this.loadSeeds(token), this.roundId ? this.loadRound(token) : this.loadModes(token)]);
169
+ },
170
+ /** The bet history took its request back: an open still on its way is dropped. */
171
+ cancel(requestId) {
172
+ if (!requestId || this.requestId !== requestId) return;
173
+ this.requestId = null;
174
+ this.token++;
175
+ this.check++;
176
+ if ($('fair').open && !this.receipt) $('fair').close();
177
+ },
178
+ show(tab) {
179
+ this.tab = tab;
180
+ for (const name of ['seeds', 'verify']) {
181
+ $(`fair-tab-${name}`).setAttribute('aria-selected', name === tab ? 'true' : 'false');
182
+ $(`fair-${name}`).hidden = name !== tab;
183
+ }
184
+ },
185
+ // Whether a round is open decides the rotation: refused by the server
186
+ // while one is, so the button says so instead of failing.
187
+ setRoundOpen(open) {
188
+ this.roundOpen = open;
189
+ this.lock(this.busy);
190
+ $('fair-seeds-note').textContent = open ? this.words.rotateAfterRound : this.words.fairSeedsNote;
191
+ $('fair-reveal').hidden = open || !(this.receipt && !this.receipt.serverSeed);
192
+ },
193
+ /** A round started or ended behind the dialog: Rotate follows it, and the pair is read again when one ends. */
194
+ roundChanged(ended) {
195
+ if (!$('fair').open || !this.ctx) return;
196
+ this.setRoundOpen(this.ctx.roundOpen());
197
+ if (ended) this.loadSeeds(this.token);
198
+ },
199
+ lock(on) {
200
+ $('fair-rotate').disabled = on || this.roundOpen || !this.seedsReady;
201
+ $('fair-reveal').disabled = on || !this.seedsReady;
202
+ $('fair-verify-run').disabled = on;
203
+ },
204
+ // A word on the tab the player is looking at: the Seeds tab's status line, or the result line.
205
+ say(text, tone) {
206
+ if (this.tab === 'verify') this.result(text, tone);
207
+ else this.note(text);
208
+ },
209
+ // The Seeds tab's status line is grey, whatever it says: red and green
210
+ // belong to the result line alone (the Originals' rule).
211
+ note(text) {
212
+ const n = $('fair-status');
213
+ n.textContent = text;
214
+ n.className = 'fr-note';
215
+ },
216
+ result(text, tone = '', lines = []) {
217
+ const r = $('fair-result');
218
+ r.textContent = text;
219
+ r.className = 'fr-result' + (tone ? ' ' + tone : '');
220
+ $('fair-draws').replaceChildren(...lines.map((t) => el('li', t)));
221
+ },
222
+
223
+ // ---- Seeds ----------------------------------------------------------------
224
+ clearSeeds() {
225
+ $('fair-hash').value = '';
226
+ $('fair-next').value = '';
227
+ $('fair-nonce').textContent = '0';
228
+ if (!this.seedTouched) $('fair-client').value = '';
229
+ },
230
+ async loadSeeds(token) {
231
+ const gen = this.seedsGen;
232
+ const r = await this.rgs.seeds();
233
+ if (token !== this.token || gen !== this.seedsGen) return;
234
+ if (r.ok) return this.showSeeds(r.body);
235
+ // Answered, if not well: the opening's read is over and Rotate may
236
+ // go (a failed read does not take the rotation away, as in Dice).
237
+ this.seedsReady = true;
238
+ this.lock(this.busy);
239
+ this.note(this.words.seedInitFailed);
240
+ },
241
+ // A pair the server just answered goes on screen — every caller hands
242
+ // one: the opening's read, the read when a round ends, a rotation's
243
+ // answer, the read-back after a lost one — so Rotate may go on it, even
244
+ // when the read of an opening the answer overtook is dropped later.
245
+ showSeeds(s) {
246
+ this.seedsReady = true;
247
+ this.lock(this.busy);
248
+ this.seeds = s;
249
+ $('fair-hash').value = typeof s.serverSeedHash === 'string' ? s.serverSeedHash : '';
250
+ $('fair-next').value = typeof s.nextServerSeedHash === 'string' ? s.nextServerSeedHash : '';
251
+ $('fair-nonce').textContent = String(Number.isInteger(s.nonce) ? s.nonce : 0);
252
+ // Until the player touches the field it follows the server; after that
253
+ // it is theirs, or the seed they typed would be wiped by this update.
254
+ if (!this.seedTouched) $('fair-client').value = typeof s.clientSeed === 'string' ? s.clientSeed : '';
255
+ this.revealed = withRevealed(this.revealed, s.previous);
256
+ this.showRevealed();
257
+ },
258
+ showRevealed() {
259
+ const box = $('fair-revealed');
260
+ const w = this.words;
261
+ if (this.revealed.length === 0) return box.replaceChildren(el('p', w.noRevealedSeeds, 'fr-note'));
262
+ box.replaceChildren(el('h3', w.revealedPairs), ...this.revealed.map((pair) => {
263
+ const b = el('button', '', 'fr-rev');
264
+ b.type = 'button';
265
+ b.append(el('span', pair.serverSeed, 'fr-rev-seed'), el('span', `${w.nonce} 1…${pair.nonce}`, 'fr-rev-meta'));
266
+ b.addEventListener('click', () => this.useRevealed(pair));
267
+ return b;
268
+ }));
269
+ },
270
+ // A revealed pair is carried into the Verify tab whole, with the hash it
271
+ // was listed under and the maths version its rounds were drawn on; the
272
+ // nonce the player supplies, there are many in a pair.
273
+ useRevealed(pair) {
274
+ this.invalidate(true);
275
+ $('fair-v-server').value = pair.serverSeed;
276
+ $('fair-v-client').value = pair.clientSeed;
277
+ $('fair-v-nonce').value = '';
278
+ this.commitment = HEX64.test(pair.serverSeedHash) ? pair.serverSeedHash : null;
279
+ this.version = pair.mathHashes.length === 1 ? pair.mathHashes[0] : null;
280
+ $('fair-v-note').textContent = pair.mathHashes.length > 1 ? this.words.pairManyVersions : '';
281
+ this.result('');
282
+ this.show('verify');
283
+ $('fair-v-nonce').focus();
284
+ },
285
+ /**
286
+ * Rotate seed pair: the player's client seed goes with it when they typed
287
+ * a new one (a client seed belongs to a pair, so a new one starts a new
288
+ * pair, the Originals' rule); the same seed, or none, keeps the current.
289
+ * The retired pair comes back revealed, held to the hashes shown before,
290
+ * and joins the list. True when the pair was rotated.
291
+ */
292
+ async rotate() {
293
+ // The shell's word on an open round is read again at the press: a
294
+ // bet may have landed, or ended, since the dialog opened.
295
+ this.setRoundOpen(this.ctx.roundOpen());
296
+ if (this.busy || this.roundOpen || !this.seedsReady) return false;
297
+ const w = this.words;
298
+ const wanted = $('fair-client').value.trim();
299
+ if (chars(wanted) > CLIENT_SEED_MAX) {
300
+ this.say(w.clientSeedLength, 'bad');
301
+ return false;
302
+ }
303
+ const before = this.seeds;
304
+ // A rotation whose answer was lost is asked for again under its own
305
+ // generation and client seed before any new one: the server answers
306
+ // it as it did the first time, never rotating twice. A new client
307
+ // seed is taken only once that one is answered or refused for good —
308
+ // and stays in the field until then (rotated()).
309
+ const retry = !!this.pending;
310
+ if (!retry) {
311
+ const clientSeed = wanted && (!before || wanted !== before.clientSeed) ? wanted : null;
312
+ this.pending = { clientSeed, generation: crypto.randomUUID() };
313
+ }
314
+ const p = this.pending;
315
+ this.busy = true;
316
+ this.lock(true);
317
+ if (retry) this.say(w.retryingRotation);
318
+ try {
319
+ const r = p.clientSeed ? await this.rgs.setClientSeed(p.generation, p.clientSeed) : await this.rgs.rotateSeeds(p.generation);
320
+ if (!r.ok) {
321
+ if (r.lost) return await this.afterLost(before);
322
+ // In flight on the server still (a retry met the first attempt):
323
+ // the generation is kept for the next press.
324
+ if (r.error.code === 'pacing') {
325
+ this.say(w.seedRotationFailed, 'bad');
326
+ return false;
327
+ }
328
+ this.pending = null;
329
+ if (r.error.code === 'round') {
330
+ this.setRoundOpen(true);
331
+ if (this.tab === 'verify') this.result(w.rotateAfterRound);
332
+ return false;
333
+ }
334
+ this.say(r.error.message || w.seedRotationFailed, 'bad');
335
+ return false;
336
+ }
337
+ this.rotated(before, r.body, await this.previousHash(r.body));
338
+ return true;
339
+ } finally {
340
+ this.busy = false;
341
+ this.lock(false);
342
+ }
343
+ },
344
+ previousHash(body) {
345
+ return body.previous && typeof body.previous.serverSeed === 'string' ? sha256Hex(body.previous.serverSeed) : Promise.resolve(null);
346
+ },
347
+ // The pair rotated: the new pair shows, the retired one joins the list,
348
+ // and the rotation is held to the hashes shown before it. A client seed
349
+ // the player typed that this rotation did not send — typed while the
350
+ // answer was on its way, or while a lost one was replayed under its own
351
+ // seed — is theirs still: it stays in the field, not wiped by the pair's
352
+ // own, and goes with the next press under a generation of its own.
353
+ rotated(before, body, previousSeedHash) {
354
+ const sent = this.pending ? this.pending.clientSeed : null;
355
+ const typed = this.seedTouched ? $('fair-client').value : '';
356
+ this.pending = null;
357
+ this.seedTouched = false;
358
+ this.seedsGen++;
359
+ this.showSeeds(body);
360
+ const kept = typed.trim();
361
+ if (kept && kept !== sent && kept !== this.seeds.clientSeed) {
362
+ $('fair-client').value = typed;
363
+ this.seedTouched = true;
364
+ }
365
+ const diffs = rotationDifferences(before, body, previousSeedHash);
366
+ this.chain = diffs.length ? diffs[0] : null;
367
+ this.note(this.chain ? this.words[this.chain] : '');
368
+ if (this.tab === 'verify') this.result(this.chain ? this.words[this.chain] : '', this.chain ? 'bad' : '');
369
+ // The round on screen was drawn on the pair this rotation retired:
370
+ // its seed is revealed now, so the receipt is read again and the
371
+ // round verified, from the Seeds tab's Rotate as from the reveal
372
+ // button — unless the chain of hashes broke, which is the finding.
373
+ const prev = body && body.previous;
374
+ if (!this.chain && this.receipt && !this.receipt.serverSeed && prev && typeof prev.serverSeedHash === 'string' && prev.serverSeedHash === this.receipt.serverSeedHash) {
375
+ this.loadRound(this.token);
376
+ }
377
+ },
378
+ // The rotation's answer was lost (every attempt): the pair is read
379
+ // again. A new hash means the rotation committed — the retired pair is
380
+ // the server's `previous` and the generation is spent. The same hash, or
381
+ // no answer, keeps the generation for the next press.
382
+ async afterLost(before) {
383
+ const s = await this.rgs.seeds();
384
+ if (s.ok && before && HEX64.test(before.serverSeedHash) && s.body.serverSeedHash !== before.serverSeedHash) {
385
+ this.rotated(before, s.body, await this.previousHash(s.body));
386
+ return true;
387
+ }
388
+ if (s.ok) {
389
+ this.seedsGen++;
390
+ this.showSeeds(s.body);
391
+ }
392
+ this.say(this.words.seedRotationFailed, 'bad');
393
+ return false;
394
+ },
395
+
396
+ // ---- Verify ----------------------------------------------------------------
397
+ resetVerify() {
398
+ this.check++;
399
+ for (const id of VERIFY_FIELDS) $(id).value = '';
400
+ this.commitment = null;
401
+ this.version = null;
402
+ $('fair-v-note').textContent = '';
403
+ $('fair-round').hidden = true;
404
+ $('fair-reveal').hidden = true;
405
+ this.result('');
406
+ },
407
+ // The player's edits take the fields over: a round on screen is no longer
408
+ // what they verify, and an older answer still on its way is dropped. A
409
+ // new seed (`pair` true) is no longer the revealed pair's either; a new
410
+ // nonce or mode is still within it.
411
+ invalidate(pair) {
412
+ this.check++;
413
+ this.receipt = null;
414
+ if (pair) {
415
+ this.commitment = null;
416
+ this.version = null;
417
+ $('fair-v-note').textContent = '';
418
+ }
419
+ $('fair-round').hidden = true;
420
+ $('fair-reveal').hidden = true;
421
+ },
422
+ // The maths pack of one version, fetched once from this origin and hashed
423
+ // here from the bytes served (never from the browser's text of them: the
424
+ // hash commits to the manifest as the studio submitted it).
425
+ pack(version) {
426
+ if (!HEX64.test(version)) return Promise.reject(new Error('version'));
427
+ const key = this.ctx.game + '@' + version;
428
+ if (!this.packs.has(key)) {
429
+ const loading = (async () => {
430
+ return loadPack(API(), this.ctx.game, version);
431
+ })();
432
+ loading.catch(() => this.packs.delete(key));
433
+ this.packs.set(key, loading);
434
+ }
435
+ return this.packs.get(key);
436
+ },
437
+ // The mode select: the pack's modes, shown only when there is more than one.
438
+ showModes(pack, want) {
439
+ const sel = $('fair-v-mode');
440
+ const modes = pack.manifest.modes;
441
+ const current = want || sel.value;
442
+ sel.replaceChildren(...modes.map((m) => { const o = el('option', typeof m.label === 'string' && m.label ? m.label : m.id); o.value = m.id; return o; }));
443
+ sel.value = modes.some((m) => m.id === current) ? current : modes[0].id;
444
+ $('fair-v-mode-field').hidden = modes.length < 2;
445
+ },
446
+ modeId(pack) {
447
+ const v = $('fair-v-mode').value;
448
+ return pack.manifest.modes.some((m) => m.id === v) ? v : pack.manifest.modes[0].id;
449
+ },
450
+ // No round: the modes of the version the shell is running, when the pack is at hand.
451
+ async loadModes(token) {
452
+ try {
453
+ const pack = await this.pack(this.ctx.mathHash);
454
+ if (token === this.token && !this.receipt) this.showModes(pack, null);
455
+ } catch {
456
+ // The pack is fetched again on Verify, which says what it finds.
457
+ }
458
+ },
459
+ /** The round's receipt: the fields filled from it, verified at once when its seed is revealed. */
460
+ async loadRound(token) {
461
+ if (!this.roundId) return;
462
+ const check = ++this.check;
463
+ const w = this.words;
464
+ this.verifying++;
465
+ try {
466
+ const r = await this.rgs.round(this.roundId);
467
+ if (token !== this.token || check !== this.check) return;
468
+ if (!r.ok) {
469
+ // Two things the server refuses with a conflict: a round still
470
+ // on the table, and a round of another currency than the session.
471
+ if (r.error.code === 'conflict') return this.result(/currency/i.test(r.error.message || '') ? w.roundOtherCurrency : w.roundStillOpen);
472
+ return this.result(r.error.code === 'round' ? w.roundNotFound : w.verifyFailed, 'bad');
473
+ }
474
+ const b = r.body;
475
+ this.receipt = b;
476
+ $('fair-round').textContent = roundLine(b, this.ctx.wallet, w);
477
+ $('fair-round').hidden = false;
478
+ $('fair-v-server').value = typeof b.serverSeed === 'string' ? b.serverSeed : '';
479
+ $('fair-v-client').value = typeof b.clientSeed === 'string' ? b.clientSeed : '';
480
+ $('fair-v-nonce').value = Number.isInteger(b.nonce) ? String(b.nonce) : '';
481
+ $('fair-reveal').hidden = true;
482
+ if (b.serverSeed) return await this.verify(b, check);
483
+ // The pair is still active: nothing to compute yet, and the rotation
484
+ // that reveals it is one press away when no round is open.
485
+ this.result(w.seedNotRevealedYet);
486
+ $('fair-reveal').hidden = this.roundOpen;
487
+ this.pack(b.mathHash).then((pack) => { if (check === this.check) this.showModes(pack, b.mode); }, () => {});
488
+ } finally {
489
+ this.verifying--;
490
+ }
491
+ },
492
+ /**
493
+ * Verify: the three fields and the mode through engine-v1 against the
494
+ * pack of the round's version (a revealed pair's when it names one, else
495
+ * the shell's when no round is on screen). With a round on screen the
496
+ * result is held against its receipt, the revealed seed against the
497
+ * commitment it was shown under.
498
+ */
499
+ async verify(receipt, check) {
500
+ const w = this.words;
501
+ const server = $('fair-v-server').value.trim();
502
+ const client = $('fair-v-client').value;
503
+ const nonce = $('fair-v-nonce').value.trim();
504
+ if (!server || !client || nonce === '') return this.result(w.verifyNeedsAll, 'bad');
505
+ this.verifying++;
506
+ try {
507
+ if (!NONCE.test(nonce)) throw new Error('nonce');
508
+ const version = receipt ? receipt.mathHash : (this.version || this.ctx.mathHash);
509
+ const seedHash = await sha256Hex(server);
510
+ if (check !== this.check) return;
511
+ // A revealed pair from the Seeds tab: the seed must hash to the
512
+ // hash it was listed under, or the draws below follow from a seed
513
+ // the house never committed to.
514
+ if (!receipt && this.commitment && seedHash !== this.commitment) return this.result(w.commitmentBroken, 'bad');
515
+ const pack = await this.pack(version);
516
+ if (check !== this.check) return;
517
+ this.showModes(pack, receipt ? receipt.mode : null);
518
+ // The pack must hash to the version asked for before anything drawn
519
+ // from it counts: tables nobody committed to verify nothing.
520
+ if (pack.recomputed !== version) return this.result(w.verifyFailed, 'bad');
521
+ const modeId = this.modeId(pack);
522
+ const bet = betFor(pack, modeId, receipt);
523
+ if (!bet.id) throw new Error('bet');
524
+ const got = await play(pack, modeId, bet.id, server, client, nonce);
525
+ if (check !== this.check) return;
526
+ const round = receipt && receipt.mode === modeId ? receipt : null;
527
+ let text = w.computedResult + ': ' + outcome(pack, got, round, bet.known, w);
528
+ let tone = '';
529
+ if (round) {
530
+ const cmp = compare(expectation(round), got, { version, recomputed: pack.recomputed }, seedHash);
531
+ // Nothing compared is no verdict: never "does not match" for a
532
+ // receipt that merely could not be read.
533
+ if (cmp.unreadable) return this.result(w.verifyFailed, 'bad');
534
+ text += ' · ' + (cmp.ok ? w.matchesRound : w.doesNotMatchRound);
535
+ tone = cmp.ok ? 'ok' : 'bad';
536
+ }
537
+ // The version the result was computed against, named as Dice names
538
+ // its derivation.
539
+ text += ' · ' + fill(w.maths, { hash: version.slice(0, 8) });
540
+ this.result(text, tone, got.draws.length > 1 ? drawLines(pack, got, w) : []);
541
+ } catch {
542
+ if (check === this.check) this.result(w.verifyFailed, 'bad');
543
+ } finally {
544
+ this.verifying--;
545
+ }
546
+ },
547
+ };
548
+ $('fair-tab-seeds').addEventListener('click', () => fair.show('seeds'));
549
+ $('fair-tab-verify').addEventListener('click', () => fair.show('verify'));
550
+ $('fair-client').addEventListener('input', () => { fair.seedTouched = true; });
551
+ $('fair-rotate').addEventListener('click', () => fair.rotate());
552
+ $('fair-verify-run').addEventListener('click', () => fair.verify(fair.receipt, ++fair.check));
553
+ for (const id of VERIFY_FIELDS) {
554
+ $(id).addEventListener('input', () => fair.invalidate(id !== 'fair-v-nonce'));
555
+ $(id).addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); fair.verify(fair.receipt, ++fair.check); } });
556
+ }
557
+ $('fair-v-mode').addEventListener('change', () => fair.invalidate(false));
558
+ // A round whose pair is still active: rotating reveals it, then the round
559
+ // verifies — unless the rotation broke the chain of hashes, which is the
560
+ // finding then, and the round is left for the player to verify by hand.
561
+ $('fair-reveal').addEventListener('click', async () => {
562
+ const token = fair.token;
563
+ if (await fair.rotate() && token === fair.token && !fair.chain) await fair.loadRound(token);
564
+ });
565
+ // The Originals' close affordances: the X, Esc (native), and a press AND
566
+ // release on the backdrop (a selection dragged out of a field and released
567
+ // outside must not close the window mid-sentence). With the content inside
568
+ // .fr-head and .fr-body, the dialog element itself is hit only on the
569
+ // backdrop.
570
+ {
571
+ const dlg = $('fair');
572
+ let pressedOut = false, releasedOut = false;
573
+ $('fair-x').addEventListener('click', () => dlg.close());
574
+ dlg.addEventListener('pointerdown', (e) => { pressedOut = e.target === dlg; });
575
+ dlg.addEventListener('pointerup', (e) => { releasedOut = e.target === dlg; });
576
+ dlg.addEventListener('click', () => {
577
+ const go = pressedOut && releasedOut;
578
+ pressedOut = releasedOut = false;
579
+ if (go) dlg.close();
580
+ });
581
+ // Closed: what was on its way is dropped, and the client seed is the server's again next time.
582
+ dlg.addEventListener('close', () => { fair.token++; fair.check++; fair.seedTouched = false; fair.requestId = null; });
583
+ }
584
+
585
+ const ui = {
586
+ // The clock the confirmation's arming reads; the tests hand in their own.
587
+ now: () => performance.now(),
588
+ frame(src) { frame.src = src; frame.hidden = false; return frame; },
589
+ loading(group, loaded, total) {
590
+ groups.set(group, [loaded, total]);
591
+ let l = 0, t = 0; for (const [a, b] of groups.values()) { l += a; t += b; }
592
+ $('progress').style.width = (t ? Math.round(l / t * 100) : 0) + '%';
593
+ $('loading-text').textContent = 'Loading ' + group;
594
+ },
595
+ ready() { $('loading').hidden = true; },
596
+ started(info) { window.__engine = info; },
597
+ round(r) { window.__rounds = (window.__rounds || []).concat(r); },
598
+ fail(message, err) { if (err) console.warn(err); $('loading').hidden = true; $('failed-text').textContent = message; $('failed').hidden = false; },
599
+ /** The player's language: the dialog's words. */
600
+ locale(lang) { fair.setLocale(lang); },
601
+ // The player has played this long: the game is paused until they say
602
+ // they want to go on.
603
+ realityCheck(minutes) {
604
+ $('rc-text').textContent = `You have been playing for ${minutes} minutes.`;
605
+ $('rc').showModal();
606
+ return new Promise((r) => $('rc').addEventListener('close', r, { once: true }));
607
+ },
608
+ /**
609
+ * The dialog: on the player's seeds (no round), or on a round's receipt.
610
+ * `ctx` is the shell's: the game, the maths version it is running, the
611
+ * wallet (how money is shown), whether a round is open right now, who
612
+ * opens it (fromGame) and, from the bet history, the request to answer.
613
+ */
614
+ fairness(rgs, roundId, ctx) { return fair.open(rgs, roundId, ctx); },
615
+ /** The bet history took a request back. */
616
+ fairnessCancel(requestId) { fair.cancel(requestId); },
617
+ /** A round started (false) or ended (true) behind the dialog. */
618
+ fairnessRoundChanged(ended) { fair.roundChanged(ended); },
619
+ /** The shell's own Provably Fair button, for a page no site bar frames. */
620
+ fairButton(open) {
621
+ const b = $('fair-open');
622
+ b.hidden = false;
623
+ b.addEventListener('click', open);
624
+ },
625
+ /**
626
+ * The autoplay session as the router reports it ({active, left, …}): the
627
+ * shell's own control over the game shows the rounds left while one runs
628
+ * and goes when it ends. Shell DOM, outside the game frame: the game can
629
+ * neither cover it nor press its Stop.
630
+ */
631
+ autoplay(s) {
632
+ $('auto').hidden = !s.active;
633
+ if (s.active) $('auto-text').textContent = fill(fair.words.autoplayLeft, { n: s.left });
634
+ },
635
+ /** The player pressed the control's Stop. */
636
+ autoplayStop(stop) { $('auto-stop').addEventListener('click', stop); },
637
+ /**
638
+ * The game asked to autoplay: the shell shows the player exactly what they
639
+ * would be approving (the bet the run is for, rounds, the stake each
640
+ * round, the most that could be staked, the stops), formatted in their
641
+ * coin by boot.js. Shell DOM, outside the game frame, which the game can
642
+ * neither cover nor pre-fill; the session begins only on the player's real
643
+ * press of Start.
644
+ */
645
+ autoplayConfirm(t) {
646
+ const w = fair.words, en = STRINGS.en;
647
+ const say = (id, s) => { $(id).textContent = s; };
648
+ say('ap-title', w.autoplayConfirmTitle ?? en.autoplayConfirmTitle);
649
+ say('ap-sub', w.autoplayConfirmSub ?? en.autoplayConfirmSub);
650
+ say('ap-l-bet', w.autoplayBet ?? en.autoplayBet);
651
+ say('ap-bet', t.bet);
652
+ say('ap-l-rounds', w.autoplayRounds ?? en.autoplayRounds);
653
+ say('ap-l-stake', w.autoplayStakeEach ?? en.autoplayStakeEach);
654
+ say('ap-l-total', w.autoplayTotal ?? en.autoplayTotal);
655
+ say('ap-l-profit', w.autoplayStopProfit ?? en.autoplayStopProfit);
656
+ say('ap-l-loss', w.autoplayStopLoss ?? en.autoplayStopLoss);
657
+ say('ap-start', w.autoplayStart ?? en.autoplayStart);
658
+ say('ap-cancel', w.autoplayCancel ?? en.autoplayCancel);
659
+ say('ap-rounds', String(t.rounds));
660
+ say('ap-stake', t.stake);
661
+ say('ap-total', t.total);
662
+ say('ap-loss', t.stopLoss);
663
+ $('ap-profit-row').hidden = !t.stopProfit;
664
+ if (t.stopProfit) say('ap-profit', t.stopProfit);
665
+ $('ap').showModal();
666
+ $('ap').focus(); // the dialog itself takes the focus (tabindex=-1, as the Provably Fair dialog): no button is pre-lit
667
+ // Start is dead until the player could have read the dialog: shown
668
+ // disabled, and a press that began before then never counts
669
+ // (onAutoplayConfirm). The timer is this opening's: a dialog closed and
670
+ // reopened within the arming keeps the new one's.
671
+ const armedAt = ui.now() + AUTOPLAY_CONFIRM_ARM_MS;
672
+ ap.armedAt = armedAt;
673
+ ap.pressAt = -Infinity;
674
+ $('ap-start').disabled = true;
675
+ setTimeout(() => { if ($('ap').open && ap.armedAt === armedAt) $('ap-start').disabled = false; }, AUTOPLAY_CONFIRM_ARM_MS);
676
+ },
677
+ /**
678
+ * The shell takes the confirmation down itself (its deadline passed, the
679
+ * reality check, the session ended, the page hidden): the router has
680
+ * already refused the game, so the close that follows cancels nothing.
681
+ */
682
+ autoplayDismiss() {
683
+ const dlg = $('ap');
684
+ if (dlg.open) dlg.close();
685
+ },
686
+ /**
687
+ * Wire the confirmation's buttons once. Start must be a real, trusted
688
+ * click on the shell's own button (`isTrusted`) — a scripted click, which
689
+ * the sandboxed game frame cannot land on this document anyway, is
690
+ * ignored — and the press must have begun on Start after the arming
691
+ * (AUTOPLAY_CONFIRM_ARM_MS): a pointer down on it, or the key that
692
+ * activates a button (Enter, Space), trusted and past the arming moment.
693
+ * A tap that was already falling when the dialog appeared, or that began
694
+ * on the game, is not a Start. Every close that was not Start is a cancel
695
+ * (the Cancel button, Esc, a press on the backdrop).
696
+ */
697
+ onAutoplayConfirm(decide) {
698
+ const dlg = $('ap'), start = $('ap-start');
699
+ let starting = false;
700
+ const began = (e) => { if (e.isTrusted && ui.now() >= ap.armedAt) ap.pressAt = ui.now(); };
701
+ start.addEventListener('pointerdown', began);
702
+ start.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') began(e); });
703
+ start.addEventListener('click', (e) => {
704
+ if (!e.isTrusted || ui.now() < ap.armedAt || ap.pressAt < ap.armedAt) return;
705
+ starting = true;
706
+ dlg.close();
707
+ });
708
+ $('ap-cancel').addEventListener('click', () => dlg.close());
709
+ dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); }); // the backdrop
710
+ dlg.addEventListener('close', () => { const ok = starting; starting = false; decide(ok); });
711
+ },
712
+ /**
713
+ * The player pressed or typed on the shell's own DOM (`doc` is this
714
+ * document), or on the site's page around it (boot.js hands the site's
715
+ * document when the shell is served from the site's origin, as in
716
+ * production: a press anywhere in that page gives this window activation
717
+ * too). Input inside the game frame never reaches either document's
718
+ * listeners (it is the frame's document's), so these fire only for the
719
+ * shell's controls and dialogs — the pill's Stop, the confirmation's
720
+ * buttons and backdrop, the reality check's Continue, the Provably Fair
721
+ * dialog's fields — and for the site's own chat, header and buttons.
722
+ * Capture phase, so nothing in the page can stop them; only trusted events
723
+ * count. The router then takes no play on this window's activation for the
724
+ * activation's lifetime: it cannot tell such a press's from a game click's.
725
+ * A Tab key is remembered besides (watchGameFocus): the focus it moves into
726
+ * the game frame is the player's entry, not a steal.
727
+ */
728
+ onShellInput(fn, doc = document) {
729
+ for (const type of INPUT_TYPES) {
730
+ doc.addEventListener(type, (e) => {
731
+ if (!e.isTrusted) return;
732
+ if (type === 'keydown' && e.key === 'Tab') lastTabAt = performance.now();
733
+ fn();
734
+ }, { capture: true, passive: true });
735
+ }
736
+ },
737
+ /**
738
+ * Watches the focus entering and leaving the game frame. Browsers fire no
739
+ * event in this document when the focus moves into the frame (Chrome fires
740
+ * neither `focus` on the frame element nor `focusin` on the document; the
741
+ * window's `blur` fires only when this document's own element held it, and
742
+ * a scripted move lands after the site's `focusout` has fired), nor when
743
+ * it moves out of the frame to the site's page (the frame's window blurs,
744
+ * the site's element gets `focusin`; this document, in between, hears
745
+ * nothing), so the entry is read off `document.activeElement`: every
746
+ * FOCUS_POLL_MS, and at once (and again a moment later) after anything that
747
+ * moves the focus — this window's blur, a focusout or a focusin in this
748
+ * document and in each other `doc` handed in (the site's, when it shares
749
+ * the origin), a trusted press or key on either. The exit is marked the
750
+ * moment it can be seen, not only when the poll finds the frame unfocused:
751
+ * a `focusin` on an element other than the frame, in this document or the
752
+ * site's, and a trusted press or key on either document (the focus is not
753
+ * in the frame when either fires) say the focus has left. A game sees its
754
+ * own window blur when the player tabs out of it and can call
755
+ * `window.focus()` a task later — inside one poll interval, before the poll
756
+ * would have seen the frame unfocused — so without that a steal-back would
757
+ * never be judged at all. On each entry `decide(viaTab, sinceExitMs)` says
758
+ * whether the focus is the player's (the router: the activation it granted
759
+ * is live, no shell or site press, no scripted focus and no exit from the
760
+ * frame within the activation's lifetime — or, with `viaTab`, the entry
761
+ * followed the player's Tab key within TAB_ENTRY_MS, a key on this or the
762
+ * site's document that the router would otherwise take for a shadowing
763
+ * press, and that brings the player back in whatever the time since
764
+ * leaving); `sinceExitMs` is how long ago the focus was last seen leaving
765
+ * the frame (Infinity before the first exit). One that is not — the game's
766
+ * `window.focus()` — is blurred straight back out of the frame, so the
767
+ * player's next keystrokes land on this document (shell input, which the
768
+ * router shadows), not in a frame that took the focus by itself; the
769
+ * blur-back is the shell's move, not the player's leaving, and does not
770
+ * reset the time since the exit. Returns a function that stops the watch
771
+ * and removes its listeners (the tests).
772
+ */
773
+ watchGameFocus(decide, docs = [document]) {
774
+ let inFrame = false;
775
+ let exitAt = -Infinity; // when the focus was last seen leaving the frame
776
+ const left = () => { if (inFrame) { inFrame = false; exitAt = performance.now(); } };
777
+ const check = () => {
778
+ const now = document.activeElement === frame;
779
+ if (now && !inFrame) {
780
+ inFrame = true;
781
+ const viaTab = performance.now() - lastTabAt < TAB_ENTRY_MS;
782
+ if (!decide(viaTab, performance.now() - exitAt)) { frame.blur(); inFrame = false; }
783
+ } else if (!now) left();
784
+ };
785
+ const soon = () => { setTimeout(check, 0); setTimeout(check, FOCUS_POLL_MS / 2); };
786
+ // A focusin for the frame element itself (were a browser to fire one)
787
+ // would be an entry; for anything else the focus is not in the frame.
788
+ const focusedElsewhere = (e) => { if (e.target !== frame) left(); soon(); };
789
+ const pressed = (e) => { if (!e.isTrusted) return; left(); soon(); };
790
+ const timers = [setInterval(check, FOCUS_POLL_MS)];
791
+ const listeners = [];
792
+ const on = (target, type, fn, opts) => { target.addEventListener(type, fn, opts); listeners.push([target, type, fn, opts]); };
793
+ for (const doc of docs) {
794
+ on(doc, 'focusout', soon, true);
795
+ on(doc, 'focusin', focusedElsewhere, true);
796
+ for (const type of INPUT_TYPES) on(doc, type, pressed, { capture: true, passive: true });
797
+ const win = doc.defaultView;
798
+ if (win) on(win, 'blur', soon);
799
+ }
800
+ return () => {
801
+ for (const t of timers) clearInterval(t);
802
+ for (const [target, type, fn, opts] of listeners) target.removeEventListener(type, fn, opts);
803
+ };
804
+ },
805
+ };
806
+ boot({ ui }).catch((e) => ui.fail('The game could not start. Please reopen it.', e));
807
+
808
+ // For the page's tests (page.test.js), which drive the dialog without a browser.
809
+ export { fair, ui };