Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6b117d9
feat(platforms): add Linux launcher via Steam Proton
charlesmerritt Mar 28, 2026
f08f2bf
docs: document Linux/Proton platform support and dev.sh workaround
charlesmerritt Mar 28, 2026
a2aef59
feat(platforms): auto-detect DISPLAY and XAUTHORITY for Linux/Proton
charlesmerritt Mar 29, 2026
c0f2945
feat(platforms): add xauth and launch message defaults and tests
charlesmerritt Mar 29, 2026
f3cf71a
docs(examples): add examples directory and clarify example-bot.md
charlesmerritt Mar 30, 2026
f29bf95
chore(gitignore): add .lua-lsp/ to .gitignore
charlesmerritt Mar 30, 2026
06c2958
feat(platforms): change proton to prefer experimental, add other stea…
charlesmerritt Mar 30, 2026
2508942
docs: update docs for proton
charlesmerritt Mar 30, 2026
5f11d73
docs(cli): add proton section to cli reference docs
charlesmerritt Mar 30, 2026
19c8b8a
docs: fix contributing doc
charlesmerritt Mar 30, 2026
ceeaabf
fix(platforms): fix import error on platforms/linux.py
charlesmerritt Mar 30, 2026
ea511a8
fix(test-platforms): fix unused value error in test_platforms.py
charlesmerritt Mar 30, 2026
bfe9a18
docs(dev): add clarity on external tool config: stylua and lua-langua…
charlesmerritt Mar 30, 2026
09e91ff
fix(dev): avoid terminal hang in dev.sh fixtures()
charlesmerritt Mar 30, 2026
1ba49d0
chore(dev): added stop command to cleanup stale balatro instances
charlesmerritt Mar 30, 2026
3f2a163
refactor(bot): ran code quality on bot.py
charlesmerritt Mar 30, 2026
c1878b5
fix(dev): fix workers limit in dev.sh lua tests
charlesmerritt Mar 30, 2026
6a29ef8
docs: fix missing warning text in contributing doc
charlesmerritt Mar 30, 2026
1645dcf
docs: add space back to warning in docs/contributing
charlesmerritt Mar 30, 2026
d407b2e
docs: document new dev.sh stop command helper
charlesmerritt Mar 30, 2026
ca55228
fix(dev): lower lua workers further
charlesmerritt Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ __marimo__/

# Lua Language Server
.luarc.json
.lua-lsp/

# Compiled Lua sources
luac.out
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Controls the game lifecycle and provides the CLI.
- **CLI** (`cli.py`): Entry point (`balatrobot`). Handles arguments like `--fast`, `--debug`, `--headless`.
- **Manager** (`manager.py`): `BalatroInstance` context manager. Starts the game process, handles logging, and waits for the API to be healthy.
- **Config** (`config.py`): Configuration management using `dataclasses` and environment variables.
- **Platform Abstraction** (`platforms/`): Cross-platform game launcher system with platform-specific implementations for macOS, Windows, and native Love2D.
- **Platform Abstraction** (`platforms/`): Cross-platform game launcher system with platform-specific implementations for macOS, Windows, Linux (Steam/Proton), and native Love2D.

### 2. Lua Layer (`src/lua/`)

