Skip to content

feat!: balatrobot v2 - #155

Open
S1M0N38 wants to merge 180 commits into
mainfrom
dev
Open

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

Conversation

@S1M0N38

@S1M0N38 S1M0N38 commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

and many more... (see the ones with completed-in-dev label)

This new version will introduce breaking changes. Many part have been refactored while other are still WIP. When everything have been stabilized, we still have to

  • bumps deps (python, smods, lovely, ...)
  • update docs

@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.
S1M0N38 and others added 30 commits July 7, 2026 18:36
Add the "fair-play bot" entry to the glossary so the consumer-side
discipline the API is built to support has a single canonical name. A
fair-play bot deliberately ignores the true `value` of `hidden` cards
and models only what a human observer could know — deducing from sort
order (`sort`) and, with the upcoming `revealed` field, from the signal
that a hidden card's identity was momentarily exposed.

The term names the audience the `revealed` field serves: the API is
omniscient (see that entry), so exposing `revealed` only matters to a
consumer that has chosen not to use that omniscience. It flags what to
avoid: "honest bot" and "legitimate bot", which imply the omniscient
path is dishonest. Pinning the term now keeps the concept consistent as
observer-knowledge signals are added to the API.
Under a flip blind (e.g. The House) the whole hand is dealt face-down. Using a
conversion consumable (Magician, Death, Sigil, ...) on a hidden card triggers
the game's flip->modify->flip animation: the card is briefly shown face-up to a
human, then flipped back, ending `hidden` despite its identity having been
exposed. A fair-play bot that refuses to read hidden-card data has no signal
that this happened.

Add an optional, transient `revealed` boolean to Card.State, emitted only by
the use endpoint. It co-occurs with `hidden: true` and tells a fair-play
consumer "you may now know this card." gamestate.lua owns a module-level
revealed registry consulted by extract_card_state; use.lua snapshots the hidden
cards a conversion consumable will flip (mirroring the use_consumeable branches
on G.hand.highlighted, and the whole G.hand.cards for Sigil/Ouija) before
G.FUNCS.use_card clears highlighting, then stamps and clears the registry
around the response extraction so `revealed` never leaks into gamestate, play,
sort or buy_and_use.

Deliberately not based on ability.wheel_flipped: that is dealing-time data,
blind-coupled, and cleared as a side effect by Card:flip()'s back->front branch.

Closes #215
Add fixtures and tests for the transient `revealed` field. A deterministic
flip-blind fixture (The House keeps the whole hand face-down while no hands or
discards have been played) provisions a conversion consumable, so the reveal
scenario is reproducible without the probabilistic The Wheel. A second fixture
provisions Sigil to exercise the whole-hand conversion branch.

Tests guard: targeted cards are revealed+hidden while an untargeted hidden card
is not; the whole-hand Sigil path reveals every card; `revealed` is transient
(absent from a plain gamestate after use); a normal face-up use never stamps
revealed; and buy_and_use never emits it. A shared helper scans every card area
and tolerates the Lua->JSON empty-table->[] quirk for flag-less cards.

Closes #215
The `hands` field of GameState (Hand type, extract_hand_info) had no
test coverage. Add a TestGamestateHands class asserting all seven
sub-fields across the reachable states:

- present_at_run_start: all 12 hands are emitted at run start
- default_values: order/level/mult/chips/played/played_this_round/
  example for every hand, sourced from G.GAME.hands init
  (vendors/balatro/game.lua:2002-2013)
- level_up_via_planet: add+use Planet cards raises level/mult/chips
  per the level_up_hand formula (common_events.lua:464)
- played_counter: playing a High Card increments played counters

Reuses the existing state-SELECTING_HAND fixture; no new fixture
needed. First task of #217.
The `Card.Modifier` sub-fields (seal, edition, enhancement, eternal,
perishable, rental) were `# TODO` stubs. test_add.py already exercised
the add→modifier→response path, but only ever asserted the *named*
modifier key. Fill the stubs with authoritative gamestate-extraction
checks that assert the modifier object is EXACTLY the expected dict
(no key leakage), per family:

- seal ×4 (Red/Blue/Gold/Purple) on a playing card
- edition ×4 on playing card and joker; e_negative on consumable
- enhancement ×8 on a playing card
- eternal, perishable (1/5/10), rental on jokers
- co-occurrence: seal + edition + enhancement on one card

Reach modifiers via the `add` endpoint, reusing the existing `add`
fixtures (no new fixtures).

