Skip to content

Commit de9d5c3

Browse files
committed
feat(lua): add sort hand endpoint
Add a dedicated `sort` endpoint that reorders the cards in hand by rank or by suit, mirroring Balatro's in-game "Sort by Rank" / "Sort by Suit" play-bar buttons. The game computes the new order for you — unlike `rearrange`, where the caller supplies the order. Only available during SELECTING_HAND. The endpoint faithfully replicates the game's behaviour rather than re-deriving it: - The `by` parameter is a manual enum check ("rank"|"suit") because the shared validator has no enum support. Any other value is BAD_REQUEST, matching the phrasing of sibling endpoints. - The in-game comparator is invoked directly: G.FUNCS.sort_hand_value for rank (A>K>...>2, then Spades>Hearts>Clubs>Diamonds) and G.FUNCS.sort_hand_suit for suit (Spades>Hearts>Clubs>Diamonds, then rank desc). We never hand-roll an ordering. - Completion is predicate alpha: wait on STATE==SELECTING_HAND with G.hand populated, then respond with the gamestate. Registered in balatrobot.lua (hand-reorder group, beside rearrange), added to the OpenRPC spec, exposed through the CLI Method enum (and its count assertion), typed in types.lua, and documented in api.md. Closes #213.
1 parent 164a810 commit de9d5c3

7 files changed

Lines changed: 140 additions & 3 deletions

File tree

