An agent that plays Balatro · v0.11.0
BalatRobot
An agent that plays Balatro unattended — deals, discards, shop purchases, pack picks, boss blinds, run after run — together with a measurement harness strict enough to say whether a change to it actually helped.
- 0human inputs
- 35%White stake · 24/69
- 3.74avg death ante · Gold
- stdlibruntime, no deps
No macros
It plays the whole game, unattended.
Balatro exposes no API, so a Steamodded Lua mod exports the full game state and takes commands back over a pair of files. The Python agent makes every call itself: which cards to play, what to discard, what to buy, when to skip a blind. It runs with nobody watching, restarts itself when the game dies, and logs every decision it makes.
The other half of the project is the harness. Balatro is variance-heavy enough that eyeballing win-rates will mislead you confidently and often, so nothing ships without a paired A/B: the same deck and seed played by both builds, only per-cell differences counted.
See it play
A real run, nobody touching it.
The game on the right, the agent's reasoning on the left. The gameplay isn't edited: it reads the board, picks the blinds, plays the hands, and shops on its own.
agent.py --explain.The loop
How a frame of Balatro becomes a move.
-
Bridge
A Steamodded mod (
balatro_solver.lua) writes the game state every frame and pollsbalatro2_cmd.txt;bridge.pywrites commands and reads the reply frombalatro2_rsp.txt. -
Read
state_parser.pyturns each response into a typedGameStateV2: deck, hand, jokers with sticker and edition flags, blind requirement, hand levels, dollars, ante. -
Decide
Different machinery for different parts of the game — and two policies you can pick between.
- ♠Card playdeterministic search against the oracle, on a wall-clock deadline
- ♥Shop & blinds
--policy learned: a sim-trained 30-feature linear policy - ♦Shop, heuristic modeoracle marginal value + a Thompson-Sampling bandit over past runs
-
Act & record
It plays the move and logs the decision with its reasons. Bandit weights update from finished runs; the learned policy's weights come from offline training in
sim/.
The scoring brain
No synergy table. An oracle instead.
balatro_solver.py scores a hypothetical play exactly the way the game does —
enhancements, editions, seals, per-card triggers, retriggers, held-card effects, joker
ordering, xMult chains, Blueprint and Brainstorm copying. So a joker's worth is just
score(board + candidate) − score(board). Photograph's combo with
Hanging Chad shows up as a number, not as a rule someone remembered to encode. There is no
hand-written synergy table anywhere in the project, and there should never be one.
In the deployed shop path, that marginal is a percentage gain against a representative hand for the build being committed to. It buys at +18%, drops to +8% while the board owns no scoring joker at all, and needs a 45% margin to sell something already on board — 15% if the swap is a flat board's first xMult.
The older heuristic policy scores each joker 0–100 from eight independent reads of the board, buying above 50 and committing above 75. The bars are each component's share of that range:
- ts0–40 · Thompson-Sampling signal from past runs
- power0–32 · raw xMult / mult / chips punch
- alignment0–30 · how often it actually triggers
- build target−15–25 · fit with the run's committed build
- deck fit0–15 · does the deck hold what it needs
- synergy−20–20 · pairings, and anti-pairings
- tempo0–10 · economy vs. scoring, by ante
- econ floor−30 · penalty for banking money with nothing that scores
Where it stands
Two suites, and a gap that isn't subtle.
White certification is 15 decks × 5 frozen seeds — 75 cells, paired, resumable. Current build: 35% (24 of 69 cells adjudicated), against a target of 90%. That figure is stale by a couple of dozen commits; a re-run advances a few cells at a time on the VM whenever no experiment round is pending.
Gold lesson seeds are 19 unique deck+seed cells, each one a run a strong human player won on video. The agent converts none of them, so the metric is average death ante: now 3.74, up from 3.26 across six measured rounds. Target is winning half.
The wall is sharp: 11 of the 19 cells die at ante 4, and 7 of those on the Small Blind — the easiest blind in the ante — with 2–6 jokers already on board. The misses are cliffs, not near-things: 1,144 scored against 4,926 needed; 1,980 against 6,072; 969 against 11,014. That points at hand levels rather than joker count.
White's 35% is entirely seed-determined, which per-deck reporting hides: seed 81TYVZPN goes 13/15 and UO7RUHNF 10/14, while RVO5RFPC is 0/15 and IEOJYMKI 0/10. Two seeds are nearly solved; three hold 40 of the 69 cells at a single win between them.
Quickstart
Four steps to a hands-off run.
# 1 · install Steamodded, then drop the mod in the Proton prefix's Mods folder
cp balatro_solver.lua "$PREFIX"/drive_c/users/steamuser/AppData/Roaming/Balatro/Mods/BalatRobot2/
# 2 · with Balatro running, confirm the bridge is live
python3 agent.py --check
# 3 · let it play (restart-on-death loop)
./run.sh
# 4 · curious what it's thinking?
python3 agent.py --explain
# measure something instead of guessing
python3 agent.py --eval --policy learned \
--decks Blue --stakes Gold --episodes 1 --eval-out out.jsonl
Runtime is the Python standard library — no third-party packages. pytest only
for the tests. --observe watches you play without sending commands;
BALATRO_SHOP_DEBUG=1 plus analyze_shopdbg.py explains why an offer
was refused.