Skip to content

Commit fe324a5

Browse files
committed
Merge remote-tracking branch 'upstream/main' into buy-card
2 parents b45bd54 + b7df1f1 commit fe324a5

File tree

11 files changed

+389
-71
lines changed

11 files changed

+389
-71
lines changed

.github/workflows/release_please.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
on:
22
push:
33
branches: [main]
4-
pull_request:
5-
branches: [main]
64
workflow_dispatch:
75
workflow_run:
86
workflows: ["Code Quality"]

docs/developing-bots.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ BalatroBot allows you to create automated players (bots) that can play Balatro b
77
A bot is a finite state machine that implements a sequence of actions to play the game.
88
The bot can be in one state at a time and have access to a set of functions that can move the bot to other states.
99

10-
| **State** | **Description** | **Functions** |
11-
| ---------------- | -------------------------------------------- | ---------------------- |
12-
| `MENU` | The main menu | `start_run` |
13-
| `BLIND_SELECT` | Selecting or skipping the blind | `skip_or_select_blind` |
14-
| `SELECTING_HAND` | Selecting cards to play or discard | `play_hand_or_discard` |
15-
| `ROUND_EVAL` | Evaluating the round outcome and cashing out | `cash_out` |
16-
| `SHOP` | Buy items and move to the next round | `shop` |
17-
| `GAME_OVER` | Game has ended ||
10+
| **State** | **Description** | **Functions** |
11+
| ---------------- | -------------------------------------------- | ---------------------------------------- |
12+
| `MENU` | The main menu | `start_run` |
13+
| `BLIND_SELECT` | Selecting or skipping the blind | `skip_or_select_blind` |
14+
| `SELECTING_HAND` | Selecting cards to play or discard | `play_hand_or_discard`, `rearrange_hand` |
15+
| `ROUND_EVAL` | Evaluating the round outcome and cashing out | `cash_out` |
16+
| `SHOP` | Buy items and move to the next round | `shop` |
17+
| `GAME_OVER` | Game has ended | |
1818

1919
Developing a bot boils down to provide the action name and its parameters for each of the
2020

