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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The maths kit: a game's outcomes as weighted rows, its RTP exactly, and the
|
|
3
|
+
* pack the engine verifies (SPEC §1). A row is { weight, data, pay } where
|
|
4
|
+
* pay is { betId: multBp } (10000 = 1×, the total stake back) and data is
|
|
5
|
+
* what the bundle receives to show the round.
|
|
6
|
+
*
|
|
7
|
+
* import { writePack, fit } from './pack.mjs';
|
|
8
|
+
* const rows = fit(simulated, 9600); // exact 96.00% (balance() when no row pays nothing)
|
|
9
|
+
* writePack('math', { game: 'my-slot', rtpBp: 9600, modes: [{ id: 'base', rows, bets: ['spin'] }] });
|
|
10
|
+
*
|
|
11
|
+
* Every number here is an integer: weights are BigInt, RTP is a fraction.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const MAX_SUM = 1n << 63n;
|
|
17
|
+
|
|
18
|
+
// A small seeded generator for building tables (never for play: the engine draws).
|
|
19
|
+
export function rng(seed) {
|
|
20
|
+
let a = seed >>> 0;
|
|
21
|
+
const next = () => {
|
|
22
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
23
|
+
let t = a;
|
|
24
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
25
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
26
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
27
|
+
};
|
|
28
|
+
next.int = (n) => Math.floor(next() * n);
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function binomial(n, k) {
|
|
33
|
+
let r = 1n;
|
|
34
|
+
for (let i = 1n; i <= BigInt(k); i++) r = (r * BigInt(n - k) + r * i) / i;
|
|
35
|
+
return r;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const gcd = (a, b) => { while (b) [a, b] = [b, a % b]; return a < 0n ? -a : a; };
|
|
39
|
+
|
|
40
|
+
/** RTP of one bet over rows, exactly: { num, den } in basis points, and a 4-decimal string. */
|
|
41
|
+
export function rtp(rows, bet) {
|
|
42
|
+
let num = 0n, den = 0n;
|
|
43
|
+
for (const r of rows) { den += r.weight; num += r.weight * BigInt(r.pay[bet] || 0); }
|
|
44
|
+
return { num, den, bp: bp4(num, den) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const bp4 = (num, den) => {
|
|
48
|
+
const x = (num * 10000n) / den; // bp × 10^4, truncated as the verifier reports it
|
|
49
|
+
return `${x / 10000n}.${String(x % 10000n).padStart(4, '0')}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Weights so that one bet's RTP is exactly targetBp: the paying rows keep
|
|
54
|
+
* their proportions (all multiplied by one integer), the rows that pay
|
|
55
|
+
* nothing share what is left in their own proportions. Needs rows that pay
|
|
56
|
+
* nothing, and paying rows that alone return more than the target.
|
|
57
|
+
*/
|
|
58
|
+
export function fit(rows, targetBp, bet = Object.keys(rows[0].pay)[0]) {
|
|
59
|
+
const T = BigInt(targetBp);
|
|
60
|
+
const wins = rows.filter((r) => r.pay[bet] > 0), zeros = rows.filter((r) => !(r.pay[bet] > 0));
|
|
61
|
+
if (!zeros.length) throw new Error('fit: no row pays nothing');
|
|
62
|
+
const S = wins.reduce((s, r) => s + r.weight * (BigInt(r.pay[bet]) - T), 0n); // what the wins return above T
|
|
63
|
+
if (S <= 0n) throw new Error(`fit: the paying rows alone return less than ${targetBp / 100}%`);
|
|
64
|
+
const k = T / gcd(S, T); // so that k·S is a multiple of T
|
|
65
|
+
const Z = (k * S) / T; // the weight the zero rows share
|
|
66
|
+
const zw = zeros.reduce((s, r) => s + r.weight, 0n);
|
|
67
|
+
let given = 0n;
|
|
68
|
+
const out = rows.map((r) => {
|
|
69
|
+
if (r.pay[bet] > 0) return { ...r, weight: r.weight * k };
|
|
70
|
+
const w = (Z * r.weight) / zw;
|
|
71
|
+
given += w;
|
|
72
|
+
return { ...r, weight: w };
|
|
73
|
+
});
|
|
74
|
+
// The rounding remainder goes to the first zero row; a zero weight is dropped.
|
|
75
|
+
const first = out.find((r) => !(r.pay[bet] > 0));
|
|
76
|
+
first.weight += Z - given;
|
|
77
|
+
const kept = out.filter((r) => r.weight > 0n);
|
|
78
|
+
const check = rtp(kept, bet);
|
|
79
|
+
if (check.num !== T * check.den) throw new Error('fit: not exact (a bug)');
|
|
80
|
+
return kept;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Weights so that one bet's RTP is exactly targetBp when every row pays
|
|
85
|
+
* something (a bonus buy, plinko): rows paying more than the target are
|
|
86
|
+
* all multiplied by one integer, rows paying less by another, so each
|
|
87
|
+
* group keeps its own proportions and the two balance at the target.
|
|
88
|
+
*/
|
|
89
|
+
export function balance(rows, targetBp, bet = Object.keys(rows[0].pay)[0]) {
|
|
90
|
+
const T = BigInt(targetBp);
|
|
91
|
+
const over = rows.reduce((s, r) => s + (BigInt(r.pay[bet] || 0) > T ? r.weight * (BigInt(r.pay[bet]) - T) : 0n), 0n);
|
|
92
|
+
const under = rows.reduce((s, r) => s + (BigInt(r.pay[bet] || 0) < T ? r.weight * (T - BigInt(r.pay[bet] || 0)) : 0n), 0n);
|
|
93
|
+
if (!over || !under) throw new Error('balance: needs rows on both sides of the target');
|
|
94
|
+
const g = gcd(over, under);
|
|
95
|
+
const out = rows.map((r) => ({ ...r, weight: r.weight * (BigInt(r.pay[bet] || 0) > T ? under / g : BigInt(r.pay[bet] || 0) < T ? over / g : 1n) }));
|
|
96
|
+
const check = rtp(out, bet);
|
|
97
|
+
if (check.num !== T * check.den) throw new Error('balance: not exact (a bug)');
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Rows with the same data are one row with the sum of their weights. */
|
|
102
|
+
export function merge(rows) {
|
|
103
|
+
const by = new Map();
|
|
104
|
+
for (const r of rows) {
|
|
105
|
+
const key = JSON.stringify(r.data);
|
|
106
|
+
const x = by.get(key);
|
|
107
|
+
if (x) x.weight += r.weight;
|
|
108
|
+
else by.set(key, { ...r, weight: r.weight });
|
|
109
|
+
}
|
|
110
|
+
return [...by.values()];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Inclusive [lo, hi] ranges of sorted indexes.
|
|
114
|
+
function ranges(ids) {
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const i of ids) {
|
|
117
|
+
const last = out.at(-1);
|
|
118
|
+
if (last && last[1] === i - 1) last[1] = i;
|
|
119
|
+
else out.push([i, i]);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Writes manifest.json and one table per mode. A mode is { id, label?, rows,
|
|
126
|
+
* bets: [id | {id, label}] }; its rows are sorted by what the first bet pays,
|
|
127
|
+
* so each pay tier is one range. Prints every bet's exact RTP, hit rate and
|
|
128
|
+
* largest multiplier; returns them.
|
|
129
|
+
*/
|
|
130
|
+
export function writePack(dir, { game, rtpBp, modes }) {
|
|
131
|
+
mkdirSync(dir, { recursive: true });
|
|
132
|
+
const manifest = { format: 'house-engine-math/1', game, rtpBp, modes: [] };
|
|
133
|
+
const report = [];
|
|
134
|
+
for (const mode of modes) {
|
|
135
|
+
const bets = mode.bets.map((b) => (typeof b === 'string' ? { id: b } : b));
|
|
136
|
+
const first = bets[0].id;
|
|
137
|
+
const rows = [...mode.rows].sort((a, b) => (a.pay[first] || 0) - (b.pay[first] || 0));
|
|
138
|
+
if (rows.length < 2 || rows.length > 1_000_000) throw new Error(`${mode.id}: 2 to 1,000,000 rows, not ${rows.length}`);
|
|
139
|
+
const sum = rows.reduce((s, r) => s + r.weight, 0n);
|
|
140
|
+
if (sum > MAX_SUM) throw new Error(`${mode.id}: weights sum to more than 2^63`);
|
|
141
|
+
const lines = rows.map((r) => {
|
|
142
|
+
if (r.weight <= 0n) throw new Error(`${mode.id}: a weight is not positive`);
|
|
143
|
+
const json = r.data === undefined ? '' : JSON.stringify(r.data);
|
|
144
|
+
if (json.length > 4096) throw new Error(`${mode.id}: a row's data is over 4096 bytes`);
|
|
145
|
+
return json ? `${r.weight}\t${json}` : String(r.weight);
|
|
146
|
+
});
|
|
147
|
+
const table = `${game}-${mode.id}.tsv`;
|
|
148
|
+
writeFileSync(join(dir, table), lines.join('\n') + '\n');
|
|
149
|
+
manifest.modes.push({
|
|
150
|
+
id: mode.id, ...(mode.label ? { label: mode.label } : {}), kind: 'table', table,
|
|
151
|
+
bets: bets.map((b) => {
|
|
152
|
+
const tiers = new Map();
|
|
153
|
+
rows.forEach((r, i) => { const m = r.pay[b.id] || 0; if (m > 0) (tiers.get(m) || tiers.set(m, []).get(m)).push(i); });
|
|
154
|
+
const { num, den, bp } = rtp(rows, b.id);
|
|
155
|
+
const hit = rows.reduce((s, r) => s + (r.pay[b.id] > 0 ? r.weight : 0n), 0n);
|
|
156
|
+
const max = Math.max(...tiers.keys());
|
|
157
|
+
report.push({ mode: mode.id, bet: b.id, rtp: bp, hit: bp4(hit * 10000n, den), max: max / 10000, rows: rows.length, exact: num % den === 0n });
|
|
158
|
+
return { id: b.id, ...(b.label ? { label: b.label } : {}), tiers: [...tiers].map(([multBp, ids]) => ({ outcomes: ranges(ids), multBp })) };
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
writeFileSync(join(dir, 'manifest.json'), JSON.stringify(manifest) + '\n');
|
|
163
|
+
for (const r of report) console.log(`${r.mode}/${r.bet}: RTP ${r.rtp} bp hit ${r.hit} bp max ${r.max}× rows ${r.rows}`);
|
|
164
|
+
return report;
|
|
165
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@housegames/engine-math",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "The maths kit: outcomes as weighted rows, exact RTP, an exact fit to a target, the pack the engine verifies; a video slot model (reels, lines, wilds, scatters, free spins) and a tumble model (scatter pays, tumbles, multipliers).",
|
|
7
|
+
"exports": { "./pack.mjs": "./pack.mjs", "./slot.mjs": "./slot.mjs", "./tumble.mjs": "./tumble.mjs" }
|
|
8
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A video slot for the maths kit: reels, paylines, a wild, a scatter that
|
|
3
|
+
* starts free spins (with a multiplier, retriggers), and a win cap. It plays
|
|
4
|
+
* rounds with a seeded generator to make the table's rows ("books"): every
|
|
5
|
+
* row is a whole round, base spin and free spins, with what it pays. fit()
|
|
6
|
+
* then makes the table's RTP exact; the table is the game.
|
|
7
|
+
*
|
|
8
|
+
* Pays are in basis points of the total stake (10000 = 1×) for 3, 4, 5 of a
|
|
9
|
+
* kind on a line, left to right; scatters pay anywhere, times the stake.
|
|
10
|
+
*
|
|
11
|
+
* A row's data, for the bundle: { b: spin, f: [spin…], x: free multiplier }
|
|
12
|
+
* where a spin is [stop0…stopN, winBp, lineMask, scatterCount].
|
|
13
|
+
*/
|
|
14
|
+
export function slot(cfg) {
|
|
15
|
+
const { reels, freeReels = reels, rows, lines, pays, wild, scatter, maxWinBp } = cfg;
|
|
16
|
+
const n = reels.length;
|
|
17
|
+
|
|
18
|
+
const grid = (strips, stops) => stops.map((s, r) => Array.from({ length: rows }, (_, y) => strips[r][(s + y) % strips[r].length]));
|
|
19
|
+
|
|
20
|
+
// One spin's wins: each line pays its best run from the left (wilds stand in).
|
|
21
|
+
function evaluate(strips, stops) {
|
|
22
|
+
const g = grid(strips, stops);
|
|
23
|
+
let win = 0, mask = 0;
|
|
24
|
+
lines.forEach((line, li) => {
|
|
25
|
+
const cells = line.map((y, r) => g[r][y]);
|
|
26
|
+
const sym = cells.find((c) => c !== wild) ?? wild;
|
|
27
|
+
if (sym === scatter.symbol) return;
|
|
28
|
+
let count = 0;
|
|
29
|
+
while (count < n && (cells[count] === sym || cells[count] === wild)) count++;
|
|
30
|
+
const pay = (pays[sym] || {})[count] || 0;
|
|
31
|
+
// A run of wilds alone pays as wilds when that is more.
|
|
32
|
+
let wc = 0;
|
|
33
|
+
while (wc < n && cells[wc] === wild) wc++;
|
|
34
|
+
const best = Math.max(pay, (pays[wild] || {})[wc] || 0);
|
|
35
|
+
if (best > 0) { win += best; mask |= 1 << li; }
|
|
36
|
+
});
|
|
37
|
+
const scatters = g.flat().filter((c) => c === scatter.symbol).length;
|
|
38
|
+
win += (scatter.pays[scatters] || 0);
|
|
39
|
+
return { win, mask, scatters };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const spin = (strips, next) => strips.map((s) => next.int(s.length));
|
|
43
|
+
|
|
44
|
+
/** One whole round: the row's data and what it pays (bp of the stake).
|
|
45
|
+
* A bought round is the free spins alone, as three scatters start them. */
|
|
46
|
+
function round(next, buy = false) {
|
|
47
|
+
const stops = buy ? null : spin(reels, next);
|
|
48
|
+
const base = buy ? { win: 0, scatters: 3 } : evaluate(reels, stops);
|
|
49
|
+
const data = buy ? { buy: 1 } : { b: [...stops, base.win, base.mask, base.scatters] };
|
|
50
|
+
let total = base.win;
|
|
51
|
+
let left = scatter.spins[base.scatters] || 0;
|
|
52
|
+
if (left) {
|
|
53
|
+
data.f = [];
|
|
54
|
+
data.x = scatter.multiplier;
|
|
55
|
+
while (left > 0 && data.f.length < scatter.cap) {
|
|
56
|
+
left--;
|
|
57
|
+
const s = spin(freeReels, next);
|
|
58
|
+
const e = evaluate(freeReels, s);
|
|
59
|
+
data.f.push([...s, e.win * scatter.multiplier, e.mask, e.scatters]);
|
|
60
|
+
total += e.win * scatter.multiplier;
|
|
61
|
+
left += scatter.retrigger[e.scatters] || 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (total > maxWinBp) { total = maxWinBp; data.cap = 1; }
|
|
65
|
+
return { data, pay: total };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { evaluate, round, grid };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Plays `count` rounds (bought ones with `buy`) and keeps them as rows: every paying round, and up to
|
|
73
|
+
* `losing` different losing rounds that share the weight of every losing
|
|
74
|
+
* round played (so the rows keep the played RTP and hit rate before fit).
|
|
75
|
+
*/
|
|
76
|
+
export function books(game, next, count, losing = 4000, buy = false) {
|
|
77
|
+
const wins = new Map(), lose = new Map();
|
|
78
|
+
let lost = 0n;
|
|
79
|
+
for (let i = 0; i < count; i++) {
|
|
80
|
+
const { data, pay } = game.round(next, buy);
|
|
81
|
+
if (pay <= 0) {
|
|
82
|
+
lost++;
|
|
83
|
+
if (lose.size < losing) lose.set(JSON.stringify(data), { weight: 0n, data, pay: { spin: 0 } });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const key = JSON.stringify(data);
|
|
87
|
+
const r = wins.get(key);
|
|
88
|
+
if (r) r.weight += 1n;
|
|
89
|
+
else wins.set(key, { weight: 1n, data, pay: { spin: pay } });
|
|
90
|
+
}
|
|
91
|
+
const zeros = [...lose.values()];
|
|
92
|
+
zeros.forEach((r, i) => { r.weight = lost / BigInt(zeros.length) + (BigInt(i) < lost % BigInt(zeros.length) ? 1n : 0n); });
|
|
93
|
+
return [...zeros.filter((r) => r.weight > 0n), ...wins.values()];
|
|
94
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tumbling, scatter-pays slot for the maths kit: a cols × rows grid, every
|
|
3
|
+
* cell drawn by symbol weight; a symbol pays when enough of it is anywhere
|
|
4
|
+
* on the grid; winners vanish, the rest fall, new symbols drop in, until
|
|
5
|
+
* nothing pays. Scatters start free spins, where multiplier symbols land
|
|
6
|
+
* and multiply the spin's win. Like slot.mjs, it plays whole rounds into
|
|
7
|
+
* rows; fit() then makes the RTP exact.
|
|
8
|
+
*
|
|
9
|
+
* Symbols are single characters. Pays are basis points of the stake by
|
|
10
|
+
* count: { A: [[8, 2000], [10, 5000], [12, 10000]] } pays the highest band
|
|
11
|
+
* the count reaches.
|
|
12
|
+
*
|
|
13
|
+
* A row's data, for the page:
|
|
14
|
+
* { b: spin, f: [spin…] } spin = [grid, [tumble…], multipliers?]
|
|
15
|
+
* grid the first grid, column by column, top to bottom ("ABCDE…")
|
|
16
|
+
* tumble [winners, winBp, fill0, fill1, …]: the symbols that paid, what
|
|
17
|
+
* they paid, and per column the new symbols that drop in (top first)
|
|
18
|
+
* multipliers free spins only: the values of the multiplier symbols on the
|
|
19
|
+
* last grid, summed onto the spin's win when it won
|
|
20
|
+
*/
|
|
21
|
+
export function tumble(cfg) {
|
|
22
|
+
const { cols, rows, weights, freeWeights = weights, pays, scatter, bomb, maxWinBp } = cfg;
|
|
23
|
+
const pick = (w, next) => {
|
|
24
|
+
const total = w.reduce((s, [, n]) => s + n, 0);
|
|
25
|
+
let x = next() * total;
|
|
26
|
+
for (const [s, n] of w) if ((x -= n) < 0) return s;
|
|
27
|
+
return w[w.length - 1][0];
|
|
28
|
+
};
|
|
29
|
+
const table = (o) => Object.entries(o);
|
|
30
|
+
const base = table(weights), free = table(freeWeights);
|
|
31
|
+
const counts = Object.keys(scatter.spins).map(Number), least = Math.min(...counts), most = Math.max(...counts);
|
|
32
|
+
|
|
33
|
+
// One spin: the grid, its tumbles, and (free spins) its multipliers.
|
|
34
|
+
function spin(next, inFree) {
|
|
35
|
+
const w = inFree ? free : base;
|
|
36
|
+
const draw = () => {
|
|
37
|
+
const s = pick(w, next);
|
|
38
|
+
return s === bomb?.symbol && !inFree ? pick(base.filter(([x]) => x !== bomb.symbol), next) : s;
|
|
39
|
+
};
|
|
40
|
+
let grid = Array.from({ length: cols }, () => Array.from({ length: rows }, draw));
|
|
41
|
+
const first = grid.map((c) => c.join('')).join('');
|
|
42
|
+
const tumbles = [];
|
|
43
|
+
let win = 0;
|
|
44
|
+
for (;;) {
|
|
45
|
+
const counts = {};
|
|
46
|
+
for (const c of grid) for (const s of c) counts[s] = (counts[s] || 0) + 1;
|
|
47
|
+
let paid = '', bp = 0;
|
|
48
|
+
for (const [s, bands] of Object.entries(pays)) {
|
|
49
|
+
const band = bands.filter(([n]) => counts[s] >= n).at(-1);
|
|
50
|
+
if (band) { paid += s; bp += band[1]; }
|
|
51
|
+
}
|
|
52
|
+
if (!paid) break;
|
|
53
|
+
win += bp;
|
|
54
|
+
const fills = grid.map((c) => {
|
|
55
|
+
const kept = c.filter((s) => !paid.includes(s));
|
|
56
|
+
const add = Array.from({ length: rows - kept.length }, draw);
|
|
57
|
+
c.splice(0, rows, ...add, ...kept); // new symbols on top
|
|
58
|
+
return add.join('');
|
|
59
|
+
});
|
|
60
|
+
tumbles.push([paid, bp, ...fills]);
|
|
61
|
+
}
|
|
62
|
+
const out = [first, tumbles];
|
|
63
|
+
const scatters = grid.flat().filter((s) => s === scatter.symbol).length; // on the last grid
|
|
64
|
+
let total = win;
|
|
65
|
+
if (inFree && bomb) {
|
|
66
|
+
const bombs = grid.flat().filter((s) => s === bomb.symbol).map(() => pick(bomb.values, next));
|
|
67
|
+
if (bombs.length) out.push(bombs);
|
|
68
|
+
if (win && bombs.length) total = win * bombs.reduce((a, b) => a + b, 0);
|
|
69
|
+
}
|
|
70
|
+
return { out, total, scatters };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One whole round: the row's data and what it pays (bp of the stake). */
|
|
74
|
+
function round(next) {
|
|
75
|
+
const b = spin(next, false);
|
|
76
|
+
const data = { b: b.out };
|
|
77
|
+
let total = b.total + (scatter.pays[Math.min(b.scatters, most)] || 0);
|
|
78
|
+
let left = b.scatters >= least ? scatter.spins[Math.min(b.scatters, most)] : 0;
|
|
79
|
+
if (left) {
|
|
80
|
+
data.f = [];
|
|
81
|
+
while (left > 0 && data.f.length < scatter.cap) {
|
|
82
|
+
left--;
|
|
83
|
+
const f = spin(next, true);
|
|
84
|
+
data.f.push(f.out);
|
|
85
|
+
total += f.total;
|
|
86
|
+
if (f.scatters >= scatter.retrigger.count) left += scatter.retrigger.spins;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (total > maxWinBp) { total = maxWinBp; data.cap = 1; }
|
|
90
|
+
return { data, pay: total };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { round };
|
|
94
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# For agents
|
|
2
|
+
|
|
3
|
+
This is a House Engine game (Housebets). Read the guide first: `engine_guide` (MCP) or `engine-sdk/GUIDE.md`. Then `hbengine verify`, `hbengine dev`, `hbengine upload` (or the `engine_*` tools). The math is `math/`, the frontend `bundle/`; never compute money or outcomes in the frontend.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Lines — a video slot from the maths kit
|
|
2
|
+
|
|
3
|
+
Five reels, three rows, ten lines, a wild, a scatter that pays anywhere and
|
|
4
|
+
starts 10–15 free spins with wins ×2 (three scatters in free spins add five
|
|
5
|
+
more, up to 60), a 5,000× cap. RTP exactly 96.00%, hit rate 31.4%.
|
|
6
|
+
**Bonus buy**: a second mode plays the free spins alone; its price is what
|
|
7
|
+
they return on average (27 spins, the stake is the price), balanced to
|
|
8
|
+
exactly 96.00%.
|
|
9
|
+
|
|
10
|
+
| path | what |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `math/gen.mjs` | the model (reels, lines, pays, free spins), 400,000 rounds played into rows, `fit` to exactly 96%; 50,000 bought rounds `balance`d to 96%; also writes `bundle/model.json` |
|
|
13
|
+
| `math-kit/` | the maths kit (`packages/engine-math`): `pack.mjs` (rows, exact RTP, fit, the pack), `slot.mjs` (reels, lines, wilds, scatters, free spins) |
|
|
14
|
+
| `bundle/game.js` | spins to the drawn stops, draws the paying lines, plays the free spins the row holds |
|
|
15
|
+
|
|
16
|
+
A row is a whole round: `{"b":[5 stops, win, lineMask, scatters], "f":[[…] per free spin], "x":2}`.
|
|
17
|
+
The engine pays the row's tier; the page shows what the row says.
|
|
18
|
+
|
|
19
|
+
node math/gen.mjs my-slot # change the model, play it into a new table
|
|
20
|
+
npx hbengine verify . # the engine's exact RTP, hit rate, max win
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"groups": {"boot": {"files": ["index.html", "game.js", "game.css", "model.json", "i18n.json"]}}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
body { margin: 0; background: #08141a; }
|
|
2
|
+
.slot { container-type: inline-size; display: flex; flex-direction: column; align-items: center; gap: 14px; width: 100%; padding: 16px 12px; box-sizing: border-box; }
|
|
3
|
+
.banner, .win { height: 24px; font: 700 16px/24px 'Red Hat Display', 'Figtree', system-ui, sans-serif; color: #F9F9FB; font-variant-numeric: tabular-nums; }
|
|
4
|
+
.banner { color: #8F96A8; font-size: 14px; }
|
|
5
|
+
.win { color: #FDDC73; }
|
|
6
|
+
.board { --c: min(112px, (100cqi - 64px) / 5); position: relative; padding: 8px; border-radius: 14px; background: #061218; box-shadow: inset 0 0 0 1px #0D2A38; }
|
|
7
|
+
.reels { display: grid; grid-template-columns: repeat(5, var(--c)); gap: 6px; }
|
|
8
|
+
.reel { position: relative; height: calc(var(--c) * 3); overflow: hidden; border-radius: 8px; background: #0b1f28; }
|
|
9
|
+
.strip { position: absolute; inset: 0 0 auto 0; will-change: transform; }
|
|
10
|
+
.cell { height: var(--c); display: grid; place-items: center; }
|
|
11
|
+
.cell svg { width: 62%; height: 62%; }
|
|
12
|
+
.cell b { font: 800 calc(var(--c) * .42)/1 'Red Hat Display', 'Figtree', system-ui, sans-serif; color: #8F96A8; }
|
|
13
|
+
.cell.hit { background: rgba(253, 220, 115, .1); }
|
|
14
|
+
.lines { position: absolute; inset: 8px; width: calc(100% - 16px); height: calc(100% - 16px); pointer-events: none; overflow: visible; }
|
|
15
|
+
.lines polyline { fill: none; stroke: #FDDC73; stroke-width: 3; stroke-linejoin: round; opacity: .85; }
|
|
16
|
+
@media (prefers-reduced-motion: reduce) { .strip { transition: none !important; } }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Lines: five reels, ten lines, free spins. The panel is the engine's
|
|
2
|
+
// (engine-sdk/ui.js); this file spins the reels to the stops the server drew
|
|
3
|
+
// and draws what paid. Every amount comes from the row (math/gen.mjs wrote
|
|
4
|
+
// model.json from the same model that made the rows).
|
|
5
|
+
import { mount } from '/engine-sdk/ui.js';
|
|
6
|
+
|
|
7
|
+
const model = await (await fetch('./model.json')).json();
|
|
8
|
+
// The page's own words in the player's language (bundle/i18n.json, English otherwise).
|
|
9
|
+
const I18N = await (await fetch('./i18n.json')).json();
|
|
10
|
+
let W = I18N.en;
|
|
11
|
+
const t = (k, v = {}) => W[k].replace(/\{(\w+)\}/g, (_, x) => v[x]);
|
|
12
|
+
const reelsEl = document.getElementById('reels'), linesEl = document.getElementById('lines');
|
|
13
|
+
const banner = document.getElementById('banner'), winEl = document.getElementById('win');
|
|
14
|
+
const N = model.reels.length, ROWS = model.rows;
|
|
15
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
const SVG = (inner, fill, stroke) => `<svg viewBox="0 0 40 40"><g fill="${fill}" stroke="${stroke}" stroke-width="2" stroke-linejoin="round">${inner}</g></svg>`;
|
|
17
|
+
// Deep fills, bright strokes (big shapes stay desaturated).
|
|
18
|
+
const ART = {
|
|
19
|
+
seven: SVG('<path d="M9 8h22l-11 25h-6l9-19H9z"/>', '#7e1f3a', '#ff6b81'),
|
|
20
|
+
gem: SVG('<path d="M8 15l6-7h12l6 7-12 17z"/>', '#0d5f6e', '#5cf6f6'),
|
|
21
|
+
star: SVG('<path d="M20 5l4.5 9.5 10.5 1.5-7.6 7.3 1.8 10.4L20 28.8l-9.2 4.9 1.8-10.4L5 16l10.5-1.5z"/>', '#7d5410', '#FDDC73'),
|
|
22
|
+
bell: SVG('<path d="M20 6c6 0 10 5 10 11v7l4 5H6l4-5v-7c0-6 4-11 10-11zM16 31h8a4 4 0 01-8 0z"/>', '#2b2f6b', '#a6adff'),
|
|
23
|
+
W: '<b class="wild">W</b>', S: SVG('<circle cx="20" cy="20" r="13"/><circle cx="20" cy="20" r="6" fill="none"/>', '#1f5a3a', '#7cf2b0'),
|
|
24
|
+
};
|
|
25
|
+
const art = (s) => ART[s] || `<b>${s}</b>`;
|
|
26
|
+
|
|
27
|
+
let c = 96; // the cell size: CSS sets it from the zone's width, read before each spin
|
|
28
|
+
const reels = [];
|
|
29
|
+
for (let r = 0; r < N; r++) {
|
|
30
|
+
const reel = document.createElement('div');
|
|
31
|
+
reel.className = 'reel';
|
|
32
|
+
const strip = document.createElement('div');
|
|
33
|
+
strip.className = 'strip';
|
|
34
|
+
reel.append(strip);
|
|
35
|
+
reelsEl.append(reel);
|
|
36
|
+
reels.push(strip);
|
|
37
|
+
}
|
|
38
|
+
const cellsOf = (strip) => [...strip.children];
|
|
39
|
+
|
|
40
|
+
function show(strips, stops) { // the resting grid
|
|
41
|
+
reels.forEach((strip, r) => {
|
|
42
|
+
strip.style.transition = 'none';
|
|
43
|
+
strip.style.transform = 'translateY(0)';
|
|
44
|
+
strip.innerHTML = Array.from({ length: ROWS }, (_, y) => `<div class="cell">${art(strips[r][(stops[r] + y) % strips[r].length])}</div>`).join('');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Spins every reel to its stops: a strip of passing symbols ending on the result.
|
|
49
|
+
function spin(strips, stops, ms) {
|
|
50
|
+
return Promise.all(reels.map((strip, r) => {
|
|
51
|
+
const s = strips[r], pass = 8 + r * 3, from = cellsOf(strip).slice(0, ROWS).map((e) => e.innerHTML);
|
|
52
|
+
const passing = Array.from({ length: pass }, (_, i) => art(s[(stops[r] + ROWS + i * 7) % s.length]));
|
|
53
|
+
const end = Array.from({ length: ROWS }, (_, y) => art(s[(stops[r] + y) % s.length]));
|
|
54
|
+
strip.innerHTML = [...end, ...passing, ...from].map((h) => `<div class="cell">${h}</div>`).join('');
|
|
55
|
+
strip.style.transition = 'none';
|
|
56
|
+
strip.style.transform = `translateY(-${(pass + ROWS) * c}px)`;
|
|
57
|
+
void strip.offsetWidth;
|
|
58
|
+
strip.style.transition = `transform ${ms + r * 120}ms cubic-bezier(.15, .7, .2, 1)`;
|
|
59
|
+
strip.style.transform = 'translateY(0)';
|
|
60
|
+
return wait(ms + r * 120);
|
|
61
|
+
})).then(() => reels.forEach((strip) => cellsOf(strip).slice(ROWS).forEach((e) => e.remove())));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function paint([mask, scatters], strips, stops) {
|
|
65
|
+
linesEl.innerHTML = '';
|
|
66
|
+
const step = c + 6;
|
|
67
|
+
model.lines.forEach((line, i) => {
|
|
68
|
+
if (!(mask & (1 << i))) return;
|
|
69
|
+
const pts = line.map((y, r) => `${r * step + c / 2},${y * c + c / 2}`).join(' ');
|
|
70
|
+
linesEl.insertAdjacentHTML('beforeend', `<polyline points="${pts}"/>`);
|
|
71
|
+
line.forEach((y, r) => cellsOf(reels[r])[y]?.classList.add('hit'));
|
|
72
|
+
});
|
|
73
|
+
if (scatters >= 3) reels.forEach((strip, r) => cellsOf(strip).forEach((e, y) => { if (strips[r][(stops[r] + y) % strips[r].length] === model.scatter.symbol) e.classList.add('hit'); }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const times = (bp) => (bp / 10000).toFixed(2) + '×';
|
|
77
|
+
|
|
78
|
+
const measure = () => { c = reelsEl.firstElementChild.getBoundingClientRect().height / ROWS || c; };
|
|
79
|
+
show(model.reels, [0, 0, 0, 0, 0]);
|
|
80
|
+
|
|
81
|
+
const game = await mount(document.getElementById('game'), {
|
|
82
|
+
labels: Object.fromEntries(Object.entries(I18N).map(([l, w]) => [l, w.modes])),
|
|
83
|
+
async reveal(round) {
|
|
84
|
+
const d = round.data[0];
|
|
85
|
+
// A bought round is the free spins alone; its stake was their price.
|
|
86
|
+
const [stops, [win, mask, scatters]] = d.buy ? [null, [0, 0, 0]] : [d.b.slice(0, N), d.b.slice(N)];
|
|
87
|
+
linesEl.innerHTML = ''; winEl.textContent = ''; banner.textContent = '';
|
|
88
|
+
measure();
|
|
89
|
+
if (stops) {
|
|
90
|
+
await spin(model.reels, stops, 700);
|
|
91
|
+
paint([mask, scatters], model.reels, stops);
|
|
92
|
+
}
|
|
93
|
+
let total = win;
|
|
94
|
+
// Shown per stake: a bought round's stake was the price of model.price spins.
|
|
95
|
+
const per = (bp) => times(Math.floor(bp / (d.buy ? model.price : 1)));
|
|
96
|
+
if (win) winEl.textContent = times(total);
|
|
97
|
+
if (d.f) {
|
|
98
|
+
// A tap on the reels skips to the end of the free spins.
|
|
99
|
+
let skip = false;
|
|
100
|
+
const tap = () => { skip = true; };
|
|
101
|
+
reelsEl.addEventListener('pointerdown', tap);
|
|
102
|
+
await wait(700);
|
|
103
|
+
for (let i = 0; i < d.f.length; i++) {
|
|
104
|
+
const s = d.f[i];
|
|
105
|
+
total += s[N];
|
|
106
|
+
if (skip && i < d.f.length - 1) continue;
|
|
107
|
+
banner.textContent = `${t('freeSpins', { i: i + 1, n: d.f.length })} · ${t('winsTimes', { x: d.x })}` + (skip ? '' : ` · ${t('tapToSkip')}`);
|
|
108
|
+
linesEl.innerHTML = '';
|
|
109
|
+
await spin(model.freeReels, s.slice(0, N), skip ? 120 : 380);
|
|
110
|
+
paint(s.slice(N + 1), model.freeReels, s.slice(0, N));
|
|
111
|
+
if (total) winEl.textContent = per(total);
|
|
112
|
+
if (!skip) await wait(s[N] ? 420 : 120);
|
|
113
|
+
}
|
|
114
|
+
reelsEl.removeEventListener('pointerdown', tap);
|
|
115
|
+
if (stops) show(model.reels, stops);
|
|
116
|
+
banner.textContent = t('freePaid', { w: per(total - win) });
|
|
117
|
+
}
|
|
118
|
+
if (d.cap) winEl.textContent = `${per(total)} · ${t('maxWin')}`;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
W = I18N[(game.engine.config.locale || 'en').slice(0, 2)] || I18N.en;
|
|
122
|
+
let hinted = false;
|
|
123
|
+
document.getElementById('game').addEventListener('select', (e) => {
|
|
124
|
+
if (e.detail.mode === 'buy') { banner.textContent = t('buyHint', { n: model.price }); hinted = true; } else if (hinted) { banner.textContent = ''; hinted = false; }
|
|
125
|
+
});
|