feat(snake): modularize engine and add tournament tools

- Split active strategies, reusable engine code, core classes, and legacy snakes.
- Replace implicit snake imports with explicit module registrations.
- Extract Prism duel, spatial, and survival behavior into focused mixins.
- Improve duel scoring with food races, pressure, caches, and depth metrics.
- Add deterministic arena scenarios and paired seeded engine tournaments.
- Expand benchmark telemetry and bump Prism to version 1.3.0.
- Update documentation and tests for the new package layout and tooling.
This commit is contained in:
2026-08-01 20:25:07 +02:00
parent cb6c8d4dc8
commit 3a9af3f54d
39 changed files with 1447 additions and 768 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ import argparse
import time
from server.GameBoard import GameBoard
from snakes.BestBattleSnake import BestBattleSnake
from snakes.legacy.BestBattleSnake import BestBattleSnake
def build_game_state() -> dict:
return {
@@ -0,0 +1,54 @@
import io
import unittest
from unittest.mock import patch
from scripts.run_seeded_snake_tournament import ENGINE_USER_AGENT, run_game
class _Completed:
returncode = 0
stdout = "INFO Game completed after 42 turns. Prism was the winner.\n"
class _OutputFile:
def __init__(self, *args, **kwargs):
self.file = io.BytesIO(
b'{"turn":0}\n'
b'{"winnerId":"snake-id","winnerName":"Prism","isDraw":false}\n'
)
self.name = "arena-output.jsonl"
def __enter__(self):
return self
def __exit__(self, *args):
self.file.close()
def seek(self, offset):
return self.file.seek(offset)
def __iter__(self):
return iter(self.file)
class TestSeededSnakeTournament(unittest.TestCase):
def test_proxy_identifies_requests_as_battlesnake_engine(self):
self.assertIn("BattlesnakeEngine", ENGINE_USER_AGENT)
@patch("scripts.run_seeded_snake_tournament.subprocess.run", return_value=_Completed())
@patch("scripts.run_seeded_snake_tournament.tempfile.NamedTemporaryFile", _OutputFile)
def test_run_game_reads_official_engine_result(self, run):
result = run_game(
cli="battlesnake", seed=7, game_type="standard", map_name="standard",
players=[("Apex", "http://host:9001"), ("Prism", "http://host:9002")],
width=11, height=11, timeout_ms=500,
)
self.assertEqual(result, {
"seed": 7, "winner": "Prism", "draw": False, "turns": 42,
})
command = run.call_args.args[0]
self.assertIn("--seed", command)
self.assertIn("--output", command)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,31 @@
import unittest
from scripts.snake_arena_scenarios import SCENARIOS, synthetic_states
class TestSnakeArenaScenarios(unittest.TestCase):
def test_default_corpus_rotates_through_every_scenario(self):
states = synthetic_states(len(SCENARIOS))
self.assertEqual(
{metadata["scenario"] for _, metadata in states},
set(SCENARIOS),
)
def test_scenario_filter_is_deterministic(self):
first = synthetic_states(3, ["hazard"])
second = synthetic_states(3, ["hazard"])
self.assertEqual(first, second)
self.assertTrue(all(metadata["scenario"] == "hazard" for _, metadata in first))
def test_generated_you_is_present_on_board(self):
for board, metadata in synthetic_states(20):
with self.subTest(scenario=metadata["scenario"]):
ids = {snake["id"] for snake in board["snakes"]}
self.assertIn(metadata["you"]["id"], ids)
self.assertGreater(board["width"], 0)
self.assertGreater(board["height"], 0)
if __name__ == "__main__":
unittest.main()
@@ -2,11 +2,11 @@ import unittest
from time import perf_counter
from snakes import SnakeBuilder, get_snake_version
from snakes.ApexBattleSnake import ApexBattleSnake
from snakes.bitboard import BitBoard
from snakes.bitboard_duel_search import BitboardDuelSearch
from snakes.compact_survival_search import CompactSurvivalSearch
from snakes.PrismBattleSnake_GPT_5_6_Sol import PrismBattleSnake_GPT_5_6_Sol
from snakes.engine.bitboard import BitBoard
from snakes.engine.duel_search import BitboardDuelSearch
from snakes.engine.survival_search import CompactSurvivalSearch
from snakes.strategies.apex import ApexBattleSnake
from snakes.strategies.prism import PrismBattleSnake_GPT_5_6_Sol
class TestBitBoard(unittest.TestCase):
@@ -52,8 +52,9 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
snake = PrismBattleSnake_GPT_5_6_Sol()
self.assertEqual(snake.name, "PrismBattleSnake")
self.assertEqual(snake.version, "1.2.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.2.0")
self.assertEqual(snake.version, "1.3.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.3.0")
self.assertGreaterEqual(snake._planning_depth, 4)
self.assertIsInstance(SnakeBuilder.build("PrismBattleSnake_GPT_5_6_Sol"), PrismBattleSnake_GPT_5_6_Sol)
def test_bitboard_primitives_match_apex(self):
@@ -175,6 +176,21 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
self.assertIs(snake._duel_search_context, first_context)
self.assertGreater(first_context.nodes, 0)
self.assertGreater(first_context.completed_depth, 0)
def test_duel_evaluation_prioritizes_reachable_food_when_starving(self):
board = BitBoard(5, 3)
search = BitboardDuelSearch(
board=board, food={(2, 1)}, hazards=set(), hazard_count={},
hazard_damage=15, deadline=None,
)
my_body = [{"x": 0, "y": 1}, {"x": 0, "y": 0}]
enemy_body = [{"x": 4, "y": 1}, {"x": 4, "y": 0}]
hungry = search.search_depth(my_body, enemy_body, 15, 100, 0, set())
healthy = search.search_depth(my_body, enemy_body, 100, 100, 0, set())
self.assertLess(hungry, healthy)
def test_compact_rollout_models_lethal_enemy_head_response(self):
board = BitBoard(3, 3)
@@ -205,6 +221,7 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
search.search_selected(mine, enemies, (2, 1), depth=3)
self.assertGreater(search.cache_hits, hits_before)
self.assertGreaterEqual(search.completed_depth, 3)
def test_bitboard_duel_search_reuses_transpositions(self):
board = BitBoard(5, 5)
+35
View File
@@ -0,0 +1,35 @@
import unittest
from snakes import SNAKE_REGISTRATIONS, SnakeBuilder
from snakes.engine import (
BitBoard,
BitboardDuelMixin,
BitboardSpatialMixin,
BitboardSurvivalMixin,
)
from snakes.strategies.prism import PrismBattleSnake_GPT_5_6_Sol
class TestSnakePackageLayout(unittest.TestCase):
def test_registry_uses_explicit_package_modules(self):
for name, registration in SNAKE_REGISTRATIONS.items():
with self.subTest(name=name):
self.assertTrue(registration.module.startswith("snakes."))
self.assertNotEqual(registration.module, f"snakes.{name}")
def test_registry_builds_active_strategies_after_package_move(self):
for name in ("ApexBattleSnake", "PrismBattleSnake_GPT_5_6_Sol"):
with self.subTest(name=name):
snake = SnakeBuilder.build(name)
self.assertEqual(snake.__class__.__name__, name)
def test_prism_composes_reusable_engine_mixins(self):
snake = PrismBattleSnake_GPT_5_6_Sol()
self.assertIsInstance(snake, BitboardDuelMixin)
self.assertIsInstance(snake, BitboardSpatialMixin)
self.assertIsInstance(snake, BitboardSurvivalMixin)
self.assertIsInstance(snake._get_bb(11, 11), BitBoard)
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -5,8 +5,8 @@ and that the bitboard engine itself is sound.
"""
import unittest
from snakes.SupremeBattleSnake_ClaudeOpus4_6 import SupremeBattleSnake_ClaudeOpus4_6 as SupremeBattleSnake
from snakes.bitboard import BitBoard
from snakes.legacy.SupremeBattleSnake_ClaudeOpus4_6 import SupremeBattleSnake_ClaudeOpus4_6 as SupremeBattleSnake
from snakes.engine.bitboard import BitBoard
from server.GameBoard import GameBoard
# ── Helpers ───────────────────────────────────────────────────────────────────
@@ -348,7 +348,7 @@ class TestParityWithApex(unittest.TestCase):
def test_trapped_corner(self):
"""Both snakes should survive a forced single-exit scenario."""
from snakes.ApexBattleSnake import ApexBattleSnake
from snakes.strategies.apex import ApexBattleSnake
state = gs(my_body=[(1, 1), (1, 2), (2, 2), (2, 1)],
other_bodies=[], foods=[(5, 5)], width=7, height=7)
+1 -1
View File
@@ -1,6 +1,6 @@
import unittest
from snakes.UltimateBattleSnake import UltimateBattleSnake
from snakes.legacy.UltimateBattleSnake import UltimateBattleSnake
from server.GameBoard import GameBoard
# ── Helpers ───────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -1,6 +1,6 @@
import unittest
from snakes.BestBattleSnake import BestBattleSnake
from snakes.legacy.BestBattleSnake import BestBattleSnake
from server.GameBoard import GameBoard
def make_board(game_state):
+1 -1
View File
@@ -1,5 +1,5 @@
import unittest
from snakes.MasterSnake import MasterSnake
from snakes.legacy.MasterSnake import MasterSnake
class TestMasterSnake(unittest.TestCase):
def setUp(self):
+1 -1
View File
@@ -12,7 +12,7 @@ in the folder where this file exists:
"""
import unittest
from snakes.LogicSnake import avoid_my_neck
from snakes.legacy.LogicSnake import avoid_my_neck
class AvoidNeckTest(unittest.TestCase):