Skip to content

Expose scoring-critical game state + endpoint robustness fixes#181

Open
DrLatBC wants to merge 10 commits intocoder:devfrom
DrLatBC:main
Open

Expose scoring-critical game state + endpoint robustness fixes#181
DrLatBC wants to merge 10 commits intocoder:devfrom
DrLatBC:main

Conversation

@DrLatBC
Copy link
Copy Markdown

@DrLatBC DrLatBC commented Apr 5, 2026

Summary

These changes come from building a scoring engine and integration test suite on top of the API. Each fix addresses a real issue encountered during automated play — most caused hangs, incorrect state, or missing data that forced workarounds on the bot side. Everything is independent and easy to cherry-pick or drop.


New Game State Fields

Card Editions — Detection Overhaul

The existing edition detection relies on card.edition.type, which isn't always present. Some editions only expose boolean flags (edition.holo, edition.foil), and SMODS editions use edition.key (e.g. e_holo). This expands detection to handle all three formats with fallbacks via get_chip_mult() and get_chip_bonus() for edge cases where the edition isn't in the card.edition table at all.

Additionally, the numeric scoring values are now exposed:

  • edition_mult — Holographic mult bonus
  • edition_chips — Foil chip bonus
  • edition_x_mult — Polychrome x-mult (sourced from get_edition() to avoid contamination from Glass card's x2 overwriting the edition field)

Card Enhancements

  • enhancement_x_mult — Exposes Glass card's x2 multiplier separately from edition x_mult. Without this, there's no way to distinguish a Glass card's x2 from a Polychrome edition's x1.5 when both live in the same field.

Card Values

  • perma_bonus — Permanent chip bonus accumulated from Hiker joker. This value grows over time and isn't derivable from the card's base stats.
  • rarity — Joker rarity tier (1=Common, 2=Uncommon, 3=Rare, 4=Legendary). Useful for evaluating joker value during shop decisions. Allows easy expansion of mod jokers when a joker list would break.
  • ability — Joker scoring values flattened from card.ability.extra and config fields. Includes t_mult, t_chips, mult, x_mult, driver_tally, loyalty_remaining, and any fields from ability.extra. This is the only way to get actual numeric values for scaling jokers (e.g. how much mult has Ride the Bus accumulated) without parsing the UI description text.

Round Info

  • most_played_poker_hand — The hand type that The Ox boss locks at blind start. The game stores this in G.GAME.current_round.most_played_poker_hand and displays it in the boss blind's UI text. Without this field, bots have to replicate the game's tiebreaker logic — which has a non-deterministic bug (maybe intentional?) where _order is never updated in the selection loop, making ties depend on Lua's pairs() hash iteration order.
  • ancient_suit — Ancient Joker's current rotating suit as a single-letter code (H/D/C/S). This rotates each round and affects which cards get the retrigger — previously unavailable through the API.

New Endpoint Feature

setblind parameter

Accepts a boss blind key (e.g. "bl_ox", "bl_flint") to force the upcoming boss blind and rebuilds the blind select UI to reflect the change. Mirrors the logic from G.FUNCS.reroll_boss in button_callbacks.lua. Primarily useful for integration testing specific boss interactions without playing through entire runs, but could also support tools that let users practice against specific bosses. With your voucher add on the dev branch (awesome btw! legit ran into that problem today) this completes the ability to debug pretty much any situation in the game given enough persistence.


Endless Mode Support

Win Overlay Auto-Dismiss

When the bot wins a run (defeats ante 8 boss), Balatro shows a "YOU WIN" overlay that sets G.SETTINGS.paused = true. This blocks all event processing, which means the play endpoint's completion handler never fires — the bot just hangs forever.

This adds a two-phase dismissal in love.update (which runs even when paused):

  1. Phase 1: Detects the overlay is visible, calls exit_overlay_menu() to dismiss it
  2. Phase 2: On the next frame, confirms the overlay is fully gone before unblocking the play endpoint

The play endpoint now waits for this dismissal to complete before looking for the cash-out button, and the start endpoint resets the overlay flags when beginning a new run. This allows bots to continue playing into endless mode (ante 9+) without any special handling on the bot side. Without delay it got weird trying to fire at the same second, resulting in actions BEHIND the end screen and then an immediate crash.

This could probably be turned into a param of some kind incase you didn't want llm's to drift off and forget they should stop at ante 9. Made it auto since my personal use case doesn't care, but worth considering.


Bug Fixes

sell — Invisible Joker Completion Hang

The sell endpoint checked count_decreased as one of its completion conditions — verifying the card count dropped by 1. Invisible Joker spawns a duplicate of a random joker when sold, leaving the count unchanged. This caused the endpoint to hang indefinitely waiting for a condition that would never be true. Fix: removed the count check entirely, relying on card_gone + money_increased which uniquely identifies completion regardless of spawn behavior.

buy / pack — Black Hole Pack Type Misdetection

Both endpoints inferred pack type by checking the first card's ability.set — if it was "Tarot" or "Spectral", the endpoint would wait for hand cards to be dealt. Black Hole is set=Spectral but appears in Celestial packs (which don't deal hand cards), causing the endpoint to wait for hand cards that would never arrive. Fix: instead of inferring from card type, check whether hand cards are actually dealt (G.hand.cards exists and is non-empty).

buy — Small Deck Hand Count Assumption

When waiting for hand cards during pack opening, the endpoint assumed the hand would always have hand_limit cards. With small decks (Abandoned deck, late-game thin decks), fewer cards exist than the hand limit allows. Fix: use math.min(deck_size, hand_limit) as the expected count.

