Files
snake-python/snakes/engine/perimeter.py
T
daniel156161 5efab77248 feat!: adopt Python 3.15 and support BattlesnakeArena
- Require Python 3.15 in project metadata, runtime pin, and Docker stages.
- Use lazy imports for database backends and type-only dependencies.
- Defer benchmark dependencies until their code paths are used.
- Rely on deferred annotations and use Path.info for file discovery.
- Accept BattlesnakeArena alongside BattlesnakeEngine on game endpoints.
- Advance quart_common for Python 3.15 and multi-agent validation.
- Add pytest development dependencies and refresh the uv lockfile.
- Test lazy backend loading, import order, and fresh file discovery.
- Document the runtime requirements and locked uv workflow.

BREAKING CHANGE: Python 3.15 or newer is required; earlier runtimes cannot parse the lazy import syntax.
2026-11-03 10:02:40 +01:00

77 lines
2.3 KiB
Python

"""Safety-gated board geometry scoring for perimeter-aware snakes."""
def _on_edge(point: tuple[int, int], width: int, height: int) -> bool:
x, y = point
return x in {0, width - 1} or y in {0, height - 1}
def _same_edge(
first: tuple[int, int], second: tuple[int, int], width: int, height: int,
) -> bool:
boundaries = ((0, 0), (0, width - 1), (1, 0), (1, height - 1))
return any(
first[index] == boundary and second[index] == boundary
for index, boundary in boundaries
)
def perimeter_geometry_score(
*,
point: tuple[int, int],
current_head: tuple[int, int],
width: int,
height: int,
occupancy: float,
snake_length: int,
reachable_space: int,
required_space: int,
liberties: int,
next_options: int,
safe_next_options: int,
tail_escape: bool,
dead_end: bool,
losing_head_to_head: bool,
) -> float:
"""Reward useful perimeter lanes without overriding tactical safety.
The normal move scorer already values liberties heavily, which naturally
makes wall cells less attractive. This adjustment offsets that bias only
when the wall position has room, a tail route, and multiple safe exits.
"""
x, y = point
cx, cy = (width - 1) / 2.0, (height - 1) / 2.0
center_score = 1.0 - (abs(x - cx) + abs(y - cy)) / max(1.0, cx + cy)
center_weight = max(2.0, 6.0 * (1.0 - min(1.0, occupancy / 0.5)))
score = center_score * center_weight
if not _on_edge(point, width, height):
return score
safely_usable = (
not dead_end
and not losing_head_to_head
and tail_escape
and liberties >= 2
and next_options >= 2
and safe_next_options >= 2
and reachable_space >= required_space + max(4, required_space // 2)
)
if not safely_usable:
return score
phase = min(1.0, occupancy / 0.34)
length_factor = min(1.0, snake_length / 12.0)
space_margin = min(
1.0,
max(0, reachable_space - required_space) / max(1, required_space),
)
score += 24.0 + phase * 12.0 + length_factor * 8.0 + space_margin * 8.0
# Continuing along one edge is more useful than repeatedly entering and
# leaving it: it keeps the body ordered and leaves the interior available.
if _same_edge(current_head, point, width, height):
score += 10.0
# Corners remove two exits. They remain usable, but should not become goals.
if x in {0, width - 1} and y in {0, height - 1}:
score -= 18.0
return score