"""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, articulation detection, distance maps, pathfinding) replaced by a bitboard engine that uses integer arithmetic instead of Python sets/deques. Key speedups: S1: Bitboard flood fill — replaces BFS deque+set with integer bit-expansion. ~60× faster per call, eliminates _neighbors() generator overhead. S2: Bitboard territory — dual-BFS expansion on ints replaces per-cell distance-map comparison loop. S3: Bitboard articulation — partition sizes via bit-flood instead of _bounded_bfs with sets. S4: Bitboard distance map — BFS via bit-expansion + bit-extract. S5: Bitboard path distance — early-exit BFS on ints. S6: Bitboard nearest food — BFS food search on ints. S7: Per-turn BitBoard instance cached for board dimensions. S8: Blocked-set → bitboard conversion cached within a turn to avoid redundant O(n) conversions for the same frozen set. S9: Survival-tree uses bitboards natively — enemy body/attack bits precomputed once at tree root, no per-node set/dict rebuilds. S10: _legal_moves override uses bitboard neighbour mask instead of per-direction Python loop + _in_bounds calls. S11: _future_survival_tree inlines legal-move check with bitboard ops. S12: Duel minimax uses tuple bodies and bitboard move generation. S13: Iterative deepening reuses a transposition table and move-order hints. S14: Candidate duel moves and enemy replies resolve on the same root turn. S15: Candidate moves share one duel transposition/search context per turn. 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 from server.GameBoard import GameBoard from snakes.engine.bitboard import BitBoard from snakes.engine.duel import BitboardDuelMixin from snakes.engine.duel_search import BitboardDuelSearch from snakes.engine.spatial import BitboardSpatialMixin from snakes.engine.survival import BitboardSurvivalMixin from snakes.engine.survival_search import CompactSurvivalSearch from snakes.strategies.apex import ApexBattleSnake # Direction offsets for coord-dict → tuple conversion _DIR_DELTAS = ((0, 1), (0, -1), (-1, 0), (1, 0)) _DIR_NAMES = ("up", "down", "left", "right") class PrismBattleSnake_GPT_5_6_Sol( BitboardDuelMixin, BitboardSurvivalMixin, BitboardSpatialMixin, ApexBattleSnake, ): VERSION = "1.4.0" def __init__(self) -> None: super().__init__() self.name = "PrismBattleSnake" self.version = self.VERSION # Prism's compact state search is fast enough to inspect one additional # turn. The existing deadline checks and Apex timeout reserve still cap the # work on difficult positions. self._planning_depth = max(self._planning_depth, 4) # S7: cached BitBoard instance (reused while board dimensions stay the same) self._bb: BitBoard | None = None self._bb_w: int = 0 self._bb_h: int = 0 # S9: precomputed enemy state for survival tree (set per turn in choose_move) self._enemy_body_bits: int = 0 # all enemy body cells as bitboard self._enemy_tail_bits: int = 0 # enemy tails that will vacate self._enemy_attack_danger: int = 0 # tiles where enemy len >= our len self._enemy_attack_opportunity: int = 0 # tiles where enemy len < our len # Shared per-turn search contexts. Candidate moves overlap heavily, so # rebuilding their transposition tables wastes most iterative-deepening work. self._duel_search_context: BitboardDuelSearch | None = None self._survival_search_context: CompactSurvivalSearch | None = None # ── choose_move override: precompute enemy bits ────────────────────────── def choose_move(self, game_data: GameBoard) -> str: bb = self._get_bb(game_data.get_width(), game_data.get_height()) self._duel_search_context = None self._survival_search_context = None # S9: precompute enemy body / tail / attack bitboards for survival tree other_snakes = game_data.get_other_snakes() my_snake = game_data.get_my_snake() my_len = my_snake.get("length", len(my_snake["body"])) food_set = {(f["x"], f["y"]) for f in game_data.get_food()} all_occupied = { (seg["x"], seg["y"]) for snake in [my_snake, *other_snakes] for seg in snake["body"] } game_type = game_data.get_type() is_constrictor = game_type == "constrictor" w = bb.width enemy_body_bits = 0 enemy_tail_bits = 0 enemy_attack_danger = 0 enemy_attack_opportunity = 0 for snake in other_snakes: for seg in snake["body"]: enemy_body_bits |= 1 << (seg["y"] * w + seg["x"]) body = snake["body"] # Check if tail will vacate if not is_constrictor and len(body) >= 2: tail_stacked = ( body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"] ) if not tail_stacked: can_grow = self._enemy_can_grow_this_turn( snake, food_set, all_occupied ) if not can_grow: enemy_tail_bits |= 1 << (body[-1]["y"] * w + body[-1]["x"]) # Attack map: tiles enemy head can reach in 1 move eh = snake["head"] e_len = snake.get("length", len(body)) ehx, ehy = eh["x"], eh["y"] for dx, dy in _DIR_DELTAS: nx, ny = ehx + dx, ehy + dy if 0 <= nx < w and 0 <= ny < bb.height: bit = 1 << (ny * w + nx) if e_len >= my_len: enemy_attack_danger |= bit else: enemy_attack_opportunity |= bit self._enemy_body_bits = enemy_body_bits self._enemy_tail_bits = enemy_tail_bits self._enemy_attack_danger = enemy_attack_danger self._enemy_attack_opportunity = enemy_attack_opportunity move = super().choose_move(game_data) history = self.get_history() if history: thinking = history[-1] if self._duel_search_context is not None: thinking["prism_duel_depth"] = self._duel_search_context.completed_depth thinking["prism_duel_nodes"] = self._duel_search_context.nodes thinking["prism_duel_cache_hits"] = ( self._duel_search_context.cache_hits + self._duel_search_context.evaluation_cache_hits ) thinking["prism_duel_deadline_exits"] = ( self._duel_search_context.deadline_exits ) if self._survival_search_context is not None: thinking["prism_rollout_depth"] = ( self._survival_search_context.completed_depth ) 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 ) return move