diff --git a/README.md b/README.md index edf47d8..90a05c9 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/snakes/__init__.py b/snakes/__init__.py index 71c7dc9..97ee760 100644 --- a/snakes/__init__.py +++ b/snakes/__init__.py @@ -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"), diff --git a/snakes/engine/survival_search.py b/snakes/engine/survival_search.py index caa5c28..d02ce9a 100644 --- a/snakes/engine/survival_search.py +++ b/snakes/engine/survival_search.py @@ -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: diff --git a/snakes/strategies/prism.py b/snakes/strategies/prism.py index c279e66..2645758 100644 --- a/snakes/strategies/prism.py +++ b/snakes/strategies/prism.py @@ -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 diff --git a/tests/snakes/test_PrismBattleSnake_GPT_5_6_Sol.py b/tests/snakes/test_PrismBattleSnake_GPT_5_6_Sol.py index 9ee14b2..53077dd 100644 --- a/tests/snakes/test_PrismBattleSnake_GPT_5_6_Sol.py +++ b/tests/snakes/test_PrismBattleSnake_GPT_5_6_Sol.py @@ -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):