Skip to content

feat!: balatrobot v2#155

Open
S1M0N38 wants to merge 121 commits into
mainfrom
dev
Open

feat!: balatrobot v2#155
S1M0N38 wants to merge 121 commits into
mainfrom
dev

Conversation

@S1M0N38 S1M0N38 changed the title BalatroBot v2 feat!: balatrobot v2 Feb 24, 2026
@S1M0N38 S1M0N38 marked this pull request as ready for review February 25, 2026 12:09
Copilot AI review requested due to automatic review settings February 25, 2026 12:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces version 2 of the balatrobot API with breaking changes (!). The main focus is on restructuring how tags are represented in the game state and improving error messages across all endpoints to be more actionable and helpful.

Changes:

  • Restructured tag representation from flat tag_name/tag_effect fields to nested tag objects with key, name, and effect fields
  • Added tags array to gamestate for tracking accumulated player-owned tags
  • Enhanced error messages across all endpoints with actionable guidance (e.g., suggesting reroll, sell, etc.)
  • Added support for selling jokers when Buffoon packs are open (SMODS_BOOSTER_OPENED state)
  • Implemented voucher effect extraction using game's localize function
  • Added comprehensive Tag enum definitions and test coverage

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/lua/utils/types.lua Updated Blind type to use nested Tag object instead of flat tag_name/tag_effect fields; added Tag class definition
src/lua/utils/openrpc.json Updated OpenRPC schema to reflect Tag object structure and enhanced sell endpoint description
src/lua/utils/gamestate.lua Implemented voucher effect extraction, tag ownership tracking, and updated blind tag structure
src/lua/utils/enums.lua Added comprehensive Tag.Key enum definitions for all Balatro tag types
src/lua/endpoints/sell.lua Added support for SMODS_BOOSTER_OPENED state with Buffoon pack validation
src/lua/endpoints/skip.lua Enhanced error message with actionable guidance
src/lua/endpoints/buy.lua Enhanced error messages with actionable guidance
src/lua/endpoints/add.lua Updated to support pack additions and refactored voucher handling to use dedicated SMODS function
src/lua/endpoints/use.lua Enhanced error messages with actionable guidance
src/lua/endpoints/play.lua Enhanced error message with actionable guidance
src/lua/endpoints/discard.lua Enhanced error messages with actionable guidance
src/lua/endpoints/pack.lua Enhanced error messages with actionable guidance
tests/lua/endpoints/test_skip.py Added tests for tag accumulation after skipping blinds
tests/lua/endpoints/test_pack.py Added tests for selling jokers during Buffoon pack selection
tests/lua/endpoints/test_gamestate.py Added comprehensive test coverage for voucher effects and tag structure
tests/lua/endpoints/test_buy.py Updated error message expectations
tests/lua/endpoints/test_add.py Updated error message expectations
docs/api.md Updated documentation to reflect new Tag structure and enhanced endpoint descriptions
Comments suppressed due to low confidence (4)