Investigation finding (documented, not fixed): a card with no modifiers
serializes as `[]` not `{}` — an rxi/json.lua quirk where empty Lua
tables become JSON arrays. Already pinned by
test_modifier_absent_fields; out of scope for this coverage task.

Part of #217.
The `Card.State.debuff` sub-field was a `# TODO` stub. Reach it by
injecting a debuff boss via `set`, skipping Small+Big, then selecting the
Boss — landing in SELECTING_HAND with the boss's debuff rules applied to
the dealt hand (Blind:debuff_card → card:set_debuff).

- parametrize over the four suit-debuff bosses (bl_club/bl_goad/bl_head/
  bl_window): matching-suit cards get exactly {debuff: true}; all others
  stay un-debuffed
- bl_plant: face cards (J/Q/K) get exactly {debuff: true}

The extractor is a pass-through (if card.debuff then state.debuff = true);
bl_plant guarantees a positive hit. The suit positive case is conditional
on the seed-dependent dealt hand containing that suit (an 8-card hand may
miss any one suit); negatives always run.

Add helpers _reach_boss_selecting_hand and _is_card_debuffed (robust to
the empty-state `[]` serialization quirk) for reuse by the sibling hidden
and highlight state tests.

Part of #217.
The `Card.State.hidden` sub-field was a `# TODO` stub. The extractor sets
state.hidden when card.facing == "back" (face-down), which boss blinds
trigger via Blind:stay_flipped. Reach with the same boss-injection helper:

- bl_house: hides the entire first hand (deterministic) — every card has
  exactly {hidden: true}
- bl_mark: hides face cards (J/Q/K) only — faces get exactly {hidden:
  true}, numbered cards stay visible

The probabilistic/conditional bosses (bl_wheel, bl_fish) are intentionally
not asserted. Add a _is_card_hidden helper (robust to the empty-state `[]`
serialization quirk) alongside _is_card_debuffed.

