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:
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""PrismBattleSnake_GPT_5_6_Sol v1.0.1
|
||||
"""PrismBattleSnake_GPT_5_6_Sol v1.1.0
|
||||
|
||||
Built on ApexBattleSnake v1.0.0. All strategic logic is inherited.
|
||||
Performance improvement: all spatial primitives (flood fill, territory,
|
||||
@@ -25,6 +25,7 @@ Key speedups:
|
||||
S11: _future_survival_tree inlines legal-move check with bitboard ops.
|
||||
S12: Duel minimax uses tuple bodies and bitboard move generation.
|
||||
S13: Iterative deepening reuses a transposition table and move-order hints.
|
||||
S14: Candidate duel moves and enemy replies resolve on the same root turn.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -41,7 +42,7 @@ _DIR_DELTAS = ((0, 1), (0, -1), (-1, 0), (1, 0))
|
||||
_DIR_NAMES = ("up", "down", "left", "right")
|
||||
|
||||
class PrismBattleSnake_GPT_5_6_Sol(ApexBattleSnake):
|
||||
VERSION = "1.0.1"
|
||||
VERSION = "1.1.0"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -268,6 +269,28 @@ class PrismBattleSnake_GPT_5_6_Sol(ApexBattleSnake):
|
||||
deadline=deadline,
|
||||
)
|
||||
|
||||
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]:
|
||||
"""Resolve our selected move and every enemy reply simultaneously."""
|
||||
search = self._new_duel_search(
|
||||
food_set, hazard_set, hazard_count, hazard_damage,
|
||||
width, height, deadline,
|
||||
)
|
||||
return search.search_candidate(
|
||||
my_body=my_body,
|
||||
enemy_body=enemy_body,
|
||||
my_target=my_target,
|
||||
my_health=my_health,
|
||||
enemy_health=enemy_health,
|
||||
max_depth=max_depth,
|
||||
previous_hazards=previous_hazard_set if previous_hazard_set is not None else hazard_set,
|
||||
)
|
||||
|
||||
def _minimax_sim_id(
|
||||
self, my_body: list, enemy_body: list, food_set: set, hazard_set: set,
|
||||
my_health: int, enemy_health: int, hazard_damage: int, hazard_count: dict,
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ SNAKE_REGISTRY = {
|
||||
"UltimateBattleSnake": "4.5.0",
|
||||
"ApexBattleSnake": "1.0.0",
|
||||
"SupremeBattleSnake_ClaudeOpus4_6": "1.0.0",
|
||||
"PrismBattleSnake_GPT_5_6_Sol": "1.0.1",
|
||||
"PrismBattleSnake_GPT_5_6_Sol": "1.1.0",
|
||||
}
|
||||
|
||||
DEFAULT_SNAKE_CONFIG = {
|
||||
|
||||
@@ -87,6 +87,47 @@ class BitboardDuelSearch:
|
||||
|
||||
return result, completed_depth
|
||||
|
||||
def search_candidate(
|
||||
self,
|
||||
my_body: list[dict],
|
||||
enemy_body: list[dict],
|
||||
my_target: tuple[int, int],
|
||||
my_health: int,
|
||||
enemy_health: int,
|
||||
max_depth: int,
|
||||
previous_hazards: Iterable[tuple[int, int]],
|
||||
) -> tuple[float, int]:
|
||||
"""Evaluate one selected move against every simultaneous enemy reply.
|
||||
|
||||
``max_depth`` counts the selected root turn, so a completed depth of one
|
||||
means all opponent replies to that move were resolved.
|
||||
"""
|
||||
state = DuelState(
|
||||
my_body=self.body_from_dicts(my_body),
|
||||
enemy_body=self.body_from_dicts(enemy_body),
|
||||
food_bits=self.food_bits,
|
||||
my_health=my_health,
|
||||
enemy_health=enemy_health,
|
||||
previous_hazard_bits=self.board.set_to_bits(set(previous_hazards)),
|
||||
)
|
||||
target_idx = self.board.idx(my_target[0], my_target[1])
|
||||
if not self.board.neighbors_of(state.my_body[0]) & (1 << target_idx):
|
||||
return self.LOSS, 0
|
||||
|
||||
result = self._evaluate(state)
|
||||
completed_depth = 0
|
||||
for depth in range(1, max_depth + 1):
|
||||
if self._out_of_time(5.0):
|
||||
break
|
||||
value, completed = self._search_selected_move(
|
||||
state, target_idx, depth, -float("inf"), float("inf")
|
||||
)
|
||||
if not completed:
|
||||
break
|
||||
result = value
|
||||
completed_depth = depth
|
||||
return result, completed_depth
|
||||
|
||||
def search_depth(
|
||||
self,
|
||||
my_body: list[dict],
|
||||
@@ -107,6 +148,46 @@ class BitboardDuelSearch:
|
||||
value, _ = self._search(state, depth, -float("inf"), float("inf"))
|
||||
return value
|
||||
|
||||
def _search_selected_move(
|
||||
self,
|
||||
state: DuelState,
|
||||
my_target: int,
|
||||
depth: int,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
) -> tuple[float, bool]:
|
||||
"""Resolve the selected root move with the opponent on the same turn."""
|
||||
self.nodes += 1
|
||||
if self._out_of_time():
|
||||
return self._evaluate(state), False
|
||||
|
||||
enemy_moves = self._candidate_targets(state.enemy_body)
|
||||
if not enemy_moves:
|
||||
return self.WIN + depth, True
|
||||
enemy_moves = self._ordered_moves(enemy_moves, state, depth, False)
|
||||
worst = float("inf")
|
||||
|
||||
for enemy_target in enemy_moves:
|
||||
if self._out_of_time():
|
||||
return (worst if worst != float("inf") else self._evaluate(state)), False
|
||||
child, terminal = self._advance(state, my_target, enemy_target)
|
||||
if terminal is not None:
|
||||
value = terminal
|
||||
completed = True
|
||||
elif depth <= 1:
|
||||
value = self._evaluate(child)
|
||||
completed = True
|
||||
else:
|
||||
value, completed = self._search(child, depth - 1, alpha, beta)
|
||||
if not completed:
|
||||
return (worst if worst != float("inf") else value), False
|
||||
worst = min(worst, value)
|
||||
beta = min(beta, worst)
|
||||
if beta <= alpha:
|
||||
break
|
||||
|
||||
return worst, True
|
||||
|
||||
def _search(self, state: DuelState, depth: int, alpha: float, beta: float) -> tuple[float, bool]:
|
||||
self.nodes += 1
|
||||
if self._out_of_time():
|
||||
|
||||
Reference in New Issue
Block a user