Jackdaw is a Balatro simulator built for RL research. It features a 1:1 Python reimplementation of the Balatro engine, a Gymnasium-style environment with entity-based observations and a factored action space, and a validation bridge to play against live Balatro via BalatroBot.
With BalatroBot already existing, why build a separate simulator?
While BalatroBot lets you input actions with no latency (minus network time), this can cause race conditions and actually crash the game. Parallelizing is also important for RL training — you can run multiple instances of BalatroBot, but it's more overhead and complexity than just running multiple simulators in Python. Finally, having a pure Python implementation allows for easier debugging, introspection, and customization of the game logic, which is really helpful for research.
uv add git+https://github.com/TylerFlar/jackdaw-balatro.gitgit clone https://github.com/TylerFlar/jackdaw-balatro
cd jackdaw-balatro
uv sync --devfrom jackdaw.env import BalatroEnvironment, DirectAdapter
env = BalatroEnvironment(adapter_factory=DirectAdapter)
obs, mask, info = env.reset()
while not info.get("done"):
action = your_model.act(obs, mask)
obs, terminated, truncated, mask, info = env.step(action)Jackdaw includes a Gymnasium-compatible wrapper and a ready-to-run MaskablePPO training script. Install the training extras (PyTorch, Stable-Baselines3, sb3-contrib):
uv sync --extra trainTrain an agent:
python scripts/train_ppo.py --total-timesteps 500000Monitor in tensorboard (balatro/mean_ante_reached, balatro/win_rate, etc.):
tensorboard --logdir runs/balatro_ppoSee the RL Project Guide for details on the gymnasium wrapper, reward shaping, and writing custom agents.
Swap the adapter to play against real Balatro via BalatroBot — the interface is identical:
from jackdaw.bridge import LiveBackend, BridgeAdapter
env = BalatroEnvironment(
adapter_factory=lambda: BridgeAdapter(LiveBackend("127.0.0.1", 12346))
)
# Same obs, same masks, same actionsjackdaw/
engine/ Deterministic game simulator (30 modules)
game.py step() — core state transition function
run_init.py initialize_run() — seeded game setup
runner.py simulate_run() — full game with an agent
scoring.py 14-phase scoring pipeline
jokers.py All 150 joker effects
rng.py Bit-exact 3-layer PRNG (matches LuaJIT 2.1)
shop.py Shop population, buy/sell/reroll
actions.py Action types, GamePhase enum
env/ RL environment
balatro_env.py BalatroEnvironment — factored action interface
gymnasium_wrapper.py BalatroGymnasiumEnv — SB3-compatible flat Discrete + action masking
observation.py Entity-based encoding (235-dim global + variable entities)
action_space.py 21-type factored action space
game_interface.py GameAdapter protocol (DirectAdapter, BridgeAdapter)
agents.py Agent protocol + RandomAgent baseline
bridge/ Validation bridge to live Balatro via BalatroBot
cli/ CLI tools & 250+ validation scenarios
scripts/
train_ppo.py MaskablePPO training with tensorboard logging
jackdaw validate # Run all ~250 validation scenarios
jackdaw validate --category jokers # Joker scenarios only
jackdaw validate --scenario joker_jolly # Single scenario
jackdaw validate --host 127.0.0.1 --port 12346One of the biggest ways to contribute is by writing custom validation scenarios! See the validation docs to get started.
Requires Python 3.12+ and uv.
uv sync --dev # install with dev dependencies
pytest # run tests
pytest --cov=jackdaw # with coverage
pytest -m benchmark # performance benchmarks
pytest -m live # live validation (needs BalatroBot)
ruff check . # lint
ruff format . # formatMIT
