fix: preserve gameplay data and correct duel evaluation
- Preserve snake customizations across database migrations and merges. - Lazily load optional storage backends for SQLite maintenance scripts. - Match Apex territory and nearest-food tie-breaking semantics. - Resolve duel occupancy after simultaneous movement and food growth. - Recompute simulated head-to-head danger after body growth. - Add regression coverage and declare the aiofiles dependency.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""PrismBattleSnake_GPT_5_6_Sol v1.0.0
|
||||
"""PrismBattleSnake_GPT_5_6_Sol v1.0.1
|
||||
|
||||
Built on ApexBattleSnake v1.0.0. All strategic logic is inherited.
|
||||
Performance improvement: all spatial primitives (flood fill, territory,
|
||||
@@ -41,7 +41,7 @@ _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"
|
||||
VERSION = "1.0.1"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -83,6 +83,11 @@ class PrismBattleSnake_GPT_5_6_Sol(ApexBattleSnake):
|
||||
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
|
||||
@@ -100,7 +105,7 @@ class PrismBattleSnake_GPT_5_6_Sol(ApexBattleSnake):
|
||||
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)
|
||||
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"])
|
||||
|
||||
@@ -354,17 +359,25 @@ class PrismBattleSnake_GPT_5_6_Sol(ApexBattleSnake):
|
||||
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
|
||||
# Rebuild danger for the simulated length. The root-turn danger mask is
|
||||
# stale after eating and includes enemy moves blocked in this future body.
|
||||
danger_here = 0
|
||||
for enemy in other_snakes:
|
||||
enemy_len = enemy.get("length", len(enemy["body"]))
|
||||
if enemy_len < body_len:
|
||||
continue
|
||||
enemy_head = enemy["head"]
|
||||
enemy_idx = enemy_head["y"] * w + enemy_head["x"]
|
||||
danger_here |= bb._neighbor_masks[enemy_idx]
|
||||
danger_here &= ~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
|
||||
next_opts = liberties
|
||||
sc = reachable * 1.9 + liberties * 14.0 + next_opts * 11.0 + en_safe * 26.0
|
||||
if en_safe == 1:
|
||||
sc -= 420.0
|
||||
return sc
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ SNAKE_REGISTRY = {
|
||||
"UltimateBattleSnake": "4.5.0",
|
||||
"ApexBattleSnake": "1.0.0",
|
||||
"SupremeBattleSnake_ClaudeOpus4_6": "1.0.0",
|
||||
"PrismBattleSnake_GPT_5_6_Sol": "1.0.0",
|
||||
"PrismBattleSnake_GPT_5_6_Sol": "1.0.1",
|
||||
}
|
||||
|
||||
DEFAULT_SNAKE_CONFIG = {
|
||||
|
||||
+66
-59
@@ -127,8 +127,9 @@ class BitBoard:
|
||||
) -> int:
|
||||
"""Simultaneous BFS from *my_idx* and all enemies.
|
||||
|
||||
Returns (my_cells − enemy_cells). Cells equidistant from both sides are
|
||||
counted for neither (contested).
|
||||
Returns Apex-compatible territory over cells reachable from ``my_idx``:
|
||||
+1 when we arrive first, -1 when an enemy arrives first, and 0 for ties.
|
||||
Enemy-only disconnected regions are not counted.
|
||||
"""
|
||||
if not enemy_indices:
|
||||
return 0
|
||||
@@ -139,48 +140,44 @@ class BitBoard:
|
||||
nlc = self._not_leftcol
|
||||
|
||||
my_front = 1 << my_idx
|
||||
my_terr = my_front
|
||||
my_seen = my_front
|
||||
|
||||
en_front = 0
|
||||
for ei in enemy_indices:
|
||||
en_front |= 1 << ei
|
||||
en_terr = en_front
|
||||
en_seen = en_front
|
||||
|
||||
remaining = free & ~my_terr & ~en_terr
|
||||
# Each side must expand independently. A cell reached at the same depth is
|
||||
# unclaimed, but it is not a wall: both sides may route through it later.
|
||||
# Match Apex semantics by scoring only cells reachable from our head:
|
||||
# ours when we arrive first, theirs when an enemy arrives first, and zero
|
||||
# on ties. Enemy-only disconnected regions are intentionally ignored.
|
||||
score = (my_front & ~en_front).bit_count()
|
||||
enemy_before = 0
|
||||
while my_front:
|
||||
my_exp = (
|
||||
((my_front & nrc) << 1)
|
||||
| ((my_front & nlc) >> 1)
|
||||
| (my_front << w)
|
||||
| (my_front >> w)
|
||||
) & free & ~my_seen
|
||||
en_exp = (
|
||||
((en_front & nrc) << 1)
|
||||
| ((en_front & nlc) >> 1)
|
||||
| (en_front << w)
|
||||
| (en_front >> w)
|
||||
) & free & ~en_seen
|
||||
|
||||
while (my_front or en_front) and remaining:
|
||||
# Expand both sides simultaneously (same BFS depth → ties go to neither)
|
||||
my_exp = 0
|
||||
if my_front:
|
||||
my_exp = (
|
||||
((my_front & nrc) << 1)
|
||||
| ((my_front & nlc) >> 1)
|
||||
| (my_front << w)
|
||||
| (my_front >> w)
|
||||
) & remaining
|
||||
|
||||
en_exp = 0
|
||||
if en_front:
|
||||
en_exp = (
|
||||
((en_front & nrc) << 1)
|
||||
| ((en_front & nlc) >> 1)
|
||||
| (en_front << w)
|
||||
| (en_front >> w)
|
||||
) & remaining
|
||||
|
||||
# Contested cells (reached by both at the same depth) → neither claims
|
||||
contested = my_exp & en_exp
|
||||
my_exp &= ~contested
|
||||
en_exp &= ~contested
|
||||
|
||||
my_terr |= my_exp
|
||||
en_terr |= en_exp
|
||||
remaining &= ~(my_exp | en_exp | contested)
|
||||
enemy_before |= en_front
|
||||
score += (my_exp & ~enemy_before & ~en_exp).bit_count()
|
||||
score -= (my_exp & enemy_before).bit_count()
|
||||
|
||||
my_seen |= my_exp
|
||||
en_seen |= en_exp
|
||||
my_front = my_exp
|
||||
en_front = en_exp
|
||||
|
||||
return my_terr.bit_count() - en_terr.bit_count()
|
||||
return score
|
||||
|
||||
# ── Partition sizes (for articulation-point detection) ────────────────────
|
||||
|
||||
@@ -324,32 +321,42 @@ class BitBoard:
|
||||
if start_bit & food_bits:
|
||||
return 0, start_idx
|
||||
|
||||
frontier = start_bit
|
||||
seen = frontier
|
||||
dist = 0
|
||||
# Preserve Apex's deterministic up/down/left/right BFS tie-breaking. A
|
||||
# pure bit frontier finds the right distance but selects the lowest flat
|
||||
# index when several foods are equally close, which can change contested-
|
||||
# food scoring and therefore the selected move.
|
||||
queue = [start_idx]
|
||||
distances = [0]
|
||||
seen = start_bit
|
||||
cursor = 0
|
||||
w = self.width
|
||||
nrc = self._not_rightcol
|
||||
nlc = self._not_leftcol
|
||||
size = self.size
|
||||
|
||||
while frontier:
|
||||
dist += 1
|
||||
expanded = (
|
||||
((frontier & nrc) << 1)
|
||||
| ((frontier & nlc) >> 1)
|
||||
| (frontier << w)
|
||||
| (frontier >> w)
|
||||
) & free & ~seen
|
||||
|
||||
if not expanded:
|
||||
break
|
||||
|
||||
hit = expanded & food_bits
|
||||
if hit:
|
||||
# Return the first (lowest-index) food cell found
|
||||
first_bit = hit & (-hit)
|
||||
return dist, first_bit.bit_length() - 1
|
||||
|
||||
seen |= expanded
|
||||
frontier = expanded
|
||||
while cursor < len(queue):
|
||||
cell = queue[cursor]
|
||||
dist = distances[cursor]
|
||||
cursor += 1
|
||||
x = cell % w
|
||||
candidates = (
|
||||
cell + w,
|
||||
cell - w,
|
||||
cell - 1,
|
||||
cell + 1,
|
||||
)
|
||||
for direction, neighbor in enumerate(candidates):
|
||||
if neighbor < 0 or neighbor >= size:
|
||||
continue
|
||||
if direction == 2 and x == 0:
|
||||
continue
|
||||
if direction == 3 and x == w - 1:
|
||||
continue
|
||||
bit = 1 << neighbor
|
||||
if bit & seen or not bit & free:
|
||||
continue
|
||||
if bit & food_bits:
|
||||
return dist + 1, neighbor
|
||||
seen |= bit
|
||||
queue.append(neighbor)
|
||||
distances.append(dist + 1)
|
||||
|
||||
return None, None
|
||||
|
||||
@@ -129,8 +129,8 @@ class BitboardDuelSearch:
|
||||
if alpha >= beta:
|
||||
return cached_value, True
|
||||
|
||||
my_moves = self._legal_targets(state.my_body, state.enemy_body)
|
||||
enemy_moves = self._legal_targets(state.enemy_body, state.my_body)
|
||||
my_moves = self._candidate_targets(state.my_body)
|
||||
enemy_moves = self._candidate_targets(state.enemy_body)
|
||||
if not my_moves:
|
||||
return self.LOSS - depth, True
|
||||
if not enemy_moves:
|
||||
@@ -219,14 +219,13 @@ class BitboardDuelSearch:
|
||||
)
|
||||
return child, None
|
||||
|
||||
def _legal_targets(self, body: Body, other_body: Body) -> list[int]:
|
||||
occupied = self._body_bits(body) | self._body_bits(other_body)
|
||||
if not self._tail_stacked(body):
|
||||
occupied &= ~(1 << body[-1])
|
||||
if not self._tail_stacked(other_body):
|
||||
occupied &= ~(1 << other_body[-1])
|
||||
legal = self.board.neighbors_of(body[0]) & ~occupied & self.board.board_mask
|
||||
return list(self._iter_bits(legal))
|
||||
def _candidate_targets(self, body: Body) -> list[int]:
|
||||
"""Return in-bounds targets; `_advance` resolves simultaneous collisions.
|
||||
|
||||
Delaying occupancy checks until both targets and food growth are known is
|
||||
essential: whether either tail vacates depends on that snake eating.
|
||||
"""
|
||||
return list(self._iter_bits(self.board.neighbors_of(body[0])))
|
||||
|
||||
def _ordered_moves(self, moves: list[int], state: DuelState, depth: int, mine: bool) -> list[int]:
|
||||
body = state.my_body if mine else state.enemy_body
|
||||
|
||||
Reference in New Issue
Block a user