Skip to content

Commit 4cf92b4

Browse files
committed
Merge branch 'main' into buy-card
2 parents fe324a5 + c5eebb6 commit 4cf92b4

File tree

14 files changed

+844
-525
lines changed

14 files changed

+844
-525
lines changed

balatrobot.lua

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ BALATRO_BOT_CONFIG = {
1515

1616
-- Load minimal required files
1717
assert(SMODS.load_file("src/lua/utils.lua"))()
18-
assert(SMODS.load_file("src/lua/log.lua"))()
1918
assert(SMODS.load_file("src/lua/api.lua"))()
19+
assert(SMODS.load_file("src/lua/log.lua"))()
20+
21+
-- Initialize API
22+
API.init()
2023

2124
-- Initialize Logger
2225
LOG.init()
2326

24-
-- Initialize API
25-
API.init()
27+
G.SETTINGS.skip_splash = "Yes"
2628

2729
sendInfoMessage("BalatroBot loaded - version " .. SMODS.current_mod.version, "BALATROBOT")

bots/replay.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Simple bot that replays actions from a run save (JSONL file)."""
22

3+
import argparse
34
import json
45
import logging
56
import sys
7+
import time
68
from pathlib import Path
79

810
from balatrobot.client import BalatroClient
@@ -12,30 +14,66 @@
1214
logging.basicConfig(level=logging.INFO)
1315

1416

15-
def load_steps_from_jsonl() -> list[dict]:
16-
"""Load replay steps from JSONL file."""
17-
if len(sys.argv) != 2:
18-
logger.error("Usage: python replay.py <jsonl_file>")
17+
def get_most_recent_jsonl() -> Path:
18+
"""Find the most recent JSONL file in the runs directory."""
19+
runs_dir = Path("runs")
20+
if not runs_dir.exists():
21+
logger.error("Runs directory not found")
22+
sys.exit(1)
23+
24+
jsonl_files = list(runs_dir.glob("*.jsonl"))
25+
if not jsonl_files:
26+
logger.error("No JSONL files found in runs directory")
1927
sys.exit(1)
2028

21-
jsonl_file = Path(sys.argv[1])
22-
if not jsonl_file.exists():
23-
logger.error(f"File not found: {jsonl_file}")
29+
# Sort by modification time, most recent first
30+
most_recent = max(jsonl_files, key=lambda f: f.stat().st_mtime)
31+
return most_recent
32+
33+
34+
def load_steps_from_jsonl(jsonl_path: Path) -> list[dict]:
35+
"""Load replay steps from JSONL file."""
36+
if not jsonl_path.exists():
37+
logger.error(f"File not found: {jsonl_path}")
2438
sys.exit(1)
2539

2640
try:
27-
with open(jsonl_file) as f:
41+
with open(jsonl_path) as f:
2842
steps = [json.loads(line) for line in f if line.strip()]
29-
logger.info(f"Loaded {len(steps)} steps from {jsonl_file}")
43+
logger.info(f"Loaded {len(steps)} steps from {jsonl_path}")
3044
return steps
3145
except json.JSONDecodeError as e:
32-
logger.error(f"Invalid JSON in file {jsonl_file}: {e}")
46+
logger.error(f"Invalid JSON in file {jsonl_path}: {e}")
3347
sys.exit(1)
3448

3549

3650
def main():
3751
"""Main replay function."""
38-
steps = load_steps_from_jsonl()
52+
parser = argparse.ArgumentParser(description="Replay actions from a JSONL run file")
53+
parser.add_argument(
54+
"--delay",
55+
"-d",
56+
type=float,
57+
default=0.0,
58+
help="Delay between played moves in seconds (default: 0.0)",
59+
)
60+
parser.add_argument(
61+
"--path",
62+
"-p",
63+
type=Path,
64+
help="Path to JSONL run file (default: most recent file in runs/)",
65+
)
66+
67+
args = parser.parse_args()
68+
69+
# Determine the path to use
70+
if args.path:
71+
jsonl_path = args.path
72+
else:
73+
jsonl_path = get_most_recent_jsonl()
74+
logger.info(f"Using most recent file: {jsonl_path}")
75+
76+
steps = load_steps_from_jsonl(jsonl_path)
3977

4078
try:
4179
with BalatroClient() as client:
@@ -46,6 +84,7 @@ def main():
4684
function_name = step["function"]["name"]
4785
arguments = step["function"]["arguments"]
4886
logger.info(f"Step {i + 1}/{len(steps)}: {function_name}({arguments})")
87+
time.sleep(args.delay)
4988

5089
try:
5190
response = client.send_message(function_name, arguments)

docs/protocol-api.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,13 @@ The BalatroBot API provides core functions that correspond to the main game acti
124124

125125
The following table details the parameters required for each function. Note that `get_game_state` and `go_to_menu` require no parameters:
126126

127-
| Name | Parameters |
128-
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129-
| `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 |
130-
| `skip_or_select_blind` | `action` (string): Either "select" or "skip" |
131-
| `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) |
133-
| `shop` | `action` (string): Shop action to perform ("next_round") |
127+
| Name | Parameters |
128+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129+
| `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<br>`log_path` (string, optional): Full file path for run log (must include .jsonl extension) |
130+
| `skip_or_select_blind` | `action` (string): Either "select" or "skip" |
131+
| `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) |
133+
| `shop` | `action` (string): Shop action to perform ("next_round") |
134134

135135
### Errors
136136

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ addopts = "--cov=src/balatrobot --cov-report=term-missing --cov-report=html --co
4343
[dependency-groups]
4444
dev = [
4545
"basedpyright>=1.29.5",
46+
"deepdiff>=8.5.0",
4647
"mdformat-mkdocs>=4.3.0",
4748
"mdformat-simple-breaks>=0.0.1",
4849
"mkdocs-llmstxt>=0.3.0",

src/lua/api.lua

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ API.pending_requests = {}
7070

7171
---Updates the API by processing TCP messages and pending requests
7272
---@param _ number Delta time (not used)
73+
---@diagnostic disable-next-line: duplicate-set-field
7374
function API.update(_)
7475
-- Create server socket if it doesn't exist
7576
if not API.server_socket then
@@ -206,8 +207,16 @@ end
206207
---Gets the current game state
207208
---@param _ table Arguments (not used)
208209
API.functions["get_game_state"] = function(_)
209-
local game_state = utils.get_game_state()
210-
API.send_response(game_state)
210+
---@type PendingRequest
211+
API.pending_requests["get_game_state"] = {
212+
condition = function()
213+
return #G.E_MANAGER.queues.base < EVENT_QUEUE_THRESHOLD
214+
end,
215+
action = function()
216+
local game_state = utils.get_game_state()
217+
API.send_response(game_state)
218+
end,
219+
}
211220
end
212221

213222
---Navigates to the main menu.
@@ -235,6 +244,7 @@ end
235244

236245
---Starts a new game run with specified parameters
237246
---Call G.FUNCS.start_run() to start a new game run with specified parameters.
247+
---If log_path is provided, the run log will be saved to the specified full path (must include .jsonl extension), otherwise uses runs/timestamp.jsonl.
238248
---@param args StartRunArgs The run configuration
239249
API.functions["start_run"] = function(args)
240250
-- Validate required parameters
@@ -279,7 +289,7 @@ API.functions["start_run"] = function(args)
279289
G.GAME.challenge_name = args.challenge
280290

281291
-- Start the run
282-
G.FUNCS.start_run(nil, { stake = args.stake, seed = args.seed, challenge = challenge_obj })
292+
G.FUNCS.start_run(nil, { stake = args.stake, seed = args.seed, challenge = challenge_obj, log_path = args.log_path })
283293

284294
-- Defer sending response until the run has started
285295
---@type PendingRequest

0 commit comments

Comments
 (0)