Part of #217.
blinds.boss.reroll_available is extracted by get_blinds_info behind a gate
(affordable: dollars-bankrupt_at >= 10, AND (v_retcon OR (v_directors_cut
AND not boss_rerolled))). The field's full endpoint-behavior matrix already
lives in test_reroll_boss.py; add a lean parametrized extractor-contract
test in test_gamestate.py (the field's proper home) pinning the four static
branches via the existing reroll_boss-category fixtures:

- Director's Cut + affordable ($20) → True
- Director's Cut + unaffordable ($5) → False  (affordability gate)
- no voucher → False
- Retcon + affordable ($30) → True

The post-reroll (boss_rerolled → False) and Retcon-stays-True cases remain
owned by test_reroll_boss.py; a docstring cross-references it.

Part of #217.
The top-level `challenge` field (conditionally present) had no coverage in
test_gamestate.py. The extractor guards with `if G.GAME.challenge then`, so
the challenge id is present only during a Challenge Run and omitted
(not null) otherwise. Add two tests in TestGamestateTopLevel:

- absent_in_normal_run: a non-challenge run has no challenge key at all
  (the conditional-absence contract — the gap not covered elsewhere)
- present_during_challenge_run: starting c_omelette_1 yields the bare id

test_start.py already asserts the field's value across the broader
challenge-run start matrix (deck/stake/seed/effect/conflict); a docstring
cross-references it.

Part of #217.
The `elseif ability_set == "Edition"` branch in extract_card() and the
matching `"EDITION"` value in the Card.Set enum were unreachable:
editions are stored on `card.edition` and surfaced in `modifier.edition`,
orthogonal to a card's type, so `ability.set` is never "Edition".

Verified empirically by adding edition cards of every type and edition,
then scanning the full gamestate: zero cards report set=="EDITION". The
openrpc.json CardSet spec already omitted the value.

Closes #218.
…e fixtures

Replicate G.FUNCS.unlock_all (functions/button_callbacks.lua) inside the
existing name=='BalatroBot' gate: force discovered/unlocked/alerted=true
across G.P_CENTERS/G.P_BLINDS/G.P_TAGS. all_unlocked alone bypasses
stake/challenge locks but never sets G.P_CENTERS[*].discovered, so hosts
with different profile histories (dev Mac vs fresh container) diverge on
tag/shop pools and card effect-text rendering, breaking fixture portability.

Force immediately in setup() (init_item_prototypes runs before mod load) and
re-apply via an init_item_prototypes wrapper for profile reloads. Double-
gated on profile name so a non-BalatroBot profile is never mutated.

Refs #220
Add an optional stream_port alongside the rpc port so an HLS stream port
can follow the same instance lifecycle. The field defaults to None at
every layer (Config, InstanceInfo, state file), so nothing changes unless
a port is provided — keeping the local-process platforms untouched.

- Config.stream_port: Python-only override (intentionally absent from
  ENV_MAP, so it is never serialized to BALATROBOT_* env on the host).
- InstanceInfo.stream_port + stream_url property (None when unset).
- StateFile (de)serializes stream_port; missing key reads back as None,
  so legacy state files keep resolving.
- BalatroPool allocates one stream port per instance when
  BALATROBOX_STREAM=1 and passes it through to each BalatroInstance.
- `balatrobot serve` and `balatrobot list` print the stream URL only
  when a stream port is recorded.
On Docker Desktop the published-port proxy accepts the host TCP connection
before the container's listener is ready, then resets it — surfacing as
httpx.ReadError rather than ConnectError. The retry loop only caught
ConnectError and TimeoutException, so a cold container could fail the
health check outright on the first reset instead of retrying. Catch
ReadError too, mirroring the existing connect/timeout tolerance.
Introduce the `docker` platform: instead of spawning Balatro on the host
and injecting the mod via Lovely, balatrobot drives a pre-baked
`balatrobox:latest` container (LOVE + Lovely + Steamodded + the balatrobot
mod). One container per instance; balatrobot never touches game files on
the host.

DockerLauncher.build_cmd assembles a foreground `docker run --rm -i`
argv so the existing Popen lifecycle (poll/terminate) works unchanged:
- maps the rpc port (host:container) and, when Config.stream_port is set,
  maps it to the container's internal :8080 for HLS;
- forces BALATROBOT_HOST=0.0.0.0 inside the container and forwards the
  BALATROBOT_* driving vars from config as -e flags (no blanket host-env
  forwarding);
- read-only bind-mounts local checkouts via BALATROSRC/BALATROBOT/
  DEBUGPLUS_LOCAL_REPO for dev without rebuilds, and identity-mounts
  BALATROBOT_DOCKER_MOUNTS paths read-write (used by the test suite to
  expose fixtures/temp dirs, since load/save open the exact path string);
- forwards BALATROBOX_* and *_GITHUB_* fetch vars only when set.
  BALATROBOX_PLATFORM is intentionally ignored (build/run-arch concern).

validate_paths fails fast when the docker CLI or the image is missing
(the latter with a `docker build` hint). Register `docker` in the
platform dispatcher and the serve --platform choices.

Wire the lua test suite to set BALATROBOT_DOCKER_MOUNTS to the repo root
when BALATROBOT_PLATFORM=docker, so the container can see tests/fixtures.
Document the platform in docs/cli.md and add glossary terms to CONTEXT.md.
Pin --basetmp=.pytest_tmp so pytest writes its per-session temp trees
into the repo (gitignored) instead of the system tmp dir. Keeps test
artifacts alongside the project, easier to inspect and clean up, and
stops them from littering $TMPDIR across runs.
create_temp_save_path() has no callers anywhere in the suite, so its
tempfile/uuid imports are dead weight. Remove the helper and the two
imports it was the sole user of.
Rejoin the split `and` chain for G.PROFILES[G.SETTINGS.profile] back to
one line. Pure formatting; no behavioral change to the BalatroBot
profile gate in init_item_prototypes.
Under docker, BALATROBOT_LOG_DIR (screenshot logging, JSONL recording)
must both exist inside the container and be known to the Lua mod for
writes to reach the host. BaseLauncher now exposes instance_dir on
start(), and DockerLauncher identity-mounts it read-write and forwards
BALATROBOT_LOG_DIR from it when set. Local platforms ignore the
attribute and keep using the env dict.
Drop the runtime python multiprocessing.cpu_count() probe (and the
MAX_XDIST knob) in favour of explicit per-platform defaults: CLI 2,
LUA 6 on darwin / 4 under docker / 2 otherwise. Removes a python
dependency from the Makefile and makes worker counts predictable
instead of hardware-dependent.
Expose G.GAME.last_tarot_planet — the key of the last Tarot or Planet
used this run — so consumers can reason about The Fool, which creates
exactly that card. The field is nil in init_game_object until first
Tarot/Planet use (set in misc_functions.lua), so it is conditionally
absent rather than null, mirroring challenge/seed.

Documented in OpenRPC, types.lua, and api.md.

Co-authored-by: Brent Garber <me@bgarber.work>
Pin the last_tarot_planet extractor contract across four behaviours:
the key is absent on a fresh run, appears after using a Tarot
(c_hermit) or a Planet (c_pluto), and The Fool creates exactly the
card it names. Hermit and Pluto need no card target, so they are used
bare inline — no new fixture is required.

Co-authored-by: Brent Garber <me@bgarber.work>
Add a Hand.Name alias for the 12 vanilla poker-hand names and reuse it
two ways: tighten the GameState.hands key type (string -> Hand.Name)
and add round.most_played_hand, the poker hand played most this run and
the per-round target a bot must avoid under The Ox. The value defaults
to 'High Card' and is only recomputed when a Boss blind is defeated.

Royal Flush is a display alias of Straight Flush, never a key in
G.GAME.hands, so it is excluded from the enum.

Documented in OpenRPC, types.lua, enums.lua, and api.md.
Pin the Hand.Name contract across four behaviours: round.most_played_hand
is present and a valid Hand.Name at SELECTING_HAND, defaults to
'High Card' before any Boss blind is defeated, recomputes to the
most-played hand after a Boss defeat (driven by playing a Pair), and
every hands key is a Hand.Name member (Royal Flush never appears).
last_tarot_planet is set only when a Tarot or Planet is used (guarded by
the `center.set == 'Tarot' or 'Planet'` check in misc_functions.lua), yet
it was typed as a bare `string` in both the lua annotation and the
openrpc.json schema. Any string admitted typos and even Spectral keys
that can never occur.

Tighten to a union of the existing closed enums:
- types.lua: `string?` -> `(Card.Key.Consumable.Tarot | Card.Key.Consumable.Planet)?`
- openrpc.json: `"type": "string"` -> `anyOf[TarotKey, PlanetKey]`

The single consumer is The Fool, which copies this key. Note that 'c_fool'
is a valid attainable value (using The Fool self-sets it); can_use then
gates it, so it stays within the union. Zero runtime/data change; this is
type-honesty only.
Update dev dependencies to their latest releases:
- ruff 0.15.14 -> 0.16.0
- ty 0.0.40 -> 0.0.64

Bumps both the lower-bound constraints in pyproject.toml and
the locked versions in uv.lock.
Exclude .pi-subagents/artifacts, generated by the pi-subagents
tooling, so it does not pollute the working tree or diffs.
Ruff 0.16 expanded its default rule set (59 -> 413 enabled rules),
surfacing many new violations. Apply safe auto-fixes and resolve the
rest so `make quality` passes clean on the upgraded tools.

- Modernize via UP rules: TimeoutError aliases, datetime.UTC,
  collections.abc generics, dropped redundant default type args
- Annotate async context managers with Self (PYI034)
- Pass explicit check=False on fire-and-forget subprocess calls
- Rewrite set(gen) as comprehensions, drop mutable default arg,
  use sys.exit, remove redundant global/return/shebang
- Suppress intentional patterns with noqa: broad CLI catches
  (BLE001), local-time session timestamps (DTZ005), and one-shot
  process spawns (ASYNC220/230)
- Fix ty diagnostics: use a real Path instead of mocking __str__,
  and drop now-unused ty:ignore directives
mdformat (run via `make format`) reflows the requests.post call and
adjusts blank-line spacing in the example snippet.
Expose G.GAME.starting_deck_size as a top-level integer on the
gamestate payload. The field is the deck-size baseline set at run
start (52 for a standard deck), which the Erosion joker's mult
formula references.

Always present during a run (initialized by init_game_object to 52
and reaffirmed to #G.playing_cards at run start), so it is assigned
directly alongside the other always-present scalars (won, money)
rather than guarded as conditionally absent. Documented in types.lua
and openrpc.json alongside the extraction.

Co-authored-by: Brent Garber <me@bgarber.work>
Expose the per-round targets that The Idol, Mail-In Rebate, Ancient
Joker, Castle, and To Do List pick, so bots can plan plays without
parsing the localized effect text.

The four card targets use inline table types with their exact shape
(idol: suit+rank; mail: rank; ancient/castle: suit). To Do List is a
Hand.Name[] list because each owned copy carries its own target.

Co-authored-by: Brent Garber <me@bgarber.work>
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