Expose scoring-critical game state + endpoint robustness fixes#181
Open
Expose scoring-critical game state + endpoint robustness fixes#181
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 useedition.key(e.g.e_holo). This expands detection to handle all three formats with fallbacks viaget_chip_mult()andget_chip_bonus()for edge cases where the edition isn't in thecard.editiontable at all.Additionally, the numeric scoring values are now exposed:
edition_mult— Holographic mult bonusedition_chips— Foil chip bonusedition_x_mult— Polychrome x-mult (sourced fromget_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 fromcard.ability.extraand config fields. Includest_mult,t_chips,mult,x_mult,driver_tally,loyalty_remaining, and any fields fromability.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 inG.GAME.current_round.most_played_poker_handand 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_orderis never updated in the selection loop, making ties depend on Lua'spairs()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
set—blindparameterAccepts 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 fromG.FUNCS.reroll_bossinbutton_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):exit_overlay_menu()to dismiss itThe 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 HangThe sell endpoint checked
count_decreasedas 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 oncard_gone + money_increasedwhich uniquely identifies completion regardless of spawn behavior.buy/pack— Black Hole Pack Type MisdetectionBoth 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 isset=Spectralbut 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.cardsexists and is non-empty).buy— Small Deck Hand Count AssumptionWhen waiting for hand cards during pack opening, the endpoint assumed the hand would always have
hand_limitcards. With small decks (Abandoned deck, late-game thin decks), fewer cards exist than the hand limit allows. Fix: usemath.min(deck_size, hand_limit)as the expected count.pack— Re-Entrancy Double-FireRapid or automated pack selections could trigger
use_cardwhile a previous selection's animation (e.g. Black Hole's planet upgrade sequence) was still processing, causing corrupted state. Fix: added aselection_in_progressmodule-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 MoveThe 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.rankand triggerstable.sort+set_ranks()+align_cards(), mirroring the gamepad d-pad reordering logic fromcontroller.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 StateStarting a new run immediately after a game over could fail if the previous run's state wasn't fully cleaned up. Fix: wraps
setup_runandexit_overlay_menuinpcall, and ifstart_runfails, callsdelete_run()to clean up stale state before retrying. Bug introduced by endless mode (i think?), so fixing it.play/discard— Event Flag CleanupRemoved
blockable=falseandcreated_on_pause=truefrom 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
mainthat haven't been merged intodevyet (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.