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.
- package/README.md +20 -0
- package/engine-cli/connect.mjs +73 -0
- package/engine-cli/hbengine.mjs +565 -0
- package/engine-cli/package.json +1 -0
- package/engine-cli/templates.mjs +14 -0
- package/engine-conformance/analyse-worker.mjs +12 -0
- package/engine-conformance/analyse.mjs +4458 -0
- package/engine-conformance/bundle.mjs +208 -0
- package/engine-conformance/clock.mjs +294 -0
- package/engine-conformance/csp.go +56 -0
- package/engine-conformance/csp.json +6 -0
- package/engine-conformance/csp.mjs +20 -0
- package/engine-conformance/gzip-sizes.mjs +9 -0
- package/engine-conformance/package.json +1 -0
- package/engine-conformance/picture.mjs +55 -0
- package/engine-conformance/report.go +349 -0
- package/engine-conformance/run.mjs +1217 -0
- package/engine-conformance/scan.mjs +476 -0
- package/engine-conformance/testdata/clock-overlay/bundle/assets.json +1 -0
- package/engine-conformance/testdata/clock-overlay/bundle/game.css +17 -0
- package/engine-conformance/testdata/clock-overlay/bundle/game.js +83 -0
- package/engine-conformance/testdata/clock-overlay/bundle/hbtest.json +4 -0
- package/engine-conformance/testdata/clock-overlay/bundle/index.html +34 -0
- package/engine-conformance/testdata/clock-probe/bundle/assets.json +1 -0
- package/engine-conformance/testdata/clock-probe/bundle/game.css +17 -0
- package/engine-conformance/testdata/clock-probe/bundle/game.js +65 -0
- package/engine-conformance/testdata/clock-probe/bundle/hbtest.json +4 -0
- package/engine-conformance/testdata/clock-probe/bundle/index.html +35 -0
- package/engine-conformance/testdata/clock-probe/bundle/probe-worker.js +46 -0
- package/engine-conformance/testdata/clock-probe/bundle/probe.js +210 -0
- package/engine-conformance/testdata/clock-probe/math/dice.tsv +100 -0
- package/engine-conformance/testdata/clock-probe/math/manifest.json +2925 -0
- package/engine-conformance/testdata/clock-worker/bundle/assets.json +1 -0
- package/engine-conformance/testdata/clock-worker/bundle/clock-worker.js +14 -0
- package/engine-conformance/testdata/clock-worker/bundle/game.css +17 -0
- package/engine-conformance/testdata/clock-worker/bundle/game.js +61 -0
- package/engine-conformance/testdata/clock-worker/bundle/hbtest.json +4 -0
- package/engine-conformance/testdata/clock-worker/bundle/index.html +35 -0
- package/engine-conformance/testdata/clock-worker/math/dice.tsv +100 -0
- package/engine-conformance/testdata/clock-worker/math/manifest.json +2925 -0
- package/engine-conformance/travel.mjs +1209 -0
- package/engine-conformance/vendor/acorn/LICENSE +21 -0
- package/engine-conformance/vendor/acorn/acorn.mjs +6266 -0
- package/engine-conformance/wasm-releases.json +168 -0
- package/engine-examples/README.md +23 -0
- package/engine-examples/coin/AGENTS.md +3 -0
- package/engine-examples/coin/README.md +7 -0
- package/engine-examples/coin/bundle/assets.json +1 -0
- package/engine-examples/coin/bundle/game.css +5 -0
- package/engine-examples/coin/bundle/game.js +19 -0
- package/engine-examples/coin/bundle/hbtest.json +5 -0
- package/engine-examples/coin/bundle/index.html +16 -0
- package/engine-examples/coin/math/coin.tsv +2 -0
- package/engine-examples/coin/math/manifest.json +17 -0
- package/engine-examples/crash/AGENTS.md +3 -0
- package/engine-examples/crash/README.md +14 -0
- package/engine-examples/crash/bundle/assets.json +1 -0
- package/engine-examples/crash/bundle/game.css +8 -0
- package/engine-examples/crash/bundle/game.js +90 -0
- package/engine-examples/crash/bundle/hbtest.json +5 -0
- package/engine-examples/crash/bundle/index.html +20 -0
- package/engine-examples/crash/math/crash-01.tsv +2 -0
- package/engine-examples/crash/math/crash-02.tsv +2 -0
- package/engine-examples/crash/math/crash-03.tsv +2 -0
- package/engine-examples/crash/math/crash-04.tsv +2 -0
- package/engine-examples/crash/math/crash-05.tsv +2 -0
- package/engine-examples/crash/math/crash-06.tsv +2 -0
- package/engine-examples/crash/math/crash-07.tsv +2 -0
- package/engine-examples/crash/math/crash-08.tsv +2 -0
- package/engine-examples/crash/math/crash-09.tsv +2 -0
- package/engine-examples/crash/math/crash-10.tsv +2 -0
- package/engine-examples/crash/math/crash-11.tsv +2 -0
- package/engine-examples/crash/math/crash-12.tsv +2 -0
- package/engine-examples/crash/math/crash-13.tsv +2 -0
- package/engine-examples/crash/math/crash-14.tsv +2 -0
- package/engine-examples/crash/math/crash-15.tsv +2 -0
- package/engine-examples/crash/math/crash-16.tsv +2 -0
- package/engine-examples/crash/math/crash-17.tsv +2 -0
- package/engine-examples/crash/math/crash-18.tsv +2 -0
- package/engine-examples/crash/math/crash-19.tsv +2 -0
- package/engine-examples/crash/math/crash-20.tsv +2 -0
- package/engine-examples/crash/math/crash-21.tsv +2 -0
- package/engine-examples/crash/math/crash-22.tsv +2 -0
- package/engine-examples/crash/math/crash-23.tsv +2 -0
- package/engine-examples/crash/math/crash-24.tsv +2 -0
- package/engine-examples/crash/math/crash-25.tsv +2 -0
- package/engine-examples/crash/math/crash-26.tsv +2 -0
- package/engine-examples/crash/math/crash-27.tsv +2 -0
- package/engine-examples/crash/math/crash-28.tsv +2 -0
- package/engine-examples/crash/math/crash-29.tsv +2 -0
- package/engine-examples/crash/math/crash-30.tsv +2 -0
- package/engine-examples/crash/math/crash-31.tsv +2 -0
- package/engine-examples/crash/math/crash-32.tsv +2 -0
- package/engine-examples/crash/math/crash-33.tsv +2 -0
- package/engine-examples/crash/math/crash-34.tsv +2 -0
- package/engine-examples/crash/math/crash-35.tsv +2 -0
- package/engine-examples/crash/math/crash-36.tsv +2 -0
- package/engine-examples/crash/math/crash-37.tsv +2 -0
- package/engine-examples/crash/math/crash-38.tsv +2 -0
- package/engine-examples/crash/math/crash-39.tsv +2 -0
- package/engine-examples/crash/math/crash-40.tsv +2 -0
- package/engine-examples/crash/math/crash-41.tsv +2 -0
- package/engine-examples/crash/math/crash-42.tsv +2 -0
- package/engine-examples/crash/math/crash-43.tsv +2 -0
- package/engine-examples/crash/math/crash-44.tsv +2 -0
- package/engine-examples/crash/math/crash-45.tsv +2 -0
- package/engine-examples/crash/math/crash-46.tsv +2 -0
- package/engine-examples/crash/math/crash-47.tsv +2 -0
- package/engine-examples/crash/math/crash-48.tsv +2 -0
- package/engine-examples/crash/math/crash-49.tsv +2 -0
- package/engine-examples/crash/math/crash-50.tsv +2 -0
- package/engine-examples/crash/math/crash-51.tsv +2 -0
- package/engine-examples/crash/math/crash-52.tsv +2 -0
- package/engine-examples/crash/math/crash-53.tsv +2 -0
- package/engine-examples/crash/math/crash-54.tsv +2 -0
- package/engine-examples/crash/math/crash-55.tsv +2 -0
- package/engine-examples/crash/math/crash-56.tsv +2 -0
- package/engine-examples/crash/math/crash-57.tsv +2 -0
- package/engine-examples/crash/math/crash-58.tsv +2 -0
- package/engine-examples/crash/math/crash-59.tsv +2 -0
- package/engine-examples/crash/math/crash-60.tsv +2 -0
- package/engine-examples/crash/math/crash-61.tsv +2 -0
- package/engine-examples/crash/math/crash-62.tsv +2 -0
- package/engine-examples/crash/math/crash-63.tsv +2 -0
- package/engine-examples/crash/math/crash-64.tsv +2 -0
- package/engine-examples/crash/math/gen.mjs +31 -0
- package/engine-examples/crash/math/manifest.json +1 -0
- package/engine-examples/dice/AGENTS.md +3 -0
- package/engine-examples/dice/README.md +25 -0
- package/engine-examples/dice/bundle/assets.json +1 -0
- package/engine-examples/dice/bundle/game.css +17 -0
- package/engine-examples/dice/bundle/game.js +61 -0
- package/engine-examples/dice/bundle/hbtest.json +4 -0
- package/engine-examples/dice/bundle/index.html +34 -0
- package/engine-examples/dice/math/dice.tsv +100 -0
- package/engine-examples/dice/math/manifest.json +2925 -0
- package/engine-examples/keno/AGENTS.md +3 -0
- package/engine-examples/keno/README.md +11 -0
- package/engine-examples/keno/bundle/assets.json +1 -0
- package/engine-examples/keno/bundle/game.css +14 -0
- package/engine-examples/keno/bundle/game.js +86 -0
- package/engine-examples/keno/bundle/hbtest.json +4 -0
- package/engine-examples/keno/bundle/i18n.json +342 -0
- package/engine-examples/keno/bundle/index.html +20 -0
- package/engine-examples/keno/bundle/model.json +1 -0
- package/engine-examples/keno/math/gen.mjs +35 -0
- package/engine-examples/keno/math/keno-picks-1.tsv +2 -0
- package/engine-examples/keno/math/keno-picks-10.tsv +11 -0
- package/engine-examples/keno/math/keno-picks-2.tsv +3 -0
- package/engine-examples/keno/math/keno-picks-3.tsv +4 -0
- package/engine-examples/keno/math/keno-picks-4.tsv +5 -0
- package/engine-examples/keno/math/keno-picks-5.tsv +6 -0
- package/engine-examples/keno/math/keno-picks-6.tsv +7 -0
- package/engine-examples/keno/math/keno-picks-7.tsv +8 -0
- package/engine-examples/keno/math/keno-picks-8.tsv +9 -0
- package/engine-examples/keno/math/keno-picks-9.tsv +10 -0
- package/engine-examples/keno/math/manifest.json +1 -0
- package/engine-examples/keno/math-kit/pack.mjs +165 -0
- package/engine-examples/keno/math-kit/package.json +8 -0
- package/engine-examples/keno/math-kit/slot.mjs +94 -0
- package/engine-examples/keno/math-kit/tumble.mjs +94 -0
- package/engine-examples/lines/AGENTS.md +3 -0
- package/engine-examples/lines/README.md +20 -0
- package/engine-examples/lines/bundle/assets.json +1 -0
- package/engine-examples/lines/bundle/game.css +16 -0
- package/engine-examples/lines/bundle/game.js +125 -0
- package/engine-examples/lines/bundle/hbtest.json +4 -0
- package/engine-examples/lines/bundle/i18n.json +242 -0
- package/engine-examples/lines/bundle/index.html +23 -0
- package/engine-examples/lines/bundle/model.json +1 -0
- package/engine-examples/lines/math/gen.mjs +54 -0
- package/engine-examples/lines/math-kit/pack.mjs +165 -0
- package/engine-examples/lines/math-kit/package.json +8 -0
- package/engine-examples/lines/math-kit/slot.mjs +94 -0
- package/engine-examples/lines/math-kit/tumble.mjs +94 -0
- package/engine-examples/mines/AGENTS.md +3 -0
- package/engine-examples/mines/README.md +13 -0
- package/engine-examples/mines/bundle/assets.json +1 -0
- package/engine-examples/mines/bundle/game.css +12 -0
- package/engine-examples/mines/bundle/game.js +58 -0
- package/engine-examples/mines/bundle/hbtest.json +8 -0
- package/engine-examples/mines/bundle/index.html +16 -0
- package/engine-examples/mines/math/gen.mjs +40 -0
- package/engine-examples/mines/math/manifest.json +1 -0
- package/engine-examples/mines/math/mines-1-1.tsv +2 -0
- package/engine-examples/mines/math/mines-1-2.tsv +2 -0
- package/engine-examples/mines/math/mines-1-22.tsv +2 -0
- package/engine-examples/mines/math/mines-1-23.tsv +2 -0
- package/engine-examples/mines/math/mines-1-24.tsv +2 -0
- package/engine-examples/mines/math/mines-1-3.tsv +2 -0
- package/engine-examples/mines/math/mines-10-1.tsv +2 -0
- package/engine-examples/mines/math/mines-10-10.tsv +2 -0
- package/engine-examples/mines/math/mines-10-11.tsv +2 -0
- package/engine-examples/mines/math/mines-10-12.tsv +2 -0
- package/engine-examples/mines/math/mines-10-13.tsv +2 -0
- package/engine-examples/mines/math/mines-10-14.tsv +2 -0
- package/engine-examples/mines/math/mines-10-15.tsv +2 -0
- package/engine-examples/mines/math/mines-10-2.tsv +2 -0
- package/engine-examples/mines/math/mines-10-3.tsv +2 -0
- package/engine-examples/mines/math/mines-10-4.tsv +2 -0
- package/engine-examples/mines/math/mines-10-5.tsv +2 -0
- package/engine-examples/mines/math/mines-10-6.tsv +2 -0
- package/engine-examples/mines/math/mines-10-7.tsv +2 -0
- package/engine-examples/mines/math/mines-10-8.tsv +2 -0
- package/engine-examples/mines/math/mines-10-9.tsv +2 -0
- package/engine-examples/mines/math/mines-11-1.tsv +2 -0
- package/engine-examples/mines/math/mines-11-10.tsv +2 -0
- package/engine-examples/mines/math/mines-11-11.tsv +2 -0
- package/engine-examples/mines/math/mines-11-12.tsv +2 -0
- package/engine-examples/mines/math/mines-11-13.tsv +2 -0
- package/engine-examples/mines/math/mines-11-14.tsv +2 -0
- package/engine-examples/mines/math/mines-11-2.tsv +2 -0
- package/engine-examples/mines/math/mines-11-3.tsv +2 -0
- package/engine-examples/mines/math/mines-11-4.tsv +2 -0
- package/engine-examples/mines/math/mines-11-5.tsv +2 -0
- package/engine-examples/mines/math/mines-11-6.tsv +2 -0
- package/engine-examples/mines/math/mines-11-7.tsv +2 -0
- package/engine-examples/mines/math/mines-11-8.tsv +2 -0
- package/engine-examples/mines/math/mines-11-9.tsv +2 -0
- package/engine-examples/mines/math/mines-12-1.tsv +2 -0
- package/engine-examples/mines/math/mines-12-10.tsv +2 -0
- package/engine-examples/mines/math/mines-12-11.tsv +2 -0
- package/engine-examples/mines/math/mines-12-12.tsv +2 -0
- package/engine-examples/mines/math/mines-12-13.tsv +2 -0
- package/engine-examples/mines/math/mines-12-2.tsv +2 -0
- package/engine-examples/mines/math/mines-12-3.tsv +2 -0
- package/engine-examples/mines/math/mines-12-4.tsv +2 -0
- package/engine-examples/mines/math/mines-12-5.tsv +2 -0
- package/engine-examples/mines/math/mines-12-6.tsv +2 -0
- package/engine-examples/mines/math/mines-12-7.tsv +2 -0
- package/engine-examples/mines/math/mines-12-8.tsv +2 -0
- package/engine-examples/mines/math/mines-12-9.tsv +2 -0
- package/engine-examples/mines/math/mines-13-1.tsv +2 -0
- package/engine-examples/mines/math/mines-13-10.tsv +2 -0
- package/engine-examples/mines/math/mines-13-11.tsv +2 -0
- package/engine-examples/mines/math/mines-13-12.tsv +2 -0
- package/engine-examples/mines/math/mines-13-2.tsv +2 -0
- package/engine-examples/mines/math/mines-13-3.tsv +2 -0
- package/engine-examples/mines/math/mines-13-4.tsv +2 -0
- package/engine-examples/mines/math/mines-13-5.tsv +2 -0
- package/engine-examples/mines/math/mines-13-6.tsv +2 -0
- package/engine-examples/mines/math/mines-13-7.tsv +2 -0
- package/engine-examples/mines/math/mines-13-8.tsv +2 -0
- package/engine-examples/mines/math/mines-13-9.tsv +2 -0
- package/engine-examples/mines/math/mines-14-1.tsv +2 -0
- package/engine-examples/mines/math/mines-14-10.tsv +2 -0
- package/engine-examples/mines/math/mines-14-11.tsv +2 -0
- package/engine-examples/mines/math/mines-14-2.tsv +2 -0
- package/engine-examples/mines/math/mines-14-3.tsv +2 -0
- package/engine-examples/mines/math/mines-14-4.tsv +2 -0
- package/engine-examples/mines/math/mines-14-5.tsv +2 -0
- package/engine-examples/mines/math/mines-14-6.tsv +2 -0
- package/engine-examples/mines/math/mines-14-7.tsv +2 -0
- package/engine-examples/mines/math/mines-14-8.tsv +2 -0
- package/engine-examples/mines/math/mines-14-9.tsv +2 -0
- package/engine-examples/mines/math/mines-15-1.tsv +2 -0
- package/engine-examples/mines/math/mines-15-10.tsv +2 -0
- package/engine-examples/mines/math/mines-15-2.tsv +2 -0
- package/engine-examples/mines/math/mines-15-3.tsv +2 -0
- package/engine-examples/mines/math/mines-15-4.tsv +2 -0
- package/engine-examples/mines/math/mines-15-5.tsv +2 -0
- package/engine-examples/mines/math/mines-15-6.tsv +2 -0
- package/engine-examples/mines/math/mines-15-7.tsv +2 -0
- package/engine-examples/mines/math/mines-15-8.tsv +2 -0
- package/engine-examples/mines/math/mines-15-9.tsv +2 -0
- package/engine-examples/mines/math/mines-16-1.tsv +2 -0
- package/engine-examples/mines/math/mines-16-2.tsv +2 -0
- package/engine-examples/mines/math/mines-16-3.tsv +2 -0
- package/engine-examples/mines/math/mines-16-4.tsv +2 -0
- package/engine-examples/mines/math/mines-16-5.tsv +2 -0
- package/engine-examples/mines/math/mines-16-6.tsv +2 -0
- package/engine-examples/mines/math/mines-16-7.tsv +2 -0
- package/engine-examples/mines/math/mines-16-8.tsv +2 -0
- package/engine-examples/mines/math/mines-16-9.tsv +2 -0
- package/engine-examples/mines/math/mines-17-1.tsv +2 -0
- package/engine-examples/mines/math/mines-17-2.tsv +2 -0
- package/engine-examples/mines/math/mines-17-3.tsv +2 -0
- package/engine-examples/mines/math/mines-17-4.tsv +2 -0
- package/engine-examples/mines/math/mines-17-5.tsv +2 -0
- package/engine-examples/mines/math/mines-17-6.tsv +2 -0
- package/engine-examples/mines/math/mines-17-7.tsv +2 -0
- package/engine-examples/mines/math/mines-17-8.tsv +2 -0
- package/engine-examples/mines/math/mines-18-1.tsv +2 -0
- package/engine-examples/mines/math/mines-18-2.tsv +2 -0
- package/engine-examples/mines/math/mines-18-3.tsv +2 -0
- package/engine-examples/mines/math/mines-18-4.tsv +2 -0
- package/engine-examples/mines/math/mines-18-5.tsv +2 -0
- package/engine-examples/mines/math/mines-18-6.tsv +2 -0
- package/engine-examples/mines/math/mines-18-7.tsv +2 -0
- package/engine-examples/mines/math/mines-19-1.tsv +2 -0
- package/engine-examples/mines/math/mines-19-2.tsv +2 -0
- package/engine-examples/mines/math/mines-19-3.tsv +2 -0
- package/engine-examples/mines/math/mines-19-4.tsv +2 -0
- package/engine-examples/mines/math/mines-19-5.tsv +2 -0
- package/engine-examples/mines/math/mines-19-6.tsv +2 -0
- package/engine-examples/mines/math/mines-2-1.tsv +2 -0
- package/engine-examples/mines/math/mines-2-19.tsv +2 -0
- package/engine-examples/mines/math/mines-2-2.tsv +2 -0
- package/engine-examples/mines/math/mines-2-20.tsv +2 -0
- package/engine-examples/mines/math/mines-2-21.tsv +2 -0
- package/engine-examples/mines/math/mines-2-22.tsv +2 -0
- package/engine-examples/mines/math/mines-2-23.tsv +2 -0
- package/engine-examples/mines/math/mines-2-3.tsv +2 -0
- package/engine-examples/mines/math/mines-2-4.tsv +2 -0
- package/engine-examples/mines/math/mines-2-5.tsv +2 -0
- package/engine-examples/mines/math/mines-20-1.tsv +2 -0
- package/engine-examples/mines/math/mines-20-2.tsv +2 -0
- package/engine-examples/mines/math/mines-20-3.tsv +2 -0
- package/engine-examples/mines/math/mines-20-4.tsv +2 -0
- package/engine-examples/mines/math/mines-20-5.tsv +2 -0
- package/engine-examples/mines/math/mines-21-1.tsv +2 -0
- package/engine-examples/mines/math/mines-21-2.tsv +2 -0
- package/engine-examples/mines/math/mines-21-3.tsv +2 -0
- package/engine-examples/mines/math/mines-21-4.tsv +2 -0
- package/engine-examples/mines/math/mines-22-1.tsv +2 -0
- package/engine-examples/mines/math/mines-22-2.tsv +2 -0
- package/engine-examples/mines/math/mines-22-3.tsv +2 -0
- package/engine-examples/mines/math/mines-23-1.tsv +2 -0
- package/engine-examples/mines/math/mines-23-2.tsv +2 -0
- package/engine-examples/mines/math/mines-24-1.tsv +2 -0
- package/engine-examples/mines/math/mines-3-1.tsv +2 -0
- package/engine-examples/mines/math/mines-3-17.tsv +2 -0
- package/engine-examples/mines/math/mines-3-18.tsv +2 -0
- package/engine-examples/mines/math/mines-3-19.tsv +2 -0
- package/engine-examples/mines/math/mines-3-2.tsv +2 -0
- package/engine-examples/mines/math/mines-3-20.tsv +2 -0
- package/engine-examples/mines/math/mines-3-21.tsv +2 -0
- package/engine-examples/mines/math/mines-3-22.tsv +2 -0
- package/engine-examples/mines/math/mines-3-3.tsv +2 -0
- package/engine-examples/mines/math/mines-3-4.tsv +2 -0
- package/engine-examples/mines/math/mines-3-5.tsv +2 -0
- package/engine-examples/mines/math/mines-3-6.tsv +2 -0
- package/engine-examples/mines/math/mines-4-1.tsv +2 -0
- package/engine-examples/mines/math/mines-4-13.tsv +2 -0
- package/engine-examples/mines/math/mines-4-14.tsv +2 -0
- package/engine-examples/mines/math/mines-4-15.tsv +2 -0
- package/engine-examples/mines/math/mines-4-16.tsv +2 -0
- package/engine-examples/mines/math/mines-4-17.tsv +2 -0
- package/engine-examples/mines/math/mines-4-18.tsv +2 -0
- package/engine-examples/mines/math/mines-4-19.tsv +2 -0
- package/engine-examples/mines/math/mines-4-2.tsv +2 -0
- package/engine-examples/mines/math/mines-4-20.tsv +2 -0
- package/engine-examples/mines/math/mines-4-21.tsv +2 -0
- package/engine-examples/mines/math/mines-4-3.tsv +2 -0
- package/engine-examples/mines/math/mines-4-4.tsv +2 -0
- package/engine-examples/mines/math/mines-4-5.tsv +2 -0
- package/engine-examples/mines/math/mines-4-6.tsv +2 -0
- package/engine-examples/mines/math/mines-4-7.tsv +2 -0
- package/engine-examples/mines/math/mines-4-8.tsv +2 -0
- package/engine-examples/mines/math/mines-4-9.tsv +2 -0
- package/engine-examples/mines/math/mines-5-1.tsv +2 -0
- package/engine-examples/mines/math/mines-5-10.tsv +2 -0
- package/engine-examples/mines/math/mines-5-11.tsv +2 -0
- package/engine-examples/mines/math/mines-5-12.tsv +2 -0
- package/engine-examples/mines/math/mines-5-13.tsv +2 -0
- package/engine-examples/mines/math/mines-5-14.tsv +2 -0
- package/engine-examples/mines/math/mines-5-15.tsv +2 -0
- package/engine-examples/mines/math/mines-5-16.tsv +2 -0
- package/engine-examples/mines/math/mines-5-17.tsv +2 -0
- package/engine-examples/mines/math/mines-5-18.tsv +2 -0
- package/engine-examples/mines/math/mines-5-19.tsv +2 -0
- package/engine-examples/mines/math/mines-5-2.tsv +2 -0
- package/engine-examples/mines/math/mines-5-20.tsv +2 -0
- package/engine-examples/mines/math/mines-5-3.tsv +2 -0
- package/engine-examples/mines/math/mines-5-4.tsv +2 -0
- package/engine-examples/mines/math/mines-5-5.tsv +2 -0
- package/engine-examples/mines/math/mines-5-6.tsv +2 -0
- package/engine-examples/mines/math/mines-5-7.tsv +2 -0
- package/engine-examples/mines/math/mines-5-8.tsv +2 -0
- package/engine-examples/mines/math/mines-5-9.tsv +2 -0
- package/engine-examples/mines/math/mines-6-1.tsv +2 -0
- package/engine-examples/mines/math/mines-6-10.tsv +2 -0
- package/engine-examples/mines/math/mines-6-11.tsv +2 -0
- package/engine-examples/mines/math/mines-6-12.tsv +2 -0
- package/engine-examples/mines/math/mines-6-13.tsv +2 -0
- package/engine-examples/mines/math/mines-6-14.tsv +2 -0
- package/engine-examples/mines/math/mines-6-15.tsv +2 -0
- package/engine-examples/mines/math/mines-6-16.tsv +2 -0
- package/engine-examples/mines/math/mines-6-17.tsv +2 -0
- package/engine-examples/mines/math/mines-6-18.tsv +2 -0
- package/engine-examples/mines/math/mines-6-19.tsv +2 -0
- package/engine-examples/mines/math/mines-6-2.tsv +2 -0
- package/engine-examples/mines/math/mines-6-3.tsv +2 -0
- package/engine-examples/mines/math/mines-6-4.tsv +2 -0
- package/engine-examples/mines/math/mines-6-5.tsv +2 -0
- package/engine-examples/mines/math/mines-6-6.tsv +2 -0
- package/engine-examples/mines/math/mines-6-7.tsv +2 -0
- package/engine-examples/mines/math/mines-6-8.tsv +2 -0
- package/engine-examples/mines/math/mines-6-9.tsv +2 -0
- package/engine-examples/mines/math/mines-7-1.tsv +2 -0
- package/engine-examples/mines/math/mines-7-10.tsv +2 -0
- package/engine-examples/mines/math/mines-7-11.tsv +2 -0
- package/engine-examples/mines/math/mines-7-12.tsv +2 -0
- package/engine-examples/mines/math/mines-7-13.tsv +2 -0
- package/engine-examples/mines/math/mines-7-14.tsv +2 -0
- package/engine-examples/mines/math/mines-7-15.tsv +2 -0
- package/engine-examples/mines/math/mines-7-16.tsv +2 -0
- package/engine-examples/mines/math/mines-7-17.tsv +2 -0
- package/engine-examples/mines/math/mines-7-18.tsv +2 -0
- package/engine-examples/mines/math/mines-7-2.tsv +2 -0
- package/engine-examples/mines/math/mines-7-3.tsv +2 -0
- package/engine-examples/mines/math/mines-7-4.tsv +2 -0
- package/engine-examples/mines/math/mines-7-5.tsv +2 -0
- package/engine-examples/mines/math/mines-7-6.tsv +2 -0
- package/engine-examples/mines/math/mines-7-7.tsv +2 -0
- package/engine-examples/mines/math/mines-7-8.tsv +2 -0
- package/engine-examples/mines/math/mines-7-9.tsv +2 -0
- package/engine-examples/mines/math/mines-8-1.tsv +2 -0
- package/engine-examples/mines/math/mines-8-10.tsv +2 -0
- package/engine-examples/mines/math/mines-8-11.tsv +2 -0
- package/engine-examples/mines/math/mines-8-12.tsv +2 -0
- package/engine-examples/mines/math/mines-8-13.tsv +2 -0
- package/engine-examples/mines/math/mines-8-14.tsv +2 -0
- package/engine-examples/mines/math/mines-8-15.tsv +2 -0
- package/engine-examples/mines/math/mines-8-16.tsv +2 -0
- package/engine-examples/mines/math/mines-8-17.tsv +2 -0
- package/engine-examples/mines/math/mines-8-2.tsv +2 -0
- package/engine-examples/mines/math/mines-8-3.tsv +2 -0
- package/engine-examples/mines/math/mines-8-4.tsv +2 -0
- package/engine-examples/mines/math/mines-8-5.tsv +2 -0
- package/engine-examples/mines/math/mines-8-6.tsv +2 -0
- package/engine-examples/mines/math/mines-8-7.tsv +2 -0
- package/engine-examples/mines/math/mines-8-8.tsv +2 -0
- package/engine-examples/mines/math/mines-8-9.tsv +2 -0
- package/engine-examples/mines/math/mines-9-1.tsv +2 -0
- package/engine-examples/mines/math/mines-9-10.tsv +2 -0
- package/engine-examples/mines/math/mines-9-11.tsv +2 -0
- package/engine-examples/mines/math/mines-9-12.tsv +2 -0
- package/engine-examples/mines/math/mines-9-13.tsv +2 -0
- package/engine-examples/mines/math/mines-9-14.tsv +2 -0
- package/engine-examples/mines/math/mines-9-15.tsv +2 -0
- package/engine-examples/mines/math/mines-9-16.tsv +2 -0
- package/engine-examples/mines/math/mines-9-2.tsv +2 -0
- package/engine-examples/mines/math/mines-9-3.tsv +2 -0
- package/engine-examples/mines/math/mines-9-4.tsv +2 -0
- package/engine-examples/mines/math/mines-9-5.tsv +2 -0
- package/engine-examples/mines/math/mines-9-6.tsv +2 -0
- package/engine-examples/mines/math/mines-9-7.tsv +2 -0
- package/engine-examples/mines/math/mines-9-8.tsv +2 -0
- package/engine-examples/mines/math/mines-9-9.tsv +2 -0
- package/engine-examples/plinko/AGENTS.md +3 -0
- package/engine-examples/plinko/README.md +9 -0
- package/engine-examples/plinko/bundle/assets.json +1 -0
- package/engine-examples/plinko/bundle/game.css +5 -0
- package/engine-examples/plinko/bundle/game.js +92 -0
- package/engine-examples/plinko/bundle/hbtest.json +4 -0
- package/engine-examples/plinko/bundle/index.html +16 -0
- package/engine-examples/plinko/bundle/model.json +1 -0
- package/engine-examples/plinko/math/gen.mjs +40 -0
- package/engine-examples/plinko/math/manifest.json +1 -0
- package/engine-examples/plinko/math/plinko-12-high.tsv +13 -0
- package/engine-examples/plinko/math/plinko-12-low.tsv +13 -0
- package/engine-examples/plinko/math/plinko-12-medium.tsv +13 -0
- package/engine-examples/plinko/math/plinko-16-high.tsv +17 -0
- package/engine-examples/plinko/math/plinko-16-low.tsv +17 -0
- package/engine-examples/plinko/math/plinko-16-medium.tsv +17 -0
- package/engine-examples/plinko/math/plinko-8-high.tsv +9 -0
- package/engine-examples/plinko/math/plinko-8-low.tsv +9 -0
- package/engine-examples/plinko/math/plinko-8-medium.tsv +9 -0
- package/engine-examples/plinko/math-kit/pack.mjs +165 -0
- package/engine-examples/plinko/math-kit/package.json +8 -0
- package/engine-examples/plinko/math-kit/slot.mjs +94 -0
- package/engine-examples/plinko/math-kit/tumble.mjs +94 -0
- package/engine-examples/slot/AGENTS.md +3 -0
- package/engine-examples/slot/README.md +26 -0
- package/engine-examples/slot/bundle/assets.json +1 -0
- package/engine-examples/slot/bundle/game.css +19 -0
- package/engine-examples/slot/bundle/game.js +42 -0
- package/engine-examples/slot/bundle/hbtest.json +4 -0
- package/engine-examples/slot/bundle/index.html +23 -0
- package/engine-examples/slot/math/gen.mjs +79 -0
- package/engine-examples/slot/math/manifest.json +1 -0
- package/engine-examples/slot/math/slot.tsv +110592 -0
- package/engine-examples/tower/AGENTS.md +3 -0
- package/engine-examples/tower/README.md +21 -0
- package/engine-examples/tower/bundle/assets.json +1 -0
- package/engine-examples/tower/bundle/game.css +8 -0
- package/engine-examples/tower/bundle/game.js +37 -0
- package/engine-examples/tower/bundle/hbtest.json +5 -0
- package/engine-examples/tower/bundle/index.html +16 -0
- package/engine-examples/tower/math/floor.tsv +4 -0
- package/engine-examples/tower/math/manifest.json +61 -0
- package/engine-examples/tumble/AGENTS.md +3 -0
- package/engine-examples/tumble/README.md +18 -0
- package/engine-examples/tumble/bundle/assets.json +1 -0
- package/engine-examples/tumble/bundle/game.css +13 -0
- package/engine-examples/tumble/bundle/game.js +119 -0
- package/engine-examples/tumble/bundle/hbtest.json +4 -0
- package/engine-examples/tumble/bundle/i18n.json +122 -0
- package/engine-examples/tumble/bundle/index.html +20 -0
- package/engine-examples/tumble/bundle/model.json +1 -0
- package/engine-examples/tumble/math/gen.mjs +38 -0
- package/engine-examples/tumble/math-kit/pack.mjs +165 -0
- package/engine-examples/tumble/math-kit/package.json +8 -0
- package/engine-examples/tumble/math-kit/slot.mjs +94 -0
- package/engine-examples/tumble/math-kit/tumble.mjs +94 -0
- package/engine-examples/wheel/README.md +14 -0
- package/engine-examples/wheel/bundle/assets.json +1 -0
- package/engine-examples/wheel/bundle/game.css +7 -0
- package/engine-examples/wheel/bundle/game.js +51 -0
- package/engine-examples/wheel/bundle/hbtest.json +4 -0
- package/engine-examples/wheel/bundle/index.html +19 -0
- package/engine-examples/wheel/math/high.tsv +24 -0
- package/engine-examples/wheel/math/low.tsv +24 -0
- package/engine-examples/wheel/math/manifest.json +48 -0
- package/engine-examples/wheel/math/medium.tsv +24 -0
- package/engine-math/pack.mjs +165 -0
- package/engine-math/package.json +1 -0
- package/engine-math/slot.mjs +94 -0
- package/engine-math/tumble.mjs +94 -0
- package/engine-mcp/package.json +1 -0
- package/engine-mcp/server.mjs +151 -0
- package/engine-sdk/GUIDE.md +855 -0
- package/engine-sdk/assets.js +92 -0
- package/engine-sdk/autoplay.js +82 -0
- package/engine-sdk/bet-field.js +391 -0
- package/engine-sdk/kit/fiat-coins.js +51 -0
- package/engine-sdk/kit/numeric-input.js +116 -0
- package/engine-sdk/kit/wallet-format.js +423 -0
- package/engine-sdk/marks.js +22 -0
- package/engine-sdk/messages.js +289 -0
- package/engine-sdk/money.js +89 -0
- package/engine-sdk/package.json +1 -0
- package/engine-sdk/protocol.js +263 -0
- package/engine-sdk/sdk.d.ts +138 -0
- package/engine-sdk/sdk.js +330 -0
- package/engine-sdk/stake.js +68 -0
- package/engine-sdk/ui.d.ts +36 -0
- package/engine-sdk/ui.js +340 -0
- package/engine-shell/core/faults.js +127 -0
- package/engine-shell/core/rgs.js +193 -0
- package/engine-shell/core/router.js +917 -0
- package/engine-shell/host/boot.js +408 -0
- package/engine-shell/host/engine-v1.js +438 -0
- package/engine-shell/host/fair.js +285 -0
- package/engine-shell/host/fonts/SOURCE.json +11 -0
- package/engine-shell/host/fonts/figtree-400.woff2 +0 -0
- package/engine-shell/host/fonts/figtree-500.woff2 +0 -0
- package/engine-shell/host/fonts/figtree-600.woff2 +0 -0
- package/engine-shell/host/fonts/red-hat-display-700.woff2 +0 -0
- package/engine-shell/host/index.html +230 -0
- package/engine-shell/host/page.js +809 -0
- package/engine-shell/host/sandbox-panel.js +681 -0
- package/engine-shell/host/sandbox-scenarios.js +554 -0
- package/engine-shell/host/site-bridge.js +87 -0
- package/engine-shell/host/strings.js +916 -0
- package/engine-shell/package.json +1 -0
- package/engine-shell/verifiers/badcat/coin.tsv +2 -0
- package/engine-shell/verifiers/badcat/dice-2.tsv +11 -0
- package/engine-shell/verifiers/badcat/dice-5.tsv +26 -0
- package/engine-shell/verifiers/badcat/dice-9.tsv +46 -0
- package/engine-shell/verifiers/badcat/manifest.json +3344 -0
- package/engine-shell/verifiers/badcat/rps.tsv +3 -0
- package/engine-shell/verifiers/badcat/triple.tsv +66 -0
- package/engine-shell/verifiers/engine-verify.js +358 -0
- package/engine-shell/verifiers/package.json +5 -0
- package/engine-shell/verifiers/parity/manifest.json +27 -0
- package/engine-shell/verifiers/parity/wide.tsv +5 -0
- package/engine-shell/verifiers/parity-vectors.json +244 -0
- package/engine-shell/verifiers/vectors.json +100 -0
- 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
|
+
}
|