fix: resolve duel roots and recover legacy snake data

- Resolve selected moves and enemy replies on the same simulated turn.
- Add an Apex candidate hook and bump the Prism snake to version 1.1.0.
- Rebuild benchmark states from normalized turn data when snapshots are empty.
- Synthesize missing game snake identities during legacy database migration.
- Add regression coverage for duel timing and partial legacy schemas.
This commit is contained in:
2026-08-01 19:11:32 +02:00
parent c646392b84
commit 6643eb35af
8 changed files with 329 additions and 39 deletions
+53 -8
View File
@@ -468,15 +468,11 @@ class ApexBattleSnake(TemplateSnake):
if self._time_exceeded(deadline):
break
pos = safe_moves[m]
ate = (pos["x"], pos["y"]) in food_set
fb = self._future_body(my_body, pos, ate, False)
nmy_h = 100 if ate else my_health - 1
if (pos["x"], pos["y"]) in hazard_set and not ate:
nmy_h -= hazard_damage * hazard_count.get((pos["x"], pos["y"]), 1)
mm_val, depth_done = self._minimax_sim_id(
my_body=fb, enemy_body=enemy["body"],
mm_val, depth_done = self._minimax_candidate_id(
my_body=my_body, enemy_body=enemy["body"],
my_target=(pos["x"], pos["y"]),
food_set=food_set, hazard_set=hazard_set,
my_health=nmy_h, enemy_health=enemy_health,
my_health=my_health, enemy_health=enemy_health,
hazard_damage=hazard_damage, hazard_count=hazard_count,
width=width, height=height,
max_depth=self._planning_depth,
@@ -893,6 +889,55 @@ class ApexBattleSnake(TemplateSnake):
# ── A1: Iterative deepening minimax ──────────────────────────────────────────
def _minimax_candidate_id(
self,
my_body: list,
enemy_body: list,
my_target: tuple[int, int],
food_set: set,
hazard_set: set,
my_health: int,
enemy_health: int,
hazard_damage: int,
hazard_count: dict,
width: int,
height: int,
max_depth: int,
alpha: float,
beta: float,
deadline: float | None,
previous_hazard_set: set | None = None,
) -> tuple[float, int]:
"""Evaluate a selected move before continuing the legacy duel search.
Optimized subclasses can override this hook to resolve our selected move
and the opponent's reply simultaneously at the search root.
"""
pos = {"x": my_target[0], "y": my_target[1]}
ate = my_target in food_set
future_body = self._future_body(my_body, pos, ate, False)
future_health = 100 if ate else my_health - 1
effective_previous = previous_hazard_set if previous_hazard_set is not None else hazard_set
if my_target in hazard_set and my_target in effective_previous and not ate:
future_health -= hazard_damage * hazard_count.get(my_target, 1)
return self._minimax_sim_id(
my_body=future_body,
enemy_body=enemy_body,
food_set=food_set,
hazard_set=hazard_set,
my_health=future_health,
enemy_health=enemy_health,
hazard_damage=hazard_damage,
hazard_count=hazard_count,
width=width,
height=height,
max_depth=max_depth,
alpha=alpha,
beta=beta,
deadline=deadline,
previous_hazard_set=previous_hazard_set,
)
def _minimax_sim_id(
self,
my_body: list,