diff --git a/.claude/skills/balatrobot/SKILL.md b/.claude/skills/balatrobot/SKILL.md new file mode 100644 index 00000000..2ff66237 --- /dev/null +++ b/.claude/skills/balatrobot/SKILL.md @@ -0,0 +1,169 @@ +--- +name: balatrobot +description: Run and debug BalatroBot locally. Use when you need to start Balatro with the BalatroBot Lua mod, manually test or reproduce issues via the JSON-RPC HTTP API, inspect the newest session logs under logs/, and capture screenshots into logs//artifacts/ using only the balatrobot CLI (no curl, no uvx). +allowed-tools: Bash(balatrobot:*) Bash(mkdir:*) Bash(ls:*) Bash(tail:*) Bash(PORT=:*) Bash(echo:*) Bash(jq:*) Bash(sleep:*) Read Grep +disable-model-invocation: true +--- + +# BalatroBot debug/runbook + +## Ground rules + +- Run commands from the repo root. +- Use `balatrobot ...` only (no `uvx`, no `curl`). +- Run `balatrobot api ...` requests sequentially (avoid concurrent calls). +- Prefer minimal, targeted changes while debugging; avoid large refactors. + +## Start BalatroBot + +### Choose a port (recommended) + +Pick a random port in the range `20000-30000` to avoid the port staying busy for ~20-30s after restarting: + +```bash +PORT="$((20000 + RANDOM % 10001))" +echo "Using PORT=$PORT" +``` + +Pick a new random `PORT` every time you restart `balatrobot serve` (crash/restart/kill/start). + +### Default profile (most cases) + +Use headless mode unless explicitly asked to take screenshots: + +```bash +balatrobot serve --port "$PORT" --headless --fast --debug +``` + +### Screenshot profile (only when explicitly asked) + +Use render-on-API mode for deterministic screenshots: + +```bash +balatrobot serve --port "$PORT" --render-on-api --fast --debug +``` + +## Call the API (no curl) + +Run API calls in a second terminal while `balatrobot serve ...` is running. + +Reminder: wrap JSON params in single quotes so you don't need to escape JSON double quotes. + +```bash +# Health check +balatrobot api health --port "$PORT" + +# Get full game state +balatrobot api gamestate --port "$PORT" + +# Return to menu +balatrobot api menu --port "$PORT" + +# Start a new run +balatrobot api start '{"deck":"RED","stake":"WHITE"}' --port "$PORT" + +# Select blind +balatrobot api select --port "$PORT" + +# Play cards (0-based indices) +balatrobot api play '{"cards":[0,1,2,3,4]}' --port "$PORT" +``` + +### Filter responses with `jq` (optional) + +The `balatrobot api ...` CLI prints the JSON-RPC `result` object to stdout on success (see `docs/api.md`), so `jq` filters target top-level fields like `.state` (not `.result.state`). + +```bash +# Print the current state as a raw string (MENU / BLIND_SELECT / SELECTING_HAND / ...) +balatrobot api gamestate --port "$PORT" | jq -r '.state' + +# Quick summary (useful for bug reports) +balatrobot api gamestate --port "$PORT" | jq '{state, round_num, ante_num, money, deck, stake, won}' + +# Round counters (hands/discards/chips) +balatrobot api gamestate --port "$PORT" | jq '.round | {hands_left, discards_left, chips, reroll_cost}' + +# Cards currently in hand (ids + labels) +balatrobot api gamestate --port "$PORT" | jq '.hand.cards | map({id, key, label})' + +# Joker labels (one per line) +balatrobot api gamestate --port "$PORT" | jq -r '.jokers.cards[].label' +``` + +On failure, `balatrobot api ...` prints a human-readable error to stderr and exits non-zero. To extract fields from that error output, capture stderr and use `jq` raw mode: + +```bash +balatrobot api play '{"cards":[999]}' --port "$PORT" 2>&1 >/dev/null \ + | jq -R 'capture("^Error: (?.*?) - (?.*)$")' +``` + +Always pass the same `--port` to both `serve` and `api`. + +## Logs and sessions + +### Session layout + +Each `balatrobot serve ...` run creates: + +- `logs//.log` (game + mod stdout/stderr) + +`` is a timestamp folder like `2025-12-29T19-18-18` (lexicographically sortable). + +### Find the current session + +Pick the newest session directory (works because of the timestamp format): + +```bash +SESSION="$(ls -1 logs | sort | tail -n 1)" +``` + +Use it to open/tail the log: + +```bash +tail -f "logs/$SESSION/$PORT.log" +``` + +The log filename matches the port: `logs//.log`. + +## Screenshots (write to logs//artifacts/) + +Only do this when explicitly asked for a screenshot. + +1. Start the server with the screenshot profile: + + ```bash + balatrobot serve --port "$PORT" --render-on-api --fast --debug + ``` + +2. Create the artifacts directory under the newest session: + + ```bash + SESSION="$(ls -1 logs | sort | tail -n 1)" + mkdir -p "logs/$SESSION/artifacts" + ``` + +3. Call the screenshot endpoint with an absolute path inside that folder: + + ```bash + balatrobot api screenshot "{\"path\":\"$(pwd)/logs/$SESSION/artifacts/screenshot.png\"}" --port "$PORT" + ``` + +## Debug workflow (tight loop) + +1. Reproduce with the smallest possible sequence of `balatrobot api ...` calls. +2. Capture: + - the exact `balatrobot serve ...` command, + - host/port, + - the session folder name, + - relevant excerpts from `logs//.log`, + - the JSON outputs (stdout) and errors (stderr) from `balatrobot api ...`. +3. Read the most relevant code before changing anything. +4. If needed, add minimal logging close to the suspected behavior, then re-run. + +## Where to look in the repo + +- CLI entry points: `src/balatrobot/cli/serve.py`, `src/balatrobot/cli/api.py` +- Game process + log session creation: `src/balatrobot/manager.py` +- Lua HTTP server + dispatcher: `src/lua/core/server.lua`, `src/lua/core/dispatcher.lua` +- API reference/spec: `docs/api.md`, `src/lua/utils/openrpc.json` +- Endpoint implementations: `src/lua/endpoints/*.lua` diff --git a/.gitignore b/.gitignore index f6b2be36..b291817f 100644 --- a/.gitignore +++ b/.gitignore @@ -315,6 +315,9 @@ balatro # balatrobot runs/*.jsonl +# logs +logs/ + ################################################################################ # Legacy files ################################################################################ diff --git a/Makefile b/Makefile index 51ee27ea..bfc75eb1 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ format: ## Run formatters (ruff, mdformat, stylua) ruff check --select I --fix . ruff format . @$(PRINT) "$(YELLOW)Running mdformat formatter...$(RESET)" - mdformat ./docs README.md CLAUDE.md + mdformat ./docs README.md CLAUDE.md .claude/skills/balatrobot/SKILL.md @if command -v stylua >/dev/null 2>&1; then \ $(PRINT) "$(YELLOW)Running stylua formatter...$(RESET)"; \ stylua src/lua; \