Files
snake-python/scripts/benchmark_snakes_from_db.py
T
daniel156161 6643eb35af fix: resolve duel roots and recover legacy snake data
- Resolve selected moves and enemy replies on the same simulated turn.
- Add an Apex candidate hook and bump the Prism snake to version 1.1.0.
- Rebuild benchmark states from normalized turn data when snapshots are empty.
- Synthesize missing game snake identities during legacy database migration.
- Add regression coverage for duel timing and partial legacy schemas.
2026-08-01 19:11:32 +02:00

174 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Benchmark snake move latency against sampled states from gameplay SQLite."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sqlite3
from statistics import mean, median
import sys
from time import perf_counter
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from server.GameBoard import GameBoard
from snakes import SnakeBuilder
def percentile(values: list[float], quantile: float) -> float:
ordered = sorted(values)
index = min(len(ordered) - 1, round((len(ordered) - 1) * quantile))
return ordered[index]
def load_states(db_path: str, samples: int, stride: int) -> list[tuple[dict, dict]]:
connection = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
connection.execute("PRAGMA query_only = ON")
max_id = int(connection.execute("SELECT max(id) FROM turns").fetchone()[0] or 0)
if max_id == 0:
return []
states: list[tuple[dict, dict]] = []
next_id = max(1, max_id - (samples - 1) * stride)
query = """
SELECT t.id, t.board_state_json, t.you_json, t.food_json, t.hazards_json,
g.your_snake_id, g.your_snake_name, g.width, g.height,
g.game_id, g.source, g.map_name,
g.ruleset_name, g.ruleset_version, t.turn
FROM turns AS t
JOIN games AS g ON g.game_id = t.game_id
WHERE t.id >= ?
ORDER BY t.id
LIMIT 1
"""
snake_query = """
SELECT st.snake_id, COALESCE(gs.snake_name, st.snake_name),
st.health, st.length, st.head_x, st.head_y, st.body_json,
COALESCE(gs.customizations_json, '{}')
FROM snake_turns AS st
LEFT JOIN game_snakes AS gs
ON gs.game_id = st.game_id AND gs.snake_id = st.snake_id
WHERE st.game_id = ? AND st.turn = ?
ORDER BY st.id
"""
while len(states) < samples and next_id <= max_id:
row = connection.execute(query, (next_id,)).fetchone()
if row is None:
break
board = json.loads(row[1])
you = json.loads(row[2])
if not board.get("snakes"):
snakes = []
for snake_row in connection.execute(snake_query, (row[9], row[14])):
snake_id = snake_row[0]
snake_name = snake_row[1] or (row[6] if snake_id == row[5] else snake_id)
body = json.loads(snake_row[6])
snakes.append({
"id": snake_id,
"name": snake_name,
"health": snake_row[2],
"length": snake_row[3],
"head": {"x": snake_row[4], "y": snake_row[5]},
"body": body,
"customizations": json.loads(snake_row[7]),
})
board = {
"width": row[7],
"height": row[8],
"food": json.loads(row[3]),
"hazards": json.loads(row[4]),
"snakes": snakes,
}
if not you:
you = next(
(snake for snake in board.get("snakes", []) if snake.get("id") == row[5]),
{},
)
if not you or not board.get("snakes"):
next_id = int(row[0]) + stride
continue
metadata = {
"game_id": row[9],
"source": row[10] or "custom",
"map": row[11] or "standard",
"ruleset": {
"name": row[12] or "standard",
"version": row[13] or "v1.0.0",
"settings": {},
},
"turn": int(row[14]),
}
states.append((board, {"you": you, **metadata}))
next_id = int(row[0]) + stride
connection.close()
return states
def benchmark(snake_name: str, states: list[tuple[dict, dict]], repeat: int) -> dict:
durations: list[float] = []
moves = 0
for pass_number in range(repeat):
for board_data, metadata in states:
snake = SnakeBuilder.build(snake_name)
game_id = f"benchmark-{pass_number}-{metadata['game_id']}"
board = GameBoard(
game_id=game_id,
width=board_data["width"],
height=board_data["height"],
ruleset=metadata["ruleset"],
source=metadata["source"],
map=metadata["map"],
snake_class=snake,
)
state = {
"game": {
"id": game_id,
"ruleset": metadata["ruleset"],
"source": metadata["source"],
"map": metadata["map"],
"timeout": 500,
},
"turn": metadata["turn"],
"board": board_data,
"you": metadata["you"],
}
board.read_game_data(state)
started = perf_counter()
snake.choose_move(board)
durations.append((perf_counter() - started) * 1000)
moves += 1
return {
"snake": snake_name,
"moves": moves,
"mean_ms": mean(durations),
"median_ms": median(durations),
"p95_ms": percentile(durations, 0.95),
"max_ms": max(durations),
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--database", required=True)
parser.add_argument("--snake", action="append", default=[])
parser.add_argument("--samples", type=int, default=100)
parser.add_argument("--stride", type=int, default=997)
parser.add_argument("--repeat", type=int, default=1)
args = parser.parse_args()
states = load_states(args.database, max(1, args.samples), max(1, args.stride))
if not states:
raise SystemExit("No gameplay states found")
snake_names = args.snake or ["ApexBattleSnake", "PrismBattleSnake_GPT_5_6_Sol"]
print(f"Loaded {len(states)} states from {args.database}")
for snake_name in snake_names:
result = benchmark(snake_name, states, max(1, args.repeat))
print(
f"{result['snake']}: {result['moves']} moves, "
f"mean={result['mean_ms']:.2f} ms, median={result['median_ms']:.2f} ms, "
f"p95={result['p95_ms']:.2f} ms, max={result['max_ms']:.2f} ms"
)
if __name__ == "__main__":
main()