Skip to content

Commit 617cbc9

Browse files
stirbyS1M0N38
authored andcommitted
feat: better api errors
1 parent fd564ce commit 617cbc9

File tree

4 files changed

+50
-28
lines changed

4 files changed

+50
-28
lines changed

.vscode/settings.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
2-
"python.testing.pytestArgs": [
3-
"tests"
4-
],
2+
"python.testing.pytestArgs": ["tests"],
53
"python.testing.unittestEnabled": false,
6-
"python.testing.pytestEnabled": true
4+
"python.testing.pytestEnabled": true,
5+
"stylua.targetReleaseVersion": "latest"
76
}

src/lua/api.lua

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -112,22 +112,38 @@ function API.update(_)
112112
if raw_data then
113113
local ok, data = pcall(json.decode, raw_data)
114114
if not ok then
115-
API.send_error_response("Invalid JSON", ERROR_CODES.INVALID_JSON)
115+
API.send_error_response(
116+
"Invalid JSON: message could not be parsed. Send one JSON object per line with fields 'name' and 'arguments'",
117+
ERROR_CODES.INVALID_JSON,
118+
nil
119+
)
116120
return
117121
end
118122
---@cast data APIRequest
119123
if data.name == nil then
120-
API.send_error_response("Message must contain a name", ERROR_CODES.MISSING_NAME)
124+
API.send_error_response(
125+
"Message must contain a name. Include a 'name' field, e.g. 'get_game_state'",
126+
ERROR_CODES.MISSING_NAME,
127+
nil
128+
)
121129
elseif data.arguments == nil then
122-
API.send_error_response("Message must contain arguments", ERROR_CODES.MISSING_ARGUMENTS)
130+
API.send_error_response(
131+
"Message must contain arguments. Include an 'arguments' object (use {} if no parameters)",
132+
ERROR_CODES.MISSING_ARGUMENTS,
133+
nil
134+
)
123135
else
124136
local func = API.functions[data.name]
125137
local args = data.arguments
126138
if func == nil then
127-
API.send_error_response("Unknown function name", ERROR_CODES.UNKNOWN_FUNCTION, { name = data.name })
139+
API.send_error_response(
140+
"Unknown function name. See docs for supported names. Common calls: 'get_game_state', 'start_run', 'shop', 'play_hand_or_discard'",
141+
ERROR_CODES.UNKNOWN_FUNCTION,
142+
{ name = data.name }
143+
)
128144
elseif type(args) ~= "table" then
129145
API.send_error_response(
130-
"Arguments must be a table",
146+
"Arguments must be a table. The 'arguments' field must be a JSON object/table (use {} if empty)",
131147
ERROR_CODES.INVALID_ARGUMENTS,
132148
{ received_type = type(args) }
133149
)
@@ -301,9 +317,9 @@ API.functions["skip_or_select_blind"] = function(args)
301317
-- Validate current game state is appropriate for blind selection
302318
if G.STATE ~= G.STATES.BLIND_SELECT then
303319
API.send_error_response(
304-
"Cannot skip or select blind when not in blind selection",
320+
"Cannot skip or select blind when not in blind selection. Wait until gamestate is BLIND_SELECT, or call 'shop' with action 'next_round' to advance out of the shop. Use 'get_game_state' to check the current state.",
305321
ERROR_CODES.INVALID_GAME_STATE,
306-
{ current_state = G.STATE }
322+
{ current_state = G.STATE, expected_state = G.STATES.BLIND_SELECT }
307323
)
308324
return
309325
end
@@ -380,9 +396,9 @@ API.functions["play_hand_or_discard"] = function(args)
380396
-- Validate current game state is appropriate for playing hand or discarding
381397
if G.STATE ~= G.STATES.SELECTING_HAND then
382398
API.send_error_response(
383-
"Cannot play hand or discard when not selecting hand",
399+
"Cannot play hand or discard when not in selecting hand state. First select the blind: call 'skip_or_select_blind' with action 'select' when selecting blind. Use 'get_game_state' to verify.",
384400
ERROR_CODES.INVALID_GAME_STATE,
385-
{ current_state = G.STATE }
401+
{ current_state = G.STATE, expected_state = G.STATES.SELECTING_HAND }
386402
)
387403
return
388404
end
@@ -399,7 +415,7 @@ API.functions["play_hand_or_discard"] = function(args)
399415

