"""ZenithBattleSnake_ClaudeOpus5_5 v1.2.0 Built on PrismBattleSnake_GPT_5_6_Sol v1.5.0. Prism's bitboard engine, duel search, and adversarial rollouts are inherited unchanged. Prism only runs the exact duel search on near-tied heuristic candidates (score gap <= 8). A move outside that window is never checked against every enemy reply, so a forced loss with a strong heuristic score can still be played, and a forced win with a weaker score can be skipped. Z1: Root verdict pass — after Prism decides, every safe duel candidate is resolved against all simultaneous enemy replies with the shared transposition table (cheap: most subtrees are already cached). Z2: Proven-loss veto — a candidate whose completed search proves a loss is replaced by the best non-losing candidate. Z3: Proven-win override — a candidate whose completed search proves a win is played even if the heuristic preferred another move. Z4: Elapsed-time rollout gate — Zenith keeps a larger timeout reserve than Prism. The inherited multiplayer "deepen when > 250 ms remain" gate is lowered by that extra reserve, so 3-snake rollouts deepen after the same elapsed time as Prism instead of never reaching depth 3. The duel gate is left alone: the verdict pass already spends the duel budget. """ from time import perf_counter from snakes.engine.duel_search import BitboardDuelSearch from snakes.strategies.zenith_survival import ZenithSurvivalMixin from snakes.strategies.prism import PrismBattleSnake_GPT_5_6_Sol # A completed search value beyond half of WIN/LOSS can only come from a terminal. _PROVEN_WIN = BitboardDuelSearch.WIN / 2 _PROVEN_LOSS = BitboardDuelSearch.LOSS / 2 # Reserve kept for Apex's history bookkeeping after the verdict pass. _VERDICT_RESERVE_MS = 25.0 _ZENITH_TIMEOUT_BUFFER_MS = 180 class ZenithBattleSnake_ClaudeOpus5_5(ZenithSurvivalMixin, PrismBattleSnake_GPT_5_6_Sol): VERSION = "1.2.0" def __init__(self) -> None: super().__init__() self.name = "ZenithBattleSnake" self.version = self.VERSION # Z4: deepen after the same elapsed time as the inherited Prism gates. extra_reserve = self._get_timeout_buffer_ms() - super()._get_timeout_buffer_ms() self._rollout_deepen_ms = max(120.0, self._rollout_deepen_ms - extra_reserve) def _get_timeout_buffer_ms(self): # Leave room for executor scheduling, history, JSON and network delivery. return max(_ZENITH_TIMEOUT_BUFFER_MS, super()._get_timeout_buffer_ms()) def _choose_duel_move( self, safe_moves, my_body, my_len, my_health, other_snakes, food_set, hazard_set, hazard_damage, hazard_count, previous_hazard_set, enemy_attack_map, enemy_can_grow, total_occupancy, width, height, deadline, ): best_move, scores, depth = super()._choose_duel_move( safe_moves=safe_moves, my_body=my_body, my_len=my_len, my_health=my_health, other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set, hazard_damage=hazard_damage, hazard_count=hazard_count, previous_hazard_set=previous_hazard_set, enemy_attack_map=enemy_attack_map, enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy, width=width, height=height, deadline=deadline, ) if len(safe_moves) < 2: return best_move, scores, depth verdicts = self._duel_verdicts( safe_moves=safe_moves, my_body=my_body, enemy=other_snakes[0], my_health=my_health, food_set=food_set, hazard_set=hazard_set, hazard_damage=hazard_damage, hazard_count=hazard_count, previous_hazard_set=previous_hazard_set, width=width, height=height, deadline=deadline, ) chosen = self._apply_verdicts(best_move, scores, verdicts) if chosen != best_move: self.add_to_history({ "turn": self.game_board.get_turn(), "mode": "duel", "zenith_verdict_changed_move": True, "from": best_move, "to": chosen, "verdicts": {m: round(v, 1) for m, (v, _) in verdicts.items()}, }) depth = max([depth, *(d for _, d in verdicts.values())]) return chosen, scores, depth def _duel_verdicts( self, safe_moves, my_body, enemy, my_health, food_set, hazard_set, hazard_damage, hazard_count, previous_hazard_set, width, height, deadline, ) -> dict[str, tuple[float, int]]: """Return completed exact-search values per candidate (Z1).""" verdicts: dict[str, tuple[float, int]] = {} # Search every root at equal depth. A full iterative search for the first # move could consume the deadline before the other roots were visited. search_deadline = None if deadline is None else deadline - _VERDICT_RESERVE_MS / 1000 if search_deadline is not None and perf_counter() >= search_deadline: return verdicts search = self._new_duel_search( food_set, hazard_set, hazard_count, hazard_damage, width, height, search_deadline, ) search.deadline = search_deadline for depth in range(1, self._planning_depth + 1): layer = {} for move, pos in safe_moves.items(): value, completed = search.search_candidate( my_body, enemy["body"], (pos["x"], pos["y"]), my_health, enemy.get("health", 100), depth, previous_hazard_set, ) if completed != depth: return verdicts layer[move] = (value, completed) verdicts = layer return verdicts @staticmethod def _apply_verdicts( best_move: str, scores: dict[str, float], verdicts: dict[str, tuple[float, int]], ) -> str: """Apply proven-win override (Z3) and proven-loss veto (Z2).""" def rank(move: str) -> tuple[float, float]: return verdicts[move][0], scores.get(move, float("-inf")) wins = [m for m, (v, _) in verdicts.items() if v >= _PROVEN_WIN] if wins and best_move not in wins: return max(wins, key=rank) best_verdict = verdicts.get(best_move) if best_verdict is None or best_verdict[0] > _PROVEN_LOSS: return best_move alternatives = [m for m, (v, _) in verdicts.items() if v > _PROVEN_LOSS] if not alternatives: return best_move # Prefer Prism's heuristic among surviving moves; fall back to search value. return max(alternatives, key=lambda m: (scores.get(m, float("-inf")), verdicts[m][0]))