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
ordering, aspiration windows, path-aware food races, and a deeper tactical
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
`PrismBattleSnake`.
+1 -1
View File
@@ -10,7 +10,7 @@ SNAKE_REGISTRATIONS = {
"TemplateSnake": SnakeRegistration("snakes.core.template", "1.0.0"),
"ApexBattleSnake": SnakeRegistration("snakes.strategies.apex", "1.0.0"),
"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"),
"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, ...]
EnemyBodies = tuple[Body, ...]
StateKey = tuple[Body, EnemyBodies, int, int]
EvaluationKey = tuple[Body, EnemyBodies]
class CompactSurvivalSearch:
"""Small paranoid beam search with simultaneous enemy responses.
@@ -39,9 +40,12 @@ class CompactSurvivalSearch:
self.enemy_branch = max(1, enemy_branch)
self.response_cap = max(1, response_cap)
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.nodes = 0
self.cache_hits = 0
self.evaluation_cache_hits = 0
self.completed_depth = 0
self.deadline_exits = 0
@@ -102,9 +106,8 @@ class CompactSurvivalSearch:
return cached
occupied = self._occupied(mine, enemies)
targets = list(self._iter_bits(self.board.neighbors_of(mine[0])))
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
# whether tails vacate.
ate = bool((1 << target) & food_bits)
@@ -152,6 +155,8 @@ class CompactSurvivalSearch:
return []
choices: list[list[int]] = []
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:
ranked: list[tuple[float, int]] = []
for target in self._iter_bits(self.board.neighbors_of(enemy[0])):
@@ -161,9 +166,8 @@ class CompactSurvivalSearch:
if target == my_target:
score += 1000.0 if enemy_length_after >= my_length_after else -1000.0
tx, ty = self.board.coord(target)
mx, my = self.board.coord(my_target)
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
ranked.append((score, target))
ranked.sort(reverse=True)
@@ -225,6 +229,12 @@ class CompactSurvivalSearch:
return next_mine, tuple(surviving), food_bits & ~eaten
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])
space = self.board.flood_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
if len(mine) > len(enemy):
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:
key = (mine, enemies)
cached = self.occupied_cache.get(key)
if cached is not None:
return cached
occupied = self._body_bits(mine)
for enemy in enemies:
occupied |= self._body_bits(enemy)
if len(self.occupied_cache) < 32_768:
self.occupied_cache[key] = occupied
return occupied
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.
Performance improvement: all spatial primitives (flood fill, territory,
@@ -30,6 +30,7 @@ Key speedups:
S16: Compact adversarial multiplayer rollout advances plausible enemy replies.
S17: Rollout memoization and adaptive depth spend time on ambiguous positions.
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
@@ -53,7 +54,7 @@ class PrismBattleSnake_GPT_5_6_Sol(
BitboardSpatialMixin,
ApexBattleSnake,
):
VERSION = "1.3.0"
VERSION = "1.4.0"
def __init__(self) -> None:
super().__init__()
@@ -162,6 +163,7 @@ class PrismBattleSnake_GPT_5_6_Sol(
thinking["prism_rollout_nodes"] = self._survival_search_context.nodes
thinking["prism_rollout_cache_hits"] = (
self._survival_search_context.cache_hits
+ self._survival_search_context.evaluation_cache_hits
)
thinking["prism_rollout_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()
self.assertEqual(snake.name, "PrismBattleSnake")
self.assertEqual(snake.version, "1.3.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.3.0")
self.assertEqual(snake.version, "1.4.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.4.0")
self.assertGreaterEqual(snake._planning_depth, 4)
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)
self.assertGreater(search.cache_hits, hits_before)
self.assertGreater(search.evaluation_cache_hits, 0)
self.assertGreaterEqual(search.completed_depth, 3)
def test_bitboard_duel_search_reuses_transpositions(self):