balatrobot.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ BB_ENDPOINTS = {
3838
"src/lua/endpoints/buy.lua",
3939
"src/lua/endpoints/buy_and_use.lua",
4040
"src/lua/endpoints/pack.lua",
41-
-- Rearrange endpoint
41+
-- Hand reorder endpoints (rearrange, sort)
4242
"src/lua/endpoints/rearrange.lua",
43+
"src/lua/endpoints/sort.lua",
4344
-- Sell endpoint
4445
"src/lua/endpoints/sell.lua",
4546
-- Use consumable endpoint

docs/api.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ MENU ──► BLIND_SELECT ──► SELECTING_HAND ──► ROUND_EVAL ──
130130
- [`play`](#play) - Play cards from hand
131131
- [`discard`](#discard) - Discard cards from hand
132132
- [`rearrange`](#rearrange) - Rearrange cards in hand, jokers, or consumables
133+
- [`sort`](#sort) - Sort the hand by rank or suit
133134
- [`use`](#use) - Use a consumable card
134135
- [`add`](#add) - Add a card to the game (debug/testing)
135136
- [`screenshot`](#screenshot) - Take a screenshot of the game
@@ -620,6 +621,33 @@ curl -X POST http://127.0.0.1:12346 \
620621

621622
---
622623

624+
### `sort`
625+
626+
Sort the cards in hand. This is a faithful mirror of the in-game **Sort by Rank** / **Sort by Suit** buttons in the play bar — the game computes the new order for you (unlike `rearrange`, where you supply the order). Only available during `SELECTING_HAND`.
627+
628+
**Parameters:**
629+
630+
| Name | Type | Required | Description |
631+
| ---- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
632+
| `by` | string | Yes | Sort mode: `"rank"` (A>K>...>2, then Spades>Hearts>Clubs>Diamonds) or `"suit"` (Spades>Hearts>Clubs>Diamonds, then rank desc) |
633+
634+
**Returns:** [GameState](#gamestate-schema)
635+
636+
**Errors:** `BAD_REQUEST`, `INVALID_STATE`
637+
638+
**Required State:** `SELECTING_HAND`
639+
640+
**Example:**
641+
642+
```bash
643+
# Sort hand by rank
644+
curl -X POST http://127.0.0.1:12346 \
645+
-H "Content-Type: application/json" \
646+
-d '{"jsonrpc": "2.0", "method": "sort", "params": {"by": "rank"}, "id": 1}'
647+
```
648+
649+
---
650+
623651
### `use`
624652

625653
Use a consumable card.

src/balatrobot/cli/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Method(StrEnum):
3636
SELL = "sell"
3737
SET = "set"
3838
SKIP = "skip"
39+
SORT = "sort"
3940
START = "start"
4041
USE = "use"
4142

src/lua/endpoints/sort.lua

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
-- src/lua/endpoints/sort.lua
2+
3+
-- ==========================================================================
4+
-- Sort Endpoint Params
5+
-- ==========================================================================
6+
7+
---@class Request.Endpoint.Sort.Params
8+
---@field by string Sort mode: "rank" or "suit"
9+
10+
-- ==========================================================================
11+
-- Sort Endpoint
12+
-- ==========================================================================
13+
14+
---@type Endpoint
15+
return {
16+
17+
name = "sort",
18+
19+
description = "Sort the hand by rank or suit (mirrors the in-game Sort buttons)",
20+
21+
schema = {
22+
by = {
23+
type = "string",
24+
required = true,
25+
description = 'Sort mode: "rank" or "suit"',
26+
},
27+
},
28+
29+
requires_state = { G.STATES.SELECTING_HAND },
30+
31+
---@param args Request.Endpoint.Sort.Params
32+
---@param send_response fun(response: Response.Endpoint)
33+
execute = function(args, send_response)
34+
sendDebugMessage("sort()", "BB.ENDPOINTS")
35+
36+
-- Gate: manual enum check (the validator has no enum support)
37+
if args.by ~= "rank" and args.by ~= "suit" then
38+
send_response({
39+
message = 'Sort \'by\' must be "rank" or "suit", got "' .. args.by .. '"',
40+
name = BB_ERROR_NAMES.BAD_REQUEST,
41+
})
42+
return
43+
end
44+
45+
if args.by == "rank" then
46+
G.FUNCS.sort_hand_value({})
47+
else
48+
G.FUNCS.sort_hand_suit({})
49+
end
50+
51+
sendInfoMessage("Sorting hand by " .. args.by, "BB.ENDPOINTS")
52+
53+
-- Wait for the hand to be sorted (predicate α)
54+
G.E_MANAGER:add_event(Event({
55+
trigger = "condition",
56+
blocking = false,
57+
func = function()
58+
local done = G.STATE == G.STATES.SELECTING_HAND and G.hand ~= nil
59+
if done then
60+
sendDebugMessage("sort() → ok", "BB.ENDPOINTS")
61+
send_response(BB_GAMESTATE.get_gamestate())
62+
end
63+
return done
64+
end,
65+
}))
66+
end,
67+
}

src/lua/utils/openrpc.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,45 @@
810810
}
811811
]
812812
},
813+
{
814+
"name": "sort",
815+
"summary": "Sort the hand by rank or suit",
816+
"description": "Sort the cards in hand, mirroring the in-game Sort by Rank / Sort by Suit buttons. Only available during SELECTING_HAND. Use 'rank' for descending rank (A>K>...>2, then Spades>Hearts>Clubs>Diamonds) or 'suit' for descending suit (Spades>Hearts>Clubs>Diamonds, then rank desc).",
817+
"tags": [
818+
{
819+
"$ref": "#/components/tags/cards"
820+
}
821+
],
822+
"params": [
823+
{
824+
"name": "by",
825+
"description": "Sort mode: 'rank' or 'suit'",
826+
"required": true,
827+
"schema": {
828+
"type": "string",
829+
"enum": [
830+
"rank",
831+
"suit"
832+
]
833+
}
834+
}
835+
],
836+
"result": {
837+
"name": "gamestate",
838+
"description": "Complete game state after the hand is sorted",
839+
"schema": {
840+
"$ref": "#/components/schemas/GameState"
841+
}
842+
},
843+
"errors": [
844+
{
845+
"$ref": "#/components/errors/BadRequest"
846+
},
847+
{
848+
"$ref": "#/components/errors/InvalidState"
849+
}
850+
]
851+
},
813852
{
814853
"name": "start",
815854
"summary": "Start a new game run",

src/lua/utils/types.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
---@alias Request.Endpoint.Method
152152
---| "add" | "buy" | "cash_out" | "discard" | "gamestate" | "health" | "load"
153153
---| "menu" | "next_round" | "play" | "rearrange" | "reroll" | "save"
154-
---| "screenshot" | "select" | "sell" | "set" | "skip" | "start" | "use"
154+
---| "screenshot" | "select" | "sell" | "set" | "skip" | "sort" | "start" | "use"
155155

156156
---@alias Request.Endpoint.Test.Method
157157
---| "echo" | "endpoint" | "error" | "state" | "validation"
@@ -176,6 +176,7 @@
176176
---| Request.Endpoint.Sell.Params
177177
---| Request.Endpoint.Set.Params
178178
---| Request.Endpoint.Skip.Params
179+
---| Request.Endpoint.Sort.Params
179180
---| Request.Endpoint.Start.Params
180181
---| Request.Endpoint.Use.Params
181182

tests/cli/test_api_cmd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_api_all_methods_valid(self):
5858
from balatrobot.cli.api import Method
5959

6060
methods = [m.value for m in Method]
61-
assert len(methods) == 23
61+
assert len(methods) == 24
6262
assert "health" in methods
6363
assert "gamestate" in methods
6464

0 commit comments

Comments
 (0)