Skip to content

Commit 1775952

Browse files
stirbyS1M0N38
authored andcommitted
feat: add cards argument to use_consumable
1 parent 617cbc9 commit 1775952

File tree

4 files changed

+360
-2
lines changed

4 files changed

+360
-2
lines changed

src/lua/api.lua

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,66 @@ API.functions["use_consumable"] = function(args)
11051105
return
11061106
end
11071107

1108+
-- If cards parameter is provided, handle card selection
1109+
if args.cards then
1110+
-- Validate current game state is SELECTING_HAND when cards are provided
1111+
if G.STATE ~= G.STATES.SELECTING_HAND then
1112+
API.send_error_response(
1113+
"Cannot use consumable with cards when not in selecting hand state",
1114+
ERROR_CODES.INVALID_GAME_STATE,
1115+
{ current_state = G.STATE, required_state = G.STATES.SELECTING_HAND }
1116+
)
1117+
return
1118+
end
1119+
1120+
-- Validate cards is an array
1121+
if type(args.cards) ~= "table" then
1122+
API.send_error_response(
1123+
"Invalid parameter type for cards. Expected array, got " .. tostring(type(args.cards)),
1124+
ERROR_CODES.INVALID_PARAMETER,
1125+
{ parameter = "cards", expected_type = "array" }
1126+
)
1127+
return
1128+
end
1129+
1130+
-- Validate number of cards is between 1 and 5 (inclusive) for consistency with play_hand_or_discard
1131+
if #args.cards < 1 or #args.cards > 5 then
1132+
API.send_error_response(
1133+
"Invalid number of cards. Expected 1-5, got " .. tostring(#args.cards),
1134+
ERROR_CODES.PARAMETER_OUT_OF_RANGE,
1135+
{ cards_count = #args.cards, valid_range = "1-5" }
1136+
)
1137+
return
1138+
end
1139+
1140+
-- Convert from 0-based to 1-based indexing
1141+
for i, card_index in ipairs(args.cards) do
1142+
args.cards[i] = card_index + 1
1143+
end
1144+
1145+
-- Check that all cards exist and are selectable
1146+
for _, card_index in ipairs(args.cards) do
1147+
if not G.hand or not G.hand.cards or not G.hand.cards[card_index] then
1148+
API.send_error_response(
1149+
"Invalid card index",
1150+
ERROR_CODES.INVALID_CARD_INDEX,
1151+
{ card_index = card_index - 1, hand_size = G.hand and G.hand.cards and #G.hand.cards or 0 }
1152+
)
1153+
return
1154+
end
1155+
end
1156+
1157+
-- Clear any existing highlights before selecting new cards
1158+
if G.hand then
1159+
G.hand:unhighlight_all()
1160+
end
1161+
1162+
-- Select cards for the consumable to target
1163+
for _, card_index in ipairs(args.cards) do
1164+
G.hand.cards[card_index]:click()
1165+
end
1166+
end
1167+
11081168
-- Validate that consumables exist
11091169
if not G.consumeables or not G.consumeables.cards or #G.consumeables.cards == 0 then
11101170
API.send_error_response(

src/lua/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@
7777
---@class SellConsumableArgs
7878
---@field index number The index of the consumable to sell (0-based)
7979

80+
---@class UseConsumableArgs
81+
---@field index number The index of the consumable to use (0-based)
82+
---@field cards? number[] Optional array of card indices to target (0-based)
83+
8084
---@class LoadSaveArgs
8185
---@field save_path string Path to the save file relative to Love2D save directory (e.g., "3/save.jkr")
8286

test_use_consumable.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python3
2+
"""Test script for the enhanced use_consumable endpoint with cards parameter."""
3+
4+
import time
5+
6+
from balatrobot import BalatroClient
7+
from balatrobot.enums import Actions
8+
9+
10+
def test_use_consumable():
11+
"""Test the use_consumable functionality with and without cards parameter."""
12+
with BalatroClient() as client:
13+
print("Connected to Balatro")
14+
15+
# Get initial game state
16+
state = client.send_message("get_game_state")
17+
print(f"Current state: {state['G']['STATE']}")
18+
19+
# Start a new run if needed
20+
if state["G"]["STATE"] == Actions.MENU:
21+
print("Starting new run...")
22+
state = client.send_message("start_run", {"stake": "white", "deck": "red"})
23+
print(f"Run started, new state: {state['G']['STATE']}")
24+
25+
# Navigate to a state where we can test consumables
26+
if state["G"]["STATE"] == Actions.BLIND_SELECT:
27+
print("Selecting blind...")
28+
state = client.send_message("skip_or_select_blind", {"action": "select"})
29+
time.sleep(1)
30+
31+
# Check if we have consumables
32+
consumables = state["G"].get("consumeables", {}).get("cards", [])
33+
print(f"Consumables available: {len(consumables)}")
34+
35+
if consumables:
36+
# Try using first consumable without cards (e.g., planet cards)
37+
print(f"Using consumable at index 0 (no cards)...")
38+
try:
39+
result = client.use_consumable(0)
40+
print("Successfully used consumable without cards")
41+
except Exception as e:
42+
print(f"Error using consumable without cards: {e}")
43+
44+
# Check hand cards
45+
hand_cards = state["G"].get("hand", {}).get("cards", [])
46+
print(f"Hand cards available: {len(hand_cards)}")
47+
48+
if consumables and len(hand_cards) >= 3:
49+
# Try using a consumable with specific cards selected
50+
print(f"Using consumable at index 0 with cards [0, 1, 2]...")
51+
try:
52+
result = client.use_consumable(0, cards=[0, 1, 2])
53+
print("Successfully used consumable with selected cards")
54+
except Exception as e:
55+
print(f"Error using consumable with cards: {e}")
56+
57+
# Test error cases
58+
print("\nTesting error cases...")
59+
60+
# Test invalid index
61+
try:
62+
print("Testing invalid consumable index...")
63+
result = client.use_consumable(99)
64+
print("Unexpected success with invalid index")
65+
except Exception as e:
66+
print(f"Expected error with invalid index: {e}")
67+
68+
# Test invalid card indices
69+
if consumables:
70+
try:
71+
print("Testing invalid card indices...")
72+
result = client.use_consumable(0, cards=[99, 100])
73+
print("Unexpected success with invalid card indices")
74+
except Exception as e:
75+
print(f"Expected error with invalid card indices: {e}")
76+
77+
print("\nTest completed!")
78+
79+
80+
if __name__ == "__main__":
81+
test_use_consumable()

tests/lua/endpoints/test_use_consumable.py

Lines changed: 215 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,5 +194,218 @@ def test_use_consumable_no_consumables_available(
194194
)
195195

196196

197-
# TODO: add test for other types of consumables
198-
class TestUseConsumableOtherType: ...
197+
class TestUseConsumableWithCards:
198+
"""Test use_consumable with cards parameter for consumables that target specific cards."""
199+
200+
@pytest.fixture(autouse=True)
201+
def setup_and_teardown(
202+
self, tcp_client: socket.socket
203+
) -> Generator[dict, None, None]:
204+
# Start a run and get to SELECTING_HAND state with a consumable
205+
send_and_receive_api_message(
206+
tcp_client,
207+
"start_run",
208+
{
209+
"deck": "Red Deck",
210+
"stake": 1,
211+
"seed": "TEST123",
212+
},
213+
)
214+
send_and_receive_api_message(
215+
tcp_client, "skip_or_select_blind", {"action": "select"}
216+
)
217+
218+
# Play a hand to get to shop
219+
send_and_receive_api_message(
220+
tcp_client,
221+
"play_hand_or_discard",
222+
{"action": "play_hand", "cards": [0, 1, 2, 3]},
223+
)
224+
send_and_receive_api_message(tcp_client, "cash_out", {})
225+
226+
# Buy a consumable
227+
send_and_receive_api_message(
228+
tcp_client,
229+
"shop",
230+
{"action": "buy_card", "index": 2},
231+
)
232+
233+
# Start next round to get back to SELECTING_HAND state
234+
send_and_receive_api_message(tcp_client, "shop", {"action": "next_round"})
235+
game_state = send_and_receive_api_message(
236+
tcp_client, "skip_or_select_blind", {"action": "select"}
237+
)
238+
239+
yield game_state
240+
send_and_receive_api_message(tcp_client, "go_to_menu", {})
241+
242+
def test_use_consumable_with_cards_success(
243+
self, tcp_client: socket.socket, setup_and_teardown
244+
) -> None:
245+
"""Test successfully using a consumable with specific cards selected."""
246+
game_state = setup_and_teardown
247+
248+
# Verify we're in SELECTING_HAND state
249+
assert game_state["state"] == State.SELECTING_HAND.value
250+
251+
# Skip test if no consumables available
252+
if len(game_state["consumables"]["cards"]) == 0:
253+
pytest.skip("No consumables available in this test run")
254+
255+
# Use the consumable with specific cards selected
256+
response = send_and_receive_api_message(
257+
tcp_client,
258+
"use_consumable",
259+
{"index": 0, "cards": [0, 2, 4]}, # Select cards 0, 2, and 4
260+
)
261+
262+
# Verify response is successful
263+
assert "error" not in response
264+
265+
def test_use_consumable_with_invalid_cards(
266+
self, tcp_client: socket.socket, setup_and_teardown
267+
) -> None:
268+
"""Test using consumable with invalid card indices."""
269+
game_state = setup_and_teardown
270+
271+
# Skip test if no consumables available
272+
if len(game_state["consumables"]["cards"]) == 0:
273+
pytest.skip("No consumables available in this test run")
274+
275+
# Try to use consumable with out-of-range card indices
276+
response = send_and_receive_api_message(
277+
tcp_client,
278+
"use_consumable",
279+
{"index": 0, "cards": [99, 100]}, # Invalid card indices
280+
)
281+
assert_error_response(
282+
response,
283+
"Invalid card index",
284+
expected_error_code=ErrorCode.INVALID_CARD_INDEX.value,
285+
)
286+
287+
def test_use_consumable_with_too_many_cards(
288+
self, tcp_client: socket.socket, setup_and_teardown
289+
) -> None:
290+
"""Test using consumable with more than 5 cards."""
291+
game_state = setup_and_teardown
292+
293+
# Skip test if no consumables available
294+
if len(game_state["consumables"]["cards"]) == 0:
295+
pytest.skip("No consumables available in this test run")
296+
297+
# Try to use consumable with more than 5 cards
298+
response = send_and_receive_api_message(
299+
tcp_client,
300+
"use_consumable",
301+
{"index": 0, "cards": [0, 1, 2, 3, 4, 5]}, # 6 cards - too many
302+
)
303+
assert_error_response(
304+
response,
305+
"Invalid number of cards",
306+
expected_error_code=ErrorCode.PARAMETER_OUT_OF_RANGE.value,
307+
)
308+
309+
def test_use_consumable_with_empty_cards(
310+
self, tcp_client: socket.socket, setup_and_teardown
311+
) -> None:
312+
"""Test using consumable with empty cards array."""
313+
game_state = setup_and_teardown
314+
315+
# Skip test if no consumables available
316+
if len(game_state["consumables"]["cards"]) == 0:
317+
pytest.skip("No consumables available in this test run")
318+
319+
# Try to use consumable with empty cards array
320+
response = send_and_receive_api_message(
321+
tcp_client,
322+
"use_consumable",
323+
{"index": 0, "cards": []}, # Empty array
324+
)
325+
assert_error_response(
326+
response,
327+
"Invalid number of cards",
328+
expected_error_code=ErrorCode.PARAMETER_OUT_OF_RANGE.value,
329+
)
330+
331+
def test_use_consumable_with_invalid_cards_type(
332+
self, tcp_client: socket.socket, setup_and_teardown
333+
) -> None:
334+
"""Test using consumable with non-array cards parameter."""
335+
game_state = setup_and_teardown
336+
337+
# Skip test if no consumables available
338+
if len(game_state["consumables"]["cards"]) == 0:
339+
pytest.skip("No consumables available in this test run")
340+
341+
# Try to use consumable with invalid cards type
342+
response = send_and_receive_api_message(
343+
tcp_client,
344+
"use_consumable",
345+
{"index": 0, "cards": "invalid"}, # Not an array
346+
)
347+
assert_error_response(
348+
response,
349+
"Invalid parameter type",
350+
expected_error_code=ErrorCode.INVALID_PARAMETER.value,
351+
)
352+
353+
def test_use_planet_without_cards(
354+
self, tcp_client: socket.socket, setup_and_teardown
355+
) -> None:
356+
"""Test that planet consumables still work without cards parameter."""
357+
game_state = setup_and_teardown
358+
359+
# Skip test if no consumables available
360+
if len(game_state["consumables"]["cards"]) == 0:
361+
pytest.skip("No consumables available in this test run")
362+
363+
# Use consumable without cards parameter (original behavior)
364+
response = send_and_receive_api_message(
365+
tcp_client,
366+
"use_consumable",
367+
{"index": 0}, # No cards parameter
368+
)
369+
370+
# Should still work for consumables that don't need cards
371+
assert "error" not in response
372+
373+
def test_use_consumable_with_cards_wrong_state(
374+
self, tcp_client: socket.socket
375+
) -> None:
376+
"""Test that using consumable with cards fails in non-SELECTING_HAND states."""
377+
# Start a run and get to shop state
378+
send_and_receive_api_message(
379+
tcp_client,
380+
"start_run",
381+
{"deck": "Red Deck", "stake": 1, "seed": "OOOO155"},
382+
)
383+
send_and_receive_api_message(
384+
tcp_client, "skip_or_select_blind", {"action": "select"}
385+
)
386+
send_and_receive_api_message(
387+
tcp_client,
388+
"play_hand_or_discard",
389+
{"action": "play_hand", "cards": [0, 1, 2, 3]},
390+
)
391+
send_and_receive_api_message(tcp_client, "cash_out", {})
392+
game_state = send_and_receive_api_message(
393+
tcp_client,
394+
"shop",
395+
{"action": "buy_card", "index": 1},
396+
)
397+
398+
# Verify we're in SHOP state
399+
assert game_state["state"] == State.SHOP.value
400+
401+
# Try to use consumable with cards while in SHOP state (should fail)
402+
response = send_and_receive_api_message(
403+
tcp_client, "use_consumable", {"index": 0, "cards": [0, 1, 2]}
404+
)
405+
assert_error_response(
406+
response,
407+
"Cannot use consumable with cards when not in selecting hand state",
408+
expected_error_code=ErrorCode.INVALID_GAME_STATE.value,
409+
)
410+
411+
send_and_receive_api_message(tcp_client, "go_to_menu", {})

0 commit comments

Comments
 (0)