Skip to content

fix!(lua): G.GAME state survives menu+start — a previous run's won flag marks later losses as wins #232

Description

@Khetnen

Description

Starting a new run via menu + start on a long-lived instance does not fully
reset run state. We catalogued four distinct cross-run leaks, each confirmed by
replaying the identical action trace on a vanilla-faithful simulator (fresh
G.GAME every run) and diffing the full gamestate after every action.

  1. G.GAME.won — after any win in the session, a later run's GAME_OVER
    loss reports won = true. 16 instances observed; e.g. seed ESVPE7W2: 696
    chips vs Amber Acorn, unambiguous loss, live gamestate won = true — leaked
    from the previous run's ante-8 win on the same instance.
    src/lua/utils/gamestate.lua:739 serializes the raw global
    (state_data.won = G.GAME.won), so the stale value flows straight to
    clients; play.lua:125 also branches on it.
  2. G.GAME.last_tarot_planet — a fresh run's first Fool copied the tarot
    last used in the previous seed's run.
  3. G.GAME.pool_flagsgros_michel_extinct leaked: a run that never
    owned a Gros Michel was offered Cavendish in the shop (seed LS4ECDCB).
    Extinction only rolls for owned jokers at round end, so this is
    vanilla-impossible in-run.
  4. G.GAME.used_jokers key loss (mechanism unidentified, same family):
    live's shop offered j_banner while Banner sat in play (seed LS6VZ3ES) —
    impossible under the no-repeat pool. RNG forensics showed both sides drawing
    identical raw picks from the same stream; the sim resampled per no-repeat,
    live kept the duplicate, i.e. live had lost the key.

Root cause is presumably in how the run is rebuilt through the menu→start path
versus a fresh process launch (possibly SMODS-side), but balatrobot's start
endpoint is the natural place to defend against it.

Impact

This corrupts benchmark results, not just convenience:

  • balatrollm runs its whole task list through long-lived instances (the
    executor's port pool starts instances once) and starts each run with
    menu+start. bot.py ends a run as won when gamestate["won"] is truthy — so
    a loss that occurs on an instance after that instance has produced any win can
    be recorded and published as a win (run_won in the leaderboard data and
    the CC0 dataset).
  • Leaks 2–4 make seeded runs non-reproducible: shop and consumable outcomes
    depend on what earlier seeds did in the same instance.

Suggested Fix

In start.lua, after the run initializes, explicitly reset the leak-prone keys:
G.GAME.won = false, clear last_tarot_planet, and verify pool_flags /
used_jokers are freshly built. Alternatively (or additionally), document that
benchmark-grade data requires one process per run.

Repro bundles (action trace + both gamestates + field diff) available for every
seed cited.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions