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,26 @@
|
|
|
1
|
+
# Slot — a 3-reel, 1-line example
|
|
2
|
+
|
|
3
|
+
`math/slot.tsv` is 48³ = 110,592 rows (weight 1 each, `{"stops":[a,b,c]}`): a
|
|
4
|
+
slot is recommended to have 100,000 different rounds in every mode. The
|
|
5
|
+
reel strip is in `math/gen.mjs` (regenerate with `node math/gen.mjs`) and in
|
|
6
|
+
`bundle/game.js`. The panel is the engine's (`/engine-sdk/ui.js`);
|
|
7
|
+
`bundle/game.js` only spins the reels to the stops the server drew.
|
|
8
|
+
|
|
9
|
+
Strip (48 positions): one turn of 16, `7 bar bar bell bell bell cherry cherry cherry lemon lemon lemon lemon plum plum plum`,
|
|
10
|
+
three times round the reel, the second turn rotated by 5 and the third by 11, so the windows differ.
|
|
11
|
+
|
|
12
|
+
Bet `line`, exact RTP `1,083,429,000 / 110,592` bp = **97.9663%** (`rtpBp` cap 9800): the proportions are one turn's, so the RTP is the 16-stop strip's.
|
|
13
|
+
|
|
14
|
+
| Combination | Hits / 110,592 | Multiplier |
|
|
15
|
+
| -------------- | -------------: | ---------: |
|
|
16
|
+
| Three sevens | 27 | 500× |
|
|
17
|
+
| Three bars | 216 | 50× |
|
|
18
|
+
| Three bells | 729 | 20× |
|
|
19
|
+
| Three cherries | 729 | 10× |
|
|
20
|
+
| Three plums | 729 | 8× |
|
|
21
|
+
| Three lemons | 1,728 | 5× |
|
|
22
|
+
| Two cherries | 9,477 | 2× |
|
|
23
|
+
| One cherry | 41,067 | 0.7× |
|
|
24
|
+
|
|
25
|
+
npx hbengine verify . exact RTP per bet, the mathHash
|
|
26
|
+
npx hbengine init my-slot --template slot --slug my-slot # make it yours, then: verify, dev, upload
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "groups": { "boot": { "files": ["index.html", "game.js", "game.css"] } } }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
body { margin: 0; background: #08141a; }
|
|
2
|
+
.slot { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 24px 16px; width: 100%; box-sizing: border-box; }
|
|
3
|
+
.reels { position: relative; display: grid; grid-template-columns: repeat(3, 92px); gap: 8px; padding: 10px; border-radius: 14px; background: #061218; box-shadow: inset 0 0 0 1px #0D2A38, 0 10px 32px rgba(0, 0, 0, .5); }
|
|
4
|
+
.reel { position: relative; height: 180px; width: 92px; overflow: hidden; background: radial-gradient(circle at 50% 30%, #0b2a35, #061218 80%); border-radius: 8px; }
|
|
5
|
+
.reel .strip { position: absolute; top: 0; left: 0; right: 0; will-change: transform; }
|
|
6
|
+
.sym { height: 60px; display: grid; place-items: center; font: 800 24px/1 'Figtree', system-ui, sans-serif; letter-spacing: .5px; color: #F9F9FB; border-bottom: 1px solid rgba(13, 42, 56, .5); }
|
|
7
|
+
.s-7 { color: #ff5c6c; }
|
|
8
|
+
.s-bar { color: #28F3F3; }
|
|
9
|
+
.s-bell { color: #ffd166; }
|
|
10
|
+
.s-cherry { color: #ff8ab0; }
|
|
11
|
+
.s-lemon { color: #e6f26b; }
|
|
12
|
+
.s-plum { color: #c89bff; }
|
|
13
|
+
.line { position: relative; width: 302px; height: 2px; margin-top: -116px; background: #28F3F3; opacity: .35; transition: opacity .25s, box-shadow .25s; pointer-events: none; }
|
|
14
|
+
.line.win { opacity: 1; box-shadow: 0 0 18px 2px #28F3F3; }
|
|
15
|
+
@media (max-width: 420px) {
|
|
16
|
+
.reels { grid-template-columns: repeat(3, 84px); }
|
|
17
|
+
.reel { width: 84px; }
|
|
18
|
+
.line { width: 278px; }
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Slot: three reels, one pay line. The panel is the engine's (engine-sdk/ui.js);
|
|
2
|
+
// this file spins the reels to the stops the server drew.
|
|
3
|
+
import { mount } from '/engine-sdk/ui.js';
|
|
4
|
+
|
|
5
|
+
// The reel as math/gen.mjs has it: one turn of 16 symbols three times round, each turn rotated.
|
|
6
|
+
const TURN = ['7', 'bar', 'bar', 'bell', 'bell', 'bell', 'cherry', 'cherry', 'cherry', 'lemon', 'lemon', 'lemon', 'lemon', 'plum', 'plum', 'plum'];
|
|
7
|
+
const STRIP = [0, 5, 11].flatMap((k) => [...TURN.slice(k), ...TURN.slice(0, k)]);
|
|
8
|
+
const GLYPH = { '7': '7', bar: 'BAR', bell: 'BEL', cherry: 'CHR', lemon: 'LMN', plum: 'PLM' };
|
|
9
|
+
const H = 60;
|
|
10
|
+
const line = document.getElementById('line');
|
|
11
|
+
const reels = [...document.querySelectorAll('.reel')].map((r) => {
|
|
12
|
+
const strip = document.createElement('div');
|
|
13
|
+
strip.className = 'strip';
|
|
14
|
+
for (let k = 0; k < 3; k++) for (const s of STRIP) {
|
|
15
|
+
const el = document.createElement('div');
|
|
16
|
+
el.className = 'sym s-' + s;
|
|
17
|
+
el.textContent = GLYPH[s];
|
|
18
|
+
strip.append(el);
|
|
19
|
+
}
|
|
20
|
+
r.append(strip);
|
|
21
|
+
return strip;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
|
|
26
|
+
function spin(strip, stop, dur) {
|
|
27
|
+
strip.style.transition = 'none';
|
|
28
|
+
strip.style.transform = 'translateY(0px)';
|
|
29
|
+
void strip.offsetWidth;
|
|
30
|
+
strip.style.transition = `transform ${dur}ms cubic-bezier(.2, .8, .15, 1)`;
|
|
31
|
+
strip.style.transform = `translateY(-${(STRIP.length - 1 + stop) * H}px)`;
|
|
32
|
+
return wait(dur);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await mount(document.getElementById('game'), {
|
|
36
|
+
async reveal(round) {
|
|
37
|
+
line.classList.remove('win');
|
|
38
|
+
const stops = round.data[0].stops;
|
|
39
|
+
await Promise.all(stops.map((s, i) => spin(reels[i], s, 1100 + i * 400)));
|
|
40
|
+
line.classList.toggle('win', round.payout > 0);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<title>Slot</title>
|
|
7
|
+
<link rel="stylesheet" href="./game.css">
|
|
8
|
+
<script type="module" src="/engine-sdk/bet-field.js"></script>
|
|
9
|
+
<script type="module" src="./game.js"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<hb-game id="game">
|
|
13
|
+
<div slot="zone" class="slot" aria-live="polite">
|
|
14
|
+
<div class="reels" id="reels">
|
|
15
|
+
<div class="reel"></div>
|
|
16
|
+
<div class="reel"></div>
|
|
17
|
+
<div class="reel"></div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="line" id="line" aria-hidden="true"></div>
|
|
20
|
+
</div>
|
|
21
|
+
</hb-game>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// The 16 symbols of one turn, three times round the reel, each turn rotated so the
|
|
9
|
+
// windows differ: 48 stops, 48³ = 110,592 different rounds (House Studio holds a slot
|
|
10
|
+
// to 100,000 in every mode). The proportions, and with them the RTP, are one turn's.
|
|
11
|
+
const TURN = ['7', 'bar', 'bar', 'bell', 'bell', 'bell', 'cherry', 'cherry', 'cherry', 'lemon', 'lemon', 'lemon', 'lemon', 'plum', 'plum', 'plum'];
|
|
12
|
+
export const STRIP = [0, 5, 11].flatMap((k) => [...TURN.slice(k), ...TURN.slice(0, k)]);
|
|
13
|
+
|
|
14
|
+
const PAY = {
|
|
15
|
+
seven: { multBp: 5000000, label: 'Three sevens' },
|
|
16
|
+
bar: { multBp: 500000, label: 'Three bars' },
|
|
17
|
+
bell: { multBp: 200000, label: 'Three bells' },
|
|
18
|
+
cherry: { multBp: 100000, label: 'Three cherries' },
|
|
19
|
+
plum: { multBp: 80000, label: 'Three plums' },
|
|
20
|
+
lemon: { multBp: 50000, label: 'Three lemons' },
|
|
21
|
+
'two-cherry': { multBp: 20000, label: 'Two cherries' },
|
|
22
|
+
'one-cherry': { multBp: 7000, label: 'One cherry' },
|
|
23
|
+
};
|
|
24
|
+
const ORDER = ['seven', 'bar', 'bell', 'cherry', 'plum', 'lemon', 'two-cherry', 'one-cherry'];
|
|
25
|
+
|
|
26
|
+
function classify(a, b, c) {
|
|
27
|
+
const s = [STRIP[a], STRIP[b], STRIP[c]];
|
|
28
|
+
if (s[0] === s[1] && s[1] === s[2]) return s[0] === '7' ? 'seven' : s[0];
|
|
29
|
+
const n = s.filter((x) => x === 'cherry').length;
|
|
30
|
+
if (n === 2) return 'two-cherry';
|
|
31
|
+
if (n === 1) return 'one-cherry';
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rows = [];
|
|
36
|
+
const idsByTier = Object.fromEntries(ORDER.map((t) => [t, []]));
|
|
37
|
+
const N = STRIP.length;
|
|
38
|
+
for (let a = 0; a < N; a++) for (let b = 0; b < N; b++) for (let c = 0; c < N; c++) {
|
|
39
|
+
const idx = rows.length;
|
|
40
|
+
rows.push([a, b, c]);
|
|
41
|
+
const t = classify(a, b, c);
|
|
42
|
+
if (t) idsByTier[t].push(idx);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ranges = (ids) => {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const i of ids) {
|
|
48
|
+
const last = out[out.length - 1];
|
|
49
|
+
if (last && last[1] === i - 1) last[1] = i;
|
|
50
|
+
else out.push([i, i]);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const tiers = ORDER.map((t) => ({ outcomes: ranges(idsByTier[t]), multBp: PAY[t].multBp }));
|
|
56
|
+
|
|
57
|
+
let num = 0;
|
|
58
|
+
for (const t of ORDER) num += idsByTier[t].length * PAY[t].multBp;
|
|
59
|
+
const rtpBp = Math.floor(num / rows.length);
|
|
60
|
+
if (rtpBp < 9600 || rtpBp > 9800) throw new Error('RTP out of band: ' + rtpBp);
|
|
61
|
+
|
|
62
|
+
const manifest = {
|
|
63
|
+
format: 'house-engine-math/1',
|
|
64
|
+
game: 'slot',
|
|
65
|
+
rtpBp: 9800,
|
|
66
|
+
modes: [{
|
|
67
|
+
id: 'spin',
|
|
68
|
+
label: 'Spin',
|
|
69
|
+
kind: 'table',
|
|
70
|
+
table: 'slot.tsv',
|
|
71
|
+
bets: [{ id: 'line', label: 'Line', tiers }],
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
writeFileSync(join(DIR, 'slot.tsv'), rows.map(([a, b, c]) => `1\t{"stops":[${a},${b},${c}]}`).join('\n') + '\n');
|
|
76
|
+
writeFileSync(join(DIR, 'manifest.json'), JSON.stringify(manifest) + '\n');
|
|
77
|
+
|
|
78
|
+
console.log(`rows ${rows.length} rtpBp ${rtpBp} (${(rtpBp / 100).toFixed(2)}%)`);
|
|
79
|
+
for (const t of ORDER) console.log(` ${PAY[t].label.padEnd(16)} count ${String(idsByTier[t].length).padStart(5)} mult ${(PAY[t].multBp / 10000)}x`);
|