f14d780f29
Build and Push Docker Container / build-and-push (push) Successful in 7m53s
- Stream compact live replay updates across local and clustered dashboards. - Render responsive snake bodies as SVG paths with aligned custom icons. - Add cache-busted assets, replay fallback routes, and live-follow playback. - Support PostgreSQL benchmark sampling and idempotent SQLite migration. - Add dry-run cleanup for old low-quality PostgreSQL replay payloads. - Reward safe perimeter lanes and bump Prism to version 1.5.0. - Add backend, migration, dashboard, and perimeter regression coverage.
172 lines
7.1 KiB
Python
172 lines
7.1 KiB
Python
"""PrismBattleSnake_GPT_5_6_Sol v1.5.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.5.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
|