Expand Down
41 changes: 41 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,47 @@ uvx balatrobot serve --fast
uvx balatrobot serve --love-path "/path/to/love" --lovely-path "/path/to/liblovely.dylib"
```

### Linux (Steam/Proton) Platform (Experimental)

This platform is in development, and you may encounter bugs. If you do intend to run BalatroBot on Steam Deck/Proton, please report any issues you encounter.

The `linux` platform launches Balatro via Steam's Proton compatibility layer. The CLI auto-detects Steam, Proton, and Balatro paths across all Steam library folders.

**Auto-Detected Paths:**

- `BALATROBOT_LOVE_PATH`: `~/.local/share/Steam/steamapps/common/Balatro/Balatro.exe`
- `BALATROBOT_LOVELY_PATH`: `~/.local/share/Steam/steamapps/common/Balatro/version.dll`

**Auto-Detected Environment:**

The launcher automatically configures the following when not already set:

- `DISPLAY`: Detected from X11 sockets in `/tmp/.X11-unix/` (e.g. `:0`)
- `XAUTHORITY`: Detected from `XDG_RUNTIME_DIR/xauth_*` or `~/.Xauthority`
- `WINEDLLOVERRIDES=version=n,b`: Forces Wine to load the native `version.dll` (Lovely injector) instead of Wine's built-in implementation. This is the equivalent of setting the Steam launch option `WINEDLLOVERRIDES="version=n,b" %command%`.

**Requirements:**

- Balatro installed via Steam
- [Proton](https://github.com/ValveSoftware/Proton) installed via Steam (Settings > Compatibility)
- [Lovely Injector](https://github.com/ethangreen-dev/lovely-injector) `version.dll` placed in the Balatro game directory
- Balatro must have been run at least once through Steam to create the Wine prefix
- Mods directory: `~/.local/share/Steam/steamapps/compatdata/2379780/pfx/drive_c/users/steamuser/AppData/Roaming/Balatro/Mods`

**Launch:**

```bash
# Auto-detects paths, display, and Wine configuration
uvx balatrobot serve --fast

# Or specify custom paths
uvx balatrobot serve --love-path "/path/to/Balatro.exe" --lovely-path "/path/to/version.dll"
```

!!! note "Steam Deck"

This platform works on Steam Deck, including when connected via SSH where `DISPLAY` and `XAUTHORITY` are not set. The launcher auto-detects the gamescope X server and Xauthority file. Since the read-only OS does not include `make`, use `./scripts/dev.sh` as a drop-in replacement for development tasks (see [Contributing](contributing.md#steam-deck--environments-without-make)).

### Native Platform (Linux Only)

The `native` platform runs Balatro from source code using the LÖVE framework installed via package manager. This requires specific directory structure:
Expand Down
51 changes: 48 additions & 3 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Guide for contributing to BalatroBot development.

!!! warning "Help Needed: Linux (Proton) Support"

We currently lack CLI support for **Linux (Proton)**. Contributions to implement this platform are highly welcome!
CLI support for **Linux (Proton)** is in development. Contributions to implement this platform are highly welcome!

Please refer to the existing implementations for guidance:

Expand Down Expand Up @@ -53,6 +53,10 @@ export BALATROBOT_AUDIO=0

**Setup:** Install [direnv](https://direnv.net/), then create `.envrc` in the project root with the above configuration, updating paths for your system.

### External Development Tools

`uv sync --group dev --group test` installs Python dependencies only. Non-Python tools such as `stylua` and `lua-language-server` are not managed by `uv` and must be installed separately if you want Lua formatting and Lua type checking.

### Lua LSP Configuration

The `.luarc.json` file should be placed at the root of the balatrobot repository. It configures the Lua Language Server for IDE support (autocomplete, diagnostics, type checking).
Expand All @@ -65,6 +69,27 @@ The `.luarc.json` file should be placed at the root of the balatrobot repository
- Love2D library: `path/to/love2d/library` (clone locally: [LuaCATS/love2d](https://github.com/LuaCATS/love2d))
- LuaSocket library: `path/to/luasocket/library` (clone locally: [LuaCATS/luasocket](https://github.com/LuaCATS/luasocket))

**Quick setup:** Clone the LuaCATS libraries into a `.lua-lsp/` directory (already gitignored):

```bash
mkdir -p .lua-lsp
git clone --depth 1 https://github.com/LuaCATS/love2d.git .lua-lsp/love2d
git clone --depth 1 https://github.com/LuaCATS/luasocket.git .lua-lsp/luasocket
```

Then update the `workspace.library` paths in `.luarc.json` — the Steamodded `lsp_def` path varies by platform:

| Platform | Steamodded `lsp_def` path |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| macOS | `~/Library/Application Support/Balatro/Mods/smods/lsp_def` |
| Windows | `%AppData%/Balatro/Mods/smods/lsp_def` |
| Linux (Steam/Proton) | `~/.local/share/Steam/steamapps/compatdata/2379780/pfx/drive_c/users/steamuser/AppData/Roaming/Balatro/Mods/smods/lsp_def` |
| Linux (Native) | `~/.config/love/Mods/smods/lsp_def` |

!!! note "Steamodded version suffix"

Your `smods` directory may include a version suffix (e.g. `smods-1.0.0-beta-1503a`). Use the actual directory name on your system.

**Example `.luarc.json`:**

```json
Expand Down Expand Up @@ -132,7 +157,7 @@ ln -s "$(pwd)" ~/.config/love/Mods/balatrobot/
New-Item -ItemType SymbolicLink -Path "$env:APPDATA\Balatro\Mods\balatrobot" -Target (Get-Location)
```

### 3. Install Dependencies
### 3. Install Dependencies (Windows/Mac/Linux Native only, see [Steam Deck / Environments Without Make](#steam-deck--environments-without-make))

```bash
make install
Expand All @@ -142,7 +167,7 @@ make install

