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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from snakes.UltimateBattleSnake import UltimateBattleSnake
|
||||
from snakes.legacy.UltimateBattleSnake import UltimateBattleSnake
|
||||
from server.GameBoard import GameBoard
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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,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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user