src/lua/endpoints/add.lua:409

  • The comment says "For jokers and consumables" but this else branch will also execute for vouchers and packs, creating unnecessary params that won't be used. Consider adding an explicit check: elseif card_type == "joker" or card_type == "consumable" then to match the comment and avoid creating unused params for vouchers and packs.
    else
      -- For jokers and consumables - just pass the key
      params = {
        key = args.key,
        skip_materialize = true,
        stickers = {},
        force_stickers = true,
      }

      -- Add edition if provided
      if edition_value then
        params.edition = edition_value
      end

      -- Add eternal if provided (jokers only - validation already done)
      if args.eternal then
        params.stickers[#params.stickers + 1] = "eternal"
      end

      -- Add perishable if provided (jokers only - validation already done)
      if args.perishable then
        params.stickers[#params.stickers + 1] = "perishable"
      end

      -- Add rental if provided (jokers only - validation already done)
      if args.rental then
        params.stickers[#params.stickers + 1] = "rental"
      end
    end

tests/lua/endpoints/test_skip.py:43

  • Grammar issue in comment: "because it used immediately" should be "because it is used immediately"
        assert "tag_investment" not in gamestate["tags"]  # because it used immediately

tests/lua/endpoints/test_skip.py:53

  • Grammar issue in comment: "because it used immediately" should be "because it is used immediately"
        assert "tag_investment" not in gamestate["tags"]  # because it used immediately

src/lua/utils/types.lua:58

  • Typo: "bilnd" should be "blind"
---@field status Blind.Status Status of the bilnd

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/lua/endpoints/test_skip.py Outdated
Comment on lines +52 to +53
assert gamestate["tags"][0]["key"] == "tag_polychrome"
assert "tag_investment" not in gamestate["tags"] # because it used immediately

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file has a bug that will cause test_skip_big_boss to fail. The test at line 54-58 (not shown in diff) expects the error message "Cannot skip Boss blind" but skip.lua line 39 now returns "Cannot skip Boss blind. Use select to select and play the boss blind." The expected error message in the test needs to be updated to match the new implementation.

Copilot uses AI. Check for mistakes.
Comment thread tests/lua/endpoints/test_skip.py Outdated
assert gamestate["blinds"]["big"]["status"] == "SKIPPED"
assert gamestate["blinds"]["boss"]["status"] == "SELECT"
assert gamestate["tags"][0]["key"] == "tag_polychrome"
assert "tag_investment" not in gamestate["tags"] # because it used immediately

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is checking if the string "tag_investment" is in a list of tag objects. Since gamestate["tags"] is a list of objects (each with "key", "name", "effect" fields), the in operator will never find a string match. This should likely be checking if any tag in the list has key == "tag_investment", such as: assert not any(tag["key"] == "tag_investment" for tag in gamestate["tags"])

Copilot uses AI. Check for mistakes.
Comment thread tests/lua/endpoints/test_skip.py Outdated
assert gamestate["state"] == "BLIND_SELECT"
assert gamestate["blinds"]["boss"]["status"] == "SELECT"
assert gamestate["tags"][0]["key"] == "tag_polychrome"
assert "tag_investment" not in gamestate["tags"] # because it used immediately

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is checking if the string "tag_investment" is in a list of tag objects. Since gamestate["tags"] is a list of objects (each with "key", "name", "effect" fields), the in operator will never find a string match. This should likely be checking if any tag in the list has key == "tag_investment", such as: assert not any(tag["key"] == "tag_investment" for tag in gamestate["tags"])

Copilot uses AI. Check for mistakes.
S1M0N38 added 22 commits June 11, 2026 19:23
Previously, used_vouchers extracted descriptions from static
voucher_data.description which was unreliable. Now uses
get_voucher_effect() that fetches effect text via the game's
localize() function with proper loc_vars for each voucher type.

Also adds strip_color_codes() helper and comprehensive parametrized
tests covering all 32 voucher types.

Closes #154.
Improve error messages across 6 endpoint files by adding actionable
guidance to help bots self-heal from failed tool calls.

Changes:
- buy.lua: Add endpoint suggestions for empty shop/slot errors
- use.lua: Add card parameter guidance for consumable errors
- discard.lua/play.lua: Add card limit suggestions
- pack.lua: Add pack buying and target selection hints
- skip.lua: Add boss blind selection suggestion
- Update test_buy.py to match new error messages

Closes #148.
- Remove .claude/ directory (settings.json, skills/balatrobot/SKILL.md)
- Remove CLAUDE.md in favor of AGENTS.md
- Remove .mux/ directory (init, mcp.jsonc, tool_env, tool_post)
- Remove .mdformat.toml (flags moved to Makefile)
- Add AGENTS.md with project structure and rules
- Add CONTEXT.md with glossary of domain terms
- Add .agents/skills/balatrobot/SKILL.md for pi skill
Replace verbose boilerplate with minimal, curated entries covering
macOS, Python, Lua, and project-specific ignores.
Inline --number and --exclude flags since the config file was removed.
Remove integration marker from pyproject.toml markers config.
The integration marker is no longer used. Remove auto-marking hooks
from conftest files and the @pytest.mark.integration decorator.
Rename BalatroInstance module to match its primary export.
Update import paths in tests.
Introduce BalatroPool with start/stop lifecycle, automatic port
allocation, fail-fast cleanup, and async context-manager support.
Includes InstanceInfo frozen dataclass for connection metadata.
StateFile wraps BalatroPool with a JSON state file (Jupyter pattern).
Atomic write on pool start, delete on stop. Supports PID-based liveness
checks, stale-file cleanup, and resolve-by-host:port or index.

Add platformdirs dependency for cross-platform state directory.
Replace single BalatroInstance with pool-based serve. Adds -n /
--num-instances flag for launching multiple instances. State file
is written on start and cleaned up on exit.
- Switch recommended settings profile from "fast" to "turbo"
- Remove env var mapping reference (duplicated in config.py)
- Remove BalatroBot profile requirement note (outdated)
- Add quotes around glob patterns in --requests examples
Reorder API docs to surface method lookup first
S1M0N38 and others added 13 commits June 11, 2026 19:42
Remove extra whitespace between "read" and the filename in
the API method reference paragraph.
Annotate the `instance` fixture parameter as `InstanceInfo`
for consistency with other tests in the same file.
Automates the full triage workflow: fetch issue data from GitHub,
download attachments, create a branch, reproduce the bug, and
generate an HTML investigation report. Includes a Tokyo Night
themed report template with verdict badges.
Selling an Invisible Joker at 2/2 charges causes the sell endpoint
to hang indefinitely because its on-sell ability duplicates a random
joker, keeping the card count unchanged. This test reproduces the
exact scenario from issue #195 and currently fails (red phase).
Replace the completion condition in the sell endpoint that checked
card_count == initial_count - 1 with card.removed == true. The old
check failed when jokers like Invisible Joker modify the card area
on sell (e.g. duplicating a neighbor keeps the count unchanged).

The card_gone loop is also removed as card.removed makes it
redundant -- it is an O(1) property on the specific card instance,
immune to any current or future joker side effects.

Closes #195
Replace custom ALL-CAPS enhancement values (BONUS, MULT, etc.) with
Balatros in-game registry keys (m_bonus, m_mult, etc.) so the API
exposes real G.P_CENTERS keys instead of invented aliases.

This is step 1 of the enum migration plan (ENUM_MIGRATION_PLAN.md).
Seal, Edition, Deck, and Stake are untouched (later steps).

Source changes:
- enums.lua: update Card.Modifier.Enhancement alias to m_* values
- add.lua: remove ENHANCEMENT_MAP, validate via m_ prefix and
  G.P_CENTERS lookup instead of table mapping
- gamestate.lua: use card.config.center_key instead of fragile
  ability.effect string parsing
- openrpc.json: update Enhancement const values
- docs/api.md: update Enhancement enum table

Closes #194.

Co-authored-by: icebear <icebear0828@users.noreply.github.com>
Replace custom ALL-CAPS edition values (HOLO, FOIL, POLYCHROME,
NEGATIVE) with Balatros in-game registry keys (e_holo, e_foil,
e_polychrome, e_negative) so the API exposes real G.P_CENTERS
keys instead of invented aliases.

This is step 2 of the enum migration plan (ENUM_MIGRATION_PLAN.md).
Enhancement was migrated in step 1. Seal, Deck, and Stake are
untouched (later steps).

Source changes:
- enums.lua: update Card.Modifier.Edition alias to e_* values
- add.lua: remove EDITION_MAP, validate via e_ prefix check
  instead of table mapping
- gamestate.lua: use card.edition.key instead of
  string.upper(card.edition.type)
- openrpc.json: update Edition const values and param description
- types.lua: update annotation comment
- docs/api.md: update Edition enum table and curl example
Replace custom ALL-CAPS seal values (RED, BLUE, GOLD, PURPLE) with
Balatro's in-game G.P_SEALS keys (Red, Blue, Gold, Purple) so the
API reads and writes card.seal with zero conversion.

This is step 3 of the enum migration plan. Steps 1 (Enhancement) and
2 (Edition) were completed in previous commits. Deck and Stake remain
unmigrated (later steps).

Changes:
- enums.lua: update Card.Modifier.Seal alias to Capitalized values
- add.lua: remove SEAL_MAP lookup table, validate against known
  Capitalized values directly instead
- gamestate.lua: remove string.upper() wrapping on card.seal
- openrpc.json: update Seal const values to Capitalized
- docs/api.md: update Seal enum table
- tests: update seal test values from ALL-CAPS to Capitalized
Replace custom ALL-CAPS deck values (RED, BLUE, YELLOW, etc.) with
Balatro's in-game G.P_CENTERS keys (b_red, b_blue, b_yellow, etc.) so
the API reads, writes, and starts runs using real game registry keys
instead of invented aliases.

This is step 4 of the enum migration plan. Stake remains unmigrated
(final step).

Source changes:
- enums.lua: update Deck alias to b_* values
- start.lua: remove DECK_ENUM_TO_NAME map, validate via G.P_CENTERS
  lookup instead, use deck_data.key for matching
- gamestate.lua: remove DECK_KEY_TO_NAME map and get_deck_name(),
  return raw deck_key from selected_back.effect.center.key
- openrpc.json: update Deck const values to b_*
- add.lua: remove extraneous blank line

Documentation:
- docs/api.md: update Deck enum table
- docs/cli.md: update start example to use b_red
- docs/example-bot.md: update start example to use b_red

Tests:
- tests/fixtures/fixtures.json: update deck params and fixture names
- tests/lua/endpoints/test_gamestate.py: update fixture refs and
  assertions
- tests/lua/endpoints/test_pack.py: update start param
- tests/lua/endpoints/test_start.py: update params and assertions
- tests/cli/test_api_cmd.py: update start param
Replace the `balatrobot api health` guard with the Python fixture
generator's own state-file resolution via StateFile.resolve(), and
remove the stale --fast flag hint in favor of --settings turbo
--debug --render headfull.

The hardcoded HOST/PORT constants in generate.py are replaced with
dynamic host/port discovery from ~/.balatrobot/state.json, matching
how `balatrobot api` resolves the target server.
Replace custom ALL-CAPS stake values (WHITE, RED, GREEN, etc.) with
Balatro's in-game G.P_STAKES keys (stake_white, stake_red, stake_green,
etc.) so the API reads, writes, and starts runs using real game registry
keys instead of invented aliases.

This is the final step of the enum migration plan (ENUM_MIGRATION_PLAN.md).
Enhancement (m_*), Edition (e_*), Seal (Capitalized), and Deck (b_*)
were completed in previous commits.

Source changes:
- enums.lua: update Stake alias to stake_* values
- start.lua: remove STAKE_ENUM_TO_NUMBER lookup table, use
  G.P_STAKES[args.stake] with .order/.stake_level fallback
- gamestate.lua: remove STAKE_LEVEL_TO_NAME map and get_stake_name(),
  iterate G.P_STAKES to find matching key by order/stake_level
- openrpc.json: update Stake const values to stake_*

Documentation:
- docs/api.md: update Stake enum table and curl examples

Tests:
- tests/fixtures/fixtures.json: update stake params and fixture names
- tests/lua/endpoints/test_gamestate.py: update fixture refs and
  assertions
- tests/lua/endpoints/test_pack.py: update start param
- tests/lua/endpoints/test_start.py: update params and assertions
- tests/cli/test_api_cmd.py: update start param

Closes #195.
Update all test references from ALL-CAPS (WHITE, RED) to stake_*
(stake_white, stake_red) to match the Stake enum migration.

Fixture keys and start params across all test suites now use the
G.P_STAKES-compatible keys instead of the removed custom aliases.
Wrap the long start() call across multiple lines for consistency with the
project's line-length conventions.
S1M0N38 and others added 5 commits June 12, 2026 15:07
Replace the verbose reference-style layout — one section per command, each
with --help invocation, typical usage, and all flags enumerated — with a
compact workflow-oriented format. The new layout shows the common
serve→api→stop cycle at a glance, then provides terse per-command summaries.

Remove --help commands (users can run those themselves), redundant prose,
and rarely-used flag descriptions. Emphasize auto-discovery via state file
so agents never reach for --host/--port. Merge the old standalone
`api --requests` section under `api`.

This matches the skill's purpose: a quick-start card for agents, not a
reference manual.
Add the filename pattern and dotted-path expansion rules for fixture
keys so contributors can construct or debug fixture files without
reading the generator code.
When the deck has fewer cards than the hand limit, the buy endpoint
waits forever for a full hand that can never arrive. Use the actual
deck size (instead of the hardcoded hand limit) as the target, and
change the equality check to >= so partial hands are accepted.

Closes #198

Co-authored-by: DrLatBC <drlatbc@gmail.com>
Add a fixture that reproduces the SHOP state with a thin deck (2 cards,
< hand_limit) and an Arcana Pack available. The test buys pack[1]
and asserts the gamestate response is valid, confirming the endpoint
no longer hangs.

Co-authored-by: DrLatBC <drlatbc@gmail.com>
Replace the first-card ability.set heuristic with SMODS.OPENED_BOOSTER's
authoritative draw_hand flag in both buy.lua and pack.lua.

The old code inferred pack type from the first card's set (Tarot/Spectral),
but Black Hole (set=Spectral) can appear in Celestial packs via the soul
mechanism (0.3% chance), causing needs_hand=true and an indefinite hang
since Celestial packs never deal hand cards.

Closes #199

Co-authored-by: DrLatBC <drlatbc@gmail.com>
S1M0N38 added 8 commits June 15, 2026 08:46
Winning the ante-8 boss triggers win_game(), which pauses the game and
raises the win overlay AFTER play() had already returned won=true. The
bot was left in a paused session where every subsequent endless-mode
endpoint ran on wall-clock REAL time instead of turbo.

The polling event never recovered from the pause because event.lua
ignores `created_on_pause` in config (it only honours `pause_force`).
Switch to pause_force=true, and dismiss the overlay inside the event
once ROUND_EVAL is entered (G.round_eval already exists; the delayed
win events guard against a nil G.OVERLAY_MENU). The won path now flows
through the existing cash_out_button checkpoint before responding.
Drive the win cycle live instead of via save/load fixtures: `load`
resets the run and discards the paused/overlay state, which masked
the bug entirely. The play test asserts an endless play stays
responsive (elapsed < 5s) rather than crawling on wall-clock time;
the gamestate test asserts won=true persists across the
cash_out/next_round/select cycle. Removes the now-unused
state-SELECTING_HAND--won-true fixture that could not capture the
paused/overlay state.
5s is enough for the server health check on the turbo profile; the
previous 10s wait added needless delay to the quick-reference example.
Expose G.SETTINGS.paused as GameState.paused so callers can detect a
session stuck behind a blocking overlay (win screen, pause menu, game
over). Previously the only externally observable symptom of such a
stuck state was wall-clock speed — a signal no clean assertion could
rely on. Added to the gamestate extractor, type definition, OpenRPC
schema, and API docs.
The endless-mode regression test asserted elapsed < 5s, a flaky
wall-clock check that measures speed rather than game state. Rewrite
it to assert paused=false on the endless play via the new GameState
field. Verified red against the pre-fix play.lua (paused=true failure)
and green with the fix restored.
Adopt mike so each release keeps a frozen snapshot while main and dev
stay live. The version selector (extra.version.provider: mike) reads
versions.json that mike generates on the gh-pages branch.

Routing on push:
- main  -> /latest  (plus root redirect via set-default)
- v*    -> /<version> (frozen per-tag snapshot)
- dev   -> /dev

The old `mkdocs gh-deploy --force` wiped gh-pages on every push, which
is incompatible with keeping multiple versions side by side. Mike takes
over, committing one directory per version and leaving prior versions
untouched. fetch-depth: 0 lets mike read and extend gh-pages history,
and the mkdocs-material build cache is restored.

Verified locally by dry-running all three routes against a throwaway
branch: each produced its own version directory, versions.json listed
all entries, and the root index.html redirects to /latest.
Split overloaded "profile" term into "save profile" (Balatro's
numbered in-game save slots loaded from .jkr) and "runtime profile"
(named settings directories under src/lua/profiles/). Updated
"BalatroBot profile" to reference the new terminology and added
"Avoid" notes for deprecated terms. This clarifies the distinction
between Balatro's native save system and our mod's settings override
mechanism.
Remove unnecessary command substitution from git commit heredoc
example. Using -F - to read from stdin is simpler and more
direct than wrapping the heredoc in $(cat ...).
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