pack — Re-Entrancy Double-Fire

Rapid or automated pack selections could trigger use_card while a previous selection's animation (e.g. Black Hole's planet upgrade sequence) was still processing, causing corrupted state. Fix: added a selection_in_progress module-level guard that blocks new selections until the current one completes, and clears on both success and skip.

I'm not actually 100% sure this is needed. Might be a collateral fix from when i was trying to get endless mode to work. Just a guard, worth testing maybe?

rearrange — Cards Don't Visually Move

The rearrange endpoint updated card order fields but didn't trigger the visual re-layout. Cards would be logically reordered but visually remain in their original positions until the next natural layout event. Fix: sets card.rank and triggers table.sort + set_ranks() + align_cards(), mirroring the gamepad d-pad reordering logic from controller.lua. Found some obscure bugs i wasn't able to replicate, but we're just copying the game's base logic here. nice and safe, not re-inventing the wheel here.

start — Crash on Stale Run State

Starting a new run immediately after a game over could fail if the previous run's state wasn't fully cleaned up. Fix: wraps setup_run and exit_overlay_menu in pcall, and if start_run fails, calls delete_run() to clean up stale state before retrying. Bug introduced by endless mode (i think?), so fixing it.

play / discard — Event Flag Cleanup

Removed blockable=false and created_on_pause=true from the play and discard event handlers. These flags interfered with proper event sequencing during scoring animations. These caused obscure crashes i also couldn't replicate, but did disappear entirely going from 10-20~ crashes in 2000 games to 1-2.

Note: The diff includes a few small changes from upstream main that haven't been merged into dev yet (balatrobot.json, docs links) — those aren't ours.

Extra comments

Went through and wrote up a proper pr since that last one was so piss poor. Removed my mile long debug.. Long story short, the new dispatch feature on claude is WACK LOL. The big wins here are the boss blind set, endless mode, and a couple of the bugs. Most of the bugs should be super easy to replicate. The blackhole pack one can be replicated by test_win.py in my repo. It sets the seed to GODMODE1 (i don't even know if that's a valid seed? Claude made that shit up. "seed": "GODMODE1" but it definitely tries anyways) and then the pack will show up in round 9 or 10 for the instant soft lock.

keyedout and others added 10 commits March 19, 2026 10:28
Adds two fields to extract_round_info():
- most_played_poker_hand: The Ox boss's locked hand type from G.GAME.current_round
- ancient_suit: Ancient Joker's current rotating suit, mapped to single-letter codes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a "blind" string parameter (e.g. "bl_ox", "bl_flint") that sets
the upcoming boss blind and rebuilds the UI to reflect the change.
Used by integration tests to force specific boss encounters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nge/start fixes

gamestate.lua:
- Expanded edition detection with multiple fallback paths (type, booleans, SMODS key)
- Expose edition_mult, edition_chips, edition_x_mult (via get_edition to avoid Glass contamination)
- Fallback get_chip_mult/get_chip_bonus for editions not in card.edition table
- Expose enhancement_x_mult, perma_bonus, rarity, and joker ability scoring values

buy.lua / pack.lua:
- Fix pack type detection: check hand cards dealt instead of inferring from first card's set
  (Black Hole is Spectral but appears in Celestial packs)
- Fix hand count: use min(deck_size, hand_limit) for small decks
- Add re-entrancy guard to prevent double-firing use_card during animations

sell.lua:
- Remove card count check from completion — Invisible Joker spawns replacement on sell

rearrange.lua:
- Set card.rank and trigger sort/set_ranks/align_cards for visual re-layout

start.lua:
- Wrap setup_run/exit_overlay_menu in pcall, retry with delete_run on failure
- Reset win overlay flags on new run

play.lua / discard.lua:
- Remove blockable/created_on_pause event flags
- Wait for win_overlay_dismissed before proceeding on win

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	src/lua/endpoints/sell.lua
When the bot wins a run, Balatro shows a "YOU WIN" overlay that pauses
the game. Since event handlers can't fire while paused, this adds a
two-phase dismissal in love.update that auto-dismisses the overlay so
the bot can continue into endless mode. Play endpoint now waits for
the overlay to be dismissed before returning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…anup

- Fix cash_out race condition: wait for scoring_complete() before calling
  G.FUNCS.cash_out to prevent nil round_eval crash
- Add set(debuff=true) to re-apply blind debuffs after add()
- Add highlight endpoint for toggling card highlights
- Fix voucher add to use SMODS.add_card instead of add_voucher_to_shop
- Remove sell during SMODS_BOOSTER_OPENED (simplify sell states)
- Clean up error messages (remove unnecessary usage hints)
- Remove Tag type/enum (replaced with inline tag_name/tag_effect)
- Simplify gamestate extraction, update openrpc spec and tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The scoring_complete() guard (wait for cash_out_button UI before calling
G.FUNCS.cash_out) was added to prevent a crash when cash_out was called
while scoring rows were still in-flight.

Root cause turned out to be a 5-second timeout override in the Python bot
(JackPotts bot.py) that fired on every ante 8 play at normal animation
speed. The bot would abandon requests mid-scoring, re-poll, then send
cash_out while the previous play was still animating — creating the race.

With the aggressive timeout removed from bot.py (default 30s is plenty),
cash_out is only called after scoring completes naturally. The upstream
implementation works correctly without this guard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants