perf(snake): cache survival rollout state
Build and Push Docker Container / build-and-push (push) Successful in 6m1s

- Cache occupancy bitboards for repeated multiplayer rollout positions.
- Memoize position evaluations to avoid duplicate flood-fill calculations.
- Reuse shared values while ranking simultaneous enemy responses.
- Include evaluation hits in Prism rollout telemetry.
- Document the optimization and bump Prism to version 1.4.0.
This commit is contained in:
2026-08-01 20:39:56 +02:00
parent 3a9af3f54d
commit 9b99b526e4
5 changed files with 33 additions and 11 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ accelerating hot spatial operations with a Python-integer bitboard engine. It
also shares duel transpositions across candidate moves, uses principal-variation also shares duel transpositions across candidate moves, uses principal-variation
ordering, aspiration windows, path-aware food races, and a deeper tactical ordering, aspiration windows, path-aware food races, and a deeper tactical
horizon, and runs a compact adversarial multiplayer rollout with simultaneous horizon, and runs a compact adversarial multiplayer rollout with simultaneous
enemy responses. Its filename, class, and registry enemy responses and cached occupancy/evaluation states. Its filename, class, and registry
key include the model name, while its public Battlesnake API name remains key include the model name, while its public Battlesnake API name remains
`PrismBattleSnake`. `PrismBattleSnake`.
+1 -1
View File
@@ -10,7 +10,7 @@ SNAKE_REGISTRATIONS = {
"TemplateSnake": SnakeRegistration("snakes.core.template", "1.0.0"), "TemplateSnake": SnakeRegistration("snakes.core.template", "1.0.0"),
"ApexBattleSnake": SnakeRegistration("snakes.strategies.apex", "1.0.0"), "ApexBattleSnake": SnakeRegistration("snakes.strategies.apex", "1.0.0"),
"PrismBattleSnake_GPT_5_6_Sol": SnakeRegistration( "PrismBattleSnake_GPT_5_6_Sol": SnakeRegistration(
"snakes.strategies.prism", "1.3.0" "snakes.strategies.prism", "1.4.0"
), ),
"DummSnake": SnakeRegistration("snakes.legacy.DummSnake", "1.0.0"), "DummSnake": SnakeRegistration("snakes.legacy.DummSnake", "1.0.0"),
"LogicSnake": SnakeRegistration("snakes.legacy.LogicSnake", "1.1.0"), "LogicSnake": SnakeRegistration("snakes.legacy.LogicSnake", "1.1.0"),
+24 -5
View File
@@ -10,6 +10,7 @@ from snakes.engine.bitboard import BitBoard
Body = tuple[int, ...] Body = tuple[int, ...]
EnemyBodies = tuple[Body, ...] EnemyBodies = tuple[Body, ...]
StateKey = tuple[Body, EnemyBodies, int, int] StateKey = tuple[Body, EnemyBodies, int, int]
EvaluationKey = tuple[Body, EnemyBodies]
class CompactSurvivalSearch: class CompactSurvivalSearch:
"""Small paranoid beam search with simultaneous enemy responses. """Small paranoid beam search with simultaneous enemy responses.
@@ -39,9 +40,12 @@ class CompactSurvivalSearch:
self.enemy_branch = max(1, enemy_branch) self.enemy_branch = max(1, enemy_branch)
self.response_cap = max(1, response_cap) self.response_cap = max(1, response_cap)
self.cache: dict[StateKey, float] = {} self.cache: dict[StateKey, float] = {}
self.evaluation_cache: dict[EvaluationKey, float] = {}
self.occupied_cache: dict[EvaluationKey, int] = {}
self.body_bits_cache: dict[Body, int] = {} self.body_bits_cache: dict[Body, int] = {}
self.nodes = 0 self.nodes = 0
self.cache_hits = 0 self.cache_hits = 0
self.evaluation_cache_hits = 0
self.completed_depth = 0 self.completed_depth = 0
self.deadline_exits = 0 self.deadline_exits = 0
@@ -102,9 +106,8 @@ class CompactSurvivalSearch:
return cached return cached
occupied = self._occupied(mine, enemies) occupied = self._occupied(mine, enemies)
targets = list(self._iter_bits(self.board.neighbors_of(mine[0])))
ranked: list[tuple[float, int]] = [] ranked: list[tuple[float, int]] = []
for target in targets: for target in self._iter_bits(self.board.neighbors_of(mine[0])):
# Collision legality is finalized simultaneously because eating controls # Collision legality is finalized simultaneously because eating controls
# whether tails vacate. # whether tails vacate.
ate = bool((1 << target) & food_bits) ate = bool((1 << target) & food_bits)
@@ -152,6 +155,8 @@ class CompactSurvivalSearch:
return [] return []
choices: list[list[int]] = [] choices: list[list[int]] = []
my_length_after = len(mine) + int(bool((1 << my_target) & food_bits)) my_length_after = len(mine) + int(bool((1 << my_target) & food_bits))
occupied = self._occupied(mine, enemies)
mx, my = self.board.coord(my_target)
for enemy in enemies: for enemy in enemies:
ranked: list[tuple[float, int]] = [] ranked: list[tuple[float, int]] = []
for target in self._iter_bits(self.board.neighbors_of(enemy[0])): for target in self._iter_bits(self.board.neighbors_of(enemy[0])):
@@ -161,9 +166,8 @@ class CompactSurvivalSearch:
if target == my_target: if target == my_target:
score += 1000.0 if enemy_length_after >= my_length_after else -1000.0 score += 1000.0 if enemy_length_after >= my_length_after else -1000.0
tx, ty = self.board.coord(target) tx, ty = self.board.coord(target)
mx, my = self.board.coord(my_target)
score -= abs(tx - mx) + abs(ty - my) score -= abs(tx - mx) + abs(ty - my)
score += self.board.open_neighbor_count(target, self._occupied(mine, enemies)) * 3.0 score += self.board.open_neighbor_count(target, occupied) * 3.0
score += 20.0 if ate else 0.0 score += 20.0 if ate else 0.0
ranked.append((score, target)) ranked.append((score, target))
ranked.sort(reverse=True) ranked.sort(reverse=True)
@@ -225,6 +229,12 @@ class CompactSurvivalSearch:
return next_mine, tuple(surviving), food_bits & ~eaten return next_mine, tuple(surviving), food_bits & ~eaten
def _evaluate(self, mine: Body, enemies: EnemyBodies) -> float: def _evaluate(self, mine: Body, enemies: EnemyBodies) -> float:
key = (mine, enemies)
cached = self.evaluation_cache.get(key)
if cached is not None:
self.evaluation_cache_hits += 1
return cached
blocked = self._occupied(mine, enemies) & ~(1 << mine[0]) blocked = self._occupied(mine, enemies) & ~(1 << mine[0])
space = self.board.flood_count(mine[0], blocked) space = self.board.flood_count(mine[0], blocked)
liberties = self.board.open_neighbor_count(mine[0], blocked) liberties = self.board.open_neighbor_count(mine[0], blocked)
@@ -238,12 +248,21 @@ class CompactSurvivalSearch:
enemy_pressure += max(0, 3 - enemy_liberties) * 18.0 enemy_pressure += max(0, 3 - enemy_liberties) * 18.0
if len(mine) > len(enemy): if len(mine) > len(enemy):
enemy_pressure += max(0, 8 - enemy_space) * 8.0 enemy_pressure += max(0, 8 - enemy_space) * 8.0
return space * 1.9 + liberties * 32.0 + enemy_pressure - len(enemies) * 4.0 value = space * 1.9 + liberties * 32.0 + enemy_pressure - len(enemies) * 4.0
if len(self.evaluation_cache) < 32_768:
self.evaluation_cache[key] = value
return value
def _occupied(self, mine: Body, enemies: EnemyBodies) -> int: def _occupied(self, mine: Body, enemies: EnemyBodies) -> int:
key = (mine, enemies)
cached = self.occupied_cache.get(key)
if cached is not None:
return cached
occupied = self._body_bits(mine) occupied = self._body_bits(mine)
for enemy in enemies: for enemy in enemies:
occupied |= self._body_bits(enemy) occupied |= self._body_bits(enemy)
if len(self.occupied_cache) < 32_768:
self.occupied_cache[key] = occupied
return occupied return occupied
def _body_bits(self, body: Body) -> int: def _body_bits(self, body: Body) -> int:
+4 -2
View File
@@ -1,4 +1,4 @@
"""PrismBattleSnake_GPT_5_6_Sol v1.3.0 """PrismBattleSnake_GPT_5_6_Sol v1.4.0
Built on ApexBattleSnake v1.0.0. All strategic logic is inherited. Built on ApexBattleSnake v1.0.0. All strategic logic is inherited.
Performance improvement: all spatial primitives (flood fill, territory, Performance improvement: all spatial primitives (flood fill, territory,
@@ -30,6 +30,7 @@ Key speedups:
S16: Compact adversarial multiplayer rollout advances plausible enemy replies. S16: Compact adversarial multiplayer rollout advances plausible enemy replies.
S17: Rollout memoization and adaptive depth spend time on ambiguous positions. S17: Rollout memoization and adaptive depth spend time on ambiguous positions.
S18: Prism uses a deeper tactical horizon while retaining Apex's timeout reserve. S18: Prism uses a deeper tactical horizon while retaining Apex's timeout reserve.
S19: Rollout occupancy and evaluation caches avoid repeated flood-fill work.
""" """
from __future__ import annotations from __future__ import annotations
@@ -53,7 +54,7 @@ class PrismBattleSnake_GPT_5_6_Sol(
BitboardSpatialMixin, BitboardSpatialMixin,
ApexBattleSnake, ApexBattleSnake,
): ):
VERSION = "1.3.0" VERSION = "1.4.0"
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -162,6 +163,7 @@ class PrismBattleSnake_GPT_5_6_Sol(
thinking["prism_rollout_nodes"] = self._survival_search_context.nodes thinking["prism_rollout_nodes"] = self._survival_search_context.nodes
thinking["prism_rollout_cache_hits"] = ( thinking["prism_rollout_cache_hits"] = (
self._survival_search_context.cache_hits self._survival_search_context.cache_hits
+ self._survival_search_context.evaluation_cache_hits
) )
thinking["prism_rollout_deadline_exits"] = ( thinking["prism_rollout_deadline_exits"] = (
self._survival_search_context.deadline_exits self._survival_search_context.deadline_exits
@@ -52,8 +52,8 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
snake = PrismBattleSnake_GPT_5_6_Sol() snake = PrismBattleSnake_GPT_5_6_Sol()
self.assertEqual(snake.name, "PrismBattleSnake") self.assertEqual(snake.name, "PrismBattleSnake")
self.assertEqual(snake.version, "1.3.0") self.assertEqual(snake.version, "1.4.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.3.0") self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.4.0")
self.assertGreaterEqual(snake._planning_depth, 4) self.assertGreaterEqual(snake._planning_depth, 4)
self.assertIsInstance(SnakeBuilder.build("PrismBattleSnake_GPT_5_6_Sol"), PrismBattleSnake_GPT_5_6_Sol) self.assertIsInstance(SnakeBuilder.build("PrismBattleSnake_GPT_5_6_Sol"), PrismBattleSnake_GPT_5_6_Sol)
@@ -221,6 +221,7 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
search.search_selected(mine, enemies, (2, 1), depth=3) search.search_selected(mine, enemies, (2, 1), depth=3)
self.assertGreater(search.cache_hits, hits_before) self.assertGreater(search.cache_hits, hits_before)
self.assertGreater(search.evaluation_cache_hits, 0)
self.assertGreaterEqual(search.completed_depth, 3) self.assertGreaterEqual(search.completed_depth, 3)
def test_bitboard_duel_search_reuses_transpositions(self): def test_bitboard_duel_search_reuses_transpositions(self):