"""PrismBattleSnake_GPT_5_6_Sol v1.0.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. """ from __future__ import annotations from typing import Any from time import perf_counter from snakes.ApexBattleSnake import ApexBattleSnake from snakes.bitboard import BitBoard from snakes.bitboard_duel_search import BitboardDuelSearch from server.GameBoard import GameBoard # 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(ApexBattleSnake): VERSION = "1.0.0" def __init__(self) -> None: super().__init__() self.name = "PrismBattleSnake" self.version = self.VERSION # 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 # ── BitBoard accessor ──────────────────────────────────────────────────── def _get_bb(self, width: int, height: int) -> BitBoard: """Return (possibly cached) BitBoard for the current dimensions.""" if self._bb is None or width != self._bb_w or height != self._bb_h: self._bb = BitBoard(width, height) self._bb_w = width self._bb_h = height return self._bb def _blocked_to_bits(self, blocked: set[tuple[int, int]], width: int, height: int) -> int: """Convert blocked cells to bits without stale identity-based caching.""" return self._get_bb(width, height).set_to_bits(blocked) # ── 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()) # 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()} 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) 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 return super().choose_move(game_data) # ── S1: Bitboard flood fill ────────────────────────────────────────────── def _flood_fill_count(self, start: tuple, blocked: set, width: int, height: int) -> int: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) start_idx = bb.idx(start[0], start[1]) # A7/E2: per-turn transposition cache (kept from Apex) cache_key = (start_idx, blocked_bits, width, height) cached = self._bfs_cache.get(cache_key) if cached is not None: return cached result = bb.flood_count(start_idx, blocked_bits) if len(self._bfs_cache) < self._bfs_cache_max: self._bfs_cache[cache_key] = result return result # ── S2: Bitboard territory ────────────────────────────────────────────── def _territory_fast( self, my_pos: tuple, blocked: set, width: int, height: int, deadline: float | None = None, ) -> int: if not self._enemy_heads: return 0 bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) my_idx = bb.idx(my_pos[0], my_pos[1]) enemy_idxs = [bb.idx(eh[0], eh[1]) for eh in self._enemy_heads] return bb.territory(my_idx, enemy_idxs, blocked_bits) # ── S3: Bitboard articulation penalty ──────────────────────────────────── def _articulation_penalty( self, point: tuple, blocked: set, width: int, height: int, required_space: int, ) -> float: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) point_idx = bb.idx(point[0], point[1]) sizes = bb.partition_sizes(point_idx, blocked_bits) if not sizes: return 0.0 min_size = min(sizes) if min_size < required_space: return 1500.0 elif min_size < required_space * 2: return 400.0 else: return 85.0 def _bounded_bfs(self, start: tuple, blocked: set, width: int, height: int, limit: int) -> set: """Bitboard-accelerated bounded BFS. Returns a set for API compatibility.""" bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) start_idx = bb.idx(start[0], start[1]) reachable_bits = bb.flood_fill(start_idx, blocked_bits) result: set[tuple[int, int]] = set() temp = reachable_bits w = bb.width while temp: bit = temp & (-temp) idx = bit.bit_length() - 1 result.add((idx % w, idx // w)) temp ^= bit if len(result) >= limit: break return result # ── S4: Bitboard distance map ─────────────────────────────────────────── def _distance_map(self, start: tuple, blocked: set, width: int, height: int) -> dict: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) start_idx = bb.idx(start[0], start[1]) idx_dmap = bb.distance_map(start_idx, blocked_bits) w = bb.width return {(idx % w, idx // w): d for idx, d in idx_dmap.items()} # ── S5: Bitboard path distance ────────────────────────────────────────── def _path_distance( self, start: tuple, goal: tuple, blocked: set, width: int, height: int, ) -> int | None: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) return bb.path_distance( bb.idx(start[0], start[1]), bb.idx(goal[0], goal[1]), blocked_bits, ) # ── S6: Bitboard nearest food ─────────────────────────────────────────── def _nearest_food_info( self, start: tuple, food_set: set, blocked: set, width: int, height: int, ) -> tuple[int | None, tuple | None]: if not food_set: return None, None bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) food_bits = bb.set_to_bits(food_set) start_idx = bb.idx(start[0], start[1]) dist, cell_idx = bb.nearest_food(start_idx, food_bits, blocked_bits) if dist is None or cell_idx is None: return None, None return dist, bb.coord(cell_idx) # ── Bitboard open-neighbour helpers ────────────────────────────────────── def _open_neighbor_count(self, start: tuple, blocked: set, width: int, height: int) -> int: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) return bb.open_neighbor_count(bb.idx(start[0], start[1]), blocked_bits) def _next_turn_options(self, head: dict, blocked: set, width: int, height: int) -> int: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) return bb.open_neighbor_count(bb.idx(head["x"], head["y"]), blocked_bits) # ── S12/S13: compact bitboard duel search ─────────────────────────────── def _new_duel_search( self, food_set: set, hazard_set: set, hazard_count: dict, hazard_damage: int, width: int, height: int, deadline: float | None, ) -> BitboardDuelSearch: return BitboardDuelSearch( board=self._get_bb(width, height), food=food_set, hazards=hazard_set, hazard_count=hazard_count, hazard_damage=hazard_damage, deadline=deadline, ) def _minimax_sim_id( self, my_body: list, enemy_body: list, food_set: set, hazard_set: set, my_health: int, enemy_health: int, hazard_damage: int, hazard_count: dict, width: int, height: int, max_depth: int, alpha: float, beta: float, deadline: float | None, previous_hazard_set: set | None = None, ) -> tuple[float, int]: """Run iterative deepening with one reusable compact search context.""" search = self._new_duel_search( food_set, hazard_set, hazard_count, hazard_damage, width, height, deadline, ) return search.search( my_body=my_body, enemy_body=enemy_body, my_health=my_health, enemy_health=enemy_health, max_depth=max_depth, previous_hazards=previous_hazard_set if previous_hazard_set is not None else hazard_set, ) def _minimax_sim( self, my_body: list, enemy_body: list, food_set: set, hazard_set: set, my_health: int, enemy_health: int, hazard_damage: int, hazard_count: dict, width: int, height: int, depth: int, alpha: float, beta: float, deadline: float | None, previous_hazard_set: set | None = None, ) -> float: """Compatibility entry point for tests and callers requesting one depth.""" search = self._new_duel_search( food_set, hazard_set, hazard_count, hazard_damage, width, height, deadline, ) return search.search_depth( my_body=my_body, enemy_body=enemy_body, my_health=my_health, enemy_health=enemy_health, depth=depth, previous_hazards=previous_hazard_set if previous_hazard_set is not None else hazard_set, ) # ── S9: Optimised survival tree (bitboard-native) ──────────────────────── def _future_position_score( self, my_body: list, other_snakes: list, food_set: set, is_constrictor: bool, width: int, height: int, enemy_can_grow: dict, deadline: float | None, ) -> float: """S9: Bitboard-native position scoring for the survival tree. Builds blocked bitboard directly from body lists (no intermediate set). Uses precomputed enemy bits instead of rebuilding attack map per node. """ if deadline is not None and perf_counter() >= deadline: return 0.0 bb = self._bb # already initialised in choose_move w = bb.width head = my_body[0] hx, hy = head["x"], head["y"] head_idx = hy * w + hx head_bit = 1 << head_idx body_len = len(my_body) # ── Build blocked bitboard directly (no set) ────────────────────── my_bits = 0 for seg in my_body: my_bits |= 1 << (seg["y"] * w + seg["x"]) # Own tail vacates unless stacked or constrictor if not is_constrictor and body_len >= 2: t, t2 = my_body[-1], my_body[-2] if not (t["x"] == t2["x"] and t["y"] == t2["y"]): my_bits &= ~(1 << (t["y"] * w + t["x"])) # Enemy body (precomputed) minus vacating tails en_bits = self._enemy_body_bits & ~self._enemy_tail_bits blocked_bits = (my_bits | en_bits) & ~head_bit # ── Reachable space ─────────────────────────────────────────────── reachable = bb.flood_count(head_idx, blocked_bits) required = body_len + max(3, body_len // 6) if is_constrictor else body_len if reachable < required: return -5000.0 # ── Open neighbours (liberties) ─────────────────────────────────── nb_free = bb._neighbor_masks[head_idx] & ~blocked_bits & bb.board_mask liberties = nb_free.bit_count() if liberties == 0: return -5000.0 # ── Safe next options (enemy-attack aware) ──────────────────────── # Remove tiles where an enemy of >= our length could head-to-head. # The danger bitboard was precomputed; filter out tiles blocked by # current body (enemy can't step there either). danger_here = self._enemy_attack_danger & ~blocked_bits safe_nb = nb_free & ~danger_here en_safe = safe_nb.bit_count() if en_safe == 0: return -4000.0 sc = reachable * 1.9 + liberties * 14.0 + liberties * 11.0 + en_safe * 26.0 if en_safe == 1: sc -= 420.0 return sc def _future_survival_tree( self, my_body: list, other_snakes: list, food_set: set, is_constrictor: bool, width: int, height: int, enemy_can_grow: dict, depth: int, branch: int, deadline: float | None, ) -> float: """S9/S11: Bitboard-accelerated survival tree. Inlines legal-move check with bitboard ops instead of per-direction Python loops. Uses the bitboard-native _future_position_score. """ if depth <= 0 or (deadline is not None and perf_counter() >= deadline): return 0.0 bb = self._bb w = bb.width h = bb.height head = my_body[0] hx, hy = head["x"], head["y"] head_idx = hy * w + hx body_len = len(my_body) # ── Build occupied bitboard for legal-move check ────────────────── occupied_bits = 0 for seg in my_body: occupied_bits |= 1 << (seg["y"] * w + seg["x"]) occupied_bits |= self._enemy_body_bits # Own tail can be stepped on if not stacked/constrictor passable = 0 if not is_constrictor and body_len >= 2: t, t2 = my_body[-1], my_body[-2] if not (t["x"] == t2["x"] and t["y"] == t2["y"]): passable |= 1 << (t["y"] * w + t["x"]) # Enemy vacating tails are also steppable passable |= self._enemy_tail_bits # Legal moves: free neighbours OR passable tiles legal_bits = bb._neighbor_masks[head_idx] & ((~occupied_bits & bb.board_mask) | passable) if not legal_bits: return -5000.0 # ── Precompute food bitboard once ───────────────────────────────── food_bits_local = 0 for fx, fy in food_set: food_bits_local |= 1 << (fy * w + fx) # ── Score each legal move ───────────────────────────────────────── scored: list[tuple[float, list]] = [] temp = legal_bits while temp: if deadline is not None and perf_counter() >= deadline: break bit = temp & (-temp) temp ^= bit idx = bit.bit_length() - 1 nx, ny = idx % w, idx // w pos = {"x": nx, "y": ny} ate = bool(bit & food_bits_local) fb = self._future_body(my_body, pos, ate, is_constrictor) sc = self._future_position_score( fb, other_snakes, food_set, is_constrictor, width, height, enemy_can_grow, deadline, ) scored.append((sc, fb)) if not scored: return -5000.0 DEATH = self._TREE_DEATH_THRESHOLD viable = [(sc, fb) for sc, fb in scored if sc > DEATH] if not viable: return max(sc for sc, _ in scored) viable.sort(key=lambda x: x[0], reverse=True) if depth == 1: return viable[0][0] best = viable[0][0] for sc, fb in viable[:branch]: if deadline is not None and perf_counter() >= deadline: break cont = self._future_survival_tree( fb, other_snakes, food_set, is_constrictor, width, height, enemy_can_grow, depth - 1, branch, deadline, ) total = sc + cont * 0.72 if total > best: best = total return best # ── S10: Bitboard legal moves ──────────────────────────────────────────── def _legal_moves( self, my_head, my_body: list, other_snakes: list, food_set: set, is_constrictor: bool, width: int, height: int, enemy_can_grow: dict | None = None, ): """S10: Bitboard-accelerated legal move generation.""" bb = self._get_bb(width, height) w = bb.width # Build occupied bitboard occupied = 0 for seg in my_body: occupied |= 1 << (seg["y"] * w + seg["x"]) for snake in other_snakes: for seg in snake["body"]: occupied |= 1 << (seg["y"] * w + seg["x"]) hx, hy = my_head["x"], my_head["y"] head_idx = hy * w + hx # Own tail can be stepped on passable = 0 if not is_constrictor and len(my_body) >= 2: t, t2 = my_body[-1], my_body[-2] if not (t["x"] == t2["x"] and t["y"] == t2["y"]): passable |= 1 << (t["y"] * w + t["x"]) # Enemy tails that will vacate if not is_constrictor: for snake in other_snakes: sbody = snake["body"] if len(sbody) < 2: continue st, st2 = sbody[-1], sbody[-2] if st["x"] == st2["x"] and st["y"] == st2["y"]: continue # stacked sid = snake.get("id") can_grow = None if enemy_can_grow is not None and sid is not None: can_grow = enemy_can_grow.get(sid) if can_grow is None: can_grow = self._enemy_can_grow_this_turn(snake, food_set) if not can_grow: passable |= 1 << (st["y"] * w + st["x"]) legal = bb._neighbor_masks[head_idx] & ((~occupied & bb.board_mask) | passable) safe: dict[str, dict[str, int]] = {} for name, (dx, dy) in self.DIRECTIONS.items(): nx, ny = hx + dx, hy + dy if 0 <= nx < w and 0 <= ny < bb.height: if (1 << (ny * w + nx)) & legal: safe[name] = {"x": nx, "y": ny} return safe # ── Enemy confinement (uses bitboard flood) ────────────────────────────── def _enemy_confinement_metrics( self, enemy_head: tuple, blocked: set, width: int, height: int, ) -> tuple[int, int]: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) eh_idx = bb.idx(enemy_head[0], enemy_head[1]) eb_bits = blocked_bits & ~(1 << eh_idx) space = bb.flood_count(eh_idx, eb_bits) options = bb.open_neighbor_count(eh_idx, eb_bits) return space, options def _enemy_constrictor_projection( self, other_snakes: list, blocked: set, width: int, height: int, ) -> tuple[int, int]: bb = self._get_bb(width, height) blocked_bits = self._blocked_to_bits(blocked, width, height) best_space = 0 total_opts = 0 for enemy in other_snakes: eh = (enemy["head"]["x"], enemy["head"]["y"]) eh_idx = bb.idx(eh[0], eh[1]) nb = bb.neighbors_of(eh_idx) & ~blocked_bits & bb.board_mask temp = nb while temp: total_opts += 1 bit = temp & (-temp) n_idx = bit.bit_length() - 1 sp = bb.flood_count(n_idx, blocked_bits | bit) if sp > best_space: best_space = sp temp ^= bit return best_space, total_opts