Activate the virtual environment to use the `balatrobot` command:

**macOS/Linux:**
**macOS/Linux (Native or Proton):**

```bash
source .venv/bin/activate
Expand Down Expand Up @@ -174,6 +199,7 @@ Tests use Python + pytest to communicate with the Lua API. You don't need to hav

```bash
# Run all tests (runs CLI and Lua suites separately)
# If on Proton/Steam Deck use: uv run ./scripts/dev.sh test
make test

# Run Lua tests (parallel execution recommended)
Expand Down Expand Up @@ -216,6 +242,25 @@ make all # Run quality checks + tests

The `make fixtures` command is only required if you need to explicitly generate fixtures. When running tests, missing fixtures are automatically generated if required.

### Steam Deck / Environments Without Make

On platforms where `make` is not available (e.g. Steam Deck's read-only OS), use `scripts/dev.sh` as a drop-in replacement:

```bash
./scripts/dev.sh help # Show all available commands
./scripts/dev.sh install # Install all dependencies
./scripts/dev.sh lint # Run ruff linter
./scripts/dev.sh format # Format code
./scripts/dev.sh typecheck # Run type checkers
./scripts/dev.sh quality # Run all quality checks
./scripts/dev.sh test # Run all tests
./scripts/dev.sh fixtures # Generate test fixtures
./scripts/dev.sh stop # Kill all BalatroBot and Balatro processes
./scripts/dev.sh all # Run quality checks + tests
```

The script mirrors all Makefile targets and can be used anywhere `make <target>` is referenced in these docs.

## Code Structure

```
Expand Down
4 changes: 2 additions & 2 deletions docs/example-bot.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ if __name__ == "__main__":
uvx balatrobot serve
```

2. In another terminal, run the bot:
2. In another terminal, run the bot from the repo root:

```bash
uv run bot.py
uv run examples/bot.py
```

The bot will automatically start a new game and play until it wins or loses.
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Mods/
| Linux (Steam/Proton) | `~/.local/share/Steam/steamapps/compatdata/2379780/pfx/drive_c/users/steamuser/AppData/Roaming/Balatro/Mods/` |
| Linux (Native) | `~/.config/love/Mods/balatrobot/` |

> Steam/Proton launcher not supported yet. Track progress in [#128](https://github.com/coder/balatrobot/issues/128)
> Linux (Steam/Proton) is an experimental platform via the `linux` platform. See the [CLI Reference](cli.md#linux-steamproton-platform) for details.
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link target cli.md#linux-steamproton-platform likely doesn’t match the actual heading slug in docs/cli.md (### Linux (Steam/Proton) Platform (Experimental) typically slugs to #linux-steamproton-platform-experimental in MkDocs/Material). This will produce a broken in-page link; adjust the fragment or add an explicit anchor to the CLI heading.

Suggested change
> Linux (Steam/Proton) is an experimental platform via the `linux` platform. See the [CLI Reference](cli.md#linux-steamproton-platform) for details.
> Linux (Steam/Proton) is an experimental platform via the `linux` platform. See the [CLI Reference](cli.md#linux-steamproton-platform-experimental) for details.

Copilot uses AI. Check for mistakes.

### 3. Launch Balatro

Expand Down
74 changes: 74 additions & 0 deletions examples/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests",
# ]
# ///

import requests

# BalatroBot API endpoint
URL = "http://127.0.0.1:12346"


def rpc(method: str, params: dict = {}) -> dict:
"""Send a JSON-RPC 2.0 request to the BalatroBot API."""
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
Comment on lines +14 to +21
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rpc() uses a mutable default argument (params={}), which can leak state between calls if the dict is ever mutated. Use params: dict | None = None and create a new dict inside the function when needed.

Suggested change
def rpc(method: str, params: dict = {}) -> dict:
"""Send a JSON-RPC 2.0 request to the BalatroBot API."""
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
def rpc(method: str, params: dict | None = None) -> dict:
"""Send a JSON-RPC 2.0 request to the BalatroBot API."""
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params or {},

Copilot uses AI. Check for mistakes.
"id": 1,
},
)
Comment on lines +16 to +24
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example bot’s requests.post(...) call has no timeout. If the local API is unreachable, this can hang indefinitely and make the example look broken. Consider adding a reasonable timeout (and optionally a clearer exception message for connection errors).

Suggested change
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
},
)
try:
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
},
timeout=5,
)
except requests.exceptions.RequestException as exc:
raise RuntimeError(f"Failed to connect to BalatroBot API at {URL}") from exc

