coder / coder/balatrobot

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

Open
#232 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
72
Forks
18
PR merge metrics
No merged PRs in 30d

Description

## 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_flags`** — `gros_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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.