BalatRobot

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.

Live agent log (left) · Balatro (right). Recorded from agent.py --explain.

The loop

How a frame of Balatro becomes a move.

  1. Bridge

    A Steamodded mod (balatro_solver.lua) writes the game state every frame and polls balatro2_cmd.txt; bridge.py writes commands and reads the reply from balatro2_rsp.txt.

  2. Read

    state_parser.py turns each response into a typed GameStateV2: deck, hand, jokers with sticker and edition flags, blind requirement, hand levels, dollars, ante.

  3. 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
  4. 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.

Linux · Steam/Proton · Python 3.11+ · Steamodded

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.