# Battlesnake Python Starter Project An official Battlesnake template written in Python. Get started at [play.battlesnake.com](https://play.battlesnake.com). ![Battlesnake Logo](https://media.battlesnake.com/social/StarterSnakeGitHubRepos_Python.png) This project is a great starting point for anyone wanting to program their first Battlesnake in Python. It can be run locally or easily deployed to a cloud provider of your choosing. See the [Battlesnake API Docs](https://docs.battlesnake.com/api) for more detail. ## Technologies Used This project requires [Python 3.15+](https://www.python.org/) and uses [Quart](https://quart.palletsprojects.com/) with Hypercorn. The Dockerfile uses the uv Python 3.15 runtime. ## Run Your Battlesnake Install the pinned Python runtime and locked dependencies using uv: ```sh uv python install 3.15 uv sync --locked ``` Python modernization (2026-11-03): - Python 3.15 `lazy` imports defer database backend loading and benchmark-only dependencies. Optional-dependency `try`/`except` imports keep their fallback behavior. - Python 3.14 deferred annotations replace `from __future__ import annotations`. Type-only dependencies use lazy bindings to avoid loading circular imports at startup. - Python 3.14 `Path.info` checks reuse directory-entry metadata during dataset and customization asset discovery. Paths reused after mutations retain fresh checks. - The shared `quart_common` submodule also requires Python 3.15 and already uses Python 3.14 UUIDv7 request IDs. Run tests with `uv run --locked pytest tests quart_common/tests/test_user_agent.py`. Python 3.14 is **not** a supported runtime: this project uses 3.15-only syntax. Start your Battlesnake ```sh uv run --locked python main.py ``` You should see the following output once it is running ```sh Running your Battlesnake at http://0.0.0.0:8000 * Serving Flask app 'My Battlesnake' * Debug mode: off ``` Open [localhost:8000](http://localhost:8000) in your browser and you should see ```json {"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"} ``` ## Play a Game Locally Install the [Battlesnake CLI](https://github.com/BattlesnakeOfficial/rules/tree/main/cli) * You can [download compiled binaries here](https://github.com/BattlesnakeOfficial/rules/releases) * or [install as a go package](https://github.com/BattlesnakeOfficial/rules/tree/main/cli#installation) (requires Go 1.18 or higher) Command to run a local game ```sh battlesnake play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser ``` ## Next Steps Continue with the [Battlesnake Quickstart Guide](https://docs.battlesnake.com/quickstart) to customize and improve your Battlesnake's behavior. ## Included Competitive Snake This repo retains `snakes/legacy/BestBattleSnake.py`, a stronger historical snake that combines: - collision and head-to-head risk checks - flood-fill space evaluation to avoid traps - food routing that gets more aggressive as health drops - tail access checks for better long-term survival Run it explicitly with: ```sh SNAKE=BestBattleSnake python main.py ``` Optional duel tuning (when only 2 snakes are alive): ```sh BATTLE_SNAKE_DUEL_STYLE=balanced python main.py ``` Allowed values: `safe`, `balanced`, `aggressive`. ## Snake package layout The snake code is split by responsibility: - `snakes/strategies/` — actively maintained Apex and Prism entry points - `snakes/engine/` — reusable bitboards, spatial mixins, duel search, and survival search - `snakes/core/` — shared base classes - `snakes/legacy/` — historical snakes retained for compatibility and benchmarks Snake selection still uses the existing registry names, so deployment values such as `SNAKE=PrismBattleSnake_GPT_5_6_Sol` remain unchanged. ## PrismBattleSnake_GPT_5_6_Sol `PrismBattleSnake_GPT_5_6_Sol` is a separate snake that keeps Apex's strategy while accelerating hot spatial operations with a Python-integer bitboard engine. It also shares duel transpositions across candidate moves, uses principal-variation ordering, aspiration windows, path-aware food races, and a deeper tactical horizon, and runs a compact adversarial multiplayer rollout with simultaneous enemy responses and cached occupancy/evaluation states. Its filename, class, and registry key include the model name, while its public Battlesnake API name remains `PrismBattleSnake`. Run it with: ```sh SNAKE=PrismBattleSnake_GPT_5_6_Sol python main.py ``` Benchmark Apex and Prism against sampled positions from a gameplay database: ```sh python scripts/benchmark_snakes_from_db.py \ --database /path/to/gameplay.sqlite3 \ --samples 100 ``` The benchmark samples every `--stride`-th stored turn from the newest games (SQLite is opened read-only; a `postgresql://` DSN works too) and reports mean, median, p95, and maximum move latency. Increase `--samples` for a broader but slower comparison. Run the deterministic CI-friendly arena benchmark without a gameplay database: ```sh just bench-snake-arena positions=100 ``` It rotates through duel, hazard, multiplayer, constrictor, and cramped-endgame positions. It reports latency, completed duel/rollout depth, searched nodes, cache hits, deadline exits, and move disagreements between Apex and Prism. Use `--scenario hazard` (repeatable) when invoking the Python script to isolate a scenario. Add `output=data/arena-report.json` to save a machine-readable report. For representative strategy evaluation, provide recorded positions to `scripts/benchmark_snake_arena.py --database /path/to/gameplay.sqlite3`. Run paired seeded games through the official local Battlesnake rules engine: ```sh just bench-snake-tournament games=20 gametype=standard map=standard ``` Each seed is played twice with Apex and Prism swapping initial engine slots. The report includes wins, draws, win rates, and average game length. Save all per-game results with `output=data/tournament-report.json`. The tournament starts both snake servers with gameplay persistence disabled, adds the engine identity header required by the API, and shuts them down when finished. ## Compact gameplay database Gameplay storage keeps game results in `games` and snake identity (name, customizations) once per game in `game_snakes`. Replay data is stored compactly: - **Running games** write one `game_turns` row per turn and one `snake_states` row per snake and turn, keyed by small integer ids (`games.gid`, `game_snakes.gsid`) instead of repeating text ids. Moves are 1-byte codes, latency is an integer, bodies are a head position plus one step code per segment, and food/hazards are only stored when they changed since the previous turn. Reasoning fields that equal stored values (`turn`, `move`, `head`, `health`, `length`, `food_count`, `board_size`, `snakes`) are stripped and restored exactly on read. - **Finished games** are packed into one zstd-compressed blob per game in `game_replays`, and their rows are deleted. Turns arriving after a game was packed are merged into the blob by the stale-game sweep. Replay loading rebuilds the normal Battlesnake board payload, so the dashboard is unchanged. Head position, length, and inferred moves are derived from the stored bodies. ### Migrate the legacy layout Databases written by older versions store replays in `turns`/`snake_turns`. Migrate them in place (SQLite path or `postgresql://` DSN). Every game is read back and compared after writing; failed games are reported and keep their legacy rows: ```sh python scripts/migrate_gameplay_to_compact.py --source /path/to/gameplay.sqlite3 python scripts/migrate_gameplay_to_compact.py --source /path/to/gameplay.sqlite3 --drop-legacy ``` `--destination` writes to a different database instead, for example from SQLite into PostgreSQL. `--minimum-quality medium` re-rates replays by structural completeness, valid moves, thinking coverage, game length, move diversity, opponent data, and terminal outcome, and keeps only replays of that tier or better. All game-result rows remain stored, so historical win/loss rates stay persistent. `--drop-legacy` only drops the old tables when every legacy replay exists in compact form. For a live PostgreSQL database: run the migration while the old version keeps recording, deploy the new version, run the migration again with `--since ` to pick up games recorded meanwhile, then run `--drop-legacy`. A PostgreSQL server configured with a SQLite `GAMEPLAY_DB_PATH` imports that legacy file automatically on startup. ### Record new games while cleanup runs Point the running server at a temporary delta database while the old database is being migrated or cleaned. After stopping the writer and flushing the delta database, merge it into the cleaned copy: ```sh python scripts/merge_gameplay_databases.py \ --base /path/to/gameplay.compact.sqlite3 \ --delta /path/to/gameplay.delta.sqlite3 \ --destination /path/to/gameplay.merged.sqlite3 \ --minimum-quality medium ``` The merger copies game by game, keeps all game results, quality-rates delta games, assigns fresh integer ids, and verifies game, snake, turn, and snake-state counts. Identical game IDs are skipped; conflicting duplicates abort the merge. After reviewing the result, `--replace-base` backs up and replaces the cleaned base. Stop the delta writer before the final merge and file swap. ## Export Training Dataset Game saves now include a `dataset` section with labeled move samples. Export all stored samples to JSONL: ```sh python -m server.DatasetExporter --input data --output data/dataset/good_moves.jsonl ``` Or with `just`: ```sh just export-dataset ``` Curate a high-quality training subset (single file): ```sh python -m server.DatasetCurator --input good_moves-2026-04-03.jsonl --output data/dataset/best_moves.jsonl ``` Curate from multiple JSONL sources (repeat `--input`): ```sh python -m server.DatasetCurator \ --input good_moves-2026-04-03.jsonl \ --input good_moves-2026-04-04.jsonl \ --output data/dataset/best_moves.jsonl ``` Curate from folder or glob: ```sh python -m server.DatasetCurator --input data/dataset --output data/dataset/best_moves.jsonl python -m server.DatasetCurator --input "good_moves-*.jsonl" --output data/dataset/best_moves.jsonl ``` Append mode (keeps existing curated rows and deduplicates against them): ```sh python -m server.DatasetCurator --input "good_moves-*.jsonl" --output data/dataset/best_moves.jsonl --append ``` Archive processed input files after curation: ```sh python -m server.DatasetCurator --input "good_moves-*.jsonl" --output data/dataset/best_moves.jsonl --append --archive-input python -m server.DatasetCurator --input "good_moves-*.jsonl" --output data/dataset/best_moves.jsonl --append --archive-input --archive-dir data/dataset/archive ``` Or with `just`: ```sh just curate-dataset just curate-dataset append=true just curate-dataset append=true archive=true archive_dir=data/dataset/archive ``` Analyze dataset quality overall and by day (best game overall/day included): ```sh python -m server.DatasetStats --input "good_moves-*.jsonl" python -m server.DatasetStats --input data/dataset --output data/dataset/stats-report.json ``` The stats report now includes both: - `best_game` (survival/length focused) - `best_pressure_game` (high-pressure quality focused: fewer safe options + strong survival) Or with `just`: ```sh just analyze-dataset just analyze-dataset input=data/dataset output=data/dataset/stats-report.json ``` To store compact dataset-only records (JSONL) and skip full per-game JSON files: ```sh STORE_DATASET_ONLY=true DATASET_JSONL_PATH=data/dataset/good_moves.jsonl python main.py ``` Optional compact storage tuning: - `DATASET_ROTATE_DAILY=true` creates one JSONL file per day (default: `true`) - `DATASET_JSONL_MAX_MB=50` rotates when file reaches max size in MB (default: `50`) - `DATASET_COMPRESS_ROTATED=true` gzip-compresses rotated/old JSONL files (default: `true`) **Note:** To play games on [play.battlesnake.com](https://play.battlesnake.com) you'll need to deploy your Battlesnake to a live web server OR use a port forwarding tool like [ngrok](https://ngrok.com/) to access your server locally.