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