Files
daniel156161 3a9af3f54d feat(snake): modularize engine and add tournament tools
- Split active strategies, reusable engine code, core classes, and legacy snakes.
- Replace implicit snake imports with explicit module registrations.
- Extract Prism duel, spatial, and survival behavior into focused mixins.
- Improve duel scoring with food races, pressure, caches, and depth metrics.
- Add deterministic arena scenarios and paired seeded engine tournaments.
- Expand benchmark telemetry and bump Prism to version 1.3.0.
- Update documentation and tests for the new package layout and tooling.
2026-08-01 20:25:07 +02:00

365 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bitboard engine for Battlesnake grid spatial operations.
Cell index = y * width + x. Bit *i* of a Python int represents cell *i*.
All heavy BFS / flood-fill / territory ops run on plain integer arithmetic —
no sets, deques, or per-cell Python objects.
Typical 11×11 board → 121-bit integers. Python big-int ops on these are
extremely fast (single C-level limb operations under the hood).
"""
from __future__ import annotations
class BitBoard:
"""Pre-computed masks and fast spatial primitives for a fixed grid size."""
__slots__ = (
"width", "height", "size", "board_mask",
"_not_rightcol", "_not_leftcol",
"_neighbor_masks",
)
def __init__(self, width: int, height: int) -> None:
self.width = width
self.height = height
self.size = width * height
self.board_mask = (1 << self.size) - 1
# Column masks — prevent bit-shift wrap-around at row boundaries
rightcol = 0
leftcol = 0
for y in range(height):
rightcol |= 1 << (y * width + width - 1)
leftcol |= 1 << (y * width)
self._not_rightcol = self.board_mask & ~rightcol
self._not_leftcol = self.board_mask & ~leftcol
# Per-cell neighbour bitmask (4-connected)
nb = [0] * self.size
for idx in range(self.size):
x, y = idx % width, idx // width
mask = 0
if x > 0:
mask |= 1 << (idx - 1)
if x < width - 1:
mask |= 1 << (idx + 1)
if y > 0:
mask |= 1 << (idx - width)
if y < height - 1:
mask |= 1 << (idx + width)
nb[idx] = mask
self._neighbor_masks = nb
# ── Coordinate helpers ────────────────────────────────────────────────────
def idx(self, x: int, y: int) -> int:
"""(x, y) → flat index."""
return y * self.width + x
def coord(self, flat: int) -> tuple[int, int]:
"""Flat index → (x, y)."""
return flat % self.width, flat // self.width
def pt_bit(self, x: int, y: int) -> int:
"""Single-cell bitmask for (x, y)."""
return 1 << (y * self.width + x)
def set_to_bits(self, points: set[tuple[int, int]]) -> int:
"""Convert a set of (x, y) tuples to a bitmask."""
w = self.width
bits = 0
for x, y in points:
bits |= 1 << (y * w + x)
return bits
def in_bounds(self, x: int, y: int) -> bool:
return 0 <= x < self.width and 0 <= y < self.height
# ── Core spatial primitives ───────────────────────────────────────────────
def flood_fill(self, start_idx: int, blocked_bits: int) -> int:
"""Return bitmask of all cells reachable from *start_idx* (inclusive)."""
free = self.board_mask & ~blocked_bits
start_bit = 1 << start_idx
# If start is blocked, return just itself
if not (start_bit & free):
return start_bit
reachable = start_bit
frontier = start_bit
w = self.width
nrc = self._not_rightcol
nlc = self._not_leftcol
while frontier:
expanded = (
((frontier & nrc) << 1)
| ((frontier & nlc) >> 1)
| (frontier << w)
| (frontier >> w)
) & free & ~reachable
if not expanded:
break
reachable |= expanded
frontier = expanded
return reachable
def flood_count(self, start_idx: int, blocked_bits: int) -> int:
"""Count of cells reachable from *start_idx*."""
return self.flood_fill(start_idx, blocked_bits).bit_count()
def open_neighbor_count(self, cell_idx: int, blocked_bits: int) -> int:
"""Number of free neighbours of *cell_idx*."""
return (self._neighbor_masks[cell_idx] & ~blocked_bits & self.board_mask).bit_count()
def neighbors_of(self, cell_idx: int) -> int:
"""Bitmask of 4-connected neighbours (may include blocked cells)."""
return self._neighbor_masks[cell_idx]
# ── Territory (dual-BFS expansion) ────────────────────────────────────────
def territory(
self,
my_idx: int,
enemy_indices: list[int],
blocked_bits: int,
) -> int:
"""Simultaneous BFS from *my_idx* and all enemies.
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
free = self.board_mask & ~blocked_bits
w = self.width
nrc = self._not_rightcol
nlc = self._not_leftcol
my_front = 1 << my_idx
my_seen = my_front
en_front = 0
for ei in enemy_indices:
en_front |= 1 << ei
en_seen = en_front
# 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
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 score
# ── Partition sizes (for articulation-point detection) ────────────────────
def partition_sizes(self, cut_idx: int, blocked_bits: int) -> list[int]:
"""Remove *cut_idx* from the free space and return sizes of each
resulting connected component among its neighbours.
Returns an empty list when the point is not a cut vertex (single component
or ≤1 free neighbour).
"""
test_blocked = blocked_bits | (1 << cut_idx)
free_nb = self._neighbor_masks[cut_idx] & ~test_blocked & self.board_mask
if free_nb.bit_count() <= 1:
return []
seen_all = 0
sizes: list[int] = []
temp = free_nb
while temp:
bit = temp & (-temp) # lowest set bit
temp ^= bit
if bit & seen_all:
continue
component = self.flood_fill(bit.bit_length() - 1, test_blocked)
seen_all |= component
sizes.append(component.bit_count())
return sizes if len(sizes) > 1 else []
# ── BFS distance map (indexed by cell idx) ────────────────────────────────
def distance_map(self, start_idx: int, blocked_bits: int) -> dict[int, int]:
"""BFS distance from *start_idx* to every reachable cell.
Returns ``{cell_idx: distance}`` — same semantics as the original
``_distance_map`` but using bitboard expansion internally.
"""
free = self.board_mask & ~blocked_bits
start_bit = 1 << start_idx
distances: dict[int, int] = {start_idx: 0}
frontier = start_bit
seen = frontier
dist = 0
w = self.width
nrc = self._not_rightcol
nlc = self._not_leftcol
while frontier:
dist += 1
expanded = (
((frontier & nrc) << 1)
| ((frontier & nlc) >> 1)
| (frontier << w)
| (frontier >> w)
) & free & ~seen
if not expanded:
break
seen |= expanded
# Extract individual bits
temp = expanded
while temp:
bit = temp & (-temp)
idx = bit.bit_length() - 1
distances[idx] = dist
temp ^= bit
frontier = expanded
return distances
# ── Path distance (BFS to single target) ──────────────────────────────────
def path_distance(
self,
start_idx: int,
goal_idx: int,
blocked_bits: int,
) -> int | None:
"""Shortest path length from *start_idx* to *goal_idx*, or ``None``."""
# Unblock the goal cell so BFS can reach it
free = (self.board_mask & ~blocked_bits) | (1 << goal_idx)
start_bit = 1 << start_idx
goal_bit = 1 << goal_idx
if start_idx == goal_idx:
return 0
frontier = start_bit
seen = frontier
dist = 0
w = self.width
nrc = self._not_rightcol
nlc = self._not_leftcol
while frontier:
dist += 1
expanded = (
((frontier & nrc) << 1)
| ((frontier & nlc) >> 1)
| (frontier << w)
| (frontier >> w)
) & free & ~seen
if not expanded:
break
if expanded & goal_bit:
return dist
seen |= expanded
frontier = expanded
return None
# ── Nearest-food BFS ──────────────────────────────────────────────────────
def nearest_food(
self,
start_idx: int,
food_bits: int,
blocked_bits: int,
) -> tuple[int | None, int | None]:
"""BFS from *start_idx* to nearest food cell.
Food cells are passable even if in *blocked_bits* (matching original
``_nearest_food_info`` semantics).
Returns ``(distance, cell_idx)`` or ``(None, None)``.
"""
if not food_bits:
return None, None
# Food tiles are always steppable
free = (self.board_mask & ~blocked_bits) | food_bits
start_bit = 1 << start_idx
# Check start
if start_bit & food_bits:
return 0, start_idx
# 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]
seen = start_bit
cursor = 0
layer_end = 1
dist = 0
w = self.width
size = self.size
while cursor < len(queue):
cell = queue[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)
if cursor == layer_end:
dist += 1
layer_end = len(queue)
return None, None