Copilot uses AI. Check for mistakes.
data = response.json()
# Raise if error, otherwise return result (contains game state)
if "error" in data:
raise Exception(data["error"]["message"])
return data["result"]


def play_game():
"""Play a complete game of Balatro."""
# Return to menu and start a new game
rpc("menu")
state = rpc("start", {"deck": "RED", "stake": "WHITE"})
print(f"Started game with seed: {state['seed']}")

# Main game loop
while state["state"] != "GAME_OVER":
match state["state"]:
case "BLIND_SELECT":
# Always select the current blind
state = rpc("select")

case "SELECTING_HAND":
# Play the first 5 cards (simple strategy)
num_cards = min(5, len(state["hand"]["cards"]))
cards = list(range(num_cards))
state = rpc("play", {"cards": cards})

case "ROUND_EVAL":
# Collect rewards and go to shop
state = rpc("cash_out")

case "SHOP":
# Skip the shop and proceed to next round
state = rpc("next_round")

case _:
# Handle any transitional states
state = rpc("gamestate")

# Game ended
if state["won"]:
print(f"Victory! Final ante: {state['ante_num']}")
else:
print(f"Game over at ante {state['ante_num']}, round {state['round_num']}")

return state["won"]


if __name__ == "__main__":
play_game()
132 changes: 132 additions & 0 deletions scripts/dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# dev.sh — Make-compatible dev task runner for environments without make (e.g. Steam Deck).
# Usage: ./scripts/dev.sh <target>
# Mirrors the targets in the project Makefile.

set -euo pipefail

YELLOW='\033[33m'
GREEN='\033[32m'
BLUE='\033[34m'
RED='\033[31m'
RESET='\033[0m'

MAX_XDIST="${MAX_XDIST:-6}"
CLI_XDIST_WORKERS="${CLI_XDIST_WORKERS:-2}"
XDIST_WORKERS=$(python -c "import multiprocessing as mp; print(min(mp.cpu_count(), $MAX_XDIST))")
LUA_XDIST_WORKERS="${LUA_XDIST_WORKERS:-2}"

print_msg() { printf "%b\n" "$1"; }

