feat(snake): add adaptive adversarial search

- Share duel search contexts and transpositions across candidate moves.
- Add aspiration windows, principal variation ordering, and body caches.
- Model simultaneous multiplayer responses with a compact beam rollout.
- Adapt search depth and response breadth to the remaining deadline.
- Add a deterministic arena benchmark with optional JSON reporting.
- Expose search metrics, document benchmarking, and bump Prism to 1.2.0.
This commit is contained in:
2026-08-01 19:26:19 +02:00
parent 6643eb35af
commit cb6c8d4dc8
9 changed files with 604 additions and 46 deletions
@@ -3,9 +3,10 @@ from time import perf_counter
from snakes import SnakeBuilder, get_snake_version
from snakes.ApexBattleSnake import ApexBattleSnake
from snakes.PrismBattleSnake_GPT_5_6_Sol import PrismBattleSnake_GPT_5_6_Sol
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
class TestBitBoard(unittest.TestCase):
@@ -51,8 +52,8 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
snake = PrismBattleSnake_GPT_5_6_Sol()
self.assertEqual(snake.name, "PrismBattleSnake")
self.assertEqual(snake.version, "1.1.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.1.0")
self.assertEqual(snake.version, "1.2.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.2.0")
self.assertIsInstance(SnakeBuilder.build("PrismBattleSnake_GPT_5_6_Sol"), PrismBattleSnake_GPT_5_6_Sol)
def test_bitboard_primitives_match_apex(self):
@@ -156,6 +157,55 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
self.assertIn(board.idx(1, 1), advanced)
def test_candidate_duel_searches_share_the_same_context(self):
snake = PrismBattleSnake_GPT_5_6_Sol()
kwargs = {
"my_body": [{"x": 0, "y": 1}, {"x": 0, "y": 0}],
"enemy_body": [{"x": 3, "y": 1}, {"x": 3, "y": 0}],
"food_set": set(), "hazard_set": set(),
"my_health": 100, "enemy_health": 100,
"hazard_damage": 15, "hazard_count": {},
"width": 4, "height": 3, "max_depth": 2,
"alpha": -1e9, "beta": 1e9, "deadline": perf_counter() + 1.0,
}
snake._minimax_candidate_id(my_target=(1, 1), **kwargs)
first_context = snake._duel_search_context
snake._minimax_candidate_id(my_target=(0, 2), **kwargs)
self.assertIs(snake._duel_search_context, first_context)
self.assertGreater(first_context.nodes, 0)
def test_compact_rollout_models_lethal_enemy_head_response(self):
board = BitBoard(3, 3)
search = CompactSurvivalSearch(
board=board, food=set(), is_constrictor=False,
deadline=perf_counter() + 1.0, branch=2,
)
mine = [{"x": 0, "y": 1}, {"x": 0, "y": 0}]
enemies = [{
"body": [{"x": 2, "y": 1}, {"x": 2, "y": 0}, {"x": 1, "y": 0}],
}]
value = search.search_selected(mine, enemies, (1, 1), depth=1)
self.assertEqual(value, search.DEATH)
def test_compact_rollout_reuses_transpositions(self):
board = BitBoard(5, 5)
search = CompactSurvivalSearch(
board=board, food=set(), is_constrictor=False,
deadline=perf_counter() + 1.0, branch=2,
)
mine = [{"x": 1, "y": 1}, {"x": 1, "y": 0}]
enemies = [{"body": [{"x": 3, "y": 3}, {"x": 3, "y": 4}]}]
search.search_selected(mine, enemies, (2, 1), depth=3)
hits_before = search.cache_hits
search.search_selected(mine, enemies, (2, 1), depth=3)
self.assertGreater(search.cache_hits, hits_before)
def test_bitboard_duel_search_reuses_transpositions(self):
board = BitBoard(5, 5)
search = BitboardDuelSearch(