perf(snake): cache survival rollout state
Build and Push Docker Container / build-and-push (push) Successful in 6m1s
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:
@@ -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
@@ -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"),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user