feat: add Zenith strategy and move latency tracing
Build and Push Docker Container / build-and-push (push) Successful in 2m18s

- Register Zenith 1.1 with balanced duel verdicts and forced-outcome overrides.
- Prioritize permanent-trail space in Constrictor and health-aware hazard routes.
- Deduct executor queue delays and return fallback directions on move timeouts.
- Correlate response headers, replay timings, and sampled structured trace logs.
- Move shared metrics publication off the response path without losing local counts.
- Allow configurable tournament opponents and add a psql helper recipe.
- Preserve empty latency values through compact replay encoding and migration.
- Update Arena game links and use default assets for missing customizations.
- Cover strategy, HTTP lifecycle, tracing, replay codecs, and dashboard behavior.
- Validate with 260 passing Python tests, 4 skips, and 2 passing JavaScript tests.
This commit is contained in:
2026-11-05 01:11:31 +01:00
parent 39a7a53873
commit a2414f0b3d
26 changed files with 758 additions and 45 deletions
+3 -2
View File
@@ -12,6 +12,9 @@ SNAKE_REGISTRATIONS = {
"PrismBattleSnake_GPT_5_6_Sol": SnakeRegistration(
"snakes.strategies.prism", "1.5.0"
),
"ZenithBattleSnake_ClaudeOpus5_5": SnakeRegistration(
"snakes.strategies.zenith", "1.1.0"
),
"DummSnake": SnakeRegistration("snakes.legacy.DummSnake", "1.0.0"),
"LogicSnake": SnakeRegistration("snakes.legacy.LogicSnake", "1.1.0"),
"MasterSnake": SnakeRegistration("snakes.legacy.MasterSnake", "1.2.0"),
@@ -42,7 +45,6 @@ DEFAULT_SNAKE_CONFIG = {
"tail": "default",
}
def build_snake(selected_snake: str):
registration = SNAKE_REGISTRATIONS.get(selected_snake)
if registration is None:
@@ -56,7 +58,6 @@ def get_snake_version(selected_snake: str) -> str | None:
registration = SNAKE_REGISTRATIONS.get(selected_snake)
return registration.version if registration is not None else None
class SnakeBuilder:
@classmethod
def build(self, selected_snake: str):
+29
View File
@@ -0,0 +1,29 @@
"""Health-cost routes to food or non-hazard refuge, with current bodies fixed."""
import heapq
def health_route_cost(start, food, hazards, stacks, damage, blocked, width, height, *, refuge=False):
"""Cost until food resets health, or until leaving hazards for refuge.
Food is consumed before elimination, so its final step needs no remaining
health. Other steps must leave health positive. Bodies remain obstacles,
including food-covered body cells; the caller removes the starting head.
"""
queue = [(0, start)]
best = {start: 0}
while queue:
cost, point = heapq.heappop(queue)
if cost != best[point]:
continue
if point in food or (refuge and point not in hazards):
return cost
x, y = point
for target in ((x, y + 1), (x, y - 1), (x - 1, y), (x + 1, y)):
nx, ny = target
if not (0 <= nx < width and 0 <= ny < height) or target in blocked:
continue
step = 0 if target in food else 1 + (damage * stacks.get(target, 1) if target in hazards else 0)
value = cost + step
if value < best.get(target, float('inf')):
best[target] = value
heapq.heappush(queue, (value, target))
return None
+1 -1
View File
@@ -235,7 +235,7 @@ class CompactSurvivalSearch:
blocked = self._occupied(mine, enemies) & ~(1 << mine[0])
space = self.board.flood_count(mine[0], blocked)
liberties = self.board.open_neighbor_count(mine[0], blocked)
if liberties == 0 or space < len(mine):
if liberties == 0 or (not self.is_constrictor and space < len(mine)):
return self.DEATH
enemy_pressure = 0.0
for enemy in enemies:
+6 -1
View File
@@ -2,5 +2,10 @@
from snakes.strategies.apex import ApexBattleSnake
from snakes.strategies.prism import PrismBattleSnake_GPT_5_6_Sol
from snakes.strategies.zenith import ZenithBattleSnake_ClaudeOpus5_5
__all__ = ("ApexBattleSnake", "PrismBattleSnake_GPT_5_6_Sol")
__all__ = (
"ApexBattleSnake",
"PrismBattleSnake_GPT_5_6_Sol",
"ZenithBattleSnake_ClaudeOpus5_5",
)
+126
View File
@@ -0,0 +1,126 @@
"""ZenithBattleSnake_ClaudeOpus5_5 v1.1.0
Built on PrismBattleSnake_GPT_5_6_Sol v1.5.0. Prism's bitboard engine, duel
search, and adversarial rollouts are inherited unchanged.
Prism only runs the exact duel search on near-tied heuristic candidates
(score gap <= 8). A move outside that window is never checked against every
enemy reply, so a forced loss with a strong heuristic score can still be
played, and a forced win with a weaker score can be skipped.
Z1: Root verdict pass — after Prism decides, every safe duel candidate is
resolved against all simultaneous enemy replies with the shared
transposition table (cheap: most subtrees are already cached).
Z2: Proven-loss veto — a candidate whose completed search proves a loss is
replaced by the best non-losing candidate.
Z3: Proven-win override — a candidate whose completed search proves a win is
played even if the heuristic preferred another move.
"""
from time import perf_counter
from snakes.engine.duel_search import BitboardDuelSearch
from snakes.strategies.zenith_survival import ZenithSurvivalMixin
from snakes.strategies.prism import PrismBattleSnake_GPT_5_6_Sol
# A completed search value beyond half of WIN/LOSS can only come from a terminal.
_PROVEN_WIN = BitboardDuelSearch.WIN / 2
_PROVEN_LOSS = BitboardDuelSearch.LOSS / 2
# Reserve kept for Apex's history bookkeeping after the verdict pass.
_VERDICT_RESERVE_MS = 25.0
class ZenithBattleSnake_ClaudeOpus5_5(ZenithSurvivalMixin, PrismBattleSnake_GPT_5_6_Sol):
VERSION = "1.1.0"
def __init__(self) -> None:
super().__init__()
self.name = "ZenithBattleSnake"
self.version = self.VERSION
def _get_timeout_buffer_ms(self):
# Leave room for executor scheduling, history, JSON and network delivery.
return max(180, super()._get_timeout_buffer_ms())
def _choose_duel_move(
self, safe_moves, my_body, my_len, my_health, other_snakes, food_set,
hazard_set, hazard_damage, hazard_count, previous_hazard_set,
enemy_attack_map, enemy_can_grow, total_occupancy, width, height, deadline,
):
best_move, scores, depth = super()._choose_duel_move(
safe_moves=safe_moves, my_body=my_body, my_len=my_len,
my_health=my_health, other_snakes=other_snakes, food_set=food_set,
hazard_set=hazard_set, hazard_damage=hazard_damage,
hazard_count=hazard_count, previous_hazard_set=previous_hazard_set,
enemy_attack_map=enemy_attack_map, enemy_can_grow=enemy_can_grow,
total_occupancy=total_occupancy, width=width, height=height,
deadline=deadline,
)
if len(safe_moves) < 2:
return best_move, scores, depth
verdicts = self._duel_verdicts(
safe_moves=safe_moves, my_body=my_body, enemy=other_snakes[0],
my_health=my_health, food_set=food_set, hazard_set=hazard_set,
hazard_damage=hazard_damage, hazard_count=hazard_count,
previous_hazard_set=previous_hazard_set, width=width, height=height,
deadline=deadline,
)
chosen = self._apply_verdicts(best_move, scores, verdicts)
if chosen != best_move:
self.add_to_history({
"turn": self.game_board.get_turn(), "mode": "duel",
"zenith_verdict_changed_move": True,
"from": best_move, "to": chosen,
"verdicts": {m: round(v, 1) for m, (v, _) in verdicts.items()},
})
depth = max([depth, *(d for _, d in verdicts.values())])
return chosen, scores, depth
def _duel_verdicts(
self, safe_moves, my_body, enemy, my_health, food_set, hazard_set,
hazard_damage, hazard_count, previous_hazard_set, width, height, deadline,
) -> dict[str, tuple[float, int]]:
"""Return completed exact-search values per candidate (Z1)."""
verdicts: dict[str, tuple[float, int]] = {}
# Search every root at equal depth. A full iterative search for the first
# move could consume the deadline before the other roots were visited.
search_deadline = None if deadline is None else deadline - _VERDICT_RESERVE_MS / 1000
if search_deadline is not None and perf_counter() >= search_deadline:
return verdicts
search = self._new_duel_search(
food_set, hazard_set, hazard_count, hazard_damage, width, height, search_deadline,
)
search.deadline = search_deadline
for depth in range(1, self._planning_depth + 1):
layer = {}
for move, pos in safe_moves.items():
value, completed = search.search_candidate(
my_body, enemy["body"], (pos["x"], pos["y"]),
my_health, enemy.get("health", 100), depth, previous_hazard_set,
)
if completed != depth:
return verdicts
layer[move] = (value, completed)
verdicts = layer
return verdicts
@staticmethod
def _apply_verdicts(
best_move: str, scores: dict[str, float],
verdicts: dict[str, tuple[float, int]],
) -> str:
"""Apply proven-win override (Z3) and proven-loss veto (Z2)."""
def rank(move: str) -> tuple[float, float]:
return verdicts[move][0], scores.get(move, float("-inf"))
wins = [m for m, (v, _) in verdicts.items() if v >= _PROVEN_WIN]
if wins and best_move not in wins:
return max(wins, key=rank)
best_verdict = verdicts.get(best_move)
if best_verdict is None or best_verdict[0] > _PROVEN_LOSS:
return best_move
alternatives = [m for m, (v, _) in verdicts.items() if v > _PROVEN_LOSS]
if not alternatives:
return best_move
# Prefer Prism's heuristic among surviving moves; fall back to search value.
return max(alternatives, key=lambda m: (scores.get(m, float("-inf")), verdicts[m][0]))
+97
View File
@@ -0,0 +1,97 @@
"""Zenith-specific permanent-trail and health-aware scoring."""
from snakes.engine.health_routes import health_route_cost
from snakes.engine.survival_search import CompactSurvivalSearch
class ZenithSurvivalMixin:
def _choose_constrictor_move(
self, safe_moves, my_body, my_len, my_health, other_snakes, food_set,
hazard_set, hazard_damage, hazard_count, previous_hazard_set,
enemy_attack_map, enemy_can_grow, total_occupancy, width, height, deadline,
):
# In constrictor bodies never vacate. Remaining free cells are a survival
# horizon, not storage for the existing body. Requiring free space >= body
# length incorrectly marks every late-game candidate dead.
bb = self._get_bb(width, height)
occupied = bb.set_to_bits({
(p['x'], p['y']) for body in [my_body, *(s['body'] for s in other_snakes)]
for p in body
})
scores = {}
for move, pos in safe_moves.items():
target = bb.idx(pos['x'], pos['y'])
space = bb.flood_count(target, occupied)
liberties = bb.open_neighbor_count(target, occupied)
danger = enemy_attack_map.get((pos['x'], pos['y']), 0) >= my_len
# Preserve free capacity first. Enemy confinement must not outweigh our
# own ability to survive, and tail access has no meaning in this mode.
scores[move] = space * 10.0 + liberties * 8.0 - (10000 if danger else 0)
if liberties == 0:
scores[move] -= 5000
# Give each candidate the same shallow horizon before considering deeper
# layers. Never let the first candidate consume everyone else's budget.
search = CompactSurvivalSearch(
bb, set(), True, deadline, branch=3, enemy_branch=2, response_cap=8,
)
ranked = scores
for depth in range(1, 5):
layer = {}
for move, pos in safe_moves.items():
if self._remaining_ms(deadline) < 15:
break
before = search.deadline_exits
value = search.search_selected(my_body, other_snakes, (pos['x'], pos['y']), depth)
if search.deadline_exits != before:
break
layer[move] = value
if len(layer) != len(safe_moves):
break
# Rollout can veto immediate/near-term death, but cannot turn temporary
# enemy confinement into an overwhelming speculative attack reward.
ranked = {m: scores[m] + v * 0.15 for m, v in layer.items()}
best = max(ranked, key=ranked.get)
if depth == 4:
return best, ranked
return max(ranked, key=ranked.get), ranked
def _score_move(self, *args, **kwargs):
score, info = super()._score_move(*args, **kwargs)
# Apex calls this hook with keywords. Constrictor uses its dedicated path.
if not kwargs or kwargs['is_constrictor']:
return score, info
point = (kwargs['pos']['x'], kwargs['pos']['y'])
food = kwargs['food_set']
hazards = kwargs['hazard_set']
damage = kwargs['hazard_damage']
stacks = kwargs['hazard_count']
health = 100 if point in food else kwargs['my_health'] - 1 - (
damage * stacks.get(point, 1) if point in hazards else 0
)
if health <= 0:
score -= 20000
info['is_survivable'] = False
return score, info
if not hazards:
return score, info
blocked = info['blocked'] - {point}
cost = health_route_cost(
point, food, hazards, stacks, damage, blocked,
kwargs['width'], kwargs['height'], refuge=True,
)
# Existing hazards deal damage on entry even when newly seen. Prefer
# leaving lethal hazard corridors before low-health urgency kicks in.
if cost is None or cost >= health:
score -= 12000
info['is_survivable'] = False
else:
score -= cost * 8.0
if health < 55:
food_cost = health_route_cost(
point, food, hazards, stacks, damage, blocked,
kwargs['width'], kwargs['height'],
)
if food_cost is None or food_cost >= health:
score -= 2500
else:
score += (55 - health) * 12.0 / (food_cost + 1)
return score, info