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:
@@ -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