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,1209 @@
1
+ /**
2
+ * Time travel: the same bundle played again under other clocks, time zones
3
+ * and languages, and once through a long session of fast rounds, so that a
4
+ * behaviour waiting for a trigger (a date, a weekend, a holiday, the Nth
5
+ * round, a language, an hour of the day, a delay) shows itself in the check
6
+ * job instead of in a player's browser. run.mjs plays the variants; this
7
+ * module holds what is decided without a browser, so it can be tested without
8
+ * one: the matrix, the clocks, the rows a long session forces, the words a
9
+ * credential prompt uses, what the game is allowed to see of a travelled
10
+ * round, the clock a worker is given, and how a variant is judged against the
11
+ * baseline.
12
+ *
13
+ * The matrix is a covering set, not the product: every clock once, every
14
+ * time zone and language at least twice, so the whole fits the job's budget
15
+ * next to the desktop and phone run. The baseline (`base`) runs under the
16
+ * same fake clock at the real time, so a variant differs from it in nothing
17
+ * but what it is meant to test. (The canary's base runs on the real clock:
18
+ * there the point is to look like a player, not like the matrix.)
19
+ *
20
+ * What a variant records and how it fails (compareVariant):
21
+ * requests URLs that left the bundle's origin, and what the policy refused:
22
+ * any FAILS the baseline (a game talks to the engine alone); one
23
+ * the baseline did not make FAILS a variant (a new network attempt)
24
+ * workers Worker / SharedWorker scripts started: one the baseline did not
25
+ * start FAILS a variant (a worker is a network and a clock of its
26
+ * own); one started from a blob the run could not read, and so
27
+ * left on the real clock, is a WARNING
28
+ * frames child frames the game made (srcdoc, about:blank, its own pages):
29
+ * one the baseline did not make FAILS a variant
30
+ * errors uncaught errors FAIL the baseline; an uncaught or console error
31
+ * new to a variant FAILS it
32
+ * dialogs alert / confirm / prompt / print / window.open attempts: any FAILS
33
+ * inputs password, email and text inputs by signature (a masked text
34
+ * field or one that autocompletes a password counts as password):
35
+ * a password or email input FAILS anywhere; a text input the
36
+ * baseline lacks FAILS
37
+ * phishing text that reads like a credential prompt (password, seed phrase,
38
+ * private key, log in, verify your account, wallet address, a
39
+ * one-time code, a card number, a crypto address; in en, de, ja,
40
+ * pt), in the DOM, in placeholders, in CSS generated content and
41
+ * in text drawn on a canvas: any FAILS
42
+ * control the bet control the long session presses must be a button a
43
+ * player could press (size, on screen, opaque, takes the pointer):
44
+ * a decoy FAILS
45
+ * visual the frame after boot, downscaled to a grey grid and compared
46
+ * cell by cell with the baseline's: a strong difference (most of
47
+ * the frame, or one contiguous region the size of a dialog) is a
48
+ * WARNING for the reviewer, both screenshots kept; the same for
49
+ * the frame after a day of game time (`late`)
50
+ */
51
+
52
+ /** Every variant of the matrix, in the order they are played. */
53
+ export const VARIANTS = Object.freeze([
54
+ { id: 'base', clock: 'now', tz: 'UTC', locale: 'en' },
55
+ { id: 'plus1d', clock: '+1d', tz: 'America/New_York', locale: 'de' },
56
+ { id: 'plus7d', clock: '+7d', tz: 'Asia/Tokyo', locale: 'ja' },
57
+ { id: 'plus30d', clock: '+30d', tz: 'UTC', locale: 'pt-BR' },
58
+ { id: 'plus365d', clock: '+365d', tz: 'America/New_York', locale: 'en' },
59
+ { id: 'saturday', clock: 'saturday', tz: 'Asia/Tokyo', locale: 'de' },
60
+ { id: 'dec25', clock: 'dec25', tz: 'UTC', locale: 'ja' },
61
+ { id: 'jan1', clock: 'jan1', tz: 'America/New_York', locale: 'pt-BR' },
62
+ { id: 'long', clock: 'now', tz: 'UTC', locale: 'en', rounds: 300 },
63
+ ].map(Object.freeze));
64
+
65
+ /** The daily canary replays these at the real date: today and the near future. */
66
+ export const CANARY_VARIANTS = Object.freeze(['base', 'plus1d', 'plus7d', 'plus30d']);
67
+
68
+ /** The screenshot a variant writes after boot, as the job uploads it and the studio keeps it. */
69
+ export const shotName = (id) => `travel-${id}.png`;
70
+ /** The screenshot a short variant writes after a day of game time, kept when it differs. */
71
+ export const lateShotName = (id) => `travel-${id}-late.png`;
72
+ /** Every screenshot name a set of variants may write (what the studio signs for). */
73
+ export const shotNames = (variants) => variants.flatMap((v) => (v.rounds ? [shotName(v.id)] : [shotName(v.id), lateShotName(v.id)]));
74
+
75
+ /**
76
+ * The variants to play, in the order they are played: the whole matrix, or
77
+ * the canary's subset. The long session is played right after the baseline,
78
+ * so the clocks that follow start from what 300 rounds left in the game's
79
+ * storage (chainStorage); the dated variants follow in the order of their
80
+ * clocks (`clockFor` at `now`, the earliest first), because each starts from
81
+ * the storage the one before it left, and a game that keeps the last time it
82
+ * was seen must never find that time ahead of its own clock: a clock that
83
+ * runs backwards from one visit to the next is a thing only a harness shows
84
+ * a game (`handedStorage` keeps the walk out of it too). The next Saturday
85
+ * is within a week, so it comes before `plus7d`; 25 December and 1 January
86
+ * come before `plus365d`.
87
+ *
88
+ * The canary's baseline runs on the real clock, so it cannot walk a day on
89
+ * (the walk is the fake clock's) and would leave the dated variants' late
90
+ * frames with nothing to compare against. So the canary plays one more,
91
+ * hidden variant right after it: the same clock, zone and language under the
92
+ * fake clock, played and walked like a short variant, whose late frame stands
93
+ * as the baseline's (`lateFor`). It is not reported and takes no screenshot of
94
+ * its own.
95
+ */
96
+ export function variantsFor(set, now = new Date()) {
97
+ if (set === 'canary') {
98
+ const [base, ...rest] = VARIANTS.filter((v) => CANARY_VARIANTS.includes(v.id));
99
+ return [base, { ...base, id: 'baseday', hidden: true, lateFor: 'base' }, ...byClock(rest, now)];
100
+ }
101
+ const [base, ...rest] = VARIANTS;
102
+ const long = rest.find((v) => v.rounds);
103
+ return [base, long, ...byClock(rest.filter((v) => v !== long), now)];
104
+ }
105
+
106
+ /** The variants in the order of their clocks at `now`, the earliest first; two at one instant keep their order. */
107
+ const byClock = (variants, now) => variants.map((v, i) => ({ v, i, t: clockFor(v, now).getTime() })).sort((a, b) => a.t - b.t || a.i - b.i).map((x) => x.v);
108
+
109
+ /**
110
+ * Which storage a variant is handed from the one before it: the storage as
111
+ * the earlier variant left it after its walk (`walked`) only when the next
112
+ * variant's clock is past the end of that walk (its instant plus WALK_MS),
113
+ * otherwise the storage as it stood before the walk (`before`: after boot
114
+ * and the round, at the variant's own instant). So the long session, at the
115
+ * real time, never starts from a storage the baseline's walk wrote a day on;
116
+ * `plus1d` after the walked baseline of the canary gets what stood at the
117
+ * real time; `plus7d` after `plus1d` gets the walked storage, since a week
118
+ * is past a day and a half. A variant that did not walk (the long session,
119
+ * the canary's real-clock baseline) hands what it left. Nothing when the
120
+ * earlier variant left nothing.
121
+ */
122
+ export function handedStorage(prev, nextAt) {
123
+ if (!prev) return null;
124
+ const at = Date.parse(prev.at);
125
+ const next = typeof nextAt === 'string' ? Date.parse(nextAt) : Number(nextAt);
126
+ if (prev.walked && Number.isFinite(at) && Number.isFinite(next) && next >= at + WALK_MS) return prev.walked;
127
+ return prev.before || prev.walked || null;
128
+ }
129
+
130
+ /**
131
+ * The instant a variant's clock is set to as it starts (an ISO string). A
132
+ * variant at the real time (`clock: 'now'`: the baseline, the long session,
133
+ * the canary's hidden walked baseline) takes the real time at its own start,
134
+ * never the matrix's: the storage it is handed was written by a variant
135
+ * before it at the real time (the baseline's, before its walk, seconds into
136
+ * its play), so its clock must be at or past anything that storage holds — a
137
+ * last-seen a few seconds ahead of a fresh clock is a clock that ran
138
+ * backwards between two visits, which only a harness shows a game. A dated
139
+ * variant keeps the instant the matrix computed at its start (`at`), so the
140
+ * day's variants are the day's whatever real time the matrix has spent.
141
+ */
142
+ export function startInstant(variant, at, now = new Date()) {
143
+ return variant && variant.clock === 'now' ? new Date(now.getTime()).toISOString() : at;
144
+ }
145
+
146
+ // ---- clocks ------------------------------------------------------------------------------
147
+
148
+ const DAY = 24 * 60 * 60 * 1000;
149
+ const HOUR = 60 * 60 * 1000;
150
+
151
+ /**
152
+ * After a short variant's one round, the clock is walked on: half an hour at
153
+ * once (a delayed timer), then a day in hourly jumps (an hour-of-day check,
154
+ * an interval that counts), the page scanned after each. Timers fire once per
155
+ * jump; nothing waits in real time.
156
+ */
157
+ export const WALK = Object.freeze({ firstMs: 30 * 60 * 1000, stepMs: HOUR, steps: 24 });
158
+ /** How far the walk takes a variant's clock past its own instant (a day and a half an hour). */
159
+ export const WALK_MS = WALK.firstMs + WALK.stepMs * WALK.steps;
160
+
161
+ /**
162
+ * The instant at which a wall clock in `tz` reads `hour`:00 on the given
163
+ * calendar day. Found through Intl: the guess is that time in UTC, read back
164
+ * in the zone, and corrected by the difference once (a second pass for a day
165
+ * whose offset changes under it).
166
+ */
167
+ export function instantAt(tz, year, month, day, hour) {
168
+ let guess = Date.UTC(year, month - 1, day, hour);
169
+ for (let i = 0; i < 2; i++) {
170
+ const wall = wallClock(tz, new Date(guess));
171
+ const want = Date.UTC(year, month - 1, day, hour);
172
+ const have = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute);
173
+ if (have === want) break;
174
+ guess += want - have;
175
+ }
176
+ return new Date(guess);
177
+ }
178
+
179
+ /** The calendar date and time a clock in `tz` shows at the instant. */
180
+ export function wallClock(tz, at) {
181
+ const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', weekday: 'short' }).formatToParts(at);
182
+ const v = (type) => parts.find((p) => p.type === type)?.value;
183
+ return { year: Number(v('year')), month: Number(v('month')), day: Number(v('day')), hour: Number(v('hour')) % 24, minute: Number(v('minute')), weekday: v('weekday') };
184
+ }
185
+
186
+ /**
187
+ * The time a variant's clock is set to, from the real time `now` and the
188
+ * variant's zone: the offsets are exact days from now; the calendar ones are
189
+ * local times of that zone (noon of the next 25 December and the next 1
190
+ * January, so a check on the local date fires; eight in the evening of the
191
+ * next Saturday, a weekend evening). "Next" is strictly after today, so a
192
+ * run on the day itself still travels.
193
+ */
194
+ export function clockFor(variant, now = new Date()) {
195
+ const { clock, tz } = variant;
196
+ const offset = /^\+(\d+)d$/.exec(clock);
197
+ if (clock === 'now') return new Date(now.getTime());
198
+ if (offset) return new Date(now.getTime() + Number(offset[1]) * DAY);
199
+ const today = wallClock(tz, now);
200
+ if (clock === 'saturday') {
201
+ const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
202
+ const ahead = ((6 - weekdays.indexOf(today.weekday)) + 7) % 7 || 7;
203
+ const d = new Date(Date.UTC(today.year, today.month - 1, today.day + ahead));
204
+ return instantAt(tz, d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), 20);
205
+ }
206
+ if (clock === 'dec25') {
207
+ const year = today.month === 12 && today.day >= 25 ? today.year + 1 : today.year;
208
+ return instantAt(tz, year, 12, 25, 12);
209
+ }
210
+ if (clock === 'jan1') return instantAt(tz, today.year + 1, 1, 1, 12);
211
+ throw new Error(`unknown clock ${clock}`);
212
+ }
213
+
214
+ // ---- what the game may see of a travelled round -----------------------------------------
215
+
216
+ /**
217
+ * A round from the sandbox tells the game it is in a test: `forced` is true,
218
+ * a forced draw's `raw` is 0, and `createdAt`/`settledAt` are the server's real
219
+ * time while the browser's clock is travelled. The run's proxy passes every
220
+ * engine answer (play, action, round, authenticate, seeds; never the game's
221
+ * own state) through this: `forced` is dropped, a zero raw becomes a random
222
+ * one as a real draw would carry, and every `…At` timestamp is moved by the
223
+ * variant's offset so the server agrees with the browser. The real shell
224
+ * drops `forced` too (router.js ROUND_FIELDS), so production tells the game
225
+ * nothing the sandbox would not.
226
+ */
227
+ export function travelAnswer(body, offsetMs = 0, random = randomRaw) {
228
+ const walk = (v) => {
229
+ if (Array.isArray(v)) return v.map(walk);
230
+ if (!v || typeof v !== 'object') return v;
231
+ const out = {};
232
+ for (const [k, x] of Object.entries(v)) {
233
+ if (k === 'forced') continue;
234
+ if (k === 'raw' && (x === 0 || x === '0')) { out[k] = typeof x === 'number' ? Number(random()) : random(); continue; }
235
+ if (offsetMs && /At$/.test(k) && typeof x === 'string') {
236
+ const t = Date.parse(x);
237
+ if (Number.isFinite(t)) { out[k] = new Date(t + offsetMs).toISOString(); continue; }
238
+ }
239
+ out[k] = walk(x);
240
+ }
241
+ return out;
242
+ };
243
+ return walk(body);
244
+ }
245
+
246
+ /** A random u64 as a decimal string, the shape of a real draw's raw. */
247
+ export function randomRaw() {
248
+ const b = new Uint8Array(8);
249
+ globalThis.crypto.getRandomValues(b);
250
+ let v = 0n;
251
+ for (const x of b) v = (v << 8n) | BigInt(x);
252
+ return v.toString();
253
+ }
254
+
255
+ // ---- the clocks a game is shown ---------------------------------------------------------
256
+ //
257
+ // Two kinds of scope read a clock: the page, whose fake clock (clock.mjs:
258
+ // Playwright's own clock implementation, installed and driven by the run with
259
+ // no global the game could find) is installed before the page's first script,
260
+ // and every worker, which no page clock reaches and which under time travel
261
+ // is given a fake clock of its own in its prelude (workerShim, clockTrap),
262
+ // moved with the page's. clockShim gives both the same shape: every clock
263
+ // API — Date, the timers, performance.now and timeOrigin (and toJSON, and
264
+ // PerformanceTiming's epochs), performance.mark and measure, every
265
+ // PerformanceEntry's times (marks, measures, resources, the navigation,
266
+ // paint, event and layout entries, whatever a PerformanceObserver hands
267
+ // over) and IntersectionObserverEntry.time, Intl.DateTimeFormat with no
268
+ // date, Event.timeStamp, document.timeline.currentTime, Animation.startTime,
269
+ // File.lastModified, Temporal.Now, scheduler.postTask's delay,
270
+ // AbortSignal.timeout, Atomics.waitAsync's timeout — behind a Proxy of a
271
+ // function with the native's name and length, so Function.prototype.toString
272
+ // says native code, descriptors keep the native flags, timer ids count from
273
+ // one, Date has the native's own properties and nothing else, and new
274
+ // Date().constructor === Date. The frame-size run, every variant of the
275
+ // matrix and the daily canary install the same shim, so what a game sees of
276
+ // the clocks has one shape in all of them: a bundle that looked for the
277
+ // matrix would find the frame-size run, and a bundle that behaves only
278
+ // outside any harness fails the frame-size run, which a reviewer sees. Only
279
+ // the values differ, and consistently: in a scope under a fake clock every
280
+ // clock reads the fake, and a timestamp taken before a jump of the clock
281
+ // keeps the time it was taken at (`fake.leadAt`: what the fake clock was
282
+ // ahead of the native one at that instant), as a real timestamp would; in a
283
+ // worker on the real clock (the frame-size run, the canary's baseline) every
284
+ // clock is the real one moved by one offset to the instant the page read
285
+ // when it started the worker (`at`), and moved on (the returned setter) if
286
+ // ever told. The functions are written to be stringified (String(fn)) into
287
+ // an init script or a worker prelude: they close over nothing but their
288
+ // arguments and tolerate a scope missing any of the constructors (node:vm in
289
+ // the tests, a worker without a document).
290
+
291
+ /**
292
+ * The clocks of a scope, one shape, one time. `o.capture` alone: returns the
293
+ * scope's native clock functions and descriptors (the page's init script
294
+ * takes them before the fake clock replaces any) and changes nothing.
295
+ * Otherwise installs the shim: `o.natives` as captured (or the scope's own,
296
+ * read now, in a worker), `o.fake` the page's fake clock to read from
297
+ * ({ now, perfNow, origin, leadAt, toNative }; none in a worker or in a page
298
+ * on the real clock), `o.at` the instant a worker's clocks are moved to.
299
+ * Returns the setter that moves them again.
300
+ */
301
+ export function clockShim(scope, o) {
302
+ 'use strict';
303
+ const TIMERS = ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'requestAnimationFrame', 'cancelAnimationFrame', 'requestIdleCallback', 'cancelIdleCallback'];
304
+ // A property of a performance entry that is an instant on the monotonic
305
+ // clock (startTime, responseEnd, processingStart, renderTime, lastInputTime,
306
+ // …): never a duration, a size or a count.
307
+ const INSTANT = /(Start|End|Time|Timestamp)$/;
308
+ const desc = (obj, k) => (obj ? Object.getOwnPropertyDescriptor(obj, k) : undefined);
309
+ // Where a property lives up an object's prototype chain, with its descriptor.
310
+ const where = (obj, k) => { for (let p = obj; p; p = Object.getPrototypeOf(p)) { const d = desc(p, k); if (d) return { on: p, d }; } return null; };
311
+ const protoOf = (C) => (typeof C === 'function' && C.prototype && typeof C.prototype === 'object' ? C.prototype : null);
312
+ // Every PerformanceEntry class of the scope (the base and whatever
313
+ // subclasses the browser has), with the descriptors of its instants, its
314
+ // duration and its toJSON, as they are before anything replaces them.
315
+ const entryClasses = () => {
316
+ const out = [];
317
+ const PE = scope.PerformanceEntry;
318
+ const base = protoOf(PE);
319
+ if (!base) return out;
320
+ for (const name of Object.getOwnPropertyNames(scope)) {
321
+ if (!/^[A-Z]/.test(name)) continue;
322
+ let proto;
323
+ try { proto = protoOf(scope[name]); } catch { continue; }
324
+ if (!proto || (proto !== base && !base.isPrototypeOf(proto))) continue;
325
+ const instants = {};
326
+ let duration = null, toJSON = null;
327
+ for (const k of Object.getOwnPropertyNames(proto)) {
328
+ const d = desc(proto, k);
329
+ if (!d) continue;
330
+ if (k === 'duration' && typeof d.get === 'function') duration = d;
331
+ else if (k === 'toJSON' && typeof d.value === 'function') toJSON = d;
332
+ else if (typeof d.get === 'function' && INSTANT.test(k)) instants[k] = d;
333
+ }
334
+ out.push({ proto, instants, duration, toJSON });
335
+ }
336
+ return out;
337
+ };
338
+ const capture = () => {
339
+ const N = { Date: scope.Date, performance: scope.performance, Intl: scope.Intl, Temporal: scope.Temporal, File: scope.File, perfDesc: desc(scope, 'performance') };
340
+ for (const k of TIMERS) N[k] = scope[k];
341
+ const perf = N.performance;
342
+ N.perfNow = perf ? where(perf, 'now') : null;
343
+ N.perfOrigin = perf ? where(perf, 'timeOrigin') : null;
344
+ N.perfToJSON = perf ? where(perf, 'toJSON') : null;
345
+ N.perfMark = perf ? where(perf, 'mark') : null;
346
+ N.perfMeasure = perf ? where(perf, 'measure') : null;
347
+ N.originValue = perf && typeof perf.timeOrigin === 'number' ? perf.timeOrigin : 0;
348
+ N.timing = scope.PerformanceTiming ? scope.PerformanceTiming.prototype : null;
349
+ N.entries = entryClasses();
350
+ const PE = protoOf(scope.PerformanceEntry);
351
+ N.entryStart = PE && desc(PE, 'startTime') && typeof desc(PE, 'startTime').get === 'function' ? desc(PE, 'startTime').get : null;
352
+ N.ioTime = protoOf(scope.IntersectionObserverEntry) ? where(scope.IntersectionObserverEntry.prototype, 'time') : null;
353
+ N.eventTs = scope.Event && scope.Event.prototype ? where(scope.Event.prototype, 'timeStamp') : null;
354
+ N.timeline = scope.document && scope.document.timeline ? where(scope.document.timeline, 'currentTime') : null;
355
+ const A = protoOf(scope.Animation);
356
+ N.animStart = A ? where(A, 'startTime') : null;
357
+ N.animCurrent = A ? where(A, 'currentTime') : null;
358
+ N.animState = A ? where(A, 'playState') : null;
359
+ N.animRate = A ? where(A, 'playbackRate') : null;
360
+ const ga = scope.document ? where(scope.document, 'getAnimations') : null;
361
+ N.getAnimations = ga && typeof ga.d.value === 'function' ? ga.d.value : null;
362
+ const DTF = N.Intl && N.Intl.DateTimeFormat;
363
+ N.dtfFormat = DTF && DTF.prototype ? where(DTF.prototype, 'format') : null;
364
+ N.dtfParts = DTF && DTF.prototype ? where(DTF.prototype, 'formatToParts') : null;
365
+ // The other timers: a scheduler's delayed task, a signal that aborts
366
+ // after a time, an atomic wait with a timeout, and the controller an
367
+ // aborting signal is made with.
368
+ let sched = null;
369
+ try { sched = scope.scheduler; } catch { sched = null; }
370
+ N.postTask = sched && typeof sched === 'object' ? where(sched, 'postTask') : null;
371
+ N.abortTimeout = scope.AbortSignal && typeof scope.AbortSignal.timeout === 'function' ? where(scope.AbortSignal, 'timeout') : null;
372
+ N.AbortController = scope.AbortController;
373
+ N.DOMException = scope.DOMException;
374
+ N.waitAsync = scope.Atomics && typeof scope.Atomics.waitAsync === 'function' ? where(scope.Atomics, 'waitAsync') : null;
375
+ return N;
376
+ };
377
+ if (o.capture) return capture();
378
+ const n = o.natives || capture();
379
+ const fake = o.fake || null;
380
+ const ND = n.Date, realNow = ND.now;
381
+ let offset = fake ? 0 : Math.round(Number(o.at === undefined || o.at === null ? realNow() : o.at) - realNow());
382
+ const now = () => (fake ? fake.now() : realNow() + offset);
383
+ const nativePerfNow = () => (n.perfNow && typeof n.perfNow.d.value === 'function' ? n.perfNow.d.value.call(n.performance) : 0);
384
+ const perfNow = () => (fake ? fake.perfNow() : nativePerfNow());
385
+ const origin = () => (fake ? fake.origin() : n.originValue + offset);
386
+ // What this clock's origin is from the real one: PerformanceTiming's epochs move by it.
387
+ const shift = () => origin() - n.originValue;
388
+ // What this clock's monotonic reading is ahead of the native one: now
389
+ // (`lead`: the jumps so far), and at a native instant `t` (`leadAt`: the
390
+ // jumps before it, so a timestamp taken before a jump keeps its time). A
391
+ // worker's clock, and a page's on the real clock, lead by nothing: the
392
+ // offset is on the origin.
393
+ const lead = () => (fake ? fake.perfNow() - nativePerfNow() : 0);
394
+ const leadAt = (t) => (fake && typeof fake.leadAt === 'function' ? fake.leadAt(t) : 0);
395
+ // A monotonic instant the game gives (a mark's startTime, a measure's
396
+ // start or end), read in this clock, as the native clock knows it.
397
+ const toNative = (v) => (fake && typeof fake.toNative === 'function' && typeof v === 'number' ? fake.toNative(v) : v);
398
+ const instant = (v) => (typeof v === 'number' && v > 0 ? v + leadAt(v) : v);
399
+ // A fake function stands in for a native one: it is given the native's name and length.
400
+ const asNative = (fn, model) => { if (fn !== model && typeof fn === 'function' && typeof model === 'function') { try { Object.defineProperty(fn, 'name', { value: model.name }); Object.defineProperty(fn, 'length', { value: model.length }); } catch { /* fixed already */ } } return fn; };
401
+ const redefine = (place, k, patch) => { if (place && place.d) { try { Object.defineProperty(place.on, k, { ...place.d, ...patch }); } catch { /* not configurable: left as it is */ } } };
402
+ const wrapGet = (place, k, fn) => { if (place && typeof place.d.get === 'function') redefine(place, k, { get: new Proxy(place.d.get, { apply(t, s, a) { return fn(Reflect.apply(t, s, a), s); } }) }); };
403
+
404
+ // Date: a constructor of the native's shape (own properties length, name,
405
+ // prototype, now, parse, UTC and nothing else) that makes the scope's Date
406
+ // (the fake's, or the native) read this clock when given no instant.
407
+ const D = scope.Date;
408
+ try { delete D.isFake; delete D.toString; } catch { /* the native has neither */ }
409
+ const T = function (...a) {
410
+ if (new.target === undefined) return String(Reflect.construct(D, [now()]));
411
+ const d = Reflect.construct(D, a.length ? a : [now()], new.target === P ? D : new.target);
412
+ if (new.target !== P && Object.getPrototypeOf(d) !== new.target.prototype) Object.setPrototypeOf(d, new.target.prototype);
413
+ return d;
414
+ };
415
+ Object.defineProperty(T, 'name', { value: 'Date' });
416
+ Object.defineProperty(T, 'length', { value: 7 });
417
+ Object.defineProperty(T, 'prototype', { value: ND.prototype, writable: false, enumerable: false, configurable: false });
418
+ Object.defineProperty(T, 'now', { value: new Proxy(asNative(D.now, ND.now), { apply() { return now(); } }), writable: true, enumerable: false, configurable: true });
419
+ for (const k of ['parse', 'UTC']) Object.defineProperty(T, k, { value: ND[k], writable: true, enumerable: false, configurable: true });
420
+ const P = new Proxy(T, {});
421
+ Object.defineProperty(ND.prototype, 'constructor', { value: P, writable: true, enumerable: false, configurable: true });
422
+ scope.Date = P;
423
+
424
+ // File: one made now is dated now by this clock (the browser dates it by
425
+ // the real wall clock); one made with a date keeps it. The constructor
426
+ // behind a Proxy of the native, which stays the prototype's constructor.
427
+ const F = n.File;
428
+ if (protoOf(F) && scope.File === F) {
429
+ const PF = new Proxy(F, { construct(t, a, nt) {
430
+ const args = Array.from(a);
431
+ const bag = args[2];
432
+ if (args.length >= 2 && (bag === undefined || bag === null || ((typeof bag === 'object' || typeof bag === 'function') && bag.lastModified === undefined))) args[2] = bag === undefined || bag === null ? { lastModified: now() } : Object.create(bag, { lastModified: { value: now(), enumerable: true } });
433
+ return Reflect.construct(t, args, nt);
434
+ } });
435
+ try { Object.defineProperty(F.prototype, 'constructor', { value: PF, writable: true, enumerable: false, configurable: true }); } catch { /* fixed already */ }
436
+ scope.File = PF;
437
+ }
438
+
439
+ // The timers: whatever stands there (the fake's or the native's) behind a
440
+ // native-shaped Proxy, their ids counted from one per family as Chrome
441
+ // counts them (timeouts and intervals together, frames apart, idle apart).
442
+ // A callback is handed on as it is: a wrapper around it would be a frame
443
+ // in its stack that production has not. So nothing says when a one-shot
444
+ // fired; its id is kept with the instant it was due and forgotten once that
445
+ // is a minute past (an interval's until it is cleared). An id the family
446
+ // never gave out clears nothing, as in production.
447
+ const ids = { timer: new Map(), frame: new Map(), idle: new Map() }, next = { timer: 0, frame: 0, idle: 0 };
448
+ const starts = { setTimeout: 'timer', setInterval: 'timer', requestAnimationFrame: 'frame', requestIdleCallback: 'idle' };
449
+ const stops = { clearTimeout: 'timer', clearInterval: 'timer', cancelAnimationFrame: 'frame', cancelIdleCallback: 'idle' };
450
+ let given = 0;
451
+ const forget = () => { const cut = now() - 60000; for (const fam of Object.keys(ids)) for (const [id, e] of ids[fam]) if (e.due < cut) ids[fam].delete(id); };
452
+ // The timers as they stand before the Proxies (the fake's, or the native's):
453
+ // what the shim's own timers below go through, so they take no id of the
454
+ // game's sequence.
455
+ const inner = {};
456
+ for (const k of TIMERS) {
457
+ const cur = scope[k];
458
+ if (typeof cur !== 'function' || typeof n[k] !== 'function') continue;
459
+ inner[k] = cur;
460
+ asNative(cur, n[k]);
461
+ if (starts[k]) {
462
+ const fam = starts[k], once = k !== 'setInterval';
463
+ scope[k] = new Proxy(cur, { apply(t, s, a) {
464
+ const id = ++next[fam];
465
+ const real = Reflect.apply(t, s, a);
466
+ ids[fam].set(id, { real, due: once ? now() + (fam === 'timer' ? Math.max(0, Number(a[1]) || 0) : 0) : Infinity });
467
+ if ((++given & 1023) === 0) forget();
468
+ return id;
469
+ } });
470
+ } else {
471
+ const fam = stops[k];
472
+ scope[k] = new Proxy(cur, { apply(t, s, a) { const e = ids[fam].get(a[0]); if (e === undefined) return undefined; ids[fam].delete(a[0]); return Reflect.apply(t, s, [e.real]); } });
473
+ }
474
+ }
475
+
476
+ // The other timers run on this clock too, through the scope's setTimeout
477
+ // (the fake's, under a fake clock), so a delay a game gives any of them
478
+ // passes with the clock and not with the real time: scheduler.postTask
479
+ // with a delay (its promise settles with the callback's result, an abort
480
+ // of its signal clears the task and rejects with the signal's reason; no
481
+ // delay goes to the native as it is, scheduler.yield with it);
482
+ // AbortSignal.timeout (a signal aborted with the native's TimeoutError
483
+ // after this clock's time); Atomics.waitAsync with a finite timeout (the
484
+ // native wait raced against this clock's timer: 'timed-out' when the
485
+ // clock gets there first).
486
+ const setT = () => inner.setTimeout || scope.setTimeout, clearT = () => inner.clearTimeout || scope.clearTimeout;
487
+ if (n.postTask && typeof n.postTask.d.value === 'function' && typeof setT() === 'function') {
488
+ redefine(n.postTask, 'postTask', { value: new Proxy(n.postTask.d.value, { apply(t, s, a) {
489
+ const cb = a[0], opts = a[1];
490
+ const delay = opts && (typeof opts === 'object' || typeof opts === 'function') ? Number(opts.delay) : NaN;
491
+ if (!(delay > 0) || typeof cb !== 'function') return Reflect.apply(t, s, a);
492
+ const signal = opts.signal && typeof opts.signal === 'object' && 'aborted' in opts.signal ? opts.signal : null;
493
+ if (signal && signal.aborted) return Reflect.apply(t, s, a); // the native rejects it as it does
494
+ // The callback runs in the fake timer itself (not through the native
495
+ // scheduler), so it observes the delay's instant — as a setTimeout
496
+ // does — and not the clock's end after a jump that spans the delay;
497
+ // its result settles the promise, as postTask's does.
498
+ return new Promise((resolve, reject) => {
499
+ let id = null;
500
+ const onAbort = () => { if (id !== null) { clearT().call(scope, id); id = null; } reject(signal.reason); };
501
+ if (signal) signal.addEventListener('abort', onAbort, { once: true });
502
+ id = setT().call(scope, () => {
503
+ id = null;
504
+ if (signal) signal.removeEventListener('abort', onAbort);
505
+ try { resolve(cb()); } catch (e) { reject(e); }
506
+ }, delay);
507
+ });
508
+ } }) });
509
+ }
510
+ if (n.abortTimeout && typeof n.abortTimeout.d.value === 'function' && typeof n.AbortController === 'function' && typeof setT() === 'function') {
511
+ redefine(n.abortTimeout, 'timeout', { value: new Proxy(n.abortTimeout.d.value, { apply(t, s, a) {
512
+ const ms = Number(a[0]);
513
+ if (!Number.isFinite(ms) || ms < 0 || ms > 2147483647) return Reflect.apply(t, s, a); // the native's TypeError, or its own clamping
514
+ const c = new n.AbortController();
515
+ setT().call(scope, () => { try { c.abort(typeof n.DOMException === 'function' ? new n.DOMException('signal timed out', 'TimeoutError') : new Error('signal timed out')); } catch { /* aborted already */ } }, ms);
516
+ return c.signal;
517
+ } }) });
518
+ }
519
+ if (n.waitAsync && typeof n.waitAsync.d.value === 'function' && typeof setT() === 'function') {
520
+ redefine(n.waitAsync, 'waitAsync', { value: new Proxy(n.waitAsync.d.value, { apply(t, s, a) {
521
+ const r = Reflect.apply(t, s, a);
522
+ const ms = a.length > 3 ? Number(a[3]) : Infinity;
523
+ if (!r || !r.async || !Number.isFinite(ms) || ms < 0 || typeof r.value?.then !== 'function') return r;
524
+ // The native wait holds its own (real-time) timeout; whichever
525
+ // comes first in this clock decides, the other is let go.
526
+ const value = new Promise((resolve) => {
527
+ let id = setT().call(scope, () => { id = null; resolve('timed-out'); }, ms);
528
+ r.value.then((v) => { if (id !== null) { clearT().call(scope, id); id = null; resolve(v); } }, () => { if (id !== null) { clearT().call(scope, id); id = null; resolve('timed-out'); } });
529
+ });
530
+ return { async: true, value };
531
+ } }) });
532
+ }
533
+
534
+ // performance: the native object (put back where a fake stood: through its
535
+ // own descriptor in a page, by removing the fake from a worker's global,
536
+ // whose performance is its prototype's), its now and timeOrigin reading
537
+ // this clock, toJSON and PerformanceTiming agreeing, mark and measure
538
+ // taking the instants the game gives in this clock.
539
+ if (n.performance) {
540
+ if (scope.performance !== n.performance) {
541
+ try { if (n.perfDesc) Object.defineProperty(scope, 'performance', n.perfDesc); else { delete scope.performance; if (scope.performance !== n.performance) scope.performance = n.performance; } } catch { /* left as it is */ }
542
+ }
543
+ if (n.perfNow && typeof n.perfNow.d.value === 'function') redefine(n.perfNow, 'now', { value: new Proxy(n.perfNow.d.value, { apply(t, s, a) { return fake ? fake.perfNow() : Reflect.apply(t, s, a); } }) });
544
+ if (n.perfOrigin && typeof n.perfOrigin.d.get === 'function') redefine(n.perfOrigin, 'timeOrigin', { get: new Proxy(n.perfOrigin.d.get, { apply(t, s, a) { return fake ? fake.origin() : Reflect.apply(t, s, a) + offset; } }) });
545
+ else if (n.perfOrigin && typeof n.perfOrigin.d.value === 'number') { try { Object.defineProperty(n.perfOrigin.on, 'timeOrigin', { get: () => origin(), set: undefined, enumerable: n.perfOrigin.d.enumerable, configurable: true }); } catch { /* left as it is */ } } // a plain number (node:vm): a getter stands in
546
+ if (n.perfToJSON && typeof n.perfToJSON.d.value === 'function') redefine(n.perfToJSON, 'toJSON', { value: new Proxy(n.perfToJSON.d.value, { apply(t, s, a) { const j = Reflect.apply(t, s, a); if (j && typeof j === 'object' && typeof j.timeOrigin === 'number') j.timeOrigin = origin(); return j; } }) });
547
+ if (n.timing) {
548
+ for (const k of Object.getOwnPropertyNames(n.timing)) {
549
+ const d = desc(n.timing, k);
550
+ if (d && typeof d.get === 'function') redefine({ on: n.timing, d }, k, { get: new Proxy(d.get, { apply(t, s, a) { const v = Reflect.apply(t, s, a); return typeof v === 'number' && v > 0 ? v + shift() : v; } }) });
551
+ else if (k === 'toJSON' && d && typeof d.value === 'function') redefine({ on: n.timing, d }, k, { value: new Proxy(d.value, { apply(t, s, a) { const j = Reflect.apply(t, s, a); if (j && typeof j === 'object') for (const f of Object.keys(j)) if (typeof j[f] === 'number' && j[f] > 0) j[f] += shift(); return j; } }) });
552
+ }
553
+ }
554
+ // An instant the game gives to mark or measure is in this clock.
555
+ const withNative = (opts, keys) => { if (!opts || (typeof opts !== 'object' && typeof opts !== 'function')) return opts; let changed = false; const copy = Object.create(opts); for (const k of keys) { if (typeof opts[k] === 'number') { Object.defineProperty(copy, k, { value: toNative(opts[k]), enumerable: true }); changed = true; } } return changed ? copy : opts; };
556
+ if (n.perfMark && typeof n.perfMark.d.value === 'function') redefine(n.perfMark, 'mark', { value: new Proxy(n.perfMark.d.value, { apply(t, s, a) { const args = Array.from(a); if (args.length > 1) args[1] = withNative(args[1], ['startTime']); return Reflect.apply(t, s, args); } }) });
557
+ if (n.perfMeasure && typeof n.perfMeasure.d.value === 'function') redefine(n.perfMeasure, 'measure', { value: new Proxy(n.perfMeasure.d.value, { apply(t, s, a) { const args = Array.from(a); if (args.length > 1) args[1] = typeof args[1] === 'number' ? toNative(args[1]) : withNative(args[1], ['start', 'end']); if (args.length > 2 && typeof args[2] === 'number') args[2] = toNative(args[2]); return Reflect.apply(t, s, args); } }) });
558
+ }
559
+ // Every performance entry (a mark, a measure, a resource, the navigation,
560
+ // a paint, an event, a layout shift, a long task: whatever getEntries or a
561
+ // PerformanceObserver gives) reads its instants in this clock, each at the
562
+ // lead of its own time, so an entry recorded before a jump keeps its time;
563
+ // its duration spans the same two instants; toJSON agrees.
564
+ const travelled = (j) => {
565
+ if (!j || typeof j !== 'object') return j;
566
+ if (Array.isArray(j)) { for (let i = 0; i < j.length; i++) j[i] = travelled(j[i]); return j; }
567
+ if (typeof j.duration === 'number' && typeof j.startTime === 'number') j.duration += leadAt(j.startTime + j.duration) - leadAt(j.startTime);
568
+ for (const k of Object.keys(j)) {
569
+ if (INSTANT.test(k) && typeof j[k] === 'number' && j[k] > 0) j[k] += leadAt(j[k]);
570
+ else if (j[k] && typeof j[k] === 'object') j[k] = travelled(j[k]);
571
+ }
572
+ return j;
573
+ };
574
+ for (const e of n.entries || []) {
575
+ for (const k of Object.keys(e.instants)) wrapGet({ on: e.proto, d: e.instants[k] }, k, instant);
576
+ if (e.duration) wrapGet({ on: e.proto, d: e.duration }, 'duration', (v, s) => { if (typeof v !== 'number' || !n.entryStart) return v; let st; try { st = n.entryStart.call(s); } catch { return v; } return typeof st === 'number' ? v + leadAt(st + v) - leadAt(st) : v; });
577
+ if (e.toJSON) redefine({ on: e.proto, d: e.toJSON }, 'toJSON', { value: new Proxy(e.toJSON.value, { apply(t, s, a) { return travelled(Reflect.apply(t, s, a)); } }) });
578
+ }
579
+ // An intersection's time, the same way.
580
+ wrapGet(n.ioTime, 'time', instant);
581
+ // An event's time stamp: the instant it was made, in this clock.
582
+ wrapGet(n.eventTs, 'timeStamp', instant);
583
+ // The document's animation timeline reads this clock (as performance.now()
584
+ // does, a frame behind it as the native is); an animation's start time is
585
+ // where it is on that timeline. Its progress is the native's: the run
586
+ // moves every running animation on with each jump of the clock
587
+ // (clock.mjs), so what is on screen, currentTime and the timeline agree.
588
+ wrapGet(n.timeline, 'currentTime', (v) => (typeof v === 'number' ? v + lead() : v));
589
+ if (n.animStart && typeof n.animStart.d.get === 'function') {
590
+ const set = typeof n.animStart.d.set === 'function' ? new Proxy(n.animStart.d.set, { apply(t, s, a) { return Reflect.apply(t, s, [typeof a[0] === 'number' ? a[0] - lead() : a[0]]); } }) : n.animStart.d.set;
591
+ redefine(n.animStart, 'startTime', { get: new Proxy(n.animStart.d.get, { apply(t, s, a) { const v = Reflect.apply(t, s, a); return typeof v === 'number' ? v + lead() : v; } }), set });
592
+ }
593
+ // Intl: the native object (put back where a fake stood); a DateTimeFormat
594
+ // given no date formats this clock's now.
595
+ if (n.Intl) {
596
+ if (scope.Intl !== n.Intl) scope.Intl = n.Intl;
597
+ const dated = (fn) => new Proxy(fn, { apply(t, s, a) { return Reflect.apply(t, s, a.length && a[0] !== undefined ? a : [Reflect.construct(P, [])]); } });
598
+ if (n.dtfFormat && typeof n.dtfFormat.d.get === 'function') {
599
+ const cache = new WeakMap();
600
+ redefine(n.dtfFormat, 'format', { get: new Proxy(n.dtfFormat.d.get, { apply(t, s, a) { const f = Reflect.apply(t, s, a); if (typeof f !== 'function') return f; let w = cache.get(f); if (!w) { w = dated(f); cache.set(f, w); } return w; } }) });
601
+ }
602
+ if (n.dtfParts && typeof n.dtfParts.d.value === 'function') redefine(n.dtfParts, 'formatToParts', { value: dated(n.dtfParts.d.value) });
603
+ }
604
+ // Temporal.Now, where the browser has it, reads this clock.
605
+ const Tm = n.Temporal;
606
+ if (Tm && Tm.Now && Tm.Instant && typeof Tm.Instant.fromEpochMilliseconds === 'function') {
607
+ const Now = Tm.Now;
608
+ const instant = () => Tm.Instant.fromEpochMilliseconds(now());
609
+ const zoned = (tz) => instant().toZonedDateTimeISO(tz === undefined ? Now.timeZoneId() : tz);
610
+ const wrap = (k, impl) => { if (typeof Now[k] === 'function') { try { Now[k] = new Proxy(Now[k], { apply(t, s, a) { return impl(...a); } }); } catch { /* not writable */ } } };
611
+ wrap('instant', instant);
612
+ wrap('zonedDateTimeISO', zoned);
613
+ wrap('plainDateTimeISO', (tz) => zoned(tz).toPlainDateTime());
614
+ wrap('plainDateISO', (tz) => zoned(tz).toPlainDate());
615
+ wrap('plainTimeISO', (tz) => zoned(tz).toPlainTime());
616
+ }
617
+ return (at) => { offset = Math.round(Number(at) - realNow()); };
618
+ }
619
+
620
+ /**
621
+ * The trap in a scope (a page or a worker): what the scope does with workers,
622
+ * and the channel the run drives its clocks through. Every Worker and
623
+ * SharedWorker the scope starts is reported through console.debug under the
624
+ * run's token (`token:worker:<script>`) and remembered. One started from a
625
+ * blob script is started from a wrapper instead: the worker prelude (the
626
+ * fake clock at the scope's current Date when the run travels — `o.clockSrc`,
627
+ * `o.startSrc`: Playwright's clock and clock.mjs startClock, as in a page —
628
+ * then clockShim, then this trap), then its own code, read from what the
629
+ * scope remembers of the Blob (its text parts at construction, and which URL
630
+ * createObjectURL gave it), never requested: the bundle's connect-src does
631
+ * not allow blob:, so a request for it would be refused and the refusal
632
+ * would land in the console as a policy violation of the game's. A blob
633
+ * whose parts the scope did not see (a File, a Blob from a response, a
634
+ * binary) starts unchanged and is reported as `token:unshimmed:blob:`. A
635
+ * same-origin script is started as it is: the run's server gives it the same
636
+ * prelude when the browser fetches it as a worker (Sec-Fetch-Dest), so its
637
+ * location and its relative imports stay its own. The replaced constructors
638
+ * are Proxies of the natives, fixed in place.
639
+ *
640
+ * The channel: a `message` event the scope hears before anything else (a
641
+ * capturing listener registered before the game's first script, which stops
642
+ * the event there, so no listener of the game's sees it) whose data is a
643
+ * string under the token. `token:at:<ms>` moves the scope's clocks to that
644
+ * instant (`hooks.setAt`: a worker told of the page's jump moves its own
645
+ * fake clock on by the difference, running the timers due in it, or, on the
646
+ * real clock, the shim's offset) and then tells the scope's own workers;
647
+ * `token:clock:<op>:<id>:<ms>` moves the page's fake clock (`hooks.jump(op,
648
+ * ms)`: runFor or fastForward), tells the workers, and answers the sender
649
+ * `token:clockdone:<id>:<now>:<location>`. An exception a timer threw during
650
+ * a jump is reported as `token:uncaught:` so the run records it as the
651
+ * uncaught error it would be in production. `hooks.realNow` is the scope's
652
+ * native Date.now, for the instant a prelude is written at.
653
+ */
654
+ export function clockTrap(scope, o, hooks) {
655
+ 'use strict';
656
+ const h = hooks || {};
657
+ const say = scope.console && typeof scope.console.debug === 'function' ? scope.console.debug.bind(scope.console) : () => {};
658
+ const uncaught = (err) => say(o.token + ':uncaught:' + String((err && err.message) || err).slice(0, 160));
659
+ const fix = (obj, name, value) => { try { Object.defineProperty(obj, name, { value, configurable: false, writable: false }); } catch { /* already fixed */ } };
660
+ const listen = scope.EventTarget && scope.EventTarget.prototype && typeof scope.EventTarget.prototype.addEventListener === 'function' ? scope.EventTarget.prototype.addEventListener : null;
661
+ const postW = scope.Worker && scope.Worker.prototype ? scope.Worker.prototype.postMessage : null;
662
+ const postP = scope.MessagePort && scope.MessagePort.prototype ? scope.MessagePort.prototype.postMessage : null;
663
+ const workers = new Set();
664
+ const tell = (w, msg) => {
665
+ try {
666
+ if (scope.SharedWorker && w instanceof scope.SharedWorker) { if (postP) postP.call(w.port, msg); } else if (postW) postW.call(w, msg);
667
+ } catch { /* gone */ }
668
+ };
669
+ const forward = () => {
670
+ const msg = o.token + ':at:' + scope.Date.now();
671
+ for (const ref of workers) {
672
+ const w = ref && typeof ref.deref === 'function' ? ref.deref() : ref;
673
+ if (!w) { workers.delete(ref); continue; }
674
+ tell(w, msg);
675
+ }
676
+ };
677
+ const prefix = o.token + ':';
678
+ const control = (e) => {
679
+ if (typeof e.data !== 'string' || !e.data.startsWith(prefix)) return;
680
+ const parts = e.data.slice(prefix.length).split(':');
681
+ if (parts[0] === 'at') {
682
+ e.stopImmediatePropagation();
683
+ // The move may be a jump of this scope's own fake clock (asynchronous:
684
+ // the timers due in it run first); the workers are told once it is done.
685
+ let moved;
686
+ try { moved = typeof h.setAt === 'function' ? h.setAt(Number(parts[1])) : undefined; } catch (err) { moved = Promise.reject(err); }
687
+ Promise.resolve(moved).then(forward, (err) => { uncaught(err); forward(); });
688
+ } else if (parts[0] === 'clock') {
689
+ e.stopImmediatePropagation();
690
+ const op = parts[1], id = parts[2], ms = Number(parts[3]);
691
+ const source = e.source;
692
+ const reply = () => { try { source.postMessage(o.token + ':clockdone:' + id + ':' + scope.Date.now() + ':' + String(scope.location && scope.location.href).slice(0, 120), '*'); } catch { /* the sender is gone */ } };
693
+ if (typeof h.jump !== 'function' || (op !== 'runFor' && op !== 'fastForward')) return; // no clock to move here: no answer, and the run says so
694
+ Promise.resolve().then(() => h.jump(op, ms)).then(() => { forward(); reply(); }, (err) => { uncaught(err); forward(); reply(); });
695
+ }
696
+ };
697
+ if (listen) {
698
+ try { listen.call(scope, 'message', control, true); } catch { /* a scope that is no event target (node:vm): no channel */ }
699
+ // A shared worker hears its pages through the ports they connect with.
700
+ const portListen = scope.MessagePort && scope.MessagePort.prototype ? scope.MessagePort.prototype.addEventListener : null;
701
+ if (portListen && typeof scope.SharedWorkerGlobalScope === 'function' && scope instanceof scope.SharedWorkerGlobalScope) {
702
+ listen.call(scope, 'connect', (e) => { for (const p of e.ports || []) portListen.call(p, 'message', control, true); }, true);
703
+ }
704
+ }
705
+ const Blob = scope.Blob, URL = scope.URL;
706
+ if (typeof Blob !== 'function' || typeof URL !== 'function' || typeof URL.createObjectURL !== 'function') return;
707
+ const textOf = new WeakMap(); // Blob -> its text, or null when a part was not text the scope could read
708
+ const blobOf = new Map(); // blob: URL -> Blob
709
+ const decoder = typeof scope.TextDecoder === 'function' ? new scope.TextDecoder() : null;
710
+ const read = (parts) => {
711
+ let s = '';
712
+ for (const p of parts) {
713
+ if (typeof p === 'string') s += p;
714
+ else if (p instanceof Blob) { const t = textOf.get(p); if (t == null) return null; s += t; }
715
+ else if (decoder && (p instanceof scope.ArrayBuffer || scope.ArrayBuffer.isView(p))) s += decoder.decode(p);
716
+ else return null;
717
+ }
718
+ return s;
719
+ };
720
+ const remember = (name) => {
721
+ const orig = scope[name];
722
+ if (typeof orig !== 'function') return;
723
+ const proxy = new Proxy(orig, {
724
+ construct(target, args, newTarget) {
725
+ const b = Reflect.construct(target, args, newTarget);
726
+ try { textOf.set(b, args[0] == null ? '' : read(Array.from(args[0]))); } catch { textOf.set(b, null); }
727
+ return b;
728
+ },
729
+ });
730
+ // new Blob([]).constructor === Blob, as in production.
731
+ try { Object.defineProperty(orig.prototype, 'constructor', { value: proxy, writable: true, enumerable: false, configurable: true }); } catch { /* fixed already */ }
732
+ fix(scope, name, proxy);
733
+ };
734
+ remember('Blob');
735
+ remember('File');
736
+ const create = URL.createObjectURL, revoke = URL.revokeObjectURL;
737
+ fix(URL, 'createObjectURL', new Proxy(create, { apply(t, self, a) { const u = Reflect.apply(t, self, a); if (a[0] instanceof Blob) blobOf.set(u, a[0]); return u; } }));
738
+ if (typeof revoke === 'function') fix(URL, 'revokeObjectURL', new Proxy(revoke, { apply(t, self, a) { blobOf.delete(String(a[0])); return Reflect.apply(t, self, a); } }));
739
+ // The prelude a worker started here is given (the same text workerShim
740
+ // writes for the run's server): the clocks at this scope's now, read at the
741
+ // native instant `wall` so the worker adds the real time it took to start.
742
+ const prelude = () => {
743
+ const at = scope.Date.now(), wall = typeof h.realNow === 'function' ? h.realNow() : at;
744
+ const sh = '(' + o.shimSrc + ')';
745
+ let s = '(() => { const scope = self; const natives = ' + sh + '(scope, { capture: true }); let fake = null, jump = null;\n';
746
+ if (o.clockSrc && o.startSrc) s += '{ const module = { exports: {} };\n' + o.clockSrc + '\nconst started = (' + o.startSrc + ')(scope, ' + JSON.stringify({ at, wall }) + ', module.exports, natives); fake = started.fake; jump = started.jump; }\n';
747
+ s += 'const setAt = ' + sh + '(scope, { natives, fake' + (o.clockSrc && o.startSrc ? '' : ', at: ' + at) + ' });\n';
748
+ s += '(' + o.trapSrc + ')(scope, ' + JSON.stringify(o) + ', { setAt: fake ? (t) => jump("runFor", Math.max(0, Math.round(Number(t) - fake.now()))) : setAt, jump, realNow: natives.Date.now }); })();\n';
749
+ return s;
750
+ };
751
+ for (const [name, shared] of [['Worker', false], ['SharedWorker', true]]) {
752
+ const orig = scope[name];
753
+ if (typeof orig !== 'function') continue;
754
+ fix(scope, name, new Proxy(orig, {
755
+ construct(target, args, newTarget) {
756
+ let abs = null;
757
+ try { abs = new URL(String(args[0]), scope.location.href); } catch { abs = null; }
758
+ const blob = !!abs && abs.protocol === 'blob:';
759
+ say(o.token + ':worker:' + (shared ? 'shared ' : '') + (abs ? (blob ? 'blob:' : abs.origin + abs.pathname) : String(args[0]).slice(0, 80)));
760
+ let w;
761
+ if (blob) {
762
+ const b = blobOf.get(abs.href);
763
+ const src = b ? textOf.get(b) : null;
764
+ if (src == null) { say(o.token + ':unshimmed:blob:'); w = Reflect.construct(target, args, newTarget); }
765
+ else {
766
+ const url = Reflect.apply(create, URL, [new Blob([prelude() + src], { type: 'text/javascript' })]);
767
+ w = Reflect.construct(target, [url, ...args.slice(1)], newTarget);
768
+ }
769
+ } else {
770
+ w = Reflect.construct(target, args, newTarget); // a same-origin script gets its prelude from the run's server
771
+ }
772
+ workers.add(typeof scope.WeakRef === 'function' ? new scope.WeakRef(w) : w);
773
+ return w;
774
+ },
775
+ }));
776
+ }
777
+ }
778
+
779
+ /**
780
+ * The lines a worker's script is given before its own (the run's server adds
781
+ * them to a same-origin worker script it serves, the trap to a blob's text):
782
+ * under time travel (`clock`: Playwright's clock source and clock.mjs
783
+ * startClock, as text) a fake clock of the worker's own at `at` (the
784
+ * instant the page's clock read when the script was served, plus the real
785
+ * time since `wall`, so its clocks agree with the page's), otherwise the clock
786
+ * shim moved to `at` by an offset; then the trap, so a worker it starts is
787
+ * treated the same and a jump of the page's clock reaches it (and, with a
788
+ * fake clock, runs the worker's timers due in the jump). Nothing without an
789
+ * instant. The text is the one clockTrap writes for a blob worker.
790
+ */
791
+ export function workerShim(at, token = '', clock = null, wall = Date.now()) {
792
+ if (!at || !Number.isFinite(Number(at))) return '';
793
+ const o = trapOptions(token, clock);
794
+ const sh = '(' + o.shimSrc + ')';
795
+ let s = '(() => { const scope = self; const natives = ' + sh + '(scope, { capture: true }); let fake = null, jump = null;\n';
796
+ if (o.clockSrc && o.startSrc) s += '{ const module = { exports: {} };\n' + o.clockSrc + '\nconst started = (' + o.startSrc + ')(scope, ' + JSON.stringify({ at: Number(at), wall: Number(wall) }) + ', module.exports, natives); fake = started.fake; jump = started.jump; }\n';
797
+ s += 'const setAt = ' + sh + '(scope, { natives, fake' + (o.clockSrc && o.startSrc ? '' : ', at: ' + Number(at)) + ' });\n';
798
+ s += '(' + o.trapSrc + ')(scope, ' + JSON.stringify(o) + ', { setAt: fake ? (t) => jump("runFor", Math.max(0, Math.round(Number(t) - fake.now()))) : setAt, jump, realNow: natives.Date.now }); })();\n';
799
+ return s;
800
+ }
801
+
802
+ /**
803
+ * What a scope's trap is given: the run's token, the two functions' sources
804
+ * for the preludes it writes, and, when the run travels, the fake clock's
805
+ * (`clock`: { source, start } — Playwright's clock source and startClock as
806
+ * text), so every worker started under a travelling page runs a fake clock
807
+ * of its own.
808
+ */
809
+ export const trapOptions = (token, clock = null) => ({ token, shimSrc: String(clockShim), trapSrc: String(clockTrap), ...(clock && clock.source && clock.start ? { clockSrc: String(clock.source), startSrc: String(clock.start) } : {}) });
810
+
811
+ /** The control message that moves a page's fake clock (clockTrap), and the answer it gives. */
812
+ export const clockCommand = (token, op, id, ms) => `${token}:clock:${op}:${id}:${ms}`;
813
+ export const clockAnswer = (token, id) => `${token}:clockdone:${id}:`;
814
+
815
+ // ---- the long session's forced rows -----------------------------------------------------
816
+
817
+ /**
818
+ * Which tiers a session of `count` rounds plays when the mode has more tiers
819
+ * than rounds: evenly spaced across the tiers sorted by pay, the lowest and
820
+ * the highest always among them, so every band of the pay table and the max
821
+ * win are seen even when every single tier cannot be.
822
+ */
823
+ export function pickTiers(tiers, count) {
824
+ const sorted = [...tiers].sort((a, b) => a.multBp - b.multBp);
825
+ if (sorted.length <= count) return sorted;
826
+ if (count <= 1) return sorted.slice(0, 1);
827
+ const out = [];
828
+ for (let i = 0; i < count; i++) {
829
+ const at = Math.round((i * (sorted.length - 1)) / (count - 1));
830
+ if (!out.length || out[out.length - 1] !== sorted[at]) out.push(sorted[at]);
831
+ }
832
+ return out;
833
+ }
834
+
835
+ /**
836
+ * The rows the long session forces, one per round, cycling: a row that pays
837
+ * nothing (when the table has one), then the first row of each pay tier of the
838
+ * mode and bet the game plays (`played`: the first play's body; the first
839
+ * mode and its first bet when nothing was played yet), lowest pay first. A
840
+ * ladder mode is not forced: its rounds play as drawn. The answer is null
841
+ * when nothing can be forced. The sandbox resolves one index against the
842
+ * played mode's table, so a row from another mode's table would be refused
843
+ * or land on another tier: the rows must be the played mode's own.
844
+ */
845
+ export function forcedRows(manifest, rounds, played = null) {
846
+ const modes = Array.isArray(manifest?.modes) ? manifest.modes : [];
847
+ const mode = (played?.mode && modes.find((m) => m.id === played.mode)) || modes[0];
848
+ const bets = Array.isArray(mode?.bets) ? mode.bets : [];
849
+ const bet = (played?.bet && bets.find((b) => b.id === played.bet)) || bets[0];
850
+ const tiers = bet?.tiers;
851
+ if (!mode || mode.kind !== 'table' || !Array.isArray(tiers) || !tiers.length || rounds < 1) return null;
852
+ const covers = (x) => tiers.some((t) => (t.outcomes || []).some(([a, b]) => x >= a && x <= b));
853
+ const last = Math.max(0, ...tiers.flatMap((t) => (t.outcomes || []).map(([, b]) => b)));
854
+ let noPay = null;
855
+ for (let x = 0; x <= Math.min(last, 100000); x++) {
856
+ if (!covers(x)) { noPay = x; break; }
857
+ }
858
+ const picks = [];
859
+ if (noPay !== null) picks.push(noPay);
860
+ for (const t of pickTiers(tiers, Math.max(1, rounds - picks.length))) {
861
+ const row = t.outcomes?.[0]?.[0];
862
+ if (Number.isInteger(row) && row >= 0) picks.push(row);
863
+ }
864
+ if (!picks.length) return null;
865
+ return Array.from({ length: rounds }, (_, i) => picks[i % picks.length]);
866
+ }
867
+
868
+ /** The share of forced rounds the sandbox must honour before the session warns. */
869
+ export const FORCED_HONOURED_SHARE = 0.9;
870
+
871
+ // ---- credential prompts ----------------------------------------------------------------
872
+
873
+ /**
874
+ * The words a credential prompt uses, in the languages the matrix plays, and
875
+ * the shapes that need no language: a crypto address, a one-time code, a
876
+ * card. A game has no sign-in, no account and no wallet of the player's: any
877
+ * of these on its screen is a prompt for something it must never ask.
878
+ */
879
+ export const CREDENTIAL_PATTERNS = Object.freeze([
880
+ // en
881
+ { lang: 'en', re: /\bpassword\b/i, say: 'password' },
882
+ { lang: 'en', re: /\bseed[\s-]?phrase\b/i, say: 'seed phrase' },
883
+ { lang: 'en', re: /\bprivate[\s-]?key\b/i, say: 'private key' },
884
+ { lang: 'en', re: /\blog[\s-]?in\b|\bsign[\s-]?in\b/i, say: 'log in' },
885
+ { lang: 'en', re: /\bverify your (account|identity|wallet)\b/i, say: 'verify your account' },
886
+ { lang: 'en', re: /\bwallet address\b/i, say: 'wallet address' },
887
+ { lang: 'en', re: /\b(one[\s-]?time|2fa|otp|verification|security|sms|authenticator) code\b/i, say: 'one-time code' },
888
+ { lang: 'en', re: /\bcvv\b|\bcvc\b|\bcard number\b|\bcredit card\b|\bdebit card\b/i, say: 'card number' },
889
+ // de
890
+ { lang: 'de', re: /\bpasswort\b|\bkennwort\b/i, say: 'Passwort' },
891
+ { lang: 'de', re: /\bseed[\s-]?phrase\b|\bwiederherstellungsphrase\b/i, say: 'Seed-Phrase' },
892
+ { lang: 'de', re: /\bprivater schl[üu]ssel\b|\bprivatschl[üu]ssel\b/i, say: 'privater Schlüssel' },
893
+ { lang: 'de', re: /\banmelden\b|\beinloggen\b/i, say: 'anmelden' },
894
+ { lang: 'de', re: /\bkonto (best[äa]tigen|verifizieren)\b/i, say: 'Konto bestätigen' },
895
+ { lang: 'de', re: /\bwallet[\s-]?adresse\b/i, say: 'Wallet-Adresse' },
896
+ { lang: 'de', re: /\bbest[äa]tigungscode\b|\bsicherheitscode\b|\beinmalcode\b/i, say: 'Bestätigungscode' },
897
+ { lang: 'de', re: /\bkartennummer\b|\bkreditkarte\b/i, say: 'Kartennummer' },
898
+ // ja
899
+ { lang: 'ja', re: /パスワード/, say: 'パスワード' },
900
+ { lang: 'ja', re: /シードフレーズ|リカバリーフレーズ/, say: 'シードフレーズ' },
901
+ { lang: 'ja', re: /秘密鍵|プライベートキー/, say: '秘密鍵' },
902
+ { lang: 'ja', re: /ログイン|サインイン/, say: 'ログイン' },
903
+ { lang: 'ja', re: /アカウント(を)?(確認|認証)/, say: 'アカウントを確認' },
904
+ { lang: 'ja', re: /ウォレット(の)?アドレス/, say: 'ウォレットアドレス' },
905
+ { lang: 'ja', re: /認証コード|確認コード|ワンタイムパスワード/, say: '認証コード' },
906
+ { lang: 'ja', re: /カード番号|クレジットカード/, say: 'カード番号' },
907
+ // pt
908
+ { lang: 'pt', re: /\bsenha\b/i, say: 'senha' },
909
+ { lang: 'pt', re: /\bfrase[\s-]?semente\b|\bfrase de recupera[çc][ãa]o\b/i, say: 'frase semente' },
910
+ { lang: 'pt', re: /\bchave privada\b/i, say: 'chave privada' },
911
+ { lang: 'pt', re: /\bfazer login\b|\biniciar sess[ãa]o\b/i, say: 'fazer login' },
912
+ { lang: 'pt', re: /\bverifi(que|car) (sua|a) conta\b/i, say: 'verifique sua conta' },
913
+ { lang: 'pt', re: /\bendere[çc]o da carteira\b/i, say: 'endereço da carteira' },
914
+ { lang: 'pt', re: /\bc[óo]digo de (verifica[çc][ãa]o|seguran[çc]a|confirma[çc][ãa]o)\b/i, say: 'código de verificação' },
915
+ { lang: 'pt', re: /\bn[úu]mero do cart[ãa]o\b|\bcart[ãa]o de cr[ée]dito\b/i, say: 'número do cartão' },
916
+ // shapes, in any language: an Ethereum, Bitcoin or Tron address is never a game's word
917
+ { lang: '*', re: /\b0x[0-9a-fA-F]{40}\b/, say: 'crypto address' },
918
+ { lang: '*', re: /\b(bc1[a-zA-HJ-NP-Z0-9]{25,62}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})\b/, say: 'crypto address' },
919
+ { lang: '*', re: /\bT[1-9A-HJ-NP-Za-km-z]{33}\b/, say: 'crypto address' },
920
+ ]);
921
+
922
+ // Letters from other scripts that look like Latin ones, and are typed in place
923
+ // of them to slip past a word list. Mapped to the Latin letter before matching.
924
+ const CONFUSABLES = { 'а': 'a', 'е': 'e', 'о': 'o', 'р': 'p', 'с': 'c', 'у': 'y', 'х': 'x', 'і': 'i', 'ѕ': 's', 'ј': 'j', 'һ': 'h', 'ԁ': 'd', 'ɡ': 'g', 'ո': 'n', 'ѵ': 'v', 'ω': 'w', 'ο': 'o', 'ν': 'v', 'α': 'a', 'ρ': 'p', 'τ': 't', 'κ': 'k', 'ι': 'i', 'А': 'A', 'В': 'B', 'Е': 'E', 'К': 'K', 'М': 'M', 'Н': 'H', 'О': 'O', 'Р': 'P', 'С': 'C', 'Т': 'T', 'Х': 'X', 'Ѵ': 'V' };
925
+ // Characters that render as nothing (or as a break only when wrapping) and are
926
+ // put inside a word to split it for a matcher while the eye reads it whole.
927
+ const INVISIBLE = /[​-‏⁠⁡-⁤­͏᠎]/g;
928
+
929
+ /**
930
+ * Text as the matcher reads it: NFKC (fullwidth and styled letters fold to
931
+ * plain ones), invisible characters gone, confusables mapped, whitespace
932
+ * collapsed. What a player reads is what is matched.
933
+ */
934
+ export function normaliseText(text) {
935
+ let t = String(text || '').normalize('NFKC').replace(INVISIBLE, '');
936
+ t = t.replace(/[Ѐ-ӿͰ-ϿԀ-ԯա-ևɐ-ʯ]/g, (c) => CONFUSABLES[c] ?? c);
937
+ return t.replace(/\s+/g, ' ');
938
+ }
939
+
940
+ /** The credential phrases found in a text, each once, as the reviewer reads them. */
941
+ export function credentialHits(text) {
942
+ const t = normaliseText(text);
943
+ const out = [];
944
+ for (const p of CREDENTIAL_PATTERNS) {
945
+ if (p.re.test(t) && !out.includes(p.say)) out.push(p.say);
946
+ }
947
+ return out;
948
+ }
949
+
950
+ /**
951
+ * An input as a variant records it: enough to tell one field from another
952
+ * across runs (its type, id and name), never its value and never its words,
953
+ * which change with the language the variant plays in. A text field that
954
+ * masks what is typed (`-webkit-text-security`) or that autocompletes a
955
+ * password or a one-time code is a password field, whatever its `type` says.
956
+ * The SDK's own fields (<hb-bet-amount>, the panel) are marked, so a game's
957
+ * own text input stands out from them.
958
+ */
959
+ export function inputSignature(i) {
960
+ let type = i.type || 'text';
961
+ if (type !== 'password' && type !== 'email' && (i.masked || /password|one-time-code/i.test(i.autocomplete || ''))) type = 'password';
962
+ return [type, i.id || '', i.name || '', i.sdk ? 'sdk' : ''].join('|');
963
+ }
964
+
965
+ // ---- the bet control -------------------------------------------------------------------
966
+
967
+ /** A control a player could press: at least this many CSS pixels each way. */
968
+ export const CONTROL_MIN_PX = 32;
969
+
970
+ /**
971
+ * Why the long session's bet control cannot be the player's button, or null
972
+ * when it can be: a 1×1 or hidden element wired to the engine, with the real
973
+ * button's handler left untested, is a decoy. `box` is what the browser
974
+ * measured once the stake was typed: width, height, left, top, viewport width
975
+ * and height, opacity, pointerEvents, visibility, disabled.
976
+ */
977
+ export function controlProblem(box) {
978
+ if (!box) return 'the bet control was not found';
979
+ const { w = 0, h = 0, left = 0, top = 0, vw = 0, vh = 0, opacity = 1, pointerEvents = 'auto', visibility = 'visible', disabled = false } = box;
980
+ if (w < CONTROL_MIN_PX || h < CONTROL_MIN_PX) return `the bet control is ${Math.round(w)}×${Math.round(h)} px, smaller than a button a player presses (${CONTROL_MIN_PX}×${CONTROL_MIN_PX})`;
981
+ if (disabled) return 'the bet control is disabled once the stake is typed';
982
+ if (left + w <= 0 || top + h <= 0 || left >= vw || top >= vh) return `the bet control is outside the frame (at ${Math.round(left)},${Math.round(top)})`;
983
+ if (!(opacity > 0.5)) return `the bet control is ${Math.round(opacity * 100)}% opaque`;
984
+ if (pointerEvents === 'none') return 'the bet control takes no pointer (pointer-events: none)';
985
+ if (visibility !== 'visible') return `the bet control is ${visibility}`;
986
+ return null;
987
+ }
988
+
989
+ // ---- the screenshot grid ---------------------------------------------------------------
990
+
991
+ /** The grid a screenshot is reduced to (columns × rows of grey cells). */
992
+ export const GRID = Object.freeze({ w: 48, h: 32 });
993
+ /** A cell differs when its grey level moves by more than this (of 255). */
994
+ export const CELL_THRESHOLD = 40;
995
+ /** A strong difference: this share of the cells differ. */
996
+ export const STRONG_SHARE = 0.3;
997
+ /** Or one contiguous region of changed cells this big: a dialog in the middle of the frame. */
998
+ export const REGION_SHARE = 0.08;
999
+
1000
+ /**
1001
+ * The cell-by-cell median of a few grids of the same frame taken a second
1002
+ * apart: a reel that was spinning, a ticker, a flicker in one capture does not
1003
+ * decide the baseline. One grid is itself; none is null.
1004
+ */
1005
+ export function medianGrid(grids) {
1006
+ const ok = (grids || []).filter((g) => Array.isArray(g) && g.length);
1007
+ if (!ok.length) return null;
1008
+ if (ok.length === 1) return ok[0];
1009
+ const n = ok[0].length;
1010
+ if (!ok.every((g) => g.length === n)) return ok[0];
1011
+ const out = new Array(n);
1012
+ const col = new Array(ok.length);
1013
+ for (let i = 0; i < n; i++) {
1014
+ for (let k = 0; k < ok.length; k++) col[k] = ok[k][i];
1015
+ col.sort((a, b) => a - b);
1016
+ out[i] = col[Math.floor(col.length / 2)];
1017
+ }
1018
+ return out;
1019
+ }
1020
+
1021
+ /**
1022
+ * How much of the frame changed between two grey grids: the share of cells
1023
+ * whose level moved by more than CELL_THRESHOLD, and the largest contiguous
1024
+ * region of them (4-connected on the grid). Animation and a round's own
1025
+ * numbers move a few scattered cells; a screen replaced by another moves
1026
+ * most; a modal moves one solid block. Strong when either is large.
1027
+ */
1028
+ export function visualDifference(a, b) {
1029
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || !a.length) return null;
1030
+ const n = a.length, w = GRID.w, h = n % w === 0 ? n / w : 1;
1031
+ const moved = new Uint8Array(n);
1032
+ let changed = 0;
1033
+ for (let i = 0; i < n; i++) if (Math.abs(a[i] - b[i]) > CELL_THRESHOLD) { moved[i] = 1; changed++; }
1034
+ // The largest region: a flood fill over the moved cells.
1035
+ let region = 0;
1036
+ const seen = new Uint8Array(n);
1037
+ const stack = [];
1038
+ for (let s = 0; s < n; s++) {
1039
+ if (!moved[s] || seen[s]) continue;
1040
+ let size = 0;
1041
+ stack.push(s);
1042
+ seen[s] = 1;
1043
+ while (stack.length) {
1044
+ const i = stack.pop();
1045
+ size++;
1046
+ const x = i % w, y = Math.floor(i / w);
1047
+ for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
1048
+ const nx = x + dx, ny = y + dy;
1049
+ if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
1050
+ const j = ny * w + nx;
1051
+ if (moved[j] && !seen[j]) { seen[j] = 1; stack.push(j); }
1052
+ }
1053
+ }
1054
+ region = Math.max(region, size);
1055
+ }
1056
+ const share = changed / n, regionShare = region / n;
1057
+ return { changed: Math.round(share * 1000) / 1000, region: Math.round(regionShare * 1000) / 1000, strong: share > STRONG_SHARE || regionShare >= REGION_SHARE };
1058
+ }
1059
+
1060
+ // ---- the budget -------------------------------------------------------------------------
1061
+
1062
+ /** What a short variant is assumed to take before any has been measured (boot, a round, the walk). */
1063
+ export const SHORT_VARIANT_MS = 60 * 1000;
1064
+ /** The least a short variant is estimated at once one has been measured. */
1065
+ export const SHORT_VARIANT_MIN_MS = 15 * 1000;
1066
+ /** The least a long session is worth starting with. */
1067
+ export const LONG_MIN_MS = 60 * 1000;
1068
+
1069
+ /**
1070
+ * What a short variant is estimated to take, from the short variants played
1071
+ * so far (the baseline does everything one does: boot, a round, the walk):
1072
+ * half again the slowest of them, never under SHORT_VARIANT_MIN_MS; the
1073
+ * pessimistic SHORT_VARIANT_MS before any was played. The constant alone
1074
+ * would skip the long session in every job: the job's matrix has about
1075
+ * 390–510 s, and seven minutes for the seven short variants after the long
1076
+ * one leaves it nothing, while a measured short variant takes 10–12 s.
1077
+ */
1078
+ export function shortVariantEstimate(variants) {
1079
+ const played = (variants || []).filter((v) => !v.rounds && !v.skipped && v.ms > 0).map((v) => v.ms);
1080
+ if (!played.length) return SHORT_VARIANT_MS;
1081
+ return Math.max(SHORT_VARIANT_MIN_MS, Math.round(Math.max(...played) * 1.5));
1082
+ }
1083
+
1084
+ /**
1085
+ * Whether the next variant fits what is left of the matrix's real time
1086
+ * (`totalMs`, none when 0), and how much of it a long session may take: the
1087
+ * rest of the matrix, kept for the `after` variants after it at `perVariantMs`
1088
+ * each (shortVariantEstimate). A variant that does not fit is skipped and
1089
+ * says so, instead of the whole matrix being killed at its deadline with no
1090
+ * report.
1091
+ */
1092
+ export function fitsBudget({ elapsedMs, totalMs, rounds, longBudgetMs, after = 0, perVariantMs = SHORT_VARIANT_MS }) {
1093
+ if (!totalMs) return { fits: true, longBudgetMs };
1094
+ const left = totalMs - elapsedMs;
1095
+ if (!rounds) return { fits: left >= perVariantMs, longBudgetMs };
1096
+ const room = left - after * perVariantMs - 15 * 1000;
1097
+ if (room < LONG_MIN_MS) return { fits: false, longBudgetMs };
1098
+ return { fits: true, longBudgetMs: Math.min(longBudgetMs, room) };
1099
+ }
1100
+
1101
+ // ---- the storage carried between canaries ------------------------------------------------
1102
+
1103
+ /** A carried storage file is read no further than this: a game's own storage, never a dump. */
1104
+ export const STORAGE_FILE_MAX = 4 * 1024 * 1024;
1105
+
1106
+ /**
1107
+ * The game's storage as a run left it (run.mjs keepStorage: Playwright's
1108
+ * storageState for the bundle's origin), read back for the next run at
1109
+ * `origin`: the bundle is served from a fresh port each run, so the origin
1110
+ * recorded is rewritten to today's. A game that stored nothing left an empty
1111
+ * list of origins, which is storage too. Anything that is not such a file is
1112
+ * null (the run then starts from empty storage, and says so). The daily
1113
+ * canary carries it from one day to the next, so a counter the game keeps
1114
+ * across visits counts the canary's visits as a player's.
1115
+ */
1116
+ export function carriedStorage(raw, origin) {
1117
+ if (typeof raw !== 'string' || !raw || raw.length > STORAGE_FILE_MAX) return null;
1118
+ let parsed;
1119
+ try { parsed = JSON.parse(raw); } catch { return null; }
1120
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.origins)) return null;
1121
+ const origins = parsed.origins.filter((o) => o && typeof o === 'object' && typeof o.origin === 'string').slice(0, 1).map((o) => ({ ...o, origin }));
1122
+ return { cookies: [], origins, keys: (origins[0]?.localStorage || []).map((e) => e.name).sort().slice(0, 50) };
1123
+ }
1124
+
1125
+ // ---- the judgement ----------------------------------------------------------------------
1126
+
1127
+ /** A URL without its query, so a cache-buster does not make a new request of the same file. */
1128
+ const bare = (u) => { try { const x = new URL(u); return x.origin + x.pathname; } catch { return String(u).split('?')[0]; } };
1129
+ /** An error message without its numbers (line, column, counter), so the same error is the same across runs. */
1130
+ const shape = (m) => String(m).replace(/\d+/g, '#').slice(0, 160);
1131
+
1132
+ /**
1133
+ * The failures and warnings of one variant against the baseline (the base
1134
+ * variant compared with itself: only what fails outright). Each failure is a
1135
+ * sentence for the check line.
1136
+ */
1137
+ export function compareVariant(variant, baseline) {
1138
+ const failures = [], warnings = [];
1139
+ const base = baseline && baseline !== variant ? baseline : null;
1140
+ const fresh = (mine, theirs, norm) => { const known = new Set((theirs || []).map(norm)); return [...new Set((mine || []).map(norm).filter((x) => !known.has(x)))]; };
1141
+ if (variant.broke) failures.push('the run broke: ' + variant.broke);
1142
+ if (!variant.boot) failures.push('the game did not boot');
1143
+ // Against the baseline: what is new. The baseline itself: anything at all,
1144
+ // as the frame-size run would say of it.
1145
+ const requests = fresh(variant.requests, base?.requests, bare);
1146
+ if (requests.length) failures.push(`${base ? 'new ' : ''}request(s) left the bundle: ${requests.slice(0, 3).join(', ')}`);
1147
+ const refused = fresh(variant.refused, base?.refused, shape);
1148
+ if (refused.length) failures.push(`the policy refused ${base ? 'something new' : 'something'}: ${refused.slice(0, 2).join(' | ')}`);
1149
+ const workers = base ? fresh(variant.workers, base.workers, bare) : [];
1150
+ if (workers.length) failures.push(`new worker(s): ${workers.slice(0, 3).join(', ')}`);
1151
+ const frames = base ? fresh(variant.frames, base.frames, String) : [];
1152
+ if (frames.length) failures.push(`new frame(s): ${frames.slice(0, 3).join(', ')}`);
1153
+ if (variant.dialogs > 0) failures.push(`${variant.dialogs} dialog attempt(s): ${(variant.dialogKinds || []).join(', ')}`);
1154
+ const errors = fresh(variant.errors, base?.errors, shape);
1155
+ if (errors.length) failures.push(`${base ? 'new ' : ''}uncaught error(s): ${errors.slice(0, 3).join(' | ')}`);
1156
+ const consoleErrors = base ? fresh(variant.consoleErrors, base.consoleErrors, shape) : [];
1157
+ if (consoleErrors.length) failures.push(`new console error(s): ${consoleErrors.slice(0, 3).join(' | ')}`);
1158
+ const secret = (variant.inputs || []).filter((i) => /^(password|email)\|/.test(i));
1159
+ if (secret.length) failures.push(`${secret[0].startsWith('email') ? 'an' : 'a'} ${secret[0].split('|')[0]} input: ${secret.slice(0, 2).join(', ')}`);
1160
+ const inputs = base ? fresh(variant.inputs, base.inputs, String).filter((i) => !secret.includes(i)) : [];
1161
+ if (inputs.length) failures.push(`new input field(s): ${inputs.slice(0, 3).join(', ')}`);
1162
+ if ((variant.phishing || []).length) failures.push(`text that reads like a credential prompt: ${variant.phishing.slice(0, 4).join(', ')}`);
1163
+ if (variant.controlProblem) failures.push(variant.controlProblem);
1164
+ // The long session: a round that stuck fails; a session its time ran out
1165
+ // on is a warning, since a trigger past its last round was not seen; so is
1166
+ // one whose forced rows the sandbox did not honour, since the tiers were
1167
+ // not all shown.
1168
+ if (variant.rounds && !variant.controlProblem && (variant.settled || 0) < variant.rounds) {
1169
+ if (variant.stuck || !variant.outOfTime) failures.push(`${variant.settled || 0} of ${variant.rounds} fast rounds settled${variant.stuck ? ': ' + variant.stuck : ''}`);
1170
+ else warnings.push(`only ${variant.settled || 0} of ${variant.rounds} fast rounds fit the session's ${Math.round((variant.budgetMs || 0) / 1000)} s (this game's rounds are long): a trigger past round ${variant.settled || 0} is not seen`);
1171
+ }
1172
+ if (variant.rounds && variant.forced && variant.forcedAsked > 0 && (variant.forcedApplied || 0) < variant.forcedAsked * FORCED_HONOURED_SHARE) {
1173
+ warnings.push(`the sandbox honoured ${variant.forcedApplied || 0} of ${variant.forcedAsked} forced rows${variant.forcedRefused ? ` (${variant.forcedRefused} refused)` : ''}: not every pay tier was shown`);
1174
+ }
1175
+ // A worker the shim could not reach (a blob whose parts the page did not
1176
+ // see) keeps the real clock: the date variants do not reach what runs in it.
1177
+ if (variant.unshimmedWorkers > 0) warnings.push(`${variant.unshimmedWorkers} worker(s) started from a blob the run could not read run on the real clock: the travelled clocks do not reach them`);
1178
+ if (variant.visual?.strong) warnings.push(`looks different from the baseline (${pct(variant.visual)} of the frame): compare the screenshots`);
1179
+ if (variant.visualLate?.strong) warnings.push(`after a day of game time, looks different from the baseline (${pct(variant.visualLate)} of the frame): compare the late screenshots`);
1180
+ if (variant.visualVsApproved?.strong) warnings.push(`looks different from the approved version (${pct(variant.visualVsApproved)} of the frame): compare the screenshots`);
1181
+ return { failures, warnings };
1182
+ }
1183
+
1184
+ /** A difference as the line says it: the share of the frame, or the region when that is what made it strong. */
1185
+ const pct = (d) => (d.changed > STRONG_SHARE || !d.region ? `${Math.round(d.changed * 100)}%` : `a region of ${Math.round(d.region * 100)}%`);
1186
+
1187
+ /** The variant as one line of the report, for a person. */
1188
+ export function describe(variant) {
1189
+ const when = variant.clock === 'now' ? 'real time' : `clock ${variant.clock === 'saturday' ? 'next Saturday' : variant.clock === 'dec25' ? 'next 25 December' : variant.clock === 'jan1' ? 'next 1 January' : variant.clock}`;
1190
+ return `${when}, ${variant.tz}, ${variant.locale}${variant.rounds ? `, ${variant.rounds} fast rounds` : ''}`;
1191
+ }
1192
+
1193
+ /**
1194
+ * The checks the run reports for the matrix, one per variant: rule
1195
+ * `time-travel`, the variant's id as the size. A failure fails the check; a
1196
+ * warning passes it with status `warning`, which the studio shows the
1197
+ * reviewer; a variant that was not played is `skipped` with the reason.
1198
+ */
1199
+ export function checksFor(variants, baseline) {
1200
+ return variants.map((v) => {
1201
+ if (v.skipped) return { size: v.id, rule: 'time-travel', ok: true, status: 'skipped', detail: `${describe(v)}: ${v.skipped}` };
1202
+ const { failures, warnings } = compareVariant(v, baseline);
1203
+ v.failures = failures;
1204
+ v.warnings = warnings;
1205
+ if (failures.length) return { size: v.id, rule: 'time-travel', ok: false, detail: `${describe(v)}: ${failures.join('; ')}` };
1206
+ if (warnings.length) return { size: v.id, rule: 'time-travel', ok: true, status: 'warning', detail: `${describe(v)}: ${warnings.join('; ')}` };
1207
+ return { size: v.id, rule: 'time-travel', ok: true, detail: `${describe(v)}: same as the baseline${v.rounds ? `, ${v.settled || 0} rounds settled${v.sessionMs ? ` in ${Math.round(v.sessionMs / 1000)} s` : ''}` : ''}` };
1208
+ });
1209
+ }