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
package/engine-sdk/ui.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frontend engine: the House Originals' panel around a studio's zone.
|
|
3
|
+
* A studio that does not want to build a panel draws only its game and
|
|
4
|
+
* gets the rest — modes and bets from its math, the stake in the player's
|
|
5
|
+
* currency, Bet, Auto, Next / Cash out on a ladder, refusals in the
|
|
6
|
+
* player's language — looking and behaving like Dice or Mines.
|
|
7
|
+
*
|
|
8
|
+
* <hb-game><canvas slot="zone"></canvas></hb-game>
|
|
9
|
+
*
|
|
10
|
+
* import { mount } from './engine-sdk/ui.js';
|
|
11
|
+
* const game = await mount(document.querySelector('hb-game'), {
|
|
12
|
+
* // A table round is drawn: show it, resolve when it is on screen.
|
|
13
|
+
* reveal: (round, zone) => animate(round.data[0]),
|
|
14
|
+
* // A ladder round moved (bet, step, cash-out, bust); optional.
|
|
15
|
+
* step: (round, zone) => animateStep(round),
|
|
16
|
+
* // false: no Next button; the zone climbs a ladder with game.advance() (a crash).
|
|
17
|
+
* next: true,
|
|
18
|
+
* // Mode and bet names by language (else the math's labels); optional.
|
|
19
|
+
* labels: { es: { base: 'Tirada', buy: 'Comprar giros gratis' }, … },
|
|
20
|
+
* });
|
|
21
|
+
* game.engine; // the SDK handle (config, money, on, state…)
|
|
22
|
+
* game.field; // the stake field (amount, reason, 'change')
|
|
23
|
+
* game.mode / game.bet; // the selection; a zone may set them
|
|
24
|
+
* el.addEventListener('select', (e) => e.detail); // {mode, bet} whenever it changes
|
|
25
|
+
*
|
|
26
|
+
* The panel is the Originals' layout: a 300px column beside the zone on
|
|
27
|
+
* desktop, the frame's full height (720px, or the screen in fullscreen),
|
|
28
|
+
* under the zone on a phone. The balance is the site's header. A mode with more than
|
|
29
|
+
* twelve bets (a dice target) is picked in the zone: it sets game.bet.
|
|
30
|
+
*/
|
|
31
|
+
import { connect, ERRORS } from './sdk.js';
|
|
32
|
+
import { wordsFor } from './bet-field.js';
|
|
33
|
+
import { autoplayRequest, autoplayStop, nextStake } from './autoplay.js';
|
|
34
|
+
|
|
35
|
+
const CSS = `
|
|
36
|
+
:host { display: block; font-family: var(--hb-font, 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); color: #F9F9FB; background: #08141a; }
|
|
37
|
+
.card { display: grid; grid-template-columns: 300px minmax(0, 1fr); min-height: 100vh; }
|
|
38
|
+
.panel { display: flex; flex-direction: column; gap: 16px; padding: 20px; padding-inline: max(20px, env(safe-area-inset-left)) max(20px, env(safe-area-inset-right)); background: #0b1f28; border-inline-end: 1px solid #0D2A38; }
|
|
39
|
+
.zone { position: relative; display: grid; place-items: center; min-height: 360px; overflow: hidden; }
|
|
40
|
+
::slotted(*) { max-width: 100%; }
|
|
41
|
+
@media (max-width: 699px) { .card { grid-template-columns: minmax(0, 1fr); min-height: 0; } .zone { order: -1; } .panel { border-inline-end: 0; border-top: 1px solid #0D2A38; padding-bottom: max(20px, env(safe-area-inset-bottom)); } }
|
|
42
|
+
@media (prefers-reduced-motion: reduce) { button { transition: none; } }
|
|
43
|
+
.tabs { display: flex; padding: 3px; gap: 3px; background: rgba(9, 102, 125, 0.15); border-radius: 8px; }
|
|
44
|
+
.tabs button { flex: 1; height: 38px; }
|
|
45
|
+
select.modes { height: 40px; padding: 0 12px; background: #0b1f28; border: 1px solid #0D2A38; border-radius: 8px; color: #F9F9FB; font: inherit; font-size: 14px; font-weight: 600; }
|
|
46
|
+
.picks { display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); gap: 8px; }
|
|
47
|
+
.picks button { height: 48px; flex-direction: column; gap: 0; line-height: 18px; }
|
|
48
|
+
.picks small { font-size: 12px; color: #8F96A8; }
|
|
49
|
+
.counts { display: flex; gap: 4px; }
|
|
50
|
+
.auto { display: grid; gap: 8px; }
|
|
51
|
+
.rule { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; }
|
|
52
|
+
.rule .tabs { padding: 2px; gap: 2px; }
|
|
53
|
+
.rule .tabs button { height: 28px; padding: 0 8px; font-size: 12px; }
|
|
54
|
+
.rule input { width: 72px; height: 32px; padding: 0 8px; background: #0b1f28; border: 1px solid #0D2A38; border-radius: 8px; color: #F9F9FB; font: inherit; font-size: 14px; text-align: right; }
|
|
55
|
+
.pct { display: inline-flex; align-items: center; gap: 4px; color: #8F96A8; font-size: 12px; }
|
|
56
|
+
.pct input { width: 48px; }
|
|
57
|
+
.counts button { flex: 1; height: 32px; font-size: 12px; }
|
|
58
|
+
button { display: inline-flex; align-items: center; justify-content: center; padding: 0 12px; border: 0; border-radius: 8px; background: rgba(9, 102, 125, 0.15); color: #8F96A8; font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 120ms ease, transform 120ms ease; }
|
|
59
|
+
button:hover { background: rgba(9, 102, 125, 0.2); color: #F9F9FB; }
|
|
60
|
+
button:active { transform: scale(0.98); }
|
|
61
|
+
button:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
|
|
62
|
+
button[aria-pressed="true"] { background: #0D2A38; color: #F9F9FB; }
|
|
63
|
+
.picks button[aria-pressed="true"] { box-shadow: inset 0 0 0 1px #28F3F3; }
|
|
64
|
+
.label { font-size: 14px; line-height: 20px; font-weight: 600; }
|
|
65
|
+
.main { height: 48px; background: #28F3F3; color: #041015; font-size: 16px; }
|
|
66
|
+
.main:hover { background: #5cf6f6; color: #041015; }
|
|
67
|
+
.ladder { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
68
|
+
.ladder [hidden] + .main, .ladder .main:only-child { grid-column: 1 / -1; }
|
|
69
|
+
[hidden] { display: none !important; }
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const COUNTS = [10, 25, 50, 100, 0];
|
|
73
|
+
const mult = (bp) => (bp / 10000).toLocaleString('en-US', { maximumFractionDigits: 2 }) + '×';
|
|
74
|
+
|
|
75
|
+
class GameElement extends HTMLElement {
|
|
76
|
+
constructor() {
|
|
77
|
+
super();
|
|
78
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
79
|
+
const style = document.createElement('style');
|
|
80
|
+
style.textContent = CSS;
|
|
81
|
+
root.append(style);
|
|
82
|
+
const tpl = document.createElement('template');
|
|
83
|
+
tpl.innerHTML = `<div class="card"><div class="panel" part="panel">
|
|
84
|
+
<div class="tabs" data-tabs></div>
|
|
85
|
+
<div class="tabs" data-modes></div>
|
|
86
|
+
<select class="modes" data-mode-select hidden aria-label="Mode"></select>
|
|
87
|
+
<div class="picks" data-bets></div>
|
|
88
|
+
<hb-bet-amount max-button></hb-bet-amount>
|
|
89
|
+
<div data-auto hidden class="auto">
|
|
90
|
+
<span class="label" data-w="numberOfBets"></span><div class="counts" data-counts></div>
|
|
91
|
+
<div class="rule"><span class="label" data-w="onWin"></span><div class="tabs" data-onwin></div><span class="pct"><input data-onwin-pct inputmode="decimal" placeholder="0">%</span></div>
|
|
92
|
+
<div class="rule"><span class="label" data-w="onLoss"></span><div class="tabs" data-onloss></div><span class="pct"><input data-onloss-pct inputmode="decimal" placeholder="0">%</span></div>
|
|
93
|
+
<div class="rule"><span class="label" data-w="stopOnProfit"></span><input data-stop-profit inputmode="decimal"></div>
|
|
94
|
+
<div class="rule"><span class="label" data-w="stopOnLoss"></span><input data-stop-loss inputmode="decimal"></div>
|
|
95
|
+
</div>
|
|
96
|
+
<button class="main" data-main></button>
|
|
97
|
+
<div class="ladder" data-ladder hidden><button class="main" data-next></button><button class="main" data-cashout></button></div>
|
|
98
|
+
</div><div class="zone" part="zone"><slot name="zone"></slot></div></div>`;
|
|
99
|
+
root.append(tpl.content);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (globalThis.customElements && !customElements.get('hb-game')) customElements.define('hb-game', GameElement);
|
|
104
|
+
|
|
105
|
+
/** Connects the panel to the engine and the zone's hooks. */
|
|
106
|
+
export async function mount(el, { reveal = async () => {}, step = async () => {}, labels = {}, next = true } = {}) {
|
|
107
|
+
const [engine] = await Promise.all([connect(), customElements.whenDefined('hb-bet-amount')]);
|
|
108
|
+
const names = labels[(engine.config.locale || 'en').slice(0, 2)] || labels.en || {};
|
|
109
|
+
const $ = (sel) => el.shadowRoot.querySelector(sel);
|
|
110
|
+
const words = wordsFor(engine.config.locale || navigator.language);
|
|
111
|
+
const field = $('hb-bet-amount').connect(engine);
|
|
112
|
+
// The stake decides whether Bet can be pressed: the button follows the field.
|
|
113
|
+
field.addEventListener('change', () => render());
|
|
114
|
+
const zone = el.querySelector('[slot="zone"]');
|
|
115
|
+
const modes = engine.config.modes;
|
|
116
|
+
const sel = { mode: modes[0].id, bet: modes[0].bets[0].id };
|
|
117
|
+
const modeOf = () => modes.find((m) => m.id === sel.mode);
|
|
118
|
+
let auto = false, count = 10, left = 0, busy = false, paused = false;
|
|
119
|
+
// Whether a shell autoplay session of this panel's is running (the shell
|
|
120
|
+
// counts it and may end it first: its Stop, a stop rule, a refusal).
|
|
121
|
+
let running = false;
|
|
122
|
+
const rule = { win: 'reset', loss: 'reset' }; // after a win / a loss: reset the stake, or increase it by the percent
|
|
123
|
+
let open = engine.config.activeRound || null; // a ladder round in play
|
|
124
|
+
// The refusal the last money call met, null after one that went: a zone
|
|
125
|
+
// that drives a ladder reads why its step was refused (`pacing` says when
|
|
126
|
+
// to go on; `needs-click` says to wait for the player).
|
|
127
|
+
let lastRefusal = null;
|
|
128
|
+
const game = {
|
|
129
|
+
engine, zone, field,
|
|
130
|
+
get refusal() { return lastRefusal; },
|
|
131
|
+
get mode() { return sel.mode; },
|
|
132
|
+
// During a run the selection is the session's (the shell holds every
|
|
133
|
+
// play to the bet the player approved): a zone's pick waits for it to end.
|
|
134
|
+
set mode(v) { if (!running && modes.some((m) => m.id === v)) { sel.mode = v; sel.bet = modeOf().bets[0].id; select(); } },
|
|
135
|
+
get bet() { return sel.bet; },
|
|
136
|
+
set bet(v) { if (!running && modeOf().bets.some((b) => b.id === v)) { sel.bet = v; select(); } },
|
|
137
|
+
// A ladder the zone drives itself (a crash climbs on a timer): the
|
|
138
|
+
// same calls as the panel's buttons, so the panel stays in step.
|
|
139
|
+
get open() { return open; },
|
|
140
|
+
advance: () => (open && !busy && !queued ? money(() => engine.action(open, 'advance')) : Promise.resolve(null)),
|
|
141
|
+
cashout: () => cashout(),
|
|
142
|
+
};
|
|
143
|
+
// A cash-out asked for while a step is with the server runs as soon as it answers.
|
|
144
|
+
let queued = false;
|
|
145
|
+
function cashout() {
|
|
146
|
+
if (busy) { queued = true; return Promise.resolve(null); }
|
|
147
|
+
return open ? money(() => engine.action(open, 'cashout')) : Promise.resolve(null);
|
|
148
|
+
}
|
|
149
|
+
// The selection changed (the panel or the zone): the field holds the stake
|
|
150
|
+
// to the bet's own highest stake under the maximum win, and the zone redraws.
|
|
151
|
+
const select = () => { field.bet = { mode: sel.mode, bet: sel.bet }; render(); el.dispatchEvent(new CustomEvent('select', { detail: { mode: sel.mode, bet: sel.bet } })); };
|
|
152
|
+
|
|
153
|
+
const picker = (box, items, onPick) => {
|
|
154
|
+
box.hidden = items.length < 2;
|
|
155
|
+
box.replaceChildren(...items.map((it) => {
|
|
156
|
+
const b = document.createElement('button');
|
|
157
|
+
b.type = 'button';
|
|
158
|
+
b.dataset.id = it.id;
|
|
159
|
+
b.append(names[it.id] || it.label || it.id);
|
|
160
|
+
if (it.maxMultBp) b.insertAdjacentHTML('beforeend', `<small>${mult(it.maxMultBp)}</small>`);
|
|
161
|
+
b.onclick = () => onPick(it.id);
|
|
162
|
+
return b;
|
|
163
|
+
}));
|
|
164
|
+
};
|
|
165
|
+
const mark = (box, id, lock) => box.querySelectorAll('button').forEach((b) => {
|
|
166
|
+
b.setAttribute('aria-pressed', b.dataset.id === id);
|
|
167
|
+
b.disabled = lock;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
function render() {
|
|
171
|
+
const m = modeOf();
|
|
172
|
+
const ladder = open && open.state === 'open';
|
|
173
|
+
const lock = busy || paused || ladder || left > 0;
|
|
174
|
+
if ($('[data-bets]').dataset.mode !== m.id) {
|
|
175
|
+
picker($('[data-bets]'), m.bets.length <= 12 ? m.bets : [], (id) => { game.bet = id; });
|
|
176
|
+
$('[data-bets]').dataset.mode = m.id;
|
|
177
|
+
}
|
|
178
|
+
mark($('[data-modes]'), sel.mode, lock);
|
|
179
|
+
Object.assign($('[data-mode-select]'), { value: sel.mode, disabled: lock });
|
|
180
|
+
mark($('[data-bets]'), sel.bet, lock);
|
|
181
|
+
field.disabled = lock;
|
|
182
|
+
// Auto is for one-draw modes, where the player's rules allow it
|
|
183
|
+
// (engine.config.rg.autoplay, the jurisdiction's word): a ladder is
|
|
184
|
+
// played by hand.
|
|
185
|
+
const noAuto = m.kind === 'ladder' || (engine.config.rg && engine.config.rg.autoplay && engine.config.rg.autoplay.allowed === false);
|
|
186
|
+
if (noAuto && auto) { auto = false; mark($('[data-tabs]'), 'manual', false); }
|
|
187
|
+
$('[data-tabs]').hidden = noAuto;
|
|
188
|
+
$('[data-auto]').hidden = !auto;
|
|
189
|
+
const main = $('[data-main]');
|
|
190
|
+
main.hidden = ladder;
|
|
191
|
+
main.textContent = left > 0 ? words.stopAutobet : auto ? words.startAutobet : words.bet;
|
|
192
|
+
// Bet needs a playable stake (never 0: there is no free round); Stop autobet never does.
|
|
193
|
+
const playable = left > 0 || (field.amount > 0 && !field.reason);
|
|
194
|
+
main.disabled = paused || (busy && left === 0) || !playable;
|
|
195
|
+
$('[data-ladder]').hidden = !ladder;
|
|
196
|
+
if (ladder) {
|
|
197
|
+
const k = open.step;
|
|
198
|
+
$('[data-next]').hidden = !next;
|
|
199
|
+
$('[data-next]').textContent = k < m.steps ? mult(m.ladderBp[k]) : '';
|
|
200
|
+
$('[data-next]').disabled = paused || busy || k >= m.steps;
|
|
201
|
+
$('[data-cashout]').hidden = !m.cashout;
|
|
202
|
+
$('[data-cashout]').textContent = `${words.cashOut} ${k > 0 ? mult(m.ladderBp[k - 1]) : ''}`;
|
|
203
|
+
$('[data-cashout]').disabled = paused || (busy && next) || k === 0 || queued;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A refusal under the stake field, in the player's language where the
|
|
208
|
+
// code has one; a money refusal's message is the server's own.
|
|
209
|
+
function refused(e) {
|
|
210
|
+
field.note(e.code === ERRORS.PACING || e.code === ERRORS.BUSY ? words.apiRateLimited
|
|
211
|
+
: e.code === ERRORS.PENDING ? words.apiRoundUnresolved
|
|
212
|
+
: e.code === ERRORS.SERVER || e.code === ERRORS.UNAVAILABLE ? words.apiInternalError
|
|
213
|
+
: e.message);
|
|
214
|
+
if (e.round) open = e.round;
|
|
215
|
+
left = 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function money(call) {
|
|
219
|
+
busy = true;
|
|
220
|
+
render();
|
|
221
|
+
let round = null;
|
|
222
|
+
lastRefusal = null;
|
|
223
|
+
try {
|
|
224
|
+
({ round } = await call());
|
|
225
|
+
open = round.state === 'open' ? round : null;
|
|
226
|
+
} catch (e) {
|
|
227
|
+
lastRefusal = e;
|
|
228
|
+
refused(e);
|
|
229
|
+
}
|
|
230
|
+
busy = false;
|
|
231
|
+
render();
|
|
232
|
+
if (!round) { queued = false; return null; }
|
|
233
|
+
await (round.kind === 'ladder' ? step : reveal)(round, zone);
|
|
234
|
+
if (round.state !== 'open') engine.revealed(round.id);
|
|
235
|
+
if (queued) { queued = false; if (open) return money(() => engine.action(open, 'cashout')); }
|
|
236
|
+
return round;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function play() {
|
|
240
|
+
// Never a stake of 0: the field's reason says `min` on an empty
|
|
241
|
+
// field, and the engine refuses it anyway (there is no free round).
|
|
242
|
+
if (field.reason || !(field.amount > 0)) { if (field.reason) field.note(field.reason.message); return null; }
|
|
243
|
+
return money(() => engine.play({ mode: sel.mode, bet: sel.bet, amount: field.amount }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// A run: the Originals' rules — on win / on loss the stake resets or
|
|
247
|
+
// grows by a percent; it stops at a profit or a loss the player named,
|
|
248
|
+
// on any refusal, or when the player says so. The run is a shell
|
|
249
|
+
// autoplay session (autoplay.js): asked for on the Start press, which is
|
|
250
|
+
// the player's click; the shell counts its rounds, caps its stake, shows
|
|
251
|
+
// its own Stop over the game and ends it first when a stop or a refusal
|
|
252
|
+
// comes — the panel follows (`autoplay` event). Its own Stop asks the shell.
|
|
253
|
+
async function run() {
|
|
254
|
+
if (left > 0) return stopRun();
|
|
255
|
+
if (!auto) return play();
|
|
256
|
+
if (field.reason || !(field.amount > 0)) { if (field.reason) field.note(field.reason.message); return null; }
|
|
257
|
+
const base = field.amount;
|
|
258
|
+
const pct = (sel) => Math.max(0, Number($(sel).value) || 0);
|
|
259
|
+
const winPct = pct('[data-onwin-pct]'), lossPct = pct('[data-onloss-pct]');
|
|
260
|
+
const stopProfit = engine.money.parse($('[data-stop-profit]').value), stopLoss = engine.money.parse($('[data-stop-loss]').value);
|
|
261
|
+
// A growing run asks for the most it could stake: the coin's max, the
|
|
262
|
+
// selected bet's own ceiling under the maximum win (field.maxStake; the
|
|
263
|
+
// shell caps the grant at it too, since the request names the bet) and
|
|
264
|
+
// the balance.
|
|
265
|
+
const req = autoplayRequest({ mode: sel.mode, bet: sel.bet, amount: base, count, rule, winPct, lossPct, stopProfit, stopLoss, limits: engine.config.limits, balance: engine.balance, maxStake: field.maxStake });
|
|
266
|
+
// The shell shows the player the terms and answers on their Start (or
|
|
267
|
+
// Cancel): the panel waits, its buttons held, so a second press cannot
|
|
268
|
+
// ask twice.
|
|
269
|
+
busy = true;
|
|
270
|
+
render();
|
|
271
|
+
let session;
|
|
272
|
+
try {
|
|
273
|
+
({ session } = await engine.autoplay.start(req));
|
|
274
|
+
} catch (e) {
|
|
275
|
+
busy = false;
|
|
276
|
+
refused(e);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
busy = false;
|
|
280
|
+
// The shell may have capped the terms (the coin's max, the balance, a
|
|
281
|
+
// loss limit of its own when none was set): the run keeps to the grant
|
|
282
|
+
// — the stops are the session's, so the panel never sends a play the
|
|
283
|
+
// shell would refuse because the session ended on its loss limit.
|
|
284
|
+
req.rounds = session.rounds;
|
|
285
|
+
req.stake = session.stake;
|
|
286
|
+
const stops = { stopProfit: session.stopOnProfit > 0 ? session.stopOnProfit : stopProfit, stopLoss: session.stopOnLoss > 0 ? session.stopOnLoss : stopLoss };
|
|
287
|
+
running = true;
|
|
288
|
+
let profit = 0;
|
|
289
|
+
for (left = req.rounds; left > 0 && running && !paused; left--) {
|
|
290
|
+
render();
|
|
291
|
+
const round = await play();
|
|
292
|
+
if (!round || !running || paused) break;
|
|
293
|
+
profit += round.payout - round.amount;
|
|
294
|
+
if (autoplayStop({ profit, ...stops })) break;
|
|
295
|
+
field.amount = nextStake({ amount: field.amount, base, won: round.payout > round.amount, rule, winPct, lossPct, cap: req.stake, decimals: engine.money.walletDecimals });
|
|
296
|
+
}
|
|
297
|
+
stopRun();
|
|
298
|
+
}
|
|
299
|
+
// The run is over, by the panel or by the shell: the session is released
|
|
300
|
+
// (a stop asked twice is harmless) and the panel is a bet panel again.
|
|
301
|
+
function stopRun() {
|
|
302
|
+
const was = running;
|
|
303
|
+
running = false;
|
|
304
|
+
left = 0;
|
|
305
|
+
render();
|
|
306
|
+
if (was) engine.autoplay.stop().catch(() => {});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Up to four modes are tabs; more (plinko's rows × risks) are one select, as the Originals do.
|
|
310
|
+
if (modes.length > 4) {
|
|
311
|
+
const s = $('[data-mode-select]');
|
|
312
|
+
s.replaceChildren(...modes.map((m) => new Option(names[m.id] || m.label || m.id, m.id)));
|
|
313
|
+
s.hidden = false;
|
|
314
|
+
$('[data-modes]').hidden = true;
|
|
315
|
+
s.onchange = () => { game.mode = s.value; };
|
|
316
|
+
} else picker($('[data-modes]'), modes, (id) => { game.mode = id; });
|
|
317
|
+
picker($('[data-tabs]'), [{ id: 'manual', label: words.manual }, { id: 'auto', label: words.auto }], (id) => { auto = id === 'auto'; left = 0; mark($('[data-tabs]'), id, false); render(); });
|
|
318
|
+
mark($('[data-tabs]'), 'manual', false);
|
|
319
|
+
for (const w of el.shadowRoot.querySelectorAll('[data-w]')) w.textContent = words[w.dataset.w];
|
|
320
|
+
picker($('[data-counts]'), COUNTS.map((n) => ({ id: String(n), label: n ? String(n) : '∞' })), (id) => { count = Number(id); mark($('[data-counts]'), id, false); });
|
|
321
|
+
mark($('[data-counts]'), '10', false);
|
|
322
|
+
for (const [key, box] of [['win', '[data-onwin]'], ['loss', '[data-onloss]']]) {
|
|
323
|
+
picker($(box), [{ id: 'reset', label: words.reset }, { id: 'increase', label: words.increase }], (id) => { rule[key] = id; mark($(box), id, false); });
|
|
324
|
+
mark($(box), 'reset', false);
|
|
325
|
+
}
|
|
326
|
+
$('[data-main]').onclick = run;
|
|
327
|
+
$('[data-next]').onclick = () => money(() => engine.action(open, 'advance'));
|
|
328
|
+
$('[data-cashout]').onclick = () => { cashout(); render(); };
|
|
329
|
+
engine.on('pause', () => { paused = true; render(); });
|
|
330
|
+
engine.on('resume', () => { paused = false; render(); });
|
|
331
|
+
engine.on('ended', () => { busy = true; left = 0; render(); });
|
|
332
|
+
// The first selection's own ceiling: a 5,000× bet takes less than a 2× one.
|
|
333
|
+
field.bet = { mode: sel.mode, bet: sel.bet };
|
|
334
|
+
// The shell ended the session (its Stop, a stop rule, a refusal, the reality check).
|
|
335
|
+
engine.on('autoplay', (s) => { if (!s.active && running) { running = false; left = 0; render(); } });
|
|
336
|
+
render();
|
|
337
|
+
if (open) await step(open, zone);
|
|
338
|
+
engine.ready();
|
|
339
|
+
return game;
|
|
340
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox faults for ADR 0006.
|
|
3
|
+
*
|
|
4
|
+
* The wrapper sits around the fetch passed to createRgs so refusal() and the
|
|
5
|
+
* lost-answer retries run unchanged, just as they do for a real response.
|
|
6
|
+
* Every request passes through untouched until the sandbox panel arms a fault.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BODY = Object.freeze({
|
|
10
|
+
session: { code: 'session-expired', error: 'session expired (sandbox test)' },
|
|
11
|
+
server: { error: 'server error (sandbox test)' },
|
|
12
|
+
unavailable: { error: 'unavailable (sandbox test)' },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function response(status, body) {
|
|
16
|
+
const text = body === undefined ? '' : JSON.stringify(body);
|
|
17
|
+
if (typeof Response === 'function') {
|
|
18
|
+
return new Response(text, { status, headers: { 'Content-Type': 'application/json' } });
|
|
19
|
+
}
|
|
20
|
+
return { status, ok: status < 400, json: async () => body };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requestUrl(input) {
|
|
24
|
+
if (typeof input === 'string') return input;
|
|
25
|
+
return input && input.url;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function affectedPath(input, init) {
|
|
29
|
+
try {
|
|
30
|
+
const pathname = new URL(requestUrl(input), globalThis.location?.href).pathname;
|
|
31
|
+
const marker = pathname.indexOf('/engine/v1/');
|
|
32
|
+
if (marker < 0) return null;
|
|
33
|
+
const method = String(init?.method || input?.method || 'GET').toUpperCase();
|
|
34
|
+
const path = pathname.slice(marker + '/engine/v1/'.length);
|
|
35
|
+
if (method !== 'POST' || !['play', 'action', 'end-round', 'balance'].includes(path)) return null;
|
|
36
|
+
return path;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function abortError() {
|
|
43
|
+
return new DOMException('The operation was aborted.', 'AbortError');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function timeout(signal) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
reject(abortError());
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
signal?.addEventListener('abort', () => reject(abortError()), { once: true });
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function slow(realFetch, input, init, ms) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const timer = setTimeout(() => realFetch(input, init).then(resolve, reject), ms);
|
|
59
|
+
init?.signal?.addEventListener('abort', () => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
reject(abortError());
|
|
62
|
+
}, { once: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function syntheticFault(fault, realFetch, input, init) {
|
|
67
|
+
if (fault.kind === 'session') return Promise.resolve(response(401, BODY.session));
|
|
68
|
+
if (fault.kind === 'server') return Promise.resolve(response(500, BODY.server));
|
|
69
|
+
if (fault.kind === 'unavailable') return Promise.resolve(response(503, BODY.unavailable));
|
|
70
|
+
if (fault.kind === 'timeout') return timeout(init?.signal);
|
|
71
|
+
if (fault.kind === 'slow') return slow(realFetch, input, init, fault.ms);
|
|
72
|
+
return realFetch(input, init);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Creates the inert fetch wrapper controlled by the sandbox test panel. */
|
|
76
|
+
export function createFaults(realFetch) {
|
|
77
|
+
let armed = null;
|
|
78
|
+
const listeners = new Set();
|
|
79
|
+
|
|
80
|
+
function notify() {
|
|
81
|
+
const snapshot = armed ? { ...armed } : null;
|
|
82
|
+
for (const listener of listeners) {
|
|
83
|
+
try {
|
|
84
|
+
listener(snapshot);
|
|
85
|
+
} catch {
|
|
86
|
+
// Status observers cannot affect fault injection.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fetch(input, init = {}) {
|
|
92
|
+
const path = affectedPath(input, init);
|
|
93
|
+
if (!armed || !path || (armed.only && armed.only !== path)) return realFetch(input, init);
|
|
94
|
+
const fault = armed;
|
|
95
|
+
fault.times -= 1;
|
|
96
|
+
if (fault.times <= 0) armed = null;
|
|
97
|
+
notify();
|
|
98
|
+
return syntheticFault(fault, realFetch, input, init);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function arm(kind, opts = {}) {
|
|
102
|
+
armed = {
|
|
103
|
+
kind,
|
|
104
|
+
times: Number.isInteger(opts.times) && opts.times > 0 ? opts.times : 1,
|
|
105
|
+
ms: Number.isFinite(opts.ms) ? opts.ms : 3000,
|
|
106
|
+
...(typeof opts.only === 'string' ? { only: opts.only } : {}),
|
|
107
|
+
};
|
|
108
|
+
notify();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clear() {
|
|
112
|
+
armed = null;
|
|
113
|
+
notify();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function state() {
|
|
117
|
+
return armed ? { ...armed } : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function onChange(listener) {
|
|
121
|
+
if (typeof listener !== 'function') return () => {};
|
|
122
|
+
listeners.add(listener);
|
|
123
|
+
return () => listeners.delete(listener);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { fetch, arm, clear, state, onChange };
|
|
127
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's client of the engine API (/engine/v1). The only code that
|
|
3
|
+
* holds the session id; the game never sees it.
|
|
4
|
+
*
|
|
5
|
+
* Every refusal comes back as { code, message, retryAfterMs? } with `code`
|
|
6
|
+
* one of the protocol's ERRORS, so the game reads one scheme whatever the
|
|
7
|
+
* server said. A play or an action whose answer was lost (the network, a
|
|
8
|
+
* timeout, a 5xx) is asked again with the SAME generation: the server
|
|
9
|
+
* answers a generation it has seen with the round it stored, so asking
|
|
10
|
+
* twice can never bet twice. A seed rotation is retried the same way under
|
|
11
|
+
* its generation, so asking twice can never rotate twice.
|
|
12
|
+
*/
|
|
13
|
+
import { ERRORS } from '../../engine-sdk/protocol.js';
|
|
14
|
+
|
|
15
|
+
const DEADLINE_MS = 10_000;
|
|
16
|
+
const LOST_ANSWER_RETRIES = 2;
|
|
17
|
+
const LOST_ANSWER_BACKOFF_MS = 600;
|
|
18
|
+
|
|
19
|
+
// Over the maximum win per round (SPEC §3.1, internal/constants/error.go
|
|
20
|
+
// MaxPayoutErrCode): the body names the most the bet takes (`maxStake`).
|
|
21
|
+
const MAX_PAYOUT_CODE = 21;
|
|
22
|
+
|
|
23
|
+
// The backend's numeric bet codes (internal/constants/error.go).
|
|
24
|
+
const BET_CODES = new Map([
|
|
25
|
+
[1, ERRORS.LIMITS], // max win at start
|
|
26
|
+
[3, ERRORS.LIMITS], // above max bet
|
|
27
|
+
[4, ERRORS.LIMITS], // below min bet
|
|
28
|
+
[6, ERRORS.INVALID], // bet parameters
|
|
29
|
+
[7, ERRORS.PACING], // duplicate inside the dedup window
|
|
30
|
+
[8, ERRORS.PACING], // previous bet still settling
|
|
31
|
+
[9, ERRORS.PENDING], // wallet answer ambiguous; the reconciler resolves it
|
|
32
|
+
[10, ERRORS.INVALID], // more decimals than the currency has
|
|
33
|
+
[11, ERRORS.INVALID], // stake rounds to zero
|
|
34
|
+
[12, ERRORS.INVALID], // negative stake
|
|
35
|
+
[13, ERRORS.PACING],
|
|
36
|
+
[15, ERRORS.UNAVAILABLE], // rate unavailable
|
|
37
|
+
[16, ERRORS.PACING], // throttled
|
|
38
|
+
[17, ERRORS.ROUND], // a round is in progress
|
|
39
|
+
[18, ERRORS.UNAVAILABLE], // idempotency guard unavailable
|
|
40
|
+
[19, ERRORS.ROUND], // round moved
|
|
41
|
+
[20, ERRORS.ROUND], // round open in another currency
|
|
42
|
+
[MAX_PAYOUT_CODE, ERRORS.LIMITS], // over the maximum win per round: the body names the most the bet takes
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// The most a bet takes, as a refusal over the maximum win names it: a plain
|
|
46
|
+
// decimal string in the coin, as the server wrote it.
|
|
47
|
+
const DECIMAL_RE = /^\d+(\.\d+)?$/;
|
|
48
|
+
|
|
49
|
+
/** Turns a refused HTTP answer into a protocol refusal. */
|
|
50
|
+
export function refusal(status, body) {
|
|
51
|
+
const message = body && typeof body.error === 'string' ? body.error : '';
|
|
52
|
+
const retryAfterMs = body && Number.isSafeInteger(body.retryAfterMs) ? body.retryAfterMs : 0;
|
|
53
|
+
// A refusal about a round carries it as it stands: `round` (codes 17 and
|
|
54
|
+
// 19) or `game` (code 20, a round in another currency).
|
|
55
|
+
const round = body && typeof body === 'object' ? (body.round ?? body.game) : undefined;
|
|
56
|
+
// A refusal over the maximum win per round carries the most the bet
|
|
57
|
+
// takes, so the game can set its field to the figure; no other does.
|
|
58
|
+
const maxStake = body && body.code === MAX_PAYOUT_CODE && typeof body.maxStake === 'string' && DECIMAL_RE.test(body.maxStake) ? body.maxStake : undefined;
|
|
59
|
+
const out = (code) => {
|
|
60
|
+
const e = { code, message };
|
|
61
|
+
if (retryAfterMs > 0) e.retryAfterMs = retryAfterMs;
|
|
62
|
+
if (round && typeof round === 'object') e.round = round;
|
|
63
|
+
if (maxStake !== undefined) e.maxStake = maxStake;
|
|
64
|
+
return e;
|
|
65
|
+
};
|
|
66
|
+
const code = body ? body.code : undefined;
|
|
67
|
+
if (typeof code === 'number' && BET_CODES.has(code)) return out(BET_CODES.get(code));
|
|
68
|
+
if (status === 401 || status === 403 || (typeof code === 'string' && code.startsWith('session-'))) return out(ERRORS.SESSION);
|
|
69
|
+
if (status === 409) return out(ERRORS.CONFLICT);
|
|
70
|
+
if (status === 404) return out(ERRORS.ROUND);
|
|
71
|
+
if (status === 429) return out(ERRORS.PACING);
|
|
72
|
+
if (status === 503) return out(ERRORS.UNAVAILABLE);
|
|
73
|
+
if (status === 400 && /insufficient/i.test(message)) return out(ERRORS.INSUFFICIENT);
|
|
74
|
+
if (status >= 400 && status < 500) return out(ERRORS.INVALID);
|
|
75
|
+
return out(ERRORS.SERVER);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* createRgs({ base, sessionId, fetch, sleep }) — base is the engine API's
|
|
80
|
+
* origin + path prefix, e.g. "https://games.housebets.com/engine/v1".
|
|
81
|
+
*/
|
|
82
|
+
export function createRgs({ base, sessionId, fetch: doFetch = globalThis.fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
|
|
83
|
+
async function call(method, path, body, { includeStatus = false } = {}) {
|
|
84
|
+
const ctrl = new AbortController();
|
|
85
|
+
const timer = setTimeout(() => ctrl.abort(), DEADLINE_MS);
|
|
86
|
+
try {
|
|
87
|
+
const res = await doFetch(base + path, {
|
|
88
|
+
method,
|
|
89
|
+
headers: { Authorization: `Bearer ${sessionId}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
|
|
90
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
91
|
+
signal: ctrl.signal,
|
|
92
|
+
credentials: 'omit',
|
|
93
|
+
cache: 'no-store',
|
|
94
|
+
});
|
|
95
|
+
let json = null;
|
|
96
|
+
try {
|
|
97
|
+
json = await res.json();
|
|
98
|
+
} catch {
|
|
99
|
+
json = null;
|
|
100
|
+
}
|
|
101
|
+
if (res.ok && res.status === 204) return { ok: true, body: {}, ...(includeStatus ? { status: res.status } : {}) };
|
|
102
|
+
if (res.ok && json && typeof json === 'object') return { ok: true, body: json, ...(includeStatus ? { status: res.status } : {}) };
|
|
103
|
+
// A 2xx whose body did not arrive whole is an answer lost on the
|
|
104
|
+
// way, not a refusal: the server may have settled, so it is asked
|
|
105
|
+
// again under the same generation.
|
|
106
|
+
if (res.ok) return { ok: false, lost: true, error: { code: ERRORS.SERVER, message: 'the answer did not arrive whole' }, ...(includeStatus ? { status: res.status } : {}) };
|
|
107
|
+
return { ok: false, lost: res.status >= 500 && res.status !== 503, error: refusal(res.status, json), ...(includeStatus ? { status: res.status } : {}) };
|
|
108
|
+
} catch {
|
|
109
|
+
return { ok: false, lost: true, error: { code: ERRORS.SERVER, message: 'no answer from the server' } };
|
|
110
|
+
} finally {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A call that is safe to repeat only under its own generation: the
|
|
116
|
+
// server stores the answer by it and replays it, so a lost answer is
|
|
117
|
+
// asked for again; a refusal is final and asked once.
|
|
118
|
+
async function retried(method, path, body) {
|
|
119
|
+
let r = await call(method, path, body);
|
|
120
|
+
let retried = false;
|
|
121
|
+
for (let i = 0; i < LOST_ANSWER_RETRIES && !r.ok && r.lost; i++) {
|
|
122
|
+
await sleep(LOST_ANSWER_BACKOFF_MS * (i + 1));
|
|
123
|
+
r = await call(method, path, body);
|
|
124
|
+
retried = true;
|
|
125
|
+
}
|
|
126
|
+
return { r, retried };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A money call is safe to repeat only under its own generation.
|
|
130
|
+
async function money(path, body) {
|
|
131
|
+
let { r, retried: again } = await retried('POST', path, body);
|
|
132
|
+
// A retry that meets the first attempt still in flight (or already
|
|
133
|
+
// claimed) learns nothing about the outcome either; and "pending" is
|
|
134
|
+
// the server saying the wallet's answer was ambiguous — the
|
|
135
|
+
// reconciler decides, so nobody may treat the stake as returned.
|
|
136
|
+
const stillOpen = again && !r.ok && r.error.code === ERRORS.PACING;
|
|
137
|
+
if (!r.ok && (r.lost || stillOpen || r.error.code === ERRORS.PENDING)) {
|
|
138
|
+
// Still no answer: the round may or may not exist. The wallet
|
|
139
|
+
// and the reconciler settle it; the game must not guess.
|
|
140
|
+
return { ok: false, unknown: true, error: { code: ERRORS.PENDING, message: 'the bet is being checked' } };
|
|
141
|
+
}
|
|
142
|
+
return r;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A seed rotation under its generation: a lost answer is asked for again
|
|
146
|
+
// (the server replays the stored answer, never rotating twice); when it is
|
|
147
|
+
// still lost the dialog is told so (`lost`) and keeps the generation for
|
|
148
|
+
// the player's next press, which the server will answer as the first.
|
|
149
|
+
async function rotation(method, path, body) {
|
|
150
|
+
const { r } = await retried(method, path, body);
|
|
151
|
+
return r;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
authenticate: (lang) => call('POST', '/authenticate', lang ? { lang } : {}),
|
|
156
|
+
balance: () => call('POST', '/balance', {}),
|
|
157
|
+
play: (generation, { mode, bet, amount }) => money('/play', { generation, mode, bet, amount }),
|
|
158
|
+
action: (generation, { roundId, step, action }) => money('/action', { generation, roundId, step, action }),
|
|
159
|
+
round: (roundId) => call('GET', `/fairness/${encodeURIComponent(roundId)}`),
|
|
160
|
+
// The player's seed pair for this game, under the session: the shell's
|
|
161
|
+
// provably-fair dialog alone calls these (the game has no request for
|
|
162
|
+
// them), so the player's site token never has to come back to the page.
|
|
163
|
+
// seeds {serverSeedHash, nextServerSeedHash, clientSeed, nonce, totalBets,
|
|
164
|
+
// previous: {serverSeed, serverSeedHash, clientSeed, nonce, retiredAt} | null}
|
|
165
|
+
// setClientSeed retires the pair (revealed as `previous`) and starts a fresh one,
|
|
166
|
+
// at nonce 0, under the new client seed — a rotation
|
|
167
|
+
// rotateSeeds the same, keeping the client seed
|
|
168
|
+
// Both take a generation (the dialog mints one per attempt): the server
|
|
169
|
+
// answers a generation it has seen with the answer it stored.
|
|
170
|
+
seeds: () => call('GET', '/seeds'),
|
|
171
|
+
setClientSeed: (generation, clientSeed) => rotation('PUT', '/seeds/client', { generation, clientSeed }),
|
|
172
|
+
rotateSeeds: (generation) => rotation('POST', '/seeds/rotate', { generation }),
|
|
173
|
+
getState: () => call('GET', '/state'),
|
|
174
|
+
putState: ({ revision, data }) => call('PUT', '/state', { revision, data }),
|
|
175
|
+
// Sandbox only (the server refuses it anywhere else): the next round resolves to these rows.
|
|
176
|
+
force: (indices, mode) => call('POST', '/sandbox/force', { indices, ...(mode ? { mode } : {}) }),
|
|
177
|
+
clearForce: () => call('DELETE', '/sandbox/force', undefined, { includeStatus: true }),
|
|
178
|
+
sandboxBalance: (amount) => call('POST', '/sandbox/balance', { amount: String(amount) }),
|
|
179
|
+
// A signal about the game (boot, error, abandon, slow), counted on the server per
|
|
180
|
+
// game and day and never kept as text: the kind is all that leaves. Fire and forget,
|
|
181
|
+
// with keepalive so the one sent as the page closes still goes. Resolves either way.
|
|
182
|
+
telemetry: (kind) => Promise.resolve()
|
|
183
|
+
.then(() => doFetch(base + '/telemetry', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { Authorization: `Bearer ${sessionId}`, 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({ kind }),
|
|
187
|
+
keepalive: true,
|
|
188
|
+
credentials: 'omit',
|
|
189
|
+
cache: 'no-store',
|
|
190
|
+
}))
|
|
191
|
+
.then(() => undefined, () => undefined),
|
|
192
|
+
};
|
|
193
|
+
}
|