fix!(lua.endpoints): cash_out fires before round-eval rows commit — rewards under-collected, sometimes $0

Aperta
#231 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
72/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Tranquilla
Stack tecnologico
lua, python
Ambito
api, backend

Direzione di ricerca

Inizia in src/lua/endpoints/cash_out.lua, in requires_state ed execute, quindi confronta l’attesa esistente lato SHOP con il flusso round-eval; bot.py mostra il caller ROUND_EVAL interessato. Verifica con i seed segnalati o con le tracce di differential-replay e considera conclusa la issue quando cash_out attende le righe di eval e preserva le rewards previste.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Description

Calling cash_out as soon as the game reaches ROUND_EVAL collects less money
than the game owes — usually short exactly one blind reward ($3/$4/$5), and
$0 in the worst case. A human cannot reproduce this: the game only spawns the
Cash Out button after the eval rows have finished paying out, but the endpoint
bypasses that gate.

Root Cause

src/lua/endpoints/cash_out.lua:

  • requires_state = { G.STATES.ROUND_EVAL } (line 22) passes the moment the
    state flips, while the dollar rows (blind reward, interest, per-joker payouts)
    are still being queued and committed by the event manager.
  • execute then calls G.FUNCS.cash_out({ config = {} }) immediately (line 28).

The endpoint carefully waits for the SHOP transition on the response side, but
does not wait for eval completion on the entry side.

Evidence

Found with a differential-replay harness that drives a vanilla-faithful simulator
and balatrobot with identical action traces and deep-diffs the full gamestate
after every action (~1,200 organic games).

  • Signature: after cash-out, live money == expected money minus exactly one
    blind reward. Confirmed 4× before we added a workaround.
  • Partial commits happen: one case committed interest but dropped the $5 boss
    reward (seed LSOYQISY).
  • Zero-payout extreme: when hands_left == 0 and there is no interest (the
    payout is only the reward row), cash-out commits $0. Reproduced
    deterministically across bosses (Psychic, Pillar) and across game sessions
    (seeds LSAZ99YB, LS88BC4Z).
  • Workaround that eliminates it completely: a flat 12s wait before calling
    cash_out (normal speed, --no-audio).

The under-collection is irreversible once collected — the money is simply gone
for the rest of the run.

Impact

Any bot that auto-cashes on seeing ROUND_EVAL is systematically poorer than
the game intends. balatrollm does exactly this (bot.py:
case "ROUND_EVAL": ... call("cash_out")), so BalatroBench run economies — and
any result that turns on being a few dollars short in the shop — are affected.

Suggested Fix

Gate execution the way the game gates the button: add a condition event that
waits until the round-eval UI has finished creating its rows (e.g. the Cash Out
button exists), and only then call G.FUNCS.cash_out. This mirrors the
endpoint's existing SHOP-side wait.

We can provide full repro bundles (action trace + both gamestates + field diff
at the divergence step) for any of the seeds above.

Lingua principale
Python
Stelle
80
Fork
23
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di coder/balatrobot

Tutte le issue di coder/balatrobot

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.