cmd_help() {
print_msg "${BLUE}BalatroBot Development Tasks${RESET}"
print_msg ""
print_msg "${YELLOW}Available targets:${RESET}"
printf " ${GREEN}%-18s${RESET} %s\n" "help" "Show this help message"
printf " ${GREEN}%-18s${RESET} %s\n" "install" "Install balatrobot and all dependencies (including dev)"
printf " ${GREEN}%-18s${RESET} %s\n" "lint" "Run ruff linter (check only)"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the help output, the lint target is described as "check only", but cmd_lint runs ruff check --fix ..., which modifies files. Update the help text (or the command) so the behavior matches what’s advertised.

Suggested change
printf " ${GREEN}%-18s${RESET} %s\n" "lint" "Run ruff linter (check only)"
printf " ${GREEN}%-18s${RESET} %s\n" "lint" "Run ruff linter (apply fixes)"

Copilot uses AI. Check for mistakes.
printf " ${GREEN}%-18s${RESET} %s\n" "format" "Run formatters (ruff, mdformat, stylua)"
printf " ${GREEN}%-18s${RESET} %s\n" "typecheck" "Run type checkers (Python and Lua)"
printf " ${GREEN}%-18s${RESET} %s\n" "quality" "Run all code quality checks"
printf " ${GREEN}%-18s${RESET} %s\n" "fixtures" "Generate fixtures"
printf " ${GREEN}%-18s${RESET} %s\n" "stop" "Stop BalatroBot and Balatro processes"
printf " ${GREEN}%-18s${RESET} %s\n" "test" "Run all tests"
printf " ${GREEN}%-18s${RESET} %s\n" "all" "Run all code quality checks and tests"
}

cmd_install() {
print_msg "${YELLOW}Installing all dependencies...${RESET}"
uv sync --group dev --group test
}

cmd_lint() {
print_msg "${YELLOW}Running ruff linter...${RESET}"
ruff check --fix --select I .
ruff check --fix .
}

cmd_format() {
print_msg "${YELLOW}Running ruff formatter...${RESET}"
ruff check --select I --fix .
ruff format .
print_msg "${YELLOW}Running mdformat formatter...${RESET}"
mdformat ./docs README.md CLAUDE.md .claude/skills/balatrobot/SKILL.md
if command -v stylua >/dev/null 2>&1; then
print_msg "${YELLOW}Running stylua formatter...${RESET}"
stylua src/lua
else
print_msg "${BLUE}Skipping stylua formatter (stylua not found)${RESET}"
fi
}

cmd_typecheck() {
print_msg "${YELLOW}Running Python type checker...${RESET}"
ty check
if command -v lua-language-server >/dev/null 2>&1 && [ -f .luarc.json ]; then
print_msg "${YELLOW}Running Lua type checker...${RESET}"
lua-language-server --check balatrobot.lua src/lua \
--configpath="$(pwd)/.luarc.json" 2>/dev/null
else
print_msg "${BLUE}Skipping Lua type checker (lua-language-server not found or .luarc.json missing)${RESET}"
fi
}

cmd_quality() {
cmd_lint
cmd_typecheck
cmd_format
print_msg "${GREEN}All checks completed${RESET}"
}

cmd_fixtures() {
print_msg "${YELLOW}Generating all fixtures...${RESET}"
if ! python tests/fixtures/generate.py; then
print_msg "${RED}Fixture generation failed. Make sure BalatroBot is already running and reachable, then try again.${RESET}"
return 1
fi
}

cmd_stop() {
local stopped=0

if pkill -f "balatrobot serve" >/dev/null 2>&1; then
print_msg "${YELLOW}Stopped BalatroBot serve process(es).${RESET}"
stopped=1
fi

if pkill -f "Balatro.exe" >/dev/null 2>&1; then
print_msg "${YELLOW}Stopped Balatro game process(es).${RESET}"
stopped=1
fi

if [ "$stopped" -eq 0 ]; then
print_msg "${BLUE}No BalatroBot or Balatro processes found.${RESET}"
fi
}

cmd_test() {
print_msg "${YELLOW}Running tests/cli with ${CLI_XDIST_WORKERS} workers...${RESET}"
pytest -n "${CLI_XDIST_WORKERS}" tests/cli
print_msg "${YELLOW}Running tests/lua with ${LUA_XDIST_WORKERS} workers...${RESET}"
pytest -n "${LUA_XDIST_WORKERS}" tests/lua
}

cmd_all() {
cmd_lint
cmd_format
cmd_typecheck
cmd_test
print_msg "${GREEN}All checks and tests completed${RESET}"
}

target="${1:-help}"
case "$target" in
help|install|lint|format|typecheck|quality|fixtures|stop|test|all)
"cmd_${target}"
;;
*)
print_msg "${RED}Unknown target: ${target}${RESET}"
cmd_help
exit 1
;;
esac
Loading
Loading