docs/protocol-api.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@ All communication uses JSON messages with a standardized structure. The protocol
4646
"name": "function_name",
4747
"arguments": {
4848
"param1": "value1",
49-
"param2": [
50-
"array",
51-
"values"
52-
]
49+
"param2": ["array", "values"]
5350
}
5451
}
5552
```
@@ -86,14 +83,14 @@ The BalatroBot API operates as a finite state machine that mirrors the natural f
8683

8784
The game progresses through these states in a typical flow: `MENU``BLIND_SELECT``SELECTING_HAND``ROUND_EVAL``SHOP``BLIND_SELECT` (or `GAME_OVER`).
8885

89-
| State | Value | Description | Available Functions |
90-
| ---------------- | ----- | ---------------------------- | ---------------------- |
91-
| `MENU` | 11 | Main menu screen | `start_run` |
92-
| `BLIND_SELECT` | 7 | Selecting or skipping blinds | `skip_or_select_blind` |
93-
| `SELECTING_HAND` | 1 | Playing or discarding cards | `play_hand_or_discard` |
94-
| `ROUND_EVAL` | 8 | Round completion evaluation | `cash_out` |
95-
| `SHOP` | 5 | Shop interface | `shop` |
96-
| `GAME_OVER` | 4 | Game ended | `go_to_menu` |
86+
| State | Value | Description | Available Functions |
87+
| ---------------- | ----- | ---------------------------- | ---------------------------------------- |
88+
| `MENU` | 11 | Main menu screen | `start_run` |
89+
| `BLIND_SELECT` | 7 | Selecting or skipping blinds | `skip_or_select_blind` |
90+
| `SELECTING_HAND` | 1 | Playing or discarding cards | `play_hand_or_discard`, `rearrange_hand` |
91+
| `ROUND_EVAL` | 8 | Round completion evaluation | `cash_out` |
92+
| `SHOP` | 5 | Shop interface | `shop` |
93+
| `GAME_OVER` | 4 | Game ended | `go_to_menu` |
9794

9895
### Validation
9996

@@ -119,6 +116,7 @@ The BalatroBot API provides core functions that correspond to the main game acti
119116
| `start_run` | Starts a new game run with specified configuration |
120117
| `skip_or_select_blind` | Handles blind selection - either select the current blind to play or skip it |
121118
| `play_hand_or_discard` | Plays selected cards or discards them |
119+
| `rearrange_hand` | Reorders the current hand according to the supplied index list |
122120
| `cash_out` | Proceeds from round completion to the shop phase |
123121
| `shop` | Performs shop actions. Currently supports proceeding to the next round |
124122

@@ -131,6 +129,7 @@ The following table details the parameters required for each function. Note that
131129
| `start_run` | `deck` (string): Deck name<br>`stake` (number): Difficulty level 1-8<br>`seed` (string, optional): Seed for run generation<br>`challenge` (string, optional): Challenge name |
132130
| `skip_or_select_blind` | `action` (string): Either "select" or "skip" |
133131
| `play_hand_or_discard` | `action` (string): Either "play_hand" or "discard"<br>`cards` (array): Card indices (0-indexed, 1-5 cards) |
132+
| `rearrange_hand` | `cards` (array): Card indices (0-indexed, exactly `hand_size` elements) |
134133
| `shop` | `action` (string): Shop action to perform ("next_round") |
135134

136135
### Errors

src/lua/api.lua

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,84 @@ API.functions["play_hand_or_discard"] = function(args)
490490
}
491491
end
492492

493+
---Rearranges the hand based on the given card indices
494+
---Call G.FUNCS.rearrange_hand(new_hand)
495+
---@param args RearrangeHandArgs The card indices to rearrange the hand with
496+
API.functions["rearrange_hand"] = function(args)
497+
-- Validate required parameters
498+
local success, error_message, error_code, context = validate_request(args, { "cards" })
499+
500+
if not success then
501+
---@cast error_message string
502+
---@cast error_code string
503+
API.send_error_response(error_message, error_code, context)
504+
return
505+
end
506+
507+
-- Validate current game state is appropriate for rearranging cards
508+
if G.STATE ~= G.STATES.SELECTING_HAND then
509+
API.send_error_response(
510+
"Cannot rearrange hand when not selecting hand",
511+
ERROR_CODES.INVALID_GAME_STATE,
512+
{ current_state = G.STATE }
513+
)
514+
return
515+
end
516+
517+
-- Validate number of cards is equal to the number of cards in hand
518+
if #args.cards ~= #G.hand.cards then
519+
API.send_error_response(
520+
"Invalid number of cards to rearrange",
521+
ERROR_CODES.PARAMETER_OUT_OF_RANGE,
522+
{ cards_count = #args.cards, valid_range = tostring(#G.hand.cards) }
523+
)
524+
return
525+
end
526+
527+
-- Convert incoming indices from 0-based to 1-based
528+
for i, card_index in ipairs(args.cards) do
529+
args.cards[i] = card_index + 1
530+
end
531+
532+
-- Create a new hand to swap card indices
533+
local new_hand = {}
534+
for _, old_index in ipairs(args.cards) do
535+
local card = G.hand.cards[old_index]
536+
if not card then
537+
API.send_error_response(
538+
"Card index out of range",
539+
ERROR_CODES.PARAMETER_OUT_OF_RANGE,
540+
{ index = old_index, max_index = #G.hand.cards }
541+
)
542+
return
543+
end
544+
table.insert(new_hand, card)
545+
end
546+
547+
G.hand.cards = new_hand
548+
549+
-- Update each card's order field so future sort('order') calls work correctly
550+
for i, card in ipairs(G.hand.cards) do
551+
card.config.card.order = i
552+
if card.config.center then
553+
card.config.center.order = i
554+
end
555+
end
556+
557+
---@type PendingRequest
558+
API.pending_requests["rearrange_hand"] = {
559+
condition = function()
560+
return G.STATE == G.STATES.SELECTING_HAND
561+
and #G.E_MANAGER.queues.base < EVENT_QUEUE_THRESHOLD
562+
and G.STATE_COMPLETE
563+
end,
564+
action = function()
565+
local game_state = utils.get_game_state()
566+
API.send_response(game_state)
567+
end,
568+
}
569+
end
570+
493571
---Cashes out from the current round to enter the shop
494572
---Call G.FUNCS.cash_out() to cash out from the current round to enter the shop.
495573
---@param _ table Arguments (not used)

src/lua/log.lua

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,71 @@ function LOG.hook_toggle_shop()
190190
sendDebugMessage("Hooked into G.FUNCS.toggle_shop for logging", "LOG")
191191
end
192192

193+
-- -----------------------------------------------------------------------------
194+
-- hand_rearrange Hook
195+
-- -----------------------------------------------------------------------------
196+
197+
---Hooks into CardArea:align_cards for hand reordering detection
198+
function LOG.hook_hand_rearrange()
199+
local original_function = CardArea.align_cards
200+
local previous_order = {}
201+
CardArea.align_cards = function(self, ...)
202+
-- Only monitor hand cards
203+
---@diagnostic disable-next-line: undefined-field
204+
if self.config and self.config.type == "hand" and self.cards then
205+
-- Call the original function with all arguments
206+
local result = original_function(self, ...)
207+
208+
---@diagnostic disable-next-line: undefined-field
209+
if self.config.card_count ~= #self.cards then
210+
-- We're drawing cards from the deck
211+
return result
212+
end
213+
214+
-- Capture current card order after alignment
215+
local current_order = {}
216+
---@diagnostic disable-next-line: undefined-field
217+
for i, card in ipairs(self.cards) do
218+
current_order[i] = card.sort_id
219+
end
220+
221+
if utils.sets_equal(previous_order, current_order) then
222+
local order_changed = false
223+
for i = 1, #current_order do
224+
if previous_order[i] ~= current_order[i] then
225+
order_changed = true
226+
break
227+
end
228+
end
229+
230+
if order_changed then
231+
-- Compute rearrangement to interpret the action
232+
-- Map every card-id → its position in the old list
233+
local lookup = {}
234+
for pos, card_id in ipairs(previous_order) do
235+
lookup[card_id] = pos - 1 -- zero-based for the API
236+
end
237+
238+
-- Walk the new order and translate
239+
local cards = {}
240+
for pos, card_id in ipairs(current_order) do
241+
cards[pos] = lookup[card_id]
242+
end
243+
244+
LOG.write("rearrange_hand", { cards = cards })
245+
end
246+
end
247+
248+
previous_order = current_order
249+
return result
250+
else
251+
-- For non-hand card areas, just call the original function
252+
return original_function(self, ...)
253+
end
254+
end
255+
sendInfoMessage("Hooked into CardArea:align_cards for hand rearrange logging", "LOG")
256+
end
257+
193258
-- TODO: add hooks for other shop functions
194259

195260
-- =============================================================================
@@ -216,6 +281,7 @@ function LOG.init()
216281
LOG.hook_discard_cards_from_highlighted()
217282
LOG.hook_cash_out()
218283
LOG.hook_toggle_shop()
284+
LOG.hook_hand_rearrange()
219285

220286
sendInfoMessage("Logger initialized", "LOG")
221287
end

src/lua/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
---@field action "play_hand" | "discard" The action to perform
4848
---@field cards number[] Array of card indices (0-based)
4949

50+
---@class RearrangeHandArgs
51+
---@field action "rearrange" The action to perform
52+
---@field cards number[] Array of card indices for every card in hand (0-based)
53+
5054
---@class ShopActionArgs
5155
---@field action "next_round" | "buy_card" The action to perform
5256
---@field index? number The index of the card to act on (buy, buy_and_use, redeem, open) (0-based)

src/lua/utils.lua

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ function utils.get_game_state()
317317
label = card.label, -- str (default "Base Card") | ... | ... | ?
318318
-- playing_card = card.config.card.playing_card, -- int. The card index in the deck for the current round ?
319319
-- sell_cost = card.sell_cost, -- int (default 1). The dollars you get if you sell this card ?
320+
sort_id = card.sort_id, -- int. Unique identifier for this card instance
320321
base = {
321322
-- These should be the valude for the original base card
322323
-- without any modifications
@@ -490,6 +491,29 @@ function utils.get_game_state()
490491
}
491492
end
492493

494+
-- ==========================================================================
495+
-- Utility Functions
496+
-- ==========================================================================
497+
498+
function utils.sets_equal(list1, list2)
499+
if #list1 ~= #list2 then
500+
return false
501+
end
502+
503+
local set = {}
504+
for _, v in ipairs(list1) do
505+
set[v] = true
506+
end
507+
508+
for _, v in ipairs(list2) do
509+
if not set[v] then
510+
return false
511+
end
512+
end
513+
514+
return true
515+
end
516+
493517
-- ==========================================================================
494518
-- Debugging Utilities
495519
-- ==========================================================================

0 commit comments

Comments
 (0)