I had a problem while creating a game, so this is a description of my problem.
Title
Optimization algorithm writes synthesized payout values (not present in any simulation) into the published lookup table, breaking book↔LUT integrity (execute_all_tests payout hash mismatch)
Summary
After running run_optimization on a custom cluster-pays game (5 reels x 5 rows, cascading tumbles, per-position grid multipliers), the resulting lookUpTable_<mode>_0.csv no longer matches the payoutMultiplier recorded for the same simulation id in books_<mode>.jsonl.zst. This violates the documented invariant in the Quickstart Guide ("Each simulation has a payoutMultiplier value... This value directly corresponds to the value in library/lookup_tables/lookUpTable_base.csv... all simulations are initially given a selection weight of 1... the optimization program is what sets these weights") and fails utils/rgs_verification.py's execute_all_tests payout-hash assertion, which mirrors the RGS's own upload-time validation described in "Required Math File Format."
Environment
StakeEngine/math-sdk, current main as of 2026-07-17 (confirmed via fresh git clone; optimization_program/src/*.rs and utils/swap_lookups.py are byte-identical to a clean clone — this is not a local modification)
- Windows 11, Python 3.13 venv, Rust/Cargo toolchain installed per docs
- Custom game: 5x5 cluster-pays,
min_cluster_size=5, cascading tumbles, per-position multiplier grid capped via config.maximum_board_mult
- 10,000 simulations per mode (
base, bonus), 10 threads, num_sim_args = {"base": 10000, "bonus": 10000}
Steps to reproduce
- Run full pipeline:
run_sims: True, run_optimization: True, run_analysis: True for both base and bonus modes.
- Run
execute_all_tests(config) (run_format_checks: True) → fails with:
AssertionError: Payout hash mismatch for base! Book payouts != LUT payouts.
- Delete the stale verification sidecar (
books_base.verification.json) to force the [FALLBACK] path in execute_all_tests, which reads books_*.jsonl.zst and lookUpTable_*_0.csv directly. Same result:
AssertionError: Mismatch in payout array.
- Independently verify via a manual diagnostic script that:
- Compares
id-keyed payoutMultiplier between books_base.jsonl.zst and lookUpTable_base_0.csv.
- Confirms both files have exactly 10,000 entries, all 10,000 ids unique (0–9999, no gaps, no duplicates) in both files, and identical ordering.
- 5,854 / 10,000 ids have a mismatched payout value between books and LUT. Values are not a scale/rounding discrepancy (e.g. off by 10x or 100x) — they map to unrelated payout tiers (example:
id=5331 book_payout=500 lut_payout=20; id=8731 book_payout=2500 lut_payout=1680).
- Bypass the automatic pipeline's LUT-write path entirely: manually run
python -m utils.swap_lookups -g <game_id> -m base -n 1 to rebuild lookUpTable_base_0.csv directly from the raw Rust optimizer output (library/optimization_files/base_0_1.csv). Re-running the diagnostic against this freshly-swapped LUT produces the exact same mismatch count (5,854) as the automatic pipeline's output.
Conclusion
Because the mismatch count is identical whether the LUT is written by the automatic run_optimization step or manually rebuilt via swap_lookups.py straight from a raw optimization_files/base_0_N.csv, and because optimization_files/base_0_N.csv itself already contains id→payout pairs that disagree with the corresponding book's recorded payoutMultiplier, the issue traces back to the Rust genetic algorithm's own output — not to any Python-side glue code, id-generation, or file-writing logic.
Two further checks narrow this down considerably:
-
Not a per-thread merge/offset bug. Mismatch rate is uniform (~55–62%) across every contiguous 1000-id block of the 10,000-id range. A thread-batch misalignment would show up as clustering (e.g. one block ~100% mismatched, another ~0%); instead it's evenly distributed everywhere.
-
Not a pure id↔payout shuffle either. Comparing the full multiset of payout values (independent of which id they're attached to) between books_base.jsonl.zst and lookUpTable_base_0.csv:
- Books contain 733 distinct payout values; the LUT contains 1010.
- 277 payout values appear in the LUT that do not exist anywhere in the 10,000 simulated books (example:
19140 i.e. 191.4x appears once in the LUT, zero times in any book).
- Existing shared values also have different occurrence counts between the two files (e.g.
260 appears 515 times in books, a different count in the LUT).
This means the optimizer is not simply reassigning existing simulated payouts to different ids (a shuffle) — it appears to be synthesizing new payout values that were never produced by any simulation, then attaching them to real simulation ids in the final LUT. This is consistent with a distribution-smoothing/interpolation step somewhere in the genetic algorithm (plausible given "genetic algorithm" terminology — breeding/mutation across candidate distributions could easily produce interpolated values between real data points) that isn't constrained to only select from the actual discrete set of simulated outcomes when constructing the final per-id win distribution.
Expected behavior
Per the documented design, the optimizer should only ever modify the weight column for each simulation id; the payout column should remain fixed to whatever payoutMultiplier the game logic originally produced for that id. Every payout value written to the final lookUpTable_<mode>_0.csv should exist verbatim in the corresponding books_<mode>.jsonl.zst output.
Additional notes
- Verified this is not caused by a local modification:
optimization_program/src/main.rs, exes.rs, setup.rs, and utils/swap_lookups.py are byte-identical to a fresh clone of this repo (Compare-Object diff, zero differences on all four files).
- Happy to share the diagnostic scripts and/or a minimal repro game config if useful.
I had a problem while creating a game, so this is a description of my problem.
Title
Optimization algorithm writes synthesized payout values (not present in any simulation) into the published lookup table, breaking book↔LUT integrity (
execute_all_testspayout hash mismatch)Summary
After running
run_optimizationon a custom cluster-pays game (5 reels x 5 rows, cascading tumbles, per-position grid multipliers), the resultinglookUpTable_<mode>_0.csvno longer matches thepayoutMultiplierrecorded for the same simulationidinbooks_<mode>.jsonl.zst. This violates the documented invariant in the Quickstart Guide ("Each simulation has apayoutMultipliervalue... This value directly corresponds to the value inlibrary/lookup_tables/lookUpTable_base.csv... all simulations are initially given a selection weight of1... the optimization program is what sets these weights") and failsutils/rgs_verification.py'sexecute_all_testspayout-hash assertion, which mirrors the RGS's own upload-time validation described in "Required Math File Format."Environment
StakeEngine/math-sdk, currentmainas of 2026-07-17 (confirmed via freshgit clone;optimization_program/src/*.rsandutils/swap_lookups.pyare byte-identical to a clean clone — this is not a local modification)min_cluster_size=5, cascading tumbles, per-position multiplier grid capped viaconfig.maximum_board_multbase,bonus), 10 threads,num_sim_args = {"base": 10000, "bonus": 10000}Steps to reproduce
run_sims: True,run_optimization: True,run_analysis: Truefor bothbaseandbonusmodes.execute_all_tests(config)(run_format_checks: True) → fails with:books_base.verification.json) to force the[FALLBACK]path inexecute_all_tests, which readsbooks_*.jsonl.zstandlookUpTable_*_0.csvdirectly. Same result:id-keyedpayoutMultiplierbetweenbooks_base.jsonl.zstandlookUpTable_base_0.csv.id=5331 book_payout=500 lut_payout=20;id=8731 book_payout=2500 lut_payout=1680).python -m utils.swap_lookups -g <game_id> -m base -n 1to rebuildlookUpTable_base_0.csvdirectly from the raw Rust optimizer output (library/optimization_files/base_0_1.csv). Re-running the diagnostic against this freshly-swapped LUT produces the exact same mismatch count (5,854) as the automatic pipeline's output.Conclusion
Because the mismatch count is identical whether the LUT is written by the automatic
run_optimizationstep or manually rebuilt viaswap_lookups.pystraight from a rawoptimization_files/base_0_N.csv, and becauseoptimization_files/base_0_N.csvitself already contains id→payout pairs that disagree with the corresponding book's recordedpayoutMultiplier, the issue traces back to the Rust genetic algorithm's own output — not to any Python-side glue code, id-generation, or file-writing logic.Two further checks narrow this down considerably:
Not a per-thread merge/offset bug. Mismatch rate is uniform (~55–62%) across every contiguous 1000-id block of the 10,000-id range. A thread-batch misalignment would show up as clustering (e.g. one block ~100% mismatched, another ~0%); instead it's evenly distributed everywhere.
Not a pure id↔payout shuffle either. Comparing the full multiset of payout values (independent of which id they're attached to) between
books_base.jsonl.zstandlookUpTable_base_0.csv:19140i.e. 191.4x appears once in the LUT, zero times in any book).260appears 515 times in books, a different count in the LUT).This means the optimizer is not simply reassigning existing simulated payouts to different ids (a shuffle) — it appears to be synthesizing new payout values that were never produced by any simulation, then attaching them to real simulation ids in the final LUT. This is consistent with a distribution-smoothing/interpolation step somewhere in the genetic algorithm (plausible given "genetic algorithm" terminology — breeding/mutation across candidate distributions could easily produce interpolated values between real data points) that isn't constrained to only select from the actual discrete set of simulated outcomes when constructing the final per-id win distribution.
Expected behavior
Per the documented design, the optimizer should only ever modify the
weightcolumn for each simulation id; thepayoutcolumn should remain fixed to whateverpayoutMultiplierthe game logic originally produced for that id. Every payout value written to the finallookUpTable_<mode>_0.csvshould exist verbatim in the correspondingbooks_<mode>.jsonl.zstoutput.Additional notes
optimization_program/src/main.rs,exes.rs,setup.rs, andutils/swap_lookups.pyare byte-identical to a fresh clone of this repo (Compare-Objectdiff, zero differences on all four files).