f14d780f29
Build and Push Docker Container / build-and-push (push) Successful in 7m53s
- Stream compact live replay updates across local and clustered dashboards. - Render responsive snake bodies as SVG paths with aligned custom icons. - Add cache-busted assets, replay fallback routes, and live-follow playback. - Support PostgreSQL benchmark sampling and idempotent SQLite migration. - Add dry-run cleanup for old low-quality PostgreSQL replay payloads. - Reward safe perimeter lanes and bump Prism to version 1.5.0. - Add backend, migration, dashboard, and perimeter regression coverage.
80 lines
2.4 KiB
Python
80 lines
2.4 KiB
Python
"""Safety-gated board geometry scoring for perimeter-aware snakes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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
|