400416
if args.action == "discard" and G.GAME.current_round.discards_left == 0 then
401417
API.send_error_response(
402-
"No discards left to perform discard",
418+
"No discards left to perform discard. Play a hand or advance the round; discards will reset next round.",
403419
ERROR_CODES.NO_DISCARDS_LEFT,
404420
{ discards_left = G.GAME.current_round.discards_left }
405421
)
@@ -476,10 +492,10 @@ API.functions["rearrange_hand"] = function(args)
476492
-- Validate current game state is appropriate for rearranging cards
477493
if G.STATE ~= G.STATES.SELECTING_HAND then
478494
API.send_error_response(
479-
"Cannot rearrange hand when not selecting hand",
480-
ERROR_CODES.INVALID_GAME_STATE,
481-
{ current_state = G.STATE }
482-
)
495+
"Cannot rearrange hand when not selecting hand. You can only rearrange while selecting your hand. You can check the current gamestate with 'get_game_state'.",
496+
ERROR_CODES.INVALID_GAME_STATE,
497+
{ current_state = G.STATE, expected_state = G.STATES.SELECTING_HAND }
498+
)
483499
return
484500
end
485501

@@ -692,9 +708,9 @@ API.functions["cash_out"] = function(_)
692708
-- Validate current game state is appropriate for cash out
693709
if G.STATE ~= G.STATES.ROUND_EVAL then
694710
API.send_error_response(
695-
"Cannot cash out when not in round evaluation",
711+
"Cannot cash out when not in round evaluation. Finish playing the hand to reach ROUND_EVAL first.",
696712
ERROR_CODES.INVALID_GAME_STATE,
697-
{ current_state = G.STATE }
713+
{ current_state = G.STATE, expected_state = G.STATES.ROUND_EVAL }
698714
)
699715
return
700716
end
@@ -726,9 +742,9 @@ API.functions["shop"] = function(args)
726742
-- Validate current game state is appropriate for shop
727743
if G.STATE ~= G.STATES.SHOP then
728744
API.send_error_response(
729-
"Cannot select shop action when not in shop",
745+
"Cannot select shop action when not in shop. Reach the shop by calling 'cash_out' during ROUND_EVAL, or finish a hand to enter evaluation.",
730746
ERROR_CODES.INVALID_GAME_STATE,
731-
{ current_state = G.STATE }
747+
{ current_state = G.STATE, expected_state = G.STATES.SHOP }
732748
)
733749
return
734750
end
@@ -771,7 +787,7 @@ API.functions["shop"] = function(args)
771787
-- Check if the card can be afforded
772788
if card.cost > G.GAME.dollars then
773789
API.send_error_response(
774-
"Card is not affordable",
790+
"Card is not affordable, choose a purchasable card or advance with 'shop' with action 'next_round'.",
775791
ERROR_CODES.INVALID_ACTION,
776792
{ index = args.index, cost = card.cost, dollars = G.GAME.dollars }
777793
)
@@ -843,7 +859,7 @@ API.functions["shop"] = function(args)
843859

844860
if dollars_before < reroll_cost then
845861
API.send_error_response(
846-
"Not enough dollars to reroll",
862+
"Not enough dollars to reroll. You may use the 'shop' function with action 'next_round' to advance to the next round.",
847863
ERROR_CODES.INVALID_ACTION,
848864
{ dollars = dollars_before, reroll_cost = reroll_cost }
849865
)
@@ -942,7 +958,7 @@ API.functions["shop"] = function(args)
942958
-- Check if the card can be afforded
943959
if card.cost > G.GAME.dollars then
944960
API.send_error_response(
945-
"Card is not affordable",
961+
"Card is not affordable. Choose a cheaper card or advance with 'shop' with action 'next_round'.",
946962
ERROR_CODES.INVALID_ACTION,
947963
{ index = args.index, cost = card.cost, dollars = G.GAME.dollars }
948964
)

tests/lua/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ def send_and_receive_api_message(
7474

7575

7676
def assert_error_response(
77-
response, expected_error_text, expected_context_keys=None, expected_error_code=None
77+
response,
78+
expected_error_text,
79+
expected_context_keys=None,
80+
expected_error_code=None,
7881
):
7982
"""
8083
Helper function to assert the format and content of an error response.

tests/lua/test_protocol_errors.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ def test_invalid_json_error(self, tcp_client: socket.socket) -> None:
3434

3535
response = receive_api_message(tcp_client)
3636
assert_error_response(
37-
response, "Invalid JSON", expected_error_code=ErrorCode.INVALID_JSON.value
37+
response,
38+
"Invalid JSON",
39+
expected_error_code=ErrorCode.INVALID_JSON.value,
3840
)
3941

4042
def test_missing_name_field_error(self, tcp_client: socket.socket) -> None:
@@ -148,7 +150,9 @@ def test_multiple_protocol_errors_sequence(self, tcp_client: socket.socket) -> N
148150
tcp_client.send(b"{ invalid }\n")
149151
response1 = receive_api_message(tcp_client)
150152
assert_error_response(
151-
response1, "Invalid JSON", expected_error_code=ErrorCode.INVALID_JSON.value
153+
response1,
154+
"Invalid JSON",
155+
expected_error_code=ErrorCode.INVALID_JSON.value,
152156
)
153157

154158
# 2. Missing name

0 commit comments

Comments
 (0)