3a9af3f54d
- 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.
217 lines
8.0 KiB
Python
217 lines
8.0 KiB
Python
"""Bitboard-backed spatial primitives shared by competitive snakes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from snakes.engine.bitboard import BitBoard
|
|
|
|
class BitboardSpatialMixin:
|
|
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)
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
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()}
|
|
|
|
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,
|
|
)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
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
|