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:
+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
|
||||
|
||||
Reference in New Issue
Block a user