- Add compact storage package: codec, records, hydrate, replay blobs, cache. - Running games write game_turns and snake_states keyed by integer ids. - Encode moves as 1-byte codes and bodies as head plus step codes. - Store food and hazards only when they changed since the previous turn. - Strip reasoning fields that duplicate stored values and restore on read. - Pack finished games into one zstd blob per game in game_replays. - Merge late turns into packed blobs during the stale-game sweep. - Extract SQLite and PostgreSQL schemas into dedicated modules. - Add read/import game bundle and list_game_ids to the backend template. - Add legacy readers and converter for the old turns/snake_turns layout. - Add migrate_gameplay_to_compact.py with in-place or cross-DB migration. - Verify every migrated game and keep legacy rows on failure. - Support --since, --minimum-quality and guarded --drop-legacy. - Add just recipe migrate-gameplay-postgres. - Rewrite merge script to copy game by game via backend bundles. - Move benchmark state loading into benchmark_states for both backends. - Update PostgreSQL cleanup script to the new replay tables. - Drop sqlite-zstd build stages from the Dockerfile. - Remove old migration scripts, normalized_turn and their tests. - Add tests for compact storage, schema, legacy migration and PostgreSQL. - Document the compact layout and migration workflow in the README.
12 KiB
Battlesnake Python Starter Project
An official Battlesnake template written in Python. Get started at play.battlesnake.com.
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 for more detail.
Technologies Used
This project requires Python 3.15+ and uses Quart with Hypercorn. The Dockerfile uses the uv Python 3.15 runtime.
Run Your Battlesnake
Install the pinned Python runtime and locked dependencies using uv:
uv python install 3.15
uv sync --locked
Python modernization (2026-11-03):
- Python 3.15
lazyimports defer database backend loading and benchmark-only dependencies. Optional-dependencytry/exceptimports 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.infochecks reuse directory-entry metadata during dataset and customization asset discovery. Paths reused after mutations retain fresh checks. - The shared
quart_commonsubmodule 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
uv run --locked python main.py
You should see the following output once it is running
Running your Battlesnake at http://0.0.0.0:8000
* Serving Flask app 'My Battlesnake'
* Debug mode: off
Open localhost:8000 in your browser and you should see
{"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"}
Play a Game Locally
Install the Battlesnake CLI
- You can download compiled binaries here
- or install as a go package (requires Go 1.18 or higher)
Command to run a local game
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 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:
SNAKE=BestBattleSnake python main.py
Optional duel tuning (when only 2 snakes are alive):
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 pointssnakes/engine/— reusable bitboards, spatial mixins, duel search, and survival searchsnakes/core/— shared base classessnakes/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:
SNAKE=PrismBattleSnake_GPT_5_6_Sol python main.py
Benchmark Apex and Prism against sampled positions from a gameplay database:
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:
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:
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_turnsrow per turn and onesnake_statesrow 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:
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 <start time of the first run> 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:
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:
python -m server.DatasetExporter --input data --output data/dataset/good_moves.jsonl
Or with just:
just export-dataset
Curate a high-quality training subset (single file):
python -m server.DatasetCurator --input good_moves-2026-04-03.jsonl --output data/dataset/best_moves.jsonl
Curate from multiple JSONL sources (repeat --input):
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:
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):
python -m server.DatasetCurator --input "good_moves-*.jsonl" --output data/dataset/best_moves.jsonl --append
Archive processed input files after curation:
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:
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):
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:
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:
STORE_DATASET_ONLY=true DATASET_JSONL_PATH=data/dataset/good_moves.jsonl python main.py
Optional compact storage tuning:
DATASET_ROTATE_DAILY=truecreates one JSONL file per day (default:true)DATASET_JSONL_MAX_MB=50rotates when file reaches max size in MB (default:50)DATASET_COMPRESS_ROTATED=truegzip-compresses rotated/old JSONL files (default:true)
Note: To play games on play.battlesnake.com you'll need to deploy your Battlesnake to a live web server OR use a port forwarding tool like ngrok to access your server locally.
