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,855 @@
|
|
|
1
|
+
# Build a House Engine game
|
|
2
|
+
|
|
3
|
+
A House Engine game is two things: a **math pack** (what can happen and
|
|
4
|
+
what it pays, as data) and a **bundle** (static files that draw it). The
|
|
5
|
+
engine draws every outcome on our server with our provably-fair stream,
|
|
6
|
+
moves the money on the player's Housebets wallet, and your bundle shows what
|
|
7
|
+
the server decided. You never compute money, randomness or balances.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
my-game/
|
|
11
|
+
hbengine.json {slug, portal, rgs}
|
|
12
|
+
math/manifest.json modes, bets, payouts (§2)
|
|
13
|
+
math/<table>.tsv weighted outcomes, one per line
|
|
14
|
+
bundle/index.html your game (§3)
|
|
15
|
+
bundle/assets.json what loads first (§3.2)
|
|
16
|
+
bundle/hbtest.json how the pipeline plays one bet (§3.3)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npx hbengine templates # coin dice slot lines tumble keno crash mines plinko tower wheel
|
|
21
|
+
npx hbengine init my-game --template lines --slug my-game
|
|
22
|
+
cd my-game
|
|
23
|
+
npx hbengine connect # a key from House Studio, one click (or hbengine login <portal> with a key)
|
|
24
|
+
npx hbengine verify # the engine's own verifier: exact RTP per bet, the mathHash
|
|
25
|
+
npx hbengine dev # play your bundle inside the real shell, against the sandbox (after a first upload)
|
|
26
|
+
npx hbengine upload # verify + lint + upload; the sandbox plays it; the dashboard has the checks
|
|
27
|
+
npx hbengine store # store.json (+ tile.png) → the store page
|
|
28
|
+
npx hbengine submit # the newest version that passed, to review
|
|
29
|
+
npx hbengine stats # rounds, GGR and RTP by coin once it is played
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The dashboard (sign up at the same address) does the rest: upload by
|
|
33
|
+
dragging the folder in, the store page and tile, the sandbox in any coin
|
|
34
|
+
and language with a forced first round, submit for review, go live,
|
|
35
|
+
reports, statements and payouts.
|
|
36
|
+
|
|
37
|
+
`hbengine connect` opens the approve page in your browser; an owner or
|
|
38
|
+
developer approves there, and the terminal shows which studio did and asks
|
|
39
|
+
`Connect to <studio> (<slug>)? [y/N]` before it keeps the key (a no revokes
|
|
40
|
+
it; so does closing the terminal at the question). An agent, or a script with
|
|
41
|
+
no terminal, passes `--studio <slug>` instead (Organisation → Profile shows
|
|
42
|
+
the slug): the portal then lets only that studio approve; without it and
|
|
43
|
+
without a terminal, `hbengine connect` stops before asking for a code. A
|
|
44
|
+
gateway hiccup while waiting is retried a few times.
|
|
45
|
+
|
|
46
|
+
With an agent: `npx hbengine connect` adds the MCP server (`npx hbengine mcp`)
|
|
47
|
+
to Claude Code, Cursor and Codex, and the same steps are tools (`engine_guide`,
|
|
48
|
+
`engine_templates`, `engine_init`, `engine_verify`, `engine_lint`,
|
|
49
|
+
`engine_upload`, `engine_store`, `engine_submit`, `engine_live`,
|
|
50
|
+
`engine_status`, `engine_stats`): an agent takes a game from a template to
|
|
51
|
+
review and, once Housebets approves it, live.
|
|
52
|
+
|
|
53
|
+
Keep hbengine current: after any command it says when npm has a newer
|
|
54
|
+
version (restart your agent to take it, or `npm i -g hbengine@latest` if you
|
|
55
|
+
installed it; `hbengine version` says which you have), and the dashboard shows
|
|
56
|
+
a notice while your studio's tools are behind.
|
|
57
|
+
|
|
58
|
+
Stuck on something of House Engine itself (a bug in the engine, the SDK or
|
|
59
|
+
these tools, a question this guide does not answer), or have an idea? Tell
|
|
60
|
+
us: `hbengine feedback --kind bug --message "…"`, the dashboard's Support
|
|
61
|
+
page, or ask your agent to send Housebets feedback (`engine_feedback`). Say
|
|
62
|
+
what you did, what happened and the exact error; hbengine adds its version,
|
|
63
|
+
node, the OS and the game. We answer by email and in `hbengine tickets`
|
|
64
|
+
(`engine_tickets`).
|
|
65
|
+
|
|
66
|
+
## 1. How it runs
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
housebets.com page ─ frame ─▶ our shell (same origin as the site)
|
|
70
|
+
· mints the player's session, talks to the engine
|
|
71
|
+
· header balance, round lifecycle, fairness dialog
|
|
72
|
+
└─ frame ─▶ your bundle, on its own origin
|
|
73
|
+
sandboxed: no network but its own files,
|
|
74
|
+
talks only to the shell (postMessage)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Your page imports the SDK from its own origin — the platform serves it
|
|
78
|
+
there, your bundle must not carry a copy:
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<script type="module" src="/engine-sdk/bet-field.js"></script>
|
|
82
|
+
<script type="module" src="./game.js"></script>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Two ways to build the frontend; both ship through the same pipeline:
|
|
86
|
+
|
|
87
|
+
- **The engine panel** (`/engine-sdk/ui.js`): you draw only the game; the
|
|
88
|
+
Originals' panel (stake in the player's currency, Bet, Auto with the
|
|
89
|
+
Originals' rules — count, on win / on loss reset or increase by a
|
|
90
|
+
percent, stop on profit / loss — mode and bet pickers from your math,
|
|
91
|
+
Next / Cash out on a ladder, refusals in the player's language) is ours,
|
|
92
|
+
and its Auto runs as a shell autoplay session (§3.1).
|
|
93
|
+
`engine-examples/coin` is 30 lines.
|
|
94
|
+
- **Your own frontend**: any static web tech (plain, Pixi, Phaser, your
|
|
95
|
+
framework's build output). You use the SDK's `connect()` and show the
|
|
96
|
+
rounds it returns, and the money fields (`<hb-bet-amount>`) so currencies
|
|
97
|
+
are right. BAD CAT (four games, a lobby, skins, music) is the reference.
|
|
98
|
+
Build it as one page with views inside it, not as pages that frame other
|
|
99
|
+
pages of the bundle (the upload's scan names such pages, `nested-pages`).
|
|
100
|
+
|
|
101
|
+
## 2. The math pack
|
|
102
|
+
|
|
103
|
+
`math/manifest.json`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "format": "house-engine-math/1", "game": "coin", "rtpBp": 9900,
|
|
107
|
+
"modes": [ { "id": "coin", "label": "Coin", "kind": "table", "table": "coin.tsv",
|
|
108
|
+
"bets": [ { "id": "heads", "label": "Heads", "tiers": [ { "outcomes": [[0, 0]], "multBp": 19800 } ] },
|
|
109
|
+
{ "id": "tails", "label": "Tails", "tiers": [ { "outcomes": [[1, 1]], "multBp": 19800 } ] } ] } ] }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`math/coin.tsv` — one outcome per line, `weight<TAB>json`; the JSON is
|
|
113
|
+
what your frontend gets back as `round.data[i]` (the dice faces, the reel
|
|
114
|
+
stops, the trap tile), the server never reads it:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
1 {"side":"heads"}
|
|
118
|
+
1 {"side":"tails"}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- **table** mode: one draw settles the bet. Each bet names payout tiers
|
|
122
|
+
over outcome index ranges; `multBp` is basis points of the stake,
|
|
123
|
+
stake included (19800 = 1.98×). A dice with 97 targets is 97 bets on one
|
|
124
|
+
100-row table; a 3-reel slot with 48-stop reels is a 110,592-row table whose tiers group the
|
|
125
|
+
paying combinations. **100,000 different rounds in every mode is
|
|
126
|
+
recommended for a slot** (each bonus buy too): under it pre-flight warns
|
|
127
|
+
and the reviewer decides (fine for a classic 3-reel, not for a video
|
|
128
|
+
slot). Rows count once when another has the same data and pays the same;
|
|
129
|
+
the verifier reports `rounds` per mode. Originals have no recommendation.
|
|
130
|
+
- **ladder** mode (`"kind": "ladder"`): every step is drawn at bet from its
|
|
131
|
+
table; `bets[].survive` are the rows that let the player go on;
|
|
132
|
+
`ladderBp[k-1]` is the cumulative payout after `k` survived steps;
|
|
133
|
+
`cashout: true` lets the player stop after any step; `autoAdvance: true`
|
|
134
|
+
plays all steps at once (all or nothing). Tower, chicken road, mines-like
|
|
135
|
+
games are ladders.
|
|
136
|
+
- Slug `^[a-z0-9][a-z0-9-]{1,23}$`; ids `^[a-z0-9][a-z0-9-]{0,31}$`;
|
|
137
|
+
weights are integers, Σ ≤ 2⁶³, ≤ 1,000,000 rows, row JSON ≤ 4 KB;
|
|
138
|
+
≤ 64 steps. Unknown fields are refused.
|
|
139
|
+
- **RTP** is computed exactly (integer arithmetic) for every bet:
|
|
140
|
+
`9000 ≤ RTP ≤ rtpBp ≤ 9900`, every bet must be able to pay. A ladder's
|
|
141
|
+
RTP is its best stop. An outcome no bet pays on (a push, a tie, a zero
|
|
142
|
+
segment) is fine — the verifier only names it. `hbengine verify` prints RTP, hit rate, max
|
|
143
|
+
multiplier and (ladders) the RTP at every stop; what it accepts the upload
|
|
144
|
+
accepts, because it is the same code (`POST /engine/v1/verify`).
|
|
145
|
+
- A ladder round's **outcome** is `round.state`: `open`, `cashed_out` or
|
|
146
|
+
`busted` (`settled` for a table). `round.busted` and `round.survived`
|
|
147
|
+
describe the drawn path and arrive when the round closes — after a
|
|
148
|
+
cash-out too, when the path would have busted later. Show the player's
|
|
149
|
+
result from `round.state` and `round.step`.
|
|
150
|
+
- The pack's **mathHash** names the version. A session is pinned to one
|
|
151
|
+
hash; a new upload is a new version; players on an open ladder finish on
|
|
152
|
+
the version they started.
|
|
153
|
+
|
|
154
|
+
### 2.1 The maths kit — slots, plinko, anything with features
|
|
155
|
+
|
|
156
|
+
`packages/engine-math` (copied into a project as `math-kit/` by the
|
|
157
|
+
`lines`, `tumble` and `plinko` templates) writes the pack from rows you
|
|
158
|
+
describe — `slot.mjs` for reels and lines, `tumble.mjs` for scatter pays
|
|
159
|
+
with tumbles:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import { fit, rng, writePack } from '../math-kit/pack.mjs';
|
|
163
|
+
import { books, slot } from '../math-kit/slot.mjs';
|
|
164
|
+
const game = slot({ reels, rows: 3, lines, pays, wild: 'W', scatter: { symbol: 'S', pays, spins, retrigger, multiplier: 2, cap: 60 }, maxWinBp });
|
|
165
|
+
const rows = fit(books(game, rng(SEED), 400_000), 9600); // every row a whole round, RTP exactly 96.00%
|
|
166
|
+
writePack('math', { game: 'my-slot', rtpBp: 9600, modes: [{ id: 'base', rows, bets: ['spin'] }] });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- A **row is a whole round** (a "book"): the base spin and every free spin
|
|
170
|
+
it starts, with what it pays in total. Features, cascades, multipliers,
|
|
171
|
+
pick bonuses — anything you can play in code — become rows the same way;
|
|
172
|
+
the page plays back what the row holds.
|
|
173
|
+
- **`fit(rows, targetBp)`** makes the RTP exactly the target: the paying
|
|
174
|
+
rows keep their proportions, the losing rows share the weight left.
|
|
175
|
+
Integer arithmetic throughout; the engine's verifier then agrees to the
|
|
176
|
+
fourth decimal.
|
|
177
|
+
- **`balance(rows, targetBp)`** does the same for a table where every row
|
|
178
|
+
pays (a bonus buy, plinko): the rows above and below the target are each
|
|
179
|
+
scaled by one integer. The `lines` template's **bonus buy** mode is the
|
|
180
|
+
free spins alone, priced at what they return on average (27 spins),
|
|
181
|
+
balanced to exactly 96%.
|
|
182
|
+
- **`writePack`** sorts rows so each pay tier is one range, checks the
|
|
183
|
+
limits (rows, Σ weights, row size) and prints RTP, hit rate and max win.
|
|
184
|
+
- Tune the model until RTP *before* the fit is near the target (the gen
|
|
185
|
+
prints it): the fit should nudge, not reshape, the hit rate.
|
|
186
|
+
- Exact games (plinko, wheels, dice) need no fit: weights are exact
|
|
187
|
+
(`binomial(n, k)`), and you solve one multiplier for the RTP — round it
|
|
188
|
+
to what the page shows, so the board shows exactly what pays.
|
|
189
|
+
- Write the model the page needs (reels, lines, pays) to `bundle/` from
|
|
190
|
+
the same script, so the page and the table can never disagree.
|
|
191
|
+
|
|
192
|
+
The studio dashboard shows every bet's RTP, hit rate, largest win and
|
|
193
|
+
volatility (σ of one round's multiplier: low < 2, medium < 5, high < 15,
|
|
194
|
+
very high above) from the verified pack; players see the same on the
|
|
195
|
+
game's page.
|
|
196
|
+
|
|
197
|
+
## 3. The bundle
|
|
198
|
+
|
|
199
|
+
`bundle/index.html` at the root, everything relative. Budgets: boot group
|
|
200
|
+
≤ 2 MB gzip (its JavaScript ≤ 250 KB gzip), any other group ≤ 25 MB gzip,
|
|
201
|
+
≤ 300 MB and ≤ 2,500 files in all, one file ≤ 8 MB (code ≤ 4 MB), plain
|
|
202
|
+
files only.
|
|
203
|
+
|
|
204
|
+
### 3.1 The SDK
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
import { connect } from '/engine-sdk/sdk.js';
|
|
208
|
+
const engine = await connect(); // waits for the shell's init
|
|
209
|
+
engine.config // game, mathHash, env, currency, limits, modes, balance, locale, wallet, state, activeRound?
|
|
210
|
+
const { round, balance } = await engine.play({ mode: 'coin', bet: 'heads', amount: field.amount });
|
|
211
|
+
round.data[0] // the row JSON you wrote in the table
|
|
212
|
+
engine.revealed(round.id); // the outcome is on screen: the header balance moves now
|
|
213
|
+
// round: { id, mode, bet, kind, amount (wallet units), state 'open'|'settled'|'cashed_out'|'busted',
|
|
214
|
+
// step, payout, multBp, data: [row JSON per draw], draws: [{index, raw, rejected}], nonce, mathHash }
|
|
215
|
+
// engine.config.limits: { min, max, maxWin, maxStake: { <mode>: { <bet>: "12.50" } } } in the wallet coin
|
|
216
|
+
// (may be empty in the sandbox); maxStake is the most each bet takes, §4.1
|
|
217
|
+
// a ladder draws every step at the bet; while it is open you see only the steps played,
|
|
218
|
+
// once it ends (bust, cash-out, autoAdvance) `data` has every step — show up to the first miss
|
|
219
|
+
// a push (a tier paying 1.00×) is `payout === amount`; a loss is `payout === 0`
|
|
220
|
+
await engine.action(round, 'advance' | 'cashout'); // ladders; pass the round as you last had it; both from the player's click
|
|
221
|
+
engine.on('balance' | 'control' | 'pause' | 'resume' | 'ended' | 'autoplay', fn)
|
|
222
|
+
// pause/resume: the shell's reality check (engine.config.rg.realityCheckSec); ended: the session is over
|
|
223
|
+
// engine.config.rg: { autoplay: { allowed }, realityCheckSec } — the jurisdiction's rules; the panel obeys them
|
|
224
|
+
// autoplay { active, left, played, stake?, reason? }: the shell's session changed (started, a play, or ended)
|
|
225
|
+
await engine.autoplay.start({ mode, bet, rounds, stake, stopOnProfit?, stopOnLoss? }) // the player's grant for one bet: the shell shows the terms, they press Start
|
|
226
|
+
engine.autoplay.stop() // end it; the shell also shows its own Stop over your game
|
|
227
|
+
engine.state.get() / engine.state.put(revision, data) // the player's own saved data, ≤ 256 KB, any device
|
|
228
|
+
// both resolve { revision, data }; put with the revision you read
|
|
229
|
+
// (0 the first time); a stale revision rejects with `conflict`
|
|
230
|
+
// save when the player changes something, not per frame: over 10 writes
|
|
231
|
+
// at once and then one a second rejects with `pacing` (retryAfterMs)
|
|
232
|
+
// keep a `version` field in it: a new release of your game reads old data
|
|
233
|
+
engine.money // §4
|
|
234
|
+
engine.openFairness(round.id); engine.reportError(msg);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
One `play`/`action` at a time; a second is refused with `busy`. Every
|
|
238
|
+
refusal is an `EngineError` with a `code` (`invalid busy
|
|
239
|
+
insufficient-balance limits pacing pending unavailable session-ended round
|
|
240
|
+
conflict server needs-click autoplay`), a message to show, and for `round`
|
|
241
|
+
the round as it now stands (redraw it). `pending` means the wallet has not
|
|
242
|
+
answered: the shell and the server settle it; show the message, do not retry
|
|
243
|
+
on your own.
|
|
244
|
+
|
|
245
|
+
**Nothing happens without the player's click.** A `play` and every ladder
|
|
246
|
+
`action` — an `advance` and a `cashout` alike — are taken only when the
|
|
247
|
+
player has just clicked or pressed a key in your page — the shell reads the
|
|
248
|
+
real browser activation a click gives it, which lasts a few seconds, and
|
|
249
|
+
checks that the focus is inside your frame and got there by the player's own
|
|
250
|
+
act (a click in your page puts it there; a click elsewhere on the site page
|
|
251
|
+
does not, and a `focus()` your script calls is undone and buys nothing) — or
|
|
252
|
+
inside an autoplay session the player started (its plays, and the steps and
|
|
253
|
+
cash-out of the ladder it opened). Call them from the handler of the
|
|
254
|
+
player's gesture, not from a timer, a load, a date or a round count, or the
|
|
255
|
+
shell refuses them with `needs-click` ("A bet needs your click", in the
|
|
256
|
+
player's language). An auto cash-out target is such a timer: the shell
|
|
257
|
+
refuses the cash-out the moment the click lapses, so put a target in your
|
|
258
|
+
maths (its own stop) rather than in your page; a round the player leaves open
|
|
259
|
+
stays open for them to come back to (the server resumes it with the session),
|
|
260
|
+
and is never your page's to close. One click
|
|
261
|
+
buys one bet: a second `play` within 600 ms, or a sixth within 5 s, is
|
|
262
|
+
refused with `pacing` and `retryAfterMs`; one tap buys at most one `advance`
|
|
263
|
+
per 100 ms and 25 per 5 s, refused the same way; a cash-out is not paced. For
|
|
264
|
+
5 s after the player pressed one of the shell's own controls (Stop on the
|
|
265
|
+
autoplay pill, Cancel on its confirmation, Continue on the reality check, a
|
|
266
|
+
key in the Provably Fair dialog) — or, in production, anything on the site's
|
|
267
|
+
page around your game (the chat, the header: the shell is a frame of that
|
|
268
|
+
page and a press there arms it as a click in your page does) — the shell
|
|
269
|
+
takes no bet on activation alone either, so a bet pressed within those 5 s
|
|
270
|
+
is refused with `needs-click` — every one of them, until 5 s have passed
|
|
271
|
+
since the last press outside your frame; show the message and let the player
|
|
272
|
+
press again. The same 5 s follow the player tabbing out of your page by
|
|
273
|
+
keyboard: a click back into it within them is refused `needs-click` and the
|
|
274
|
+
focus is put back outside (the shell cannot tell that click from a
|
|
275
|
+
`focus()` your script might call on its own `blur`, so neither gets in); a
|
|
276
|
+
Tab back into your page is let in, and so is a click 5 s after they left.
|
|
277
|
+
The read-only `round` and `state.get`, and `state.put` (a save
|
|
278
|
+
moves no money: save at boot or after a reveal as you always did) need no
|
|
279
|
+
click. A browser without `navigator.userActivation` cannot confirm a click,
|
|
280
|
+
so the shell bets nothing there and the refusal names the browsers that can
|
|
281
|
+
(current Chrome, Edge, Safari and Firefox all have it). The upload's
|
|
282
|
+
conformance run presses your Bet with a real click, so a game that bets from
|
|
283
|
+
the click handler passes as before.
|
|
284
|
+
|
|
285
|
+
A ladder your page climbs itself (a crash) advances on the Bet click's
|
|
286
|
+
activation for those few seconds and within the tap's run of steps, then the
|
|
287
|
+
shell refuses the next step: on `pacing` wait `retryAfterMs` and climb on;
|
|
288
|
+
on anything else (`null` from `game.advance()` — read `game.refusal` for why
|
|
289
|
+
— or `needs-click` from `engine.action`) stop climbing, keep the round and
|
|
290
|
+
Cash out on screen, and climb on at the player's next tap. Take that tap on
|
|
291
|
+
`click` (or `pointerup`), not `pointerdown`: a touch grants the shell its
|
|
292
|
+
activation only when the finger lifts, so a step sent from `pointerdown`
|
|
293
|
+
can be refused again on a phone. Cash out is the player's press, never the
|
|
294
|
+
page's (no auto cash-out on a timer). `engine-examples/crash` does exactly
|
|
295
|
+
this. A tile or a Next button pressed per step (mines, tower) is a click per
|
|
296
|
+
step and needs nothing more.
|
|
297
|
+
|
|
298
|
+
**Autoplay is the player's grant through the shell:**
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
const { session } = await engine.autoplay.start({ mode: game.mode, bet: game.bet, rounds: 50, stake: field.amount, stopOnProfit: 20, stopOnLoss: 10 });
|
|
302
|
+
// Call it from the player's click on your Auto button (a start with no click behind it is refused
|
|
303
|
+
// `needs-click`, like a bet). The run is for one bet (`mode`, `bet`: the player approves that bet, and
|
|
304
|
+
// a play on another is refused `autoplay`); rounds ≤ 1000; no play in the session may stake more than
|
|
305
|
+
// `stake`; the stops are optional, in wallet units, on the session's net result (a loss limit is
|
|
306
|
+
// always applied: yours, or 100× the stake, and never above the total the run could stake). The shell
|
|
307
|
+
// shows the player the terms — the bet, capped at the coin's max bet, the bet's own ceiling
|
|
308
|
+
// (engine.config.limits.maxStake[mode][bet], §4.1: the request names the bet, so the shell applies it;
|
|
309
|
+
// a bet whose figure is 0 takes nothing right now, and the start rejects `unavailable`) and the
|
|
310
|
+
// balance it last saw — in its own dialog over your frame and resolves this only when they press
|
|
311
|
+
// Start there (its Start is dead for the dialog's first second); rejects with `autoplay` on Cancel,
|
|
312
|
+
// after 30 s unanswered, on the reality check, when the player's rules forbid autoplay
|
|
313
|
+
// (engine.config.rg.autoplay.allowed), while one is already running, and (with `retryAfterMs`;
|
|
314
|
+
// "Autoplay was just closed. You can ask again in a moment.", in the player's language) for 10 s
|
|
315
|
+
// after a Cancel — a cancelled request cannot be repeated at once, so do not retry it from your
|
|
316
|
+
// rejection handler; with `busy` while paused. `session` carries the terms as granted: keep your run
|
|
317
|
+
// to them.
|
|
318
|
+
engine.on('autoplay', ({ active, left, played, reason }) => …); // on start, after each play, at the end
|
|
319
|
+
await engine.autoplay.stop(); // always taken
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The shell counts the rounds, refuses a play on another bet, above the stake
|
|
323
|
+
or past the count, ends the session at the count (once a ladder round it
|
|
324
|
+
opened has closed; the stops wait for it too), at a stop, on any refused play, on `stop()`, on the reality check,
|
|
325
|
+
after 30 s without a play of the session (`idle`: play the granted rounds
|
|
326
|
+
now, not later), when the page is hidden (`hidden`: another tab, the phone
|
|
327
|
+
locked) and when the player presses **Stop** on the shell's own control — a
|
|
328
|
+
small pill over your frame showing the rounds left, which your page cannot
|
|
329
|
+
cover or press — and tells you of every change through the `autoplay` event
|
|
330
|
+
(`reason`: `stopped done profit loss refused paused ended idle hidden`).
|
|
331
|
+
Plays you send while a session is live need no further click. Build no
|
|
332
|
+
autoplay engine of your own that bets without the player: a click carries
|
|
333
|
+
only the first few seconds, so the shell refuses such a loop's bets with
|
|
334
|
+
`needs-click` once the player's activation lapses, and with `pacing` beyond
|
|
335
|
+
five plays in 5 s — a three-spin loop passing your test proves nothing. The
|
|
336
|
+
engine panel's Auto (`ui.js`) already runs through the shell; a custom front
|
|
337
|
+
end starts the session from its own Auto button (the player then confirms in
|
|
338
|
+
the shell's dialog) and calls `stop()` from its own Stop. A bundle already
|
|
339
|
+
live with an autoplay loop of its own (Kraken's autopick run, BAD CAT's
|
|
340
|
+
auto-bets) is cut off the same way from the day this shell deploys — its
|
|
341
|
+
Auto plays a few rounds after the click and then stops with `needs-click` —
|
|
342
|
+
until it is republished on `engine.autoplay`; the shell goes first, by
|
|
343
|
+
decision, because a game betting on its own is exactly what it exists to
|
|
344
|
+
stop.
|
|
345
|
+
|
|
346
|
+
`engine.reportError(msg)`, and an uncaught error or unhandled promise
|
|
347
|
+
rejection in your page (the SDK listens for both on `window`), is logged in
|
|
348
|
+
the player's console and counted on the engine as one error for the game's
|
|
349
|
+
day; the shell also counts a launch per open and a round left open when the
|
|
350
|
+
page closes on one. Your Reports page and `hbengine stats` show the counts
|
|
351
|
+
per day — counts only, never the text: nothing a game sends reaches the
|
|
352
|
+
studio as text.
|
|
353
|
+
|
|
354
|
+
With the engine panel instead:
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
import { mount } from '/engine-sdk/ui.js';
|
|
358
|
+
const game = await mount(document.querySelector('hb-game'), {
|
|
359
|
+
reveal: async (round, zone) => { /* animate round.data[0]; resolve when on screen */ },
|
|
360
|
+
step: async (round, zone) => { /* a ladder round moved */ },
|
|
361
|
+
});
|
|
362
|
+
game.mode = 'hard'; game.bet = 'over-50'; // when your zone picks (a mode with > 12 bets)
|
|
363
|
+
el.addEventListener('select', (e) => draw(e.detail.mode)); // the panel's pick reached your zone
|
|
364
|
+
// engine.config.modes carries ids, labels, kinds and each bet's maxMultBp — never the
|
|
365
|
+
// tables: what your zone draws (a wheel's segments, a paytable) is a constant of yours.
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### 3.2 Loading in stages — `assets.json`
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{ "groups": { "boot": { "files": ["index.html", "game.js", "game.css"] },
|
|
372
|
+
"music": { "files": ["music/a.mp3"], "load": "idle" },
|
|
373
|
+
"skins": { "files": ["skins/tabby.webp"], "load": "demand" } } }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`boot` is what the first frame needs; `idle` groups load after
|
|
377
|
+
`engine.ready()`; `demand` groups when you ask (`createAssetLoader` in
|
|
378
|
+
`/engine-sdk/assets.js`, or your own loader — the manifest is the contract).
|
|
379
|
+
Every file in exactly one group.
|
|
380
|
+
|
|
381
|
+
### 3.3 Fit — the frame, and what the site's bar does
|
|
382
|
+
|
|
383
|
+
The game is the frame, as a House Original is: the site gives it the
|
|
384
|
+
column's width × **720 px** (phone: ~364 × 720), wider in theatre mode and
|
|
385
|
+
the whole screen in fullscreen (the site's buttons). Lay the game out for
|
|
386
|
+
the box it is given — `height: 100%`, the board taking what is left — and
|
|
387
|
+
re-fit when it changes. The frame never grows to the page and never
|
|
388
|
+
scrolls: the pipeline plays your game at 1194×720 and 364×720, then
|
|
389
|
+
fullscreen at 1920×1080 and 390×844, and every control must be on screen
|
|
390
|
+
at each — and no text, image or canvas may be cut by the frame's edge (a
|
|
391
|
+
reel that runs off a phone fails). Decoration meant to bleed off the edge
|
|
392
|
+
is marked `aria-hidden="true"`; a clip inside the frame (a reel window, a
|
|
393
|
+
ticker) is yours and fine.
|
|
394
|
+
|
|
395
|
+
The site already shows the title, the balance and a bar under the frame —
|
|
396
|
+
draw none of them. Showing the balance in the game is allowed (slots do),
|
|
397
|
+
from `engine.balance` through `engine.money`. The bar's buttons reach the
|
|
398
|
+
game as `engine.on('control', ({ action }) => …)`: `sound` (toggle your
|
|
399
|
+
sound), `settings`; **Provably Fair** opens the shell's own dialog, the
|
|
400
|
+
House Originals' modal in the game window: a Seeds tab (the player's client
|
|
401
|
+
seed, the server seed hash before play, the next hash, the nonce, Rotate
|
|
402
|
+
seed pair, the revealed pairs) and a Verify tab (server seed, client seed,
|
|
403
|
+
nonce and mode in, the recomputed result out, matched against the round when
|
|
404
|
+
opened on one). It is the same for every engine game and it is the shell's,
|
|
405
|
+
so build no fairness UI of your own — no seeds view, no rotate button, no
|
|
406
|
+
verifier; `engine.openFairness(round.id)` opens the shell's dialog on a
|
|
407
|
+
round from inside your game, filled and verified, when you want a button
|
|
408
|
+
there (call it on the player's click: the shell takes it at most once every
|
|
409
|
+
two seconds, not while paused, and never over what the player has typed in
|
|
410
|
+
the dialog). `engine.round(id)` gives you the settled round without its
|
|
411
|
+
seeds. Every round is a bet: send `engine.play` a stake of at least
|
|
412
|
+
`engine.config.limits.min` and never offer a free or zero-stake spin — the
|
|
413
|
+
server refuses a stake under the minimum and a stake of 0 (a round that
|
|
414
|
+
costs nothing would still consume a nonce of the player's seed pair), demo
|
|
415
|
+
or otherwise.
|
|
416
|
+
|
|
417
|
+
### 3.4 `hbtest.json` — how the pipeline plays one bet
|
|
418
|
+
|
|
419
|
+
```json
|
|
420
|
+
{ "open": [{ "page": "index.html", "selector": "a[href='./coin.html']" }],
|
|
421
|
+
"stake": { "page": "index.html", "selector": "hb-game hb-bet-amount input", "value": "1.00" },
|
|
422
|
+
"bet": { "page": "index.html", "selector": "hb-game [data-main]" } }
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Selectors reach into shadow roots; `page` is the page's file name. The run
|
|
426
|
+
types the stake, presses the bet, and expects `engine.play` and then
|
|
427
|
+
`revealed`. A ladder's bet opens a round: add `"then": [{ "page": "index.html", "selector":
|
|
428
|
+
"hb-game [data-next]" }, { "page": "index.html", "selector": "hb-game [data-cashout]" }]`
|
|
429
|
+
so the run closes it. Each `then` step is pressed only if its control is
|
|
430
|
+
visible and enabled — a bust can end the round before Cash out exists.
|
|
431
|
+
`"shows": { "page": "index.html", "selector": "#face", "path": "data.0.side" }`
|
|
432
|
+
makes the run check that the element shows the value the server drew
|
|
433
|
+
(`path` walks the round: `data.0.side` = `round.data[0].side`).
|
|
434
|
+
A game with several screens (a lobby of games) lists the others in
|
|
435
|
+
`"visit"`: clicks made before `open`, each screen and back, e.g.
|
|
436
|
+
`[{ "page": "index.html", "selector": "[data-card='dice']" }, { "page": "index.html", "selector": "#back" }]`.
|
|
437
|
+
An error or a scroll on any of them fails the run like one on the bet's screen.
|
|
438
|
+
A custom frontend names its own elements: any selector works
|
|
439
|
+
(`#play`, `.stake input`); give the Bet button and the stake input stable
|
|
440
|
+
ids and use those.
|
|
441
|
+
The engine panel's selectors are a contract: `hb-game hb-bet-amount input`
|
|
442
|
+
(stake), `hb-game [data-main]` (Bet), `[data-next]`, `[data-cashout]`,
|
|
443
|
+
`[data-modes] button[data-id="<mode>"]`, `[data-bets] button[data-id="<bet>"]`.
|
|
444
|
+
The typed stake is parsed in every currency of the run (a fiat at 2
|
|
445
|
+
decimals, the coin at 8): keep it representable in both, `1.00` is.
|
|
446
|
+
|
|
447
|
+
### 3.5 Libraries (Pixi, Howler, GSAP …)
|
|
448
|
+
|
|
449
|
+
Ship them in the bundle under `bundle/vendor/` — nothing loads from a CDN at
|
|
450
|
+
run time. Libraries often contain code paths the player's browser will
|
|
451
|
+
refuse here (`new Function`, default CDN URLs). The scan reads a file under
|
|
452
|
+
`vendor/` as a library when its file name and the banner comment in its
|
|
453
|
+
head match a library we know (PixiJS and its `unsafe-eval` module, howler.js,
|
|
454
|
+
GSAP, three.js, Phaser, Babylon.js, Matter.js, Planck.js, anime.js, jQuery,
|
|
455
|
+
lodash, Spine, Tone.js, Hammer.js, Tween.js, p5.js, CreateJS, PlayCanvas,
|
|
456
|
+
cannon.js, Box2D, Rapier — `KNOWN_LIBRARIES` in `engine-conformance/scan.mjs`;
|
|
457
|
+
ask us to add one): what it would refuse in your own code it only names for
|
|
458
|
+
the reviewer, and the library may be minified as released. Anything else
|
|
459
|
+
under `vendor/` is read as your code (§3.7 applies, a source map and all).
|
|
460
|
+
The conformance run fails a game that trips the policy even once — configure
|
|
461
|
+
the library not to take those paths. Ship the release file as it is: never
|
|
462
|
+
edit a library file to get past the scan.
|
|
463
|
+
|
|
464
|
+
**PixiJS v8**: load its no-eval module right after it — the policy has no
|
|
465
|
+
`unsafe-eval`, and without this Pixi's renderer cannot start:
|
|
466
|
+
|
|
467
|
+
```html
|
|
468
|
+
<script src="./vendor/pixi.min.js"></script> <!-- pixi.js@8 dist/pixi.min.js -->
|
|
469
|
+
<script src="./vendor/unsafe-eval.min.js"></script> <!-- pixi.js@8 dist/packages/unsafe-eval.min.js -->
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`ticker.add(fn)` returns the ticker, not `fn`: keep `fn` to remove it when a
|
|
473
|
+
screen closes, or it runs on destroyed sprites (an error the run catches).
|
|
474
|
+
|
|
475
|
+
Pixi is ~185 KB gzip and fits the boot budget; put your textures in
|
|
476
|
+
staged groups.
|
|
477
|
+
|
|
478
|
+
### 3.6 Compressed textures and WebAssembly (KTX2 / Basis)
|
|
479
|
+
|
|
480
|
+
The policy allows WebAssembly (`'wasm-unsafe-eval'`) and workers started
|
|
481
|
+
from `blob:` (`worker-src 'self' blob:`), so a KTX2/Basis transcoder runs —
|
|
482
|
+
GPU-compressed textures keep phone memory down. But a `.wasm` is a program
|
|
483
|
+
nobody at Housebets can read, so the scan accepts WebAssembly on one footing
|
|
484
|
+
only: **a known library's released module, run by that library's own code.**
|
|
485
|
+
|
|
486
|
+
- The module's bytes must be a release we know: the scan hashes every
|
|
487
|
+
WebAssembly module in the bundle (by its first bytes, whatever the file is
|
|
488
|
+
called) and looks the SHA-256 up in
|
|
489
|
+
`packages/engine-conformance/wasm-releases.json` — three.js's
|
|
490
|
+
`basis_transcoder.wasm` and `draco_decoder.wasm`, Rapier's
|
|
491
|
+
`rapier_wasm2d_bg.wasm`/`rapier_wasm3d_bg.wasm`, Box2D's `Box2D.wasm`,
|
|
492
|
+
at the versions listed. Ship the file exactly as the package publishes it.
|
|
493
|
+
A module whose hash is not there is refused, whatever its name and
|
|
494
|
+
wherever it sits; a name like `rapier_bg.wasm` is no tie to anything.
|
|
495
|
+
Need another library or version? Ask us to add its hash (we take it from
|
|
496
|
+
the published tarball, never from a file in an upload).
|
|
497
|
+
- Only the library's own glue may reach `WebAssembly`: its `.js` under
|
|
498
|
+
`vendor/`, named and bannered as released (§3.5: `basis_transcoder.js`,
|
|
499
|
+
`draco_decoder.js`, `rapier*.js`, `Box2D*.js` are known). Your own code
|
|
500
|
+
reaching `WebAssembly` in any way — `WebAssembly.instantiate(…)`, a name
|
|
501
|
+
bound to it, `this.wa = WebAssembly`, a module embedded as base64 — is
|
|
502
|
+
refused. `typeof WebAssembly === 'object'` is a feature check, not a use.
|
|
503
|
+
- Its JS glue must not call `new Function`/`eval`. Emscripten's embind does
|
|
504
|
+
by default; the published three.js builds do not. The conformance run
|
|
505
|
+
fails a game whose transcoder trips the policy, so `hbengine test` tells
|
|
506
|
+
you at once.
|
|
507
|
+
- three.js: `new KTX2Loader().setTranscoderPath('./vendor/basis/')
|
|
508
|
+
.detectSupport(renderer)`. In Pixi or another engine, point its KTX2
|
|
509
|
+
transcoder path at the same `./vendor/` folder.
|
|
510
|
+
- Put `.ktx2` textures in staged `assets.json` groups like any other art;
|
|
511
|
+
keep a PNG/WebP fallback only if a target device lacks every supported
|
|
512
|
+
GPU format.
|
|
513
|
+
- A transcoder or physics engine you built yourself cannot ship as
|
|
514
|
+
WebAssembly: there is no source we could tie to the bytes. Ship it as
|
|
515
|
+
JavaScript (readable, or with its source map, §3.7).
|
|
516
|
+
|
|
517
|
+
### 3.7 What a bundle must not do — and why the scan reads your code
|
|
518
|
+
|
|
519
|
+
The upload's scan parses every script of the bundle (every `.js`, every
|
|
520
|
+
inline `<script>`) and reads what it reaches from the syntax, with names
|
|
521
|
+
resolved through scopes — not by matching patterns. A rule is refused in
|
|
522
|
+
every spelling: `window['ev' + 'al']` is `eval`, `(0, eval)` is `eval`,
|
|
523
|
+
`fn.constructor('…')` is the Function constructor. Write plainly; a reader
|
|
524
|
+
at Housebets has to see what your game does before a player does.
|
|
525
|
+
|
|
526
|
+
**Refused** (the upload fails, naming file and line):
|
|
527
|
+
|
|
528
|
+
- `eval` and the Function constructor in any form: `Function(…)`,
|
|
529
|
+
`new Function`, `x.constructor(…)` on anything (a function's
|
|
530
|
+
`.constructor` *is* `Function`, and the scan cannot tell a function from
|
|
531
|
+
a class instance). Reading a property off it is fine — `.constructor.name`,
|
|
532
|
+
`this.constructor.DEFAULTS`, `this.constructor.create(opts)`,
|
|
533
|
+
`new this.constructor()` with no arguments, `x.constructor === Array` —
|
|
534
|
+
but calling it, `new o.constructor(a, b)` with arguments, keeping it in a
|
|
535
|
+
name (`const C = o.constructor`, `const { constructor } = o`) or reaching
|
|
536
|
+
`.call`/`.apply`/`.bind` on it is refused: name the class instead.
|
|
537
|
+
- The global object by a computed name — `window[name]`, `globalThis[k]`,
|
|
538
|
+
`self[k]`, `top[k]`, `parent[k]`, `frames[i]`, `document[k]`,
|
|
539
|
+
`Reflect.get(window, k)`, `Object.keys(window)`, `for (k in window)`,
|
|
540
|
+
`{ ...window }`, `const { ...rest } = window`, `new Proxy(window, …)` —
|
|
541
|
+
also through a name bound to it: `const w = window`, `let w; w = window`,
|
|
542
|
+
`const { self: w } = window`, a `(function (root) { … })(window)` wrapper.
|
|
543
|
+
`window.name`, `window['name']` and `const { name } = window` are fine:
|
|
544
|
+
they say what they reach, and are judged as that name (`const { eval } =
|
|
545
|
+
window` is `eval`).
|
|
546
|
+
- A property name built at run time that spells an API: `obj['ev' + 'al']`,
|
|
547
|
+
`obj[atob('…')]`, `obj[String.fromCharCode(…)]`, `obj['\x65val']`,
|
|
548
|
+
`'lave'.split('').reverse().join('')`. Ordinary dynamic keys
|
|
549
|
+
(`sounds['win_' + level]`, `obj[key]`, `dict[lang.toLowerCase()]`) are fine.
|
|
550
|
+
- A string handed to `setTimeout`/`setInterval`; `with`; `document.write`
|
|
551
|
+
(on any document: `frame.contentDocument.write`, `node.ownerDocument.write`);
|
|
552
|
+
`createElement('script')` or a `<script>` tag in a string (its text would
|
|
553
|
+
run as code nobody read — load scripts with `<script src>` or `import()` of
|
|
554
|
+
a path); code in attributes (`onclick="…"`, `href="javascript:…"`,
|
|
555
|
+
`setAttribute('onclick', …)` however the name is spelled — `'on' + 'click'`,
|
|
556
|
+
reversed, decoded, each name of a list a loop runs over — `setAttributeNS`,
|
|
557
|
+
`toggleAttribute` and `createAttribute` too, and a handler inside markup you
|
|
558
|
+
build from a string: `innerHTML = '<img onerror=…>'`, `insertAdjacentHTML`,
|
|
559
|
+
`outerHTML`, `createContextualFragment`, `setHTMLUnsafe`,
|
|
560
|
+
`DOMParser.parseFromString`, also when the markup is split across pieces,
|
|
561
|
+
`'<img on' + 'error=…>'`) — use `addEventListener`.
|
|
562
|
+
- An attribute name the scan cannot read: `setAttribute(name, …)`,
|
|
563
|
+
`toggleAttribute(name)`, `createAttribute(name)` with `name` computed.
|
|
564
|
+
Write the name down. A `for (const key of ['title', 'alt'])` or
|
|
565
|
+
`['title', 'alt'].forEach((key) => …)` over written names is read, and so
|
|
566
|
+
is a written prefix: `'data-' + key` and `` `aria-${name}` `` are fine,
|
|
567
|
+
`'on' + type` is a handler, `prefix + '-id'` is not readable.
|
|
568
|
+
`createElement(name)` with a computed tag stays a review warning (a
|
|
569
|
+
helper's `el(tag, …)` is the common case).
|
|
570
|
+
- Markup built from text the scan cannot read exactly: `innerHTML =
|
|
571
|
+
atob(…)`, `innerHTML = decodeURIComponent(…)`, `new TextDecoder().decode(…)`,
|
|
572
|
+
a written markup string rewritten by a pattern (`TPL.replace(/{{x}}/g,
|
|
573
|
+
…)`, `.sort`, `.map` over its characters); text from outside the code at
|
|
574
|
+
a markup or URL sink: a response body (`.text()`, `.json()`), storage, a
|
|
575
|
+
cookie, a field's value, a message's data (`addEventListener('message',
|
|
576
|
+
…)` in any spelling, `onmessage`), typed text, `window.name`, the address
|
|
577
|
+
(`location.hash`, `location.search`, `document.URL`, `URLSearchParams`:
|
|
578
|
+
the game can write the address and read it back decoded) — through any
|
|
579
|
+
name, list, `.map`, `.then`, destructuring or property the text passes.
|
|
580
|
+
- Markup or a URL of which nothing is written down: `innerHTML = x`,
|
|
581
|
+
`el.href = url`, `img.src = asset(id)` with nothing the scan can read
|
|
582
|
+
behind `x`. The scan follows a value to where it is written: through the
|
|
583
|
+
names it was bound to, objects and lists you wrote (`ART[sym]`, `P[0] +
|
|
584
|
+
P[1]`), your own functions (what they return, their parameters from where
|
|
585
|
+
you call them, in any file), properties and `dataset` attributes you wrote
|
|
586
|
+
and read back, `Map.set`/`get`, imports (`import * as ART from './art.js'`,
|
|
587
|
+
a JSON module), globals another script set on `window`, and the page's own
|
|
588
|
+
elements (`document.getElementById('f').dataset.src` is the attribute as
|
|
589
|
+
written in the page). What it cannot place anywhere is refused; one
|
|
590
|
+
alternative written beside one it cannot place (`ART[sym] || sym`) is
|
|
591
|
+
named for review. As a URL, the written start fixes the scheme: `'./x/' +
|
|
592
|
+
id` and `location.origin + path` are fine, `base + '/a.png'` with `base`
|
|
593
|
+
unknown, `'java' + x`, `'data:' + x` are refused; an `Image` or an
|
|
594
|
+
`<img>` you create loads a resource and may take any address. A computed
|
|
595
|
+
part inside written markup (`` `<div class="${c}">${text}</div>` ``,
|
|
596
|
+
`'<b>' + name + '</b>'`) stands in as a word; where a tag or an attribute
|
|
597
|
+
name would go, or as an unquoted attribute value (`` `<${tag}>` ``, ``
|
|
598
|
+
`<img ${attrs}>` ``, `` `<img src=${u}>` ``), it is named for review —
|
|
599
|
+
quote values, write tags and attributes down.
|
|
600
|
+
- A constant spelled across pieces anywhere in the code (`'java' +
|
|
601
|
+
'script:x'`, `['<img o', 'nerror=x>'].join('')`, `atob('PGltZy…')` in a
|
|
602
|
+
helper) is judged where it stands, as a literal is.
|
|
603
|
+
- A method that sets attributes or builds markup or elements kept in a name,
|
|
604
|
+
bound or passed on instead of called: `const set = el.setAttribute`,
|
|
605
|
+
`el.setAttribute.bind(el)`, `document.createElement.bind(document)`,
|
|
606
|
+
`fn(el.insertAdjacentHTML)`, and its property descriptor taken
|
|
607
|
+
(`Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML')` kept, or
|
|
608
|
+
of a name not written down). Call it where you use it (`.call`, `.apply`,
|
|
609
|
+
`Reflect.apply`, `Function.prototype.call.call` and `desc.set.call(el, v)`
|
|
610
|
+
/ `desc.value.call(el, …)` are read through to the call; `if
|
|
611
|
+
(el.setAttribute)` and `typeof el.setAttribute` are checks, not references).
|
|
612
|
+
`a.protocol = 'javascript'` turns a link into a `javascript:` URL and is
|
|
613
|
+
refused like one; `navigator[k]`, `location[k]`, `screen[k]`, `crypto[k]`,
|
|
614
|
+
`performance[k]`, `Reflect.get(navigator, k)` with `k` computed are refused
|
|
615
|
+
(write the name, or a list of names a loop runs over).
|
|
616
|
+
- A `Blob`, `File` or `Response` of JavaScript or of a document whose text
|
|
617
|
+
is not written down (`new Blob([code], { type: 'text/javascript' })` with
|
|
618
|
+
`code` computed, `new Response(t, { headers: { 'content-type': 'text/html'
|
|
619
|
+
} })`, headers not written down): put a worker in a `.js` file. One written
|
|
620
|
+
down is read as code (its rules apply), as a page (`text/html`) or as XML
|
|
621
|
+
(`application/xhtml+xml`, `image/svg+xml`, any `+xml` type: prefixes
|
|
622
|
+
dropped, a DTD subset refused, as a framed document is).
|
|
623
|
+
- A `data:` URL that is a page (`data:text/html,…`), an SVG `data:` URL that
|
|
624
|
+
holds a script or a handler, a `data:` script, or a `data:` body the scan
|
|
625
|
+
cannot decode. Inline SVG icons as `data:image/svg+xml` are fine.
|
|
626
|
+
- A handler attribute, a `<script>`, a `javascript:` URL or a `data:` page in
|
|
627
|
+
a `.json` or `.txt` file's text, or in a page's data block (`<script
|
|
628
|
+
type="application/json">`, `<xmp>`, `<textarea>`): what the game fetches
|
|
629
|
+
or reads back may reach the page as markup, so data files and data blocks
|
|
630
|
+
are read as the code's strings are (`<b>` in a translation is fine). A
|
|
631
|
+
`.json` that does not parse, or a data file of more than a million values,
|
|
632
|
+
is refused: the scan reads nothing it cannot read to the end.
|
|
633
|
+
- `import()` of a URL (`https:`, `data:`, `blob:`) or of a computed address
|
|
634
|
+
(`import('./views/' + name + '.js')` — import each path literally); a `<script
|
|
635
|
+
type="importmap">`; a `<script src="data:…">`; an SVG or XML document
|
|
636
|
+
(`.svg`, `.xml`, `.xhtml`, `.xsl`) with a `<script>` (under any namespace
|
|
637
|
+
prefix: `<svg:script>` is one) or a handler attribute — a frame runs them as
|
|
638
|
+
a page. The scan reads such a document as the browser renders it, prefixes
|
|
639
|
+
dropped (`svg:onload` is `onload`), and refuses outright what it would have
|
|
640
|
+
to emulate the parser to read: an internal DTD subset (`<!DOCTYPE svg [ …
|
|
641
|
+
]>`), any `<!ENTITY>` or `<!ATTLIST>` declaration (an entity becomes markup
|
|
642
|
+
where it is referenced, an attribute default — an `onload` among them — is
|
|
643
|
+
added to every such element), and an `<?xml-stylesheet?>`. Export art as
|
|
644
|
+
plain SVG without a DTD (Illustrator: *Export As → SVG*, not the SVG 1.1
|
|
645
|
+
DTD profile; an external `<!DOCTYPE svg PUBLIC …>` with no `[ … ]` is
|
|
646
|
+
fine, browsers do not fetch it).
|
|
647
|
+
- `WebSocket`, WebRTC (`RTCPeerConnection`), `navigator.serviceWorker`,
|
|
648
|
+
`fetch('/api/…')` of a server of your own (saved data goes through
|
|
649
|
+
`engine.state`), the string `/csp-report`, any URL outside the bundle.
|
|
650
|
+
- Own CSP, `<base>`, meta refresh, `<object>`/`<embed>`, `srcdoc`, a copy of
|
|
651
|
+
`engine-sdk/`.
|
|
652
|
+
- No bet of its own: a play with no click of the player's behind it is
|
|
653
|
+
refused, and an autoplay loop that is not a shell session runs only for the
|
|
654
|
+
click's few seconds, then is refused (§3.1).
|
|
655
|
+
- WebAssembly reached by your own code, and any WebAssembly module whose
|
|
656
|
+
bytes are not a known library release (§3.6).
|
|
657
|
+
- **Minified or obfuscated code without a source map.** The measure: a file
|
|
658
|
+
with twenty names or more, nine in ten of them one or two letters, is
|
|
659
|
+
minified (every minifier's output is at 98–100%; the densest hand-written
|
|
660
|
+
game we have is at 72%). Names of the form `_0x1a2b` or ASCII text written
|
|
661
|
+
in `\x`/`\u` escapes are an obfuscator's and refused outright. Ship readable
|
|
662
|
+
code, or ship the build with its source map in the bundle — the `.map`
|
|
663
|
+
beside the file or named by its `//# sourceMappingURL=` comment (an inline
|
|
664
|
+
`data:` map works too), version 3, with `sourcesContent` (Vite: `build:
|
|
665
|
+
{ sourcemap: true }`; esbuild: `--sourcemap`). A minified inline script
|
|
666
|
+
has nowhere for a map: put built code in a `.js` file. The reviewer reads
|
|
667
|
+
the sources; the play-tests play the real bytes.
|
|
668
|
+
- A file that does not parse as JavaScript (as a module or as a classic
|
|
669
|
+
script): a browser would refuse it too.
|
|
670
|
+
|
|
671
|
+
**Named for the reviewer** (the upload passes; each is listed with file and
|
|
672
|
+
line, so the reviewer knows where to look — keep them to what the game
|
|
673
|
+
needs): reading the date or time (`Date`, `Temporal`,
|
|
674
|
+
`performance.timeOrigin`); the time zone or locale formatting
|
|
675
|
+
(`getTimezoneOffset`, `Intl.DateTimeFormat`, `toLocale…`); the language
|
|
676
|
+
outside the SDK (`navigator.language`, `Intl` — `engine.config.locale` is the
|
|
677
|
+
player's language); the browser's identity (`userAgent`, `platform`,
|
|
678
|
+
`hardwareConcurrency`, `deviceMemory`, `screen.colorDepth`,
|
|
679
|
+
`navigator.webdriver`); a timer past 60 s or with a computed delay; state
|
|
680
|
+
kept on the device (`localStorage`, `sessionStorage`, `indexedDB`, cookies —
|
|
681
|
+
per device only, and anything a player keeps across devices goes through
|
|
682
|
+
`engine.state`); a text field or form (`<input>`, `<textarea>`, `<form>`,
|
|
683
|
+
`createElement('input')`: a game takes nothing typed but the stake) and
|
|
684
|
+
words like "password" or "sign in" in its text; browser randomness
|
|
685
|
+
(`Math.random`, `crypto.getRandomValues` — for visuals only, every outcome
|
|
686
|
+
is the server's); a `fetch` of a computed address, `XMLHttpRequest`,
|
|
687
|
+
`sendBeacon`; a Worker on a built URL or `URL.createObjectURL`; reaching the
|
|
688
|
+
parent or top frame; `location` changes and `window.open`.
|
|
689
|
+
|
|
690
|
+
## 4. Currencies — never compute money
|
|
691
|
+
|
|
692
|
+
The player plays in any Housebets coin and may see it as a fiat (EUR, JPY,
|
|
693
|
+
…) at the platform's rate. `engine.money` is the Originals' own money code
|
|
694
|
+
bound to the session:
|
|
695
|
+
|
|
696
|
+
```js
|
|
697
|
+
const m = engine.money;
|
|
698
|
+
m.unit, m.sign, m.decimals // 'EUR', '€', 2 — or 'BTC', '', 8
|
|
699
|
+
m.format(amount) // a wallet amount, as the header shows it
|
|
700
|
+
m.caption(amount) // '0.00019230 BTC' under a fiat field
|
|
701
|
+
m.parse(text) // what the player typed → wallet amount, truncated down
|
|
702
|
+
m.mark // our picture of the currency (data: URI)
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
Stakes go to `engine.play` in **wallet units** at the wallet's precision;
|
|
706
|
+
`<hb-bet-amount>` does all of it (mark, sign, precision, ½ 2× max, limits,
|
|
707
|
+
remembered stake):
|
|
708
|
+
|
|
709
|
+
```html
|
|
710
|
+
<hb-bet-amount id="stake" max-button></hb-bet-amount>
|
|
711
|
+
<script type="module">
|
|
712
|
+
const field = document.getElementById('stake').connect(engine);
|
|
713
|
+
field.bet = { mode: 'coin', bet: 'heads' }; // the bet the stake is for: its own ceiling (§4.1)
|
|
714
|
+
field.amount // wallet units for engine.play
|
|
715
|
+
field.reason // {code, message} when the stake cannot be played, else null
|
|
716
|
+
</script>
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
Balances and payouts come from the server; show them with `m.format`.
|
|
720
|
+
|
|
721
|
+
**Bet levels, as a slot shows them.** A game with its own bet buttons (a
|
|
722
|
+
bet up/down, a bet menu) offers `m.levels(limits.min, limits.max)`: round
|
|
723
|
+
stakes in the player's display unit — €0.10, €0.20, €0.50, €1 … or 0.1, 1,
|
|
724
|
+
10 USDT — each with the wallet `amount` to send, the header's `text` and a
|
|
725
|
+
`label` for the button (`€2.00`, `2 USDT`). Never show a raw wallet amount
|
|
726
|
+
or a limit converted from dollars ("2,000.62019200 USDT"): the limits are
|
|
727
|
+
set in dollars and converted to the coin at the live rate, so only the
|
|
728
|
+
levels are round. Decimals never change the maths: a payout is the stake ×
|
|
729
|
+
the multiplier, cut to the wallet's precision (10⁻⁸ for a coin).
|
|
730
|
+
Bet up / down moves one level — never by a fixed amount such as the
|
|
731
|
+
minimum bet or `m.step` (the wallet's smallest unit, 0.00000001): a button
|
|
732
|
+
that adds the minimum takes a thousand clicks to go from 0.10 to 100. A
|
|
733
|
+
stake is never below `engine.config.limits.min` and never 0: there is no
|
|
734
|
+
free spin (§3.3), so a play button stays disabled until the field holds a
|
|
735
|
+
playable stake (`field.reason === null` and `field.amount > 0`; an empty
|
|
736
|
+
field's `reason` is `min`, and `engine.play` itself refuses a stake of 0
|
|
737
|
+
with `limits` before anything is sent). The engine panel (`ui.js`) does
|
|
738
|
+
exactly this.
|
|
739
|
+
|
|
740
|
+
### 4.1 Maximum win per round
|
|
741
|
+
|
|
742
|
+
No single round of any engine game pays more than the platform's **maximum
|
|
743
|
+
win per round** (1,000,000 USD unless Housebets sets another figure; the
|
|
744
|
+
same for every game), or the player's coin's own maximum win when that is
|
|
745
|
+
less. The cap is enforced **before the bet**, never by cutting a win: a win
|
|
746
|
+
reduced after the draw would change your RTP and break the round's
|
|
747
|
+
provably-fair receipt. So the server refuses a stake whose largest possible
|
|
748
|
+
win — the stake times the bet's top multiplier (`maxMultBp` of a table bet,
|
|
749
|
+
the last `ladderBp` of a ladder) — would pass the cap, with a `limits`
|
|
750
|
+
refusal that names the most the bet takes.
|
|
751
|
+
|
|
752
|
+
What this means for your game: **a high-multiplier bet has a low maximum
|
|
753
|
+
stake.** A bet that can pay 5,000× takes at most 1,000,000 / 5,000 = 200
|
|
754
|
+
USD a round; one that can pay 1,000,000× takes at most a dollar. Your Maths
|
|
755
|
+
card in House Studio (the game's page, once the maths are verified) shows
|
|
756
|
+
the figure per bet (`Max stake`) and marks one under a dollar `under $1`.
|
|
757
|
+
Choose your multipliers with that in mind: a 2× coin flip is held by the
|
|
758
|
+
coin's max bet alone.
|
|
759
|
+
|
|
760
|
+
The session says it per bet, in the wallet coin at its precision, rounded
|
|
761
|
+
down:
|
|
762
|
+
|
|
763
|
+
```js
|
|
764
|
+
engine.config.limits.maxWin // the most one round pays, in the wallet coin
|
|
765
|
+
engine.config.limits.maxStake.coin.heads // "12.50": the most this bet takes
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
Nothing to compute: tell the field which bet the stake is for
|
|
769
|
+
(`field.bet = { mode, bet }`; the engine panel does it on every
|
|
770
|
+
selection) and it holds the stake to that bet's figure, turns red and says
|
|
771
|
+
the most the bet takes when the player types more (`field.reason.code ===
|
|
772
|
+
'maxWin'`; of the coin's max bet and the bet's own figure, the lower is the
|
|
773
|
+
one named), and its max chip stops there. `engine.play` refuses a stake over
|
|
774
|
+
the figure before anything is sent, with `limits` and the same message; a
|
|
775
|
+
`limits` refusal from the server over the cap carries the figure too
|
|
776
|
+
(`err.maxStake`, a decimal string in the wallet coin), so you can set the
|
|
777
|
+
field to it (`field.value = err.maxStake`).
|
|
778
|
+
|
|
779
|
+
A session that names `"0"` for a bet (and `maxWin` 0) means nothing can be
|
|
780
|
+
staked on it until the coin's USD rate is back: the field says so (the
|
|
781
|
+
Originals' "Something went wrong" line) and hides its max chip, a play
|
|
782
|
+
button on `field.reason` stays disabled, and `engine.play` refuses with
|
|
783
|
+
`unavailable`, as the server would. A session from an older server names no
|
|
784
|
+
`maxStake` at all: then the coin's max bet alone holds in the SDK, and the
|
|
785
|
+
server is the last word anyway.
|
|
786
|
+
|
|
787
|
+
## 5. Languages
|
|
788
|
+
|
|
789
|
+
`engine.config.locale` is the player's language (the site's). The panel,
|
|
790
|
+
the field and every refusal are already in it (13 languages). Your own
|
|
791
|
+
strings: ship them as `bundle/i18n.json` — `{ "en": {…}, "ru": {…} }` —
|
|
792
|
+
and pick by `engine.config.locale`, falling back to `en` (required). The
|
|
793
|
+
engine panel names modes and bets from your math's labels; give it names
|
|
794
|
+
by language with `mount(el, { labels: { de: { base: 'Drehen' } } })`
|
|
795
|
+
(the lines and keno templates do). The
|
|
796
|
+
upload tells you which of the site's languages (`ar da de en es fi fr hi hu
|
|
797
|
+
id it ja ko pl pt ru th tr vi zh`) you lack. Numbers and money: always
|
|
798
|
+
through `engine.money`.
|
|
799
|
+
|
|
800
|
+
## 6. What the upload checks — the whole list
|
|
801
|
+
|
|
802
|
+
`hbengine upload` = `verify` + `lint` + the portal's conformance run (about
|
|
803
|
+
two minutes on real desktop and phone frames; `hbengine status` shows the
|
|
804
|
+
state meanwhile). A version is `sandbox` only when every line is `ok`:
|
|
805
|
+
|
|
806
|
+
| check | what passes |
|
|
807
|
+
|---|---|
|
|
808
|
+
| math | the pack verifies (§2): format, RTP band, every bet pays |
|
|
809
|
+
| budgets | §3 sizes; `assets.json` well formed, `boot` present, every file in one group |
|
|
810
|
+
| code | every script parsed: nothing of §3.7's refused list, in any spelling (libraries under `vendor/` we know: named, §3.5); what is named for review is listed with file and line |
|
|
811
|
+
| readable | no minified or obfuscated code without its source map; WebAssembly only as a known library's released module, run by the library (§3.6, §3.7) |
|
|
812
|
+
| engine use | the code connects, calls `play`, calls `revealed` |
|
|
813
|
+
| boot (desktop, phone) | `hello` answered and `ready()` within 25 s |
|
|
814
|
+
| idle | left untouched for 15 s, the game places no bet: a bet is always the player's act (and the shell refuses one with no click behind it, §3.1) |
|
|
815
|
+
| bet | one bet through your UI (`hbtest.json`), pressed with a real click, reaches `engine.play`, then `revealed` |
|
|
816
|
+
| shows | with `shows` in `hbtest.json`: the element shows the value of the round the server drew |
|
|
817
|
+
| draws | after the round, the play area shows something: its visible content covers at least 2% of the frame (a board with no size fails) |
|
|
818
|
+
| picture | the screenshot of the frame is not one flat colour (a warning for the reviewer for now): a canvas that drew nothing, or WebGL that could not start, shows a black frame. The run draws WebGL on the CPU (SwiftShader), so a WebGL game must start without a GPU |
|
|
819
|
+
| currency | under a fiat display (desktop) and a bare coin (phone): the stake sent equals what the Originals send for the typed amount; the screen shows that currency |
|
|
820
|
+
| outage | the next bet meets an ambiguous wallet answer: the game keeps running, the site is asked to re-read the wallet |
|
|
821
|
+
| fit | the game fits the frame it is given — 1194×720 and 364×720, then fullscreen 1920×1080 and 390×844: no page scrolls, every visible control is inside the frame, no text (or more than a tenth of a picture) is cut by its edge |
|
|
822
|
+
| errors | no uncaught error in any page |
|
|
823
|
+
| network | no request leaves the bundle's origin, and the policy refuses nothing (a blocked worker, script or style fails the run) |
|
|
824
|
+
|
|
825
|
+
Then a reviewer plays it (`sandbox → review → live`), with a kill switch at
|
|
826
|
+
every level. Money, seeds, limits and refunds are the platform's and the
|
|
827
|
+
same as for every House Original: you never handle them.
|
|
828
|
+
|
|
829
|
+
## 7. Examples
|
|
830
|
+
|
|
831
|
+
`engine-examples/`: **coin** (table, engine panel, 30 lines), **dice**
|
|
832
|
+
(97 bets, the zone picks), **slot** (110,592-row table, reels from
|
|
833
|
+
`round.data`), **lines** (a video slot from the maths kit: 5×3, ten lines,
|
|
834
|
+
wild, scatter, free spins ×2 with retriggers, 96.00% exactly), **tumble**
|
|
835
|
+
(a 6 × 5 scatter-pays grid with tumbles and free-spin multipliers, 96.00%
|
|
836
|
+
exactly), **keno** (40 numbers, 1–10 picks, hypergeometric weights; the
|
|
837
|
+
picks choose the mode), **crash** (a 64-level ladder with cash-out that
|
|
838
|
+
the page climbs itself with `game.advance()`; 99% at every level), **mines** (a ladder of reveals, 1–24 mines, exact
|
|
839
|
+
weights per reveal), **plinko**
|
|
840
|
+
(8/12/16 rows × three risks, binomial weights), **tower** (8-step ladder
|
|
841
|
+
with cash-out), **wheel** (three modes). BAD CAT (`badcat-engine/`) is a full custom frontend ported as a
|
|
842
|
+
studio would.
|
|
843
|
+
|
|
844
|
+
## 8. Review
|
|
845
|
+
|
|
846
|
+
Every version a player can see is reviewed by a person at Housebets first. Review works like a pull request.
|
|
847
|
+
|
|
848
|
+
1. Run `npx hbengine preflight` (or `engine_preflight`) first: on the newest version that passed every check (the one submit sends), it checks what a machine can of the review checklist — the RTP and max win your rules quote against the verified maths, rules in every language, the store page, phone and desktop fit, errors, load and network — and prints the facts the rules must quote. A pre-flight fail is pre-ticked on the reviewer's checklist; a pass is a note to them, not a tick, since each item asks more than a machine sees. Then `npx hbengine submit` (or `engine_submit`, or the Review tab) sends the newest version that passed every check.
|
|
849
|
+
2. The reviewer plays it on desktop and phone and goes through the **review checklist** (the game's Review tab, and `checklist` in `engine_review`): the maths against the rules and the screen, the rules in every language, phone and desktop play, sound, recovery from errors, content you own, nothing aimed at children, no outcome logic or outside servers in the bundle, an honest store page. Go through it yourself before you submit.
|
|
850
|
+
3. Anything to change is a **thread** on the version: a **must fix** blocks going live, a **suggestion** does not. Each thread names its area and says what, where and why.
|
|
851
|
+
4. Fix every must-fix thread in the project, upload the new version and submit it. Submitting marks the open must-fix threads as fixed in that version (on the Review tab you can untick any it does not fix). To ask the reviewer something instead, reply on the thread (`npx hbengine reply --thread 12 --message "…"`, `engine_reply`, or the Review tab), in at most 4,000 characters.
|
|
852
|
+
5. The reviewer checks each fix and resolves the thread, or reopens it with a reply.
|
|
853
|
+
6. With no must-fix left, the version is approved: the studio's owners and its uploaders get an email. An owner puts it live: **Make live** on the Versions tab, `npx hbengine live` in the project, or `engine_live` (with a key an owner made). The round server and the front end switch within seconds, and everyone gets an email that it is live. The first time, players find the game on Housebets once Housebets switches its catalogue page on; we email you when it is. Players keep the live version until then.
|
|
854
|
+
|
|
855
|
+
With an agent: "fix the review" is `engine_review` → change the project → `engine_upload` → `engine_preflight` → `engine_submit`; "put it live" is `engine_status` (is it approved?) → `engine_live`.
|