feat: add Zenith strategy and move latency tracing
Build and Push Docker Container / build-and-push (push) Successful in 2m18s

- Register Zenith 1.1 with balanced duel verdicts and forced-outcome overrides.
- Prioritize permanent-trail space in Constrictor and health-aware hazard routes.
- Deduct executor queue delays and return fallback directions on move timeouts.
- Correlate response headers, replay timings, and sampled structured trace logs.
- Move shared metrics publication off the response path without losing local counts.
- Allow configurable tournament opponents and add a psql helper recipe.
- Preserve empty latency values through compact replay encoding and migration.
- Update Arena game links and use default assets for missing customizations.
- Cover strategy, HTTP lifecycle, tracing, replay codecs, and dashboard behavior.
- Validate with 260 passing Python tests, 4 skips, and 2 passing JavaScript tests.
This commit is contained in:
2026-11-05 01:11:31 +01:00
parent 39a7a53873
commit a2414f0b3d
26 changed files with 758 additions and 45 deletions
+22 -15
View File
@@ -152,6 +152,10 @@ def run_game(
draw = bool(terminal.get("isDraw", winner is None))
return {"seed": seed, "winner": winner, "draw": draw, "turns": turns}
def display_name(snake: str) -> str:
"""Short engine display name, e.g. PrismBattleSnake_GPT_5_6_Sol -> Prism."""
return snake.split("_", 1)[0].removesuffix("BattleSnake") or snake
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--games", type=int, default=20, help="Number of unique seeds")
@@ -163,37 +167,39 @@ def main() -> None:
parser.add_argument("--timeout", type=int, default=500)
parser.add_argument("--base-port", type=int, default=9301)
parser.add_argument("--cli", default=str(ROOT / ".testing/tools/battlesnake-cli/battlesnake"))
parser.add_argument("--snake-a", default="ApexBattleSnake", help="Registry name of the first snake")
parser.add_argument("--snake-b", default="PrismBattleSnake_GPT_5_6_Sol", help="Registry name of the second snake")
parser.add_argument("--json-output")
args = parser.parse_args()
if args.snake_a == args.snake_b:
raise SystemExit("--snake-a and --snake-b must differ")
cli = Path(args.cli)
if not cli.is_file():
raise SystemExit(f"Battlesnake CLI not found: {cli}. Run: just build-battlesnake-cli")
apex_port, prism_port = args.base_port, args.base_port + 1
apex_proxy_port, prism_proxy_port = args.base_port + 2, args.base_port + 3
snakes = (args.snake_a, args.snake_b)
servers: list[subprocess.Popen] = []
proxies: list[tuple[ThreadingHTTPServer, Thread]] = []
results: list[dict] = []
started = time.perf_counter()
try:
servers = [
start_server("ApexBattleSnake", apex_port),
start_server("PrismBattleSnake_GPT_5_6_Sol", prism_port),
]
proxies = [
start_proxy(apex_proxy_port, apex_port),
start_proxy(prism_proxy_port, prism_port),
]
urls = {
"Apex": f"http://127.0.0.1:{apex_proxy_port}",
"Prism": f"http://127.0.0.1:{prism_proxy_port}",
}
urls: dict[str, str] = {}
labels = [display_name(snake) for snake in snakes]
if len(set(labels)) != len(labels):
labels = list(snakes)
for index, (snake, label) in enumerate(zip(snakes, labels)):
server_port = args.base_port + index
proxy_port = args.base_port + len(snakes) + index
servers.append(start_server(snake, server_port))
proxies.append(start_proxy(proxy_port, server_port))
urls[label] = f"http://127.0.0.1:{proxy_port}"
names = tuple(urls)
for offset in range(max(1, args.games)):
seed = args.seed_start + offset
# Swap engine slots for every seed. This controls for deterministic map
# spawn positions and gives each strategy both initial placements.
for order in (("Apex", "Prism"), ("Prism", "Apex")):
for order in (names, names[::-1]):
result = run_game(
cli=cli, seed=seed, game_type=args.gametype, map_name=args.map,
players=[(name, urls[name]) for name in order],
@@ -219,6 +225,7 @@ def main() -> None:
"unique_seeds": max(1, args.games),
"gametype": args.gametype,
"map": args.map,
"snakes": list(snakes),
"wins": dict(wins),
"win_rates": {
key: value / len(results) for key, value in wins.items()