update snakes with new code
Build and Push Docker Container / build-and-push (push) Successful in 1m21s
Build and Push Docker Container / build-and-push (push) Successful in 1m21s
This commit is contained in:
+103
-32
@@ -2,7 +2,7 @@ from collections.abc import Iterator
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
import random, os
|
import os
|
||||||
|
|
||||||
from server.dataset.RLBootstrapDataset import RLBootstrapDataset
|
from server.dataset.RLBootstrapDataset import RLBootstrapDataset
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ from snakes.TemplateSnake import TemplateSnake
|
|||||||
from server.GameBoard import GameBoard
|
from server.GameBoard import GameBoard
|
||||||
|
|
||||||
class BestBattleSnake(TemplateSnake):
|
class BestBattleSnake(TemplateSnake):
|
||||||
VERSION = "2.6.1"
|
VERSION = "2.7.0"
|
||||||
Point = tuple[int, int]
|
Point = tuple[int, int]
|
||||||
Coord = dict[str, int]
|
Coord = dict[str, int]
|
||||||
SnakeState = dict[str, Any]
|
SnakeState = dict[str, Any]
|
||||||
@@ -39,6 +39,7 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
self.last_move = None
|
self.last_move = None
|
||||||
self.last_game_id = None
|
self.last_game_id = None
|
||||||
self.previous_hazards = set()
|
self.previous_hazards = set()
|
||||||
|
self.hazard_stack_counts:dict[tuple[int, int], int] = {}
|
||||||
self.duel_style = self._get_duel_style()
|
self.duel_style = self._get_duel_style()
|
||||||
self.timeout_buffer_ms = self._get_timeout_buffer_ms()
|
self.timeout_buffer_ms = self._get_timeout_buffer_ms()
|
||||||
self.rl_bootstrap = RLBootstrapDataset()
|
self.rl_bootstrap = RLBootstrapDataset()
|
||||||
@@ -124,15 +125,18 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
width = game_data.get_width()
|
width = game_data.get_width()
|
||||||
height = game_data.get_height()
|
height = game_data.get_height()
|
||||||
board_area = max(1, width * height)
|
board_area = max(1, width * height)
|
||||||
occupancy_ratio = my_len / board_area
|
|
||||||
preserve_space_mode = occupancy_ratio >= 0.34 and my_health > 35
|
|
||||||
foods = game_data.get_food()
|
foods = game_data.get_food()
|
||||||
hazards = game_data.get_hazard()
|
hazards = game_data.get_hazard()
|
||||||
other_snakes = game_data.get_other_snakes()
|
other_snakes = game_data.get_other_snakes()
|
||||||
is_constrictor = game_data.get_type() == "constrictor"
|
is_constrictor = game_data.get_type() == "constrictor"
|
||||||
|
|
||||||
|
total_body_cells = len(my_body) + sum(len(snake["body"]) for snake in other_snakes)
|
||||||
|
occupancy_ratio = total_body_cells / board_area
|
||||||
|
preserve_space_mode = occupancy_ratio >= 0.34 and my_health > 35
|
||||||
|
|
||||||
food_set = {(food["x"], food["y"]) for food in foods}
|
food_set = {(food["x"], food["y"]) for food in foods}
|
||||||
hazard_set = {(hazard["x"], hazard["y"]) for hazard in hazards}
|
hazard_set = {(hazard["x"], hazard["y"]) for hazard in hazards}
|
||||||
|
self.hazard_stack_counts = self._hazard_stack_count_map(hazards)
|
||||||
previous_hazard_set = set(self.previous_hazards)
|
previous_hazard_set = set(self.previous_hazards)
|
||||||
hazard_damage = self._hazard_damage_per_turn(game_data)
|
hazard_damage = self._hazard_damage_per_turn(game_data)
|
||||||
enemy_heads = [
|
enemy_heads = [
|
||||||
@@ -159,13 +163,11 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
fallback = self._fallback_move(my_head, width, height)
|
fallback = self._fallback_move(my_head, width, height)
|
||||||
self.recent_heads.append(current_head_point)
|
self.recent_heads.append(current_head_point)
|
||||||
self.last_move = fallback
|
self.last_move = fallback
|
||||||
self.add_to_history(
|
self.add_to_history({
|
||||||
{
|
|
||||||
"turn": turn,
|
"turn": turn,
|
||||||
"move": fallback,
|
"move": fallback,
|
||||||
"reason": "no_safe_moves",
|
"reason": "no_safe_moves",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
self.rl_bootstrap.record_sample(game_data, fallback, safe_moves, "no_safe_moves")
|
self.rl_bootstrap.record_sample(game_data, fallback, safe_moves, "no_safe_moves")
|
||||||
self.previous_hazards = set(hazard_set)
|
self.previous_hazards = set(hazard_set)
|
||||||
return fallback
|
return fallback
|
||||||
@@ -302,9 +304,16 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
else:
|
else:
|
||||||
score += 70.0
|
score += 70.0
|
||||||
|
|
||||||
|
health_after_move = 100 if ate_food else my_health - 1
|
||||||
|
hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set)
|
||||||
|
if hazard_active:
|
||||||
|
health_after_move -= self._hazard_cell_damage(point, hazard_damage)
|
||||||
|
|
||||||
hunger_urgency = max(0.0, (60.0 - my_health) / 60.0)
|
hunger_urgency = max(0.0, (60.0 - my_health) / 60.0)
|
||||||
if nearest_food_dist is not None:
|
if nearest_food_dist is not None:
|
||||||
score += (28.0 + 70.0 * hunger_urgency) / (nearest_food_dist + 1)
|
score += (28.0 + 70.0 * hunger_urgency) / (nearest_food_dist + 1)
|
||||||
|
if my_health < 40 and nearest_food_dist >= health_after_move:
|
||||||
|
score -= 800.0 + (40 - my_health) * 20.0
|
||||||
elif my_health < 30:
|
elif my_health < 30:
|
||||||
score -= 150.0
|
score -= 150.0
|
||||||
|
|
||||||
@@ -323,7 +332,7 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
score -= 40.0
|
score -= 40.0
|
||||||
|
|
||||||
if point in hazard_set:
|
if point in hazard_set:
|
||||||
hazard_scale = max(0.5, hazard_damage / 14.0)
|
hazard_scale = max(0.5, self._hazard_cell_damage(point, hazard_damage) / 14.0)
|
||||||
if not ate_food:
|
if not ate_food:
|
||||||
score -= (70.0 if my_health > 35 else 250.0) * hazard_scale
|
score -= (70.0 if my_health > 35 else 250.0) * hazard_scale
|
||||||
|
|
||||||
@@ -338,10 +347,6 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
):
|
):
|
||||||
score -= 20.0
|
score -= 20.0
|
||||||
|
|
||||||
health_after_move = 100 if ate_food else my_health - 1
|
|
||||||
hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set)
|
|
||||||
if hazard_active:
|
|
||||||
health_after_move -= hazard_damage
|
|
||||||
if health_after_move <= 0:
|
if health_after_move <= 0:
|
||||||
score -= 10000.0
|
score -= 10000.0
|
||||||
|
|
||||||
@@ -364,11 +369,15 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
quick_move = (
|
quick_move = (
|
||||||
self.last_move
|
self.last_move
|
||||||
if self.last_move in safe_moves
|
if self.last_move in safe_moves
|
||||||
else random.choice(list(safe_moves.keys()))
|
else self._deterministic_move_choice(list(safe_moves.keys()))
|
||||||
)
|
)
|
||||||
self.recent_heads.append(current_head_point)
|
self.recent_heads.append(current_head_point)
|
||||||
self.last_move = quick_move
|
self.last_move = quick_move
|
||||||
self.add_to_history({"turn": turn, "move": quick_move, "reason": "timeout_budget"})
|
self.add_to_history({
|
||||||
|
"turn": turn,
|
||||||
|
"move": quick_move,
|
||||||
|
"reason": "timeout_budget"
|
||||||
|
})
|
||||||
self.rl_bootstrap.record_sample(game_data, quick_move, safe_moves, "timeout_budget")
|
self.rl_bootstrap.record_sample(game_data, quick_move, safe_moves, "timeout_budget")
|
||||||
self.previous_hazards = set(hazard_set)
|
self.previous_hazards = set(hazard_set)
|
||||||
return quick_move
|
return quick_move
|
||||||
@@ -412,7 +421,11 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
)
|
)
|
||||||
self.recent_heads.append(current_head_point)
|
self.recent_heads.append(current_head_point)
|
||||||
self.last_move = best_move
|
self.last_move = best_move
|
||||||
self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
|
self.add_to_history({
|
||||||
|
"turn": turn,
|
||||||
|
"move": best_move,
|
||||||
|
"scores": scores
|
||||||
|
})
|
||||||
self.rl_bootstrap.record_sample(game_data, best_move, safe_moves, "multi", scores)
|
self.rl_bootstrap.record_sample(game_data, best_move, safe_moves, "multi", scores)
|
||||||
self.previous_hazards = set(hazard_set)
|
self.previous_hazards = set(hazard_set)
|
||||||
return best_move
|
return best_move
|
||||||
@@ -528,11 +541,18 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
if direct_head_distance == 1:
|
if direct_head_distance == 1:
|
||||||
score -= 180.0 * duel_weights["distance_safety"]
|
score -= 180.0 * duel_weights["distance_safety"]
|
||||||
|
|
||||||
|
health_after_move = 100 if ate_food else my_health - 1
|
||||||
|
hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set)
|
||||||
|
if hazard_active:
|
||||||
|
health_after_move -= self._hazard_cell_damage(point, hazard_damage)
|
||||||
|
|
||||||
hunger_urgency = max(0.0, (65.0 - my_health) / 65.0)
|
hunger_urgency = max(0.0, (65.0 - my_health) / 65.0)
|
||||||
if nearest_food_dist is not None:
|
if nearest_food_dist is not None:
|
||||||
score += (
|
score += (
|
||||||
(25.0 + 90.0 * hunger_urgency) * duel_weights["food_bias"]
|
(25.0 + 90.0 * hunger_urgency) * duel_weights["food_bias"]
|
||||||
) / (nearest_food_dist + 1)
|
) / (nearest_food_dist + 1)
|
||||||
|
if my_health < 40 and nearest_food_dist >= health_after_move:
|
||||||
|
score -= 800.0 + (40 - my_health) * 20.0
|
||||||
|
|
||||||
if ate_food:
|
if ate_food:
|
||||||
if likely_dead_end:
|
if likely_dead_end:
|
||||||
@@ -546,7 +566,7 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
score -= 50.0
|
score -= 50.0
|
||||||
|
|
||||||
if point in hazard_set:
|
if point in hazard_set:
|
||||||
hazard_scale = max(0.5, hazard_damage / 14.0)
|
hazard_scale = max(0.5, self._hazard_cell_damage(point, hazard_damage) / 14.0)
|
||||||
if not ate_food:
|
if not ate_food:
|
||||||
score -= (70.0 if my_health > 35 else 250.0) * hazard_scale
|
score -= (70.0 if my_health > 35 else 250.0) * hazard_scale
|
||||||
|
|
||||||
@@ -561,10 +581,6 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
):
|
):
|
||||||
score -= 20.0
|
score -= 20.0
|
||||||
|
|
||||||
health_after_move = 100 if ate_food else my_health - 1
|
|
||||||
hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set)
|
|
||||||
if hazard_active:
|
|
||||||
health_after_move -= hazard_damage
|
|
||||||
if health_after_move <= 0:
|
if health_after_move <= 0:
|
||||||
score -= 10000.0
|
score -= 10000.0
|
||||||
|
|
||||||
@@ -604,7 +620,7 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
considered_moves = list(scores.keys())
|
considered_moves = list(scores.keys())
|
||||||
|
|
||||||
if not scores:
|
if not scores:
|
||||||
return random.choice(list(safe_moves.keys())), {}
|
return self._deterministic_move_choice(list(safe_moves.keys())), {}
|
||||||
|
|
||||||
best_move = self._pick_best_with_future_planning(
|
best_move = self._pick_best_with_future_planning(
|
||||||
considered_moves=considered_moves,
|
considered_moves=considered_moves,
|
||||||
@@ -744,7 +760,7 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
considered_moves = list(scores.keys())
|
considered_moves = list(scores.keys())
|
||||||
|
|
||||||
if not scores:
|
if not scores:
|
||||||
return random.choice(list(safe_moves.keys())), {}
|
return self._deterministic_move_choice(list(safe_moves.keys())), {}
|
||||||
|
|
||||||
best_move = self._pick_best_with_future_planning(
|
best_move = self._pick_best_with_future_planning(
|
||||||
considered_moves=considered_moves,
|
considered_moves=considered_moves,
|
||||||
@@ -767,6 +783,15 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
occupied = self._occupied_cells(my_body, other_snakes)
|
occupied = self._occupied_cells(my_body, other_snakes)
|
||||||
own_tail = (my_body[-1]["x"], my_body[-1]["y"])
|
own_tail = (my_body[-1]["x"], my_body[-1]["y"])
|
||||||
own_tail_stacked = self._is_tail_stacked(my_body)
|
own_tail_stacked = self._is_tail_stacked(my_body)
|
||||||
|
vacatable_enemy_tails: set[tuple[int, int]] = set()
|
||||||
|
if not is_constrictor:
|
||||||
|
for snake in other_snakes:
|
||||||
|
if self._is_tail_stacked(snake["body"]):
|
||||||
|
continue
|
||||||
|
if self._enemy_can_grow_this_turn(snake, food_set):
|
||||||
|
continue
|
||||||
|
tail = snake["body"][-1]
|
||||||
|
vacatable_enemy_tails.add((tail["x"], tail["y"]))
|
||||||
|
|
||||||
safe_moves = {}
|
safe_moves = {}
|
||||||
for move, (dx, dy) in self.DIRECTIONS.items():
|
for move, (dx, dy) in self.DIRECTIONS.items():
|
||||||
@@ -783,7 +808,12 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
is_constrictor=is_constrictor,
|
is_constrictor=is_constrictor,
|
||||||
)
|
)
|
||||||
|
|
||||||
if point in occupied and not can_step_on_tail:
|
can_step_on_enemy_tail = point in vacatable_enemy_tails
|
||||||
|
if (
|
||||||
|
point in occupied
|
||||||
|
and not can_step_on_tail
|
||||||
|
and not can_step_on_enemy_tail
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
safe_moves[move] = {"x": point[0], "y": point[1]}
|
safe_moves[move] = {"x": point[0], "y": point[1]}
|
||||||
@@ -859,9 +889,15 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
and not enemy_tail_stacked
|
and not enemy_tail_stacked
|
||||||
and not enemy_can_grow
|
and not enemy_can_grow
|
||||||
)
|
)
|
||||||
can_contest_my_tail = (not is_constrictor and point == my_tail and not my_tail_stacked)
|
can_contest_my_tail = (
|
||||||
|
not is_constrictor and point == my_tail and not my_tail_stacked
|
||||||
|
)
|
||||||
|
|
||||||
if point in occupied and not can_step_on_enemy_tail and not can_contest_my_tail:
|
if (
|
||||||
|
point in occupied
|
||||||
|
and not can_step_on_enemy_tail
|
||||||
|
and not can_contest_my_tail
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ignore impossible overlap into our occupied body, but keep our vacatable tail
|
# Ignore impossible overlap into our occupied body, but keep our vacatable tail
|
||||||
@@ -924,6 +960,19 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
settings = ruleset.get("settings", {}) if isinstance(ruleset, dict) else {}
|
settings = ruleset.get("settings", {}) if isinstance(ruleset, dict) else {}
|
||||||
return int(settings.get("hazardDamagePerTurn", 15))
|
return int(settings.get("hazardDamagePerTurn", 15))
|
||||||
|
|
||||||
|
def _hazard_stack_count_map(self, hazards:list[Coord]) -> dict[Point, int]:
|
||||||
|
counts: dict[tuple[int, int], int] = {}
|
||||||
|
for hazard in hazards:
|
||||||
|
point = (hazard["x"], hazard["y"])
|
||||||
|
counts[point] = counts.get(point, 0) + 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def _hazard_cell_damage(self, point:Point, hazard_damage:int) -> int:
|
||||||
|
stack = self.hazard_stack_counts.get(point, 0)
|
||||||
|
if stack <= 0:
|
||||||
|
return 0
|
||||||
|
return hazard_damage * stack
|
||||||
|
|
||||||
def _hazard_is_active(self, point:Point, ate_food:bool, hazard_set:set[Point], previous_hazard_set:set[Point]) -> bool:
|
def _hazard_is_active(self, point:Point, ate_food:bool, hazard_set:set[Point], previous_hazard_set:set[Point]) -> bool:
|
||||||
"""Apply royale hazard grace and food-exception behavior.
|
"""Apply royale hazard grace and food-exception behavior.
|
||||||
|
|
||||||
@@ -952,8 +1001,11 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
if len(top_moves) <= 1:
|
if len(top_moves) <= 1:
|
||||||
return top_moves[0]
|
return top_moves[0]
|
||||||
|
|
||||||
if self._time_exceeded(deadline) or self._remaining_ms(deadline) < self.future_planning_min_time_ms:
|
if (
|
||||||
return random.choice(top_moves)
|
self._time_exceeded(deadline)
|
||||||
|
or self._remaining_ms(deadline) < self.future_planning_min_time_ms
|
||||||
|
):
|
||||||
|
return self._deterministic_move_choice(top_moves, scores)
|
||||||
|
|
||||||
candidate_limit = max(1, self.future_planning_branch)
|
candidate_limit = max(1, self.future_planning_branch)
|
||||||
candidate_moves = sorted(top_moves, key=lambda move: scores[move], reverse=True)[:candidate_limit]
|
candidate_moves = sorted(top_moves, key=lambda move: scores[move], reverse=True)[:candidate_limit]
|
||||||
@@ -978,7 +1030,7 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
lookahead_bonus[move] = bonus
|
lookahead_bonus[move] = bonus
|
||||||
|
|
||||||
if not lookahead_bonus:
|
if not lookahead_bonus:
|
||||||
return random.choice(top_moves)
|
return self._deterministic_move_choice(top_moves, scores)
|
||||||
|
|
||||||
for move, bonus in lookahead_bonus.items():
|
for move, bonus in lookahead_bonus.items():
|
||||||
scores[move] += bonus
|
scores[move] += bonus
|
||||||
@@ -989,7 +1041,27 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
for move in top_moves
|
for move in top_moves
|
||||||
if refined_best - scores[move] <= max(0.5, tie_window / 2)
|
if refined_best - scores[move] <= max(0.5, tie_window / 2)
|
||||||
]
|
]
|
||||||
return random.choice(refined_top)
|
return self._deterministic_move_choice(refined_top, scores)
|
||||||
|
|
||||||
|
def _move_preference_key(self, move:str) -> tuple[int, int, int]:
|
||||||
|
same_as_last = 1 if self.last_move == move else 0
|
||||||
|
is_reverse = (
|
||||||
|
1
|
||||||
|
if self.last_move is not None and self.OPPOSITE.get(self.last_move) == move
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
direction_priority = {"up": 3, "left": 2, "right": 1, "down": 0}
|
||||||
|
return same_as_last, -is_reverse, direction_priority.get(move, -1)
|
||||||
|
|
||||||
|
def _deterministic_move_choice(self, moves:list[str], scores:dict[str, float]|None=None) -> str:
|
||||||
|
if not moves:
|
||||||
|
return "up"
|
||||||
|
if scores is None:
|
||||||
|
return max(moves, key=self._move_preference_key)
|
||||||
|
return max(
|
||||||
|
moves,
|
||||||
|
key=lambda move: (scores.get(move, -1e9), self._move_preference_key(move)),
|
||||||
|
)
|
||||||
|
|
||||||
def _future_rollout_bonus_for_move(self, move:str, safe_moves:MoveMap, my_body:list[Coord], other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, width:int, height:int, enemy_can_grow_cache:dict[Any, bool]|None, depth:int, branch_limit:int, deadline:float|None) -> float:
|
def _future_rollout_bonus_for_move(self, move:str, safe_moves:MoveMap, my_body:list[Coord], other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, width:int, height:int, enemy_can_grow_cache:dict[Any, bool]|None, depth:int, branch_limit:int, deadline:float|None) -> float:
|
||||||
pos = safe_moves.get(move)
|
pos = safe_moves.get(move)
|
||||||
@@ -1017,8 +1089,7 @@ class BestBattleSnake(TemplateSnake):
|
|||||||
)
|
)
|
||||||
return raw_score * 0.06
|
return raw_score * 0.06
|
||||||
|
|
||||||
def _future_survival_tree_score(self, my_body:list[Coord], other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, width:int, height:int, enemy_can_grow_cache:dict[Any, bool]|None, depth:int, branch_limit:int, deadline:float|None,
|
def _future_survival_tree_score(self, my_body:list[Coord], other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, width:int, height:int, enemy_can_grow_cache:dict[Any, bool]|None, depth:int, branch_limit:int, deadline:float|None) -> float:
|
||||||
) -> float:
|
|
||||||
if depth <= 0 or self._time_exceeded(deadline):
|
if depth <= 0 or self._time_exceeded(deadline):
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
|||||||
+127
-48
@@ -2,7 +2,7 @@ from collections.abc import Iterator
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
import random, os
|
import heapq, os
|
||||||
|
|
||||||
from snakes.TemplateSnake import TemplateSnake
|
from snakes.TemplateSnake import TemplateSnake
|
||||||
from server.GameBoard import GameBoard
|
from server.GameBoard import GameBoard
|
||||||
@@ -10,7 +10,7 @@ from server.dataset.RLBootstrapDataset import RLBootstrapDataset
|
|||||||
|
|
||||||
class UltimateBattleSnake(TemplateSnake):
|
class UltimateBattleSnake(TemplateSnake):
|
||||||
"""
|
"""
|
||||||
UltimateBattleSnake v4.2.0
|
UltimateBattleSnake v4.4.0
|
||||||
|
|
||||||
All improvements over BestBattleSnake:
|
All improvements over BestBattleSnake:
|
||||||
v3: #1+#9 Simultaneous minimax (both snakes move at once) with hazard/health tracking
|
v3: #1+#9 Simultaneous minimax (both snakes move at once) with hazard/health tracking
|
||||||
@@ -42,9 +42,17 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
v4.2 B4 _build_enemy_attack_map: enemy_can_grow now actually passed from choose_move and tree
|
v4.2 B4 _build_enemy_attack_map: enemy_can_grow now actually passed from choose_move and tree
|
||||||
v4.2 B5 _choose_duel_move: removed double food bias (score_move already adds it; duel now only adjusts delta)
|
v4.2 B5 _choose_duel_move: removed double food bias (score_move already adds it; duel now only adjusts delta)
|
||||||
v4.2 B6 _minimax_sim: occupancy now respects _is_tail_stacked (stacked tail not vacated)
|
v4.2 B6 _minimax_sim: occupancy now respects _is_tail_stacked (stacked tail not vacated)
|
||||||
|
v4.3 C1 hazard_count dict tracks Snail Mode stack depth; damage scaled by stack throughout
|
||||||
|
v4.3 C2 minimax: hazard damage skipped when food eaten on same tile (rules fidelity)
|
||||||
|
v4.3 C3 _hazard_will_kill: baseline -1/turn now included in health depletion math
|
||||||
|
v4.3 C4 _legal_moves: enemy tail vacate allowed when enemy won't grow (fixes false negatives)
|
||||||
|
v4.3 C5 mode detection uses both ruleset name and game map (snail mode hardening)
|
||||||
|
v4.4 D1 _minimax_sim: hazard spawn-immunity via previous_hazard_set (no damage on newly-spawned hazard)
|
||||||
|
v4.4 D2 _hazard_will_kill: Dijkstra with per-tile stack cost (was constant entry_stack for whole corridor)
|
||||||
|
v4.4 D3 all random.choice fallbacks replaced with deterministic degrade (last_move > center > lexical)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "4.2.0"
|
VERSION = "4.4.0"
|
||||||
Point = tuple[int, int]
|
Point = tuple[int, int]
|
||||||
Coord = dict[str, int]
|
Coord = dict[str, int]
|
||||||
SnakeState = dict[str, Any]
|
SnakeState = dict[str, Any]
|
||||||
@@ -148,10 +156,20 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
foods = game_data.get_food()
|
foods = game_data.get_food()
|
||||||
hazards = game_data.get_hazard()
|
hazards = game_data.get_hazard()
|
||||||
other_snakes = game_data.get_other_snakes()
|
other_snakes = game_data.get_other_snakes()
|
||||||
is_constrictor = game_data.get_type() == "constrictor"
|
# C5: use both ruleset name AND game map for robust mode detection
|
||||||
|
game_type = game_data.get_type()
|
||||||
|
game_map = game_data.get_map() if hasattr(game_data, "get_map") else None
|
||||||
|
is_constrictor = game_type == "constrictor"
|
||||||
|
is_snail = game_map in {"snail_mode", "snail"} or game_type == "snail_mode"
|
||||||
|
|
||||||
food_set: set[tuple[int, int]] = {(f["x"], f["y"]) for f in foods}
|
food_set: set[tuple[int, int]] = {(f["x"], f["y"]) for f in foods}
|
||||||
hazard_set: set[tuple[int, int]] = {(h["x"], h["y"]) for h in hazards}
|
# C1: track hazard stack depth (Snail Mode can stack multiple hazards on one tile)
|
||||||
|
hazard_set: set[tuple[int, int]] = set()
|
||||||
|
hazard_count: dict[tuple[int, int], int] = {}
|
||||||
|
for h in hazards:
|
||||||
|
pt = (h["x"], h["y"])
|
||||||
|
hazard_set.add(pt)
|
||||||
|
hazard_count[pt] = hazard_count.get(pt, 0) + 1
|
||||||
previous_hazard_set = set(self.previous_hazards)
|
previous_hazard_set = set(self.previous_hazards)
|
||||||
hazard_damage = self._hazard_damage_per_turn(game_data)
|
hazard_damage = self._hazard_damage_per_turn(game_data)
|
||||||
current_head_pt = (my_head["x"], my_head["y"])
|
current_head_pt = (my_head["x"], my_head["y"])
|
||||||
@@ -187,6 +205,7 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
safe_moves = self._legal_moves(
|
safe_moves = self._legal_moves(
|
||||||
my_head=my_head, my_body=my_body, other_snakes=other_snakes,
|
my_head=my_head, my_body=my_body, other_snakes=other_snakes,
|
||||||
food_set=food_set, is_constrictor=is_constrictor, width=width, height=height,
|
food_set=food_set, is_constrictor=is_constrictor, width=width, height=height,
|
||||||
|
enemy_can_grow=enemy_can_grow,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not safe_moves:
|
if not safe_moves:
|
||||||
@@ -207,7 +226,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
best_move, scores = self._choose_constrictor_move(
|
best_move, scores = self._choose_constrictor_move(
|
||||||
safe_moves=safe_moves, my_body=my_body, my_len=my_len, my_health=my_health,
|
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,
|
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
||||||
hazard_damage=hazard_damage, previous_hazard_set=previous_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,
|
enemy_attack_map=enemy_attack_map, enemy_can_grow=enemy_can_grow,
|
||||||
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
|
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
|
||||||
)
|
)
|
||||||
@@ -216,7 +236,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
best_move, scores = self._choose_duel_move(
|
best_move, scores = self._choose_duel_move(
|
||||||
safe_moves=safe_moves, my_body=my_body, my_len=my_len, my_health=my_health,
|
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,
|
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
||||||
hazard_damage=hazard_damage, previous_hazard_set=previous_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,
|
enemy_attack_map=enemy_attack_map, enemy_can_grow=enemy_can_grow,
|
||||||
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
|
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
|
||||||
)
|
)
|
||||||
@@ -225,7 +246,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
best_move, scores = self._choose_multi_move(
|
best_move, scores = self._choose_multi_move(
|
||||||
safe_moves=safe_moves, my_body=my_body, my_len=my_len, my_health=my_health,
|
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,
|
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
||||||
hazard_damage=hazard_damage, previous_hazard_set=previous_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,
|
enemy_attack_map=enemy_attack_map, enemy_can_grow=enemy_can_grow,
|
||||||
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
|
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
|
||||||
)
|
)
|
||||||
@@ -251,8 +273,9 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
def _choose_multi_move(
|
def _choose_multi_move(
|
||||||
self, safe_moves: MoveMap, my_body: list, my_len: int, my_health: int,
|
self, safe_moves: MoveMap, my_body: list, my_len: int, my_health: int,
|
||||||
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
||||||
previous_hazard_set: set, enemy_attack_map: dict, enemy_can_grow: dict,
|
hazard_count: dict, previous_hazard_set: set, enemy_attack_map: dict,
|
||||||
total_occupancy: float, width: int, height: int, deadline: float | None,
|
enemy_can_grow: dict, total_occupancy: float, width: int, height: int,
|
||||||
|
deadline: float | None,
|
||||||
) -> tuple[str, dict[str, float]]:
|
) -> tuple[str, dict[str, float]]:
|
||||||
scores: dict[str, float] = {}
|
scores: dict[str, float] = {}
|
||||||
safety: dict[str, dict] = {}
|
safety: dict[str, dict] = {}
|
||||||
@@ -263,7 +286,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
sc, info = self._score_move(
|
sc, info = self._score_move(
|
||||||
move=move, pos=pos, my_body=my_body, my_len=my_len, my_health=my_health,
|
move=move, pos=pos, my_body=my_body, my_len=my_len, my_health=my_health,
|
||||||
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
||||||
hazard_damage=hazard_damage, previous_hazard_set=previous_hazard_set,
|
hazard_damage=hazard_damage, hazard_count=hazard_count,
|
||||||
|
previous_hazard_set=previous_hazard_set,
|
||||||
is_constrictor=False, enemy_attack_map=enemy_attack_map,
|
is_constrictor=False, enemy_attack_map=enemy_attack_map,
|
||||||
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
|
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
|
||||||
width=width, height=height, deadline=deadline,
|
width=width, height=height, deadline=deadline,
|
||||||
@@ -275,7 +299,7 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
safety[move] = info
|
safety[move] = info
|
||||||
|
|
||||||
if not scores:
|
if not scores:
|
||||||
quick = self.last_move if self.last_move in safe_moves else random.choice(list(safe_moves))
|
quick = self._deterministic_fallback(safe_moves, width, height)
|
||||||
self.add_to_history({
|
self.add_to_history({
|
||||||
"turn": self.game_board.get_turn(), "mode": "multi",
|
"turn": self.game_board.get_turn(), "mode": "multi",
|
||||||
"move": quick, "reason": "timeout_budget",
|
"move": quick, "reason": "timeout_budget",
|
||||||
@@ -324,8 +348,9 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
def _choose_duel_move(
|
def _choose_duel_move(
|
||||||
self, safe_moves: MoveMap, my_body: list, my_len: int, my_health: int,
|
self, safe_moves: MoveMap, my_body: list, my_len: int, my_health: int,
|
||||||
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
||||||
previous_hazard_set: set, enemy_attack_map: dict, enemy_can_grow: dict,
|
hazard_count: dict, previous_hazard_set: set, enemy_attack_map: dict,
|
||||||
total_occupancy: float, width: int, height: int, deadline: float | None,
|
enemy_can_grow: dict, total_occupancy: float, width: int, height: int,
|
||||||
|
deadline: float | None,
|
||||||
) -> tuple[str, dict[str, float]]:
|
) -> tuple[str, dict[str, float]]:
|
||||||
enemy = other_snakes[0]
|
enemy = other_snakes[0]
|
||||||
enemy_len = enemy.get("length", len(enemy["body"]))
|
enemy_len = enemy.get("length", len(enemy["body"]))
|
||||||
@@ -344,7 +369,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
sc, info = self._score_move(
|
sc, info = self._score_move(
|
||||||
move=move, pos=pos, my_body=my_body, my_len=my_len, my_health=my_health,
|
move=move, pos=pos, my_body=my_body, my_len=my_len, my_health=my_health,
|
||||||
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
||||||
hazard_damage=hazard_damage, previous_hazard_set=previous_hazard_set,
|
hazard_damage=hazard_damage, hazard_count=hazard_count,
|
||||||
|
previous_hazard_set=previous_hazard_set,
|
||||||
is_constrictor=False, enemy_attack_map=enemy_attack_map,
|
is_constrictor=False, enemy_attack_map=enemy_attack_map,
|
||||||
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
|
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
|
||||||
width=width, height=height, deadline=deadline,
|
width=width, height=height, deadline=deadline,
|
||||||
@@ -399,7 +425,7 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
safety[move] = info
|
safety[move] = info
|
||||||
|
|
||||||
if not scores:
|
if not scores:
|
||||||
return random.choice(list(safe_moves)), {}
|
return self._deterministic_fallback(safe_moves, width, height), {}
|
||||||
|
|
||||||
best_move = self._select_best(scores, safety, safe_moves, list(scores))
|
best_move = self._select_best(scores, safety, safe_moves, list(scores))
|
||||||
|
|
||||||
@@ -416,15 +442,17 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
ate = (pos["x"], pos["y"]) in food_set
|
ate = (pos["x"], pos["y"]) in food_set
|
||||||
fb = self._future_body(my_body, pos, ate, False)
|
fb = self._future_body(my_body, pos, ate, False)
|
||||||
nmy_h = 100 if ate else my_health - 1
|
nmy_h = 100 if ate else my_health - 1
|
||||||
if (pos["x"], pos["y"]) in hazard_set:
|
# C2: food eaten on hazard tile — no hazard penalty this turn (rules fidelity)
|
||||||
nmy_h -= hazard_damage
|
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 = self._minimax_sim(
|
mm_val = self._minimax_sim(
|
||||||
my_body=fb, enemy_body=enemy["body"],
|
my_body=fb, enemy_body=enemy["body"],
|
||||||
food_set=food_set, hazard_set=hazard_set,
|
food_set=food_set, hazard_set=hazard_set,
|
||||||
my_health=nmy_h, enemy_health=enemy_health,
|
my_health=nmy_h, enemy_health=enemy_health,
|
||||||
hazard_damage=hazard_damage,
|
hazard_damage=hazard_damage, hazard_count=hazard_count,
|
||||||
width=width, height=height, depth=2,
|
width=width, height=height, depth=2,
|
||||||
alpha=-1e9, beta=1e9, deadline=deadline,
|
alpha=-1e9, beta=1e9, deadline=deadline,
|
||||||
|
previous_hazard_set=previous_hazard_set, # D1: spawn-immunity for first minimax level
|
||||||
)
|
)
|
||||||
mm_scores[m] = scores[m] + mm_val * 0.10
|
mm_scores[m] = scores[m] + mm_val * 0.10
|
||||||
if mm_scores:
|
if mm_scores:
|
||||||
@@ -476,8 +504,9 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
def _choose_constrictor_move(
|
def _choose_constrictor_move(
|
||||||
self, safe_moves: MoveMap, my_body: list, my_len: int, my_health: int,
|
self, safe_moves: MoveMap, my_body: list, my_len: int, my_health: int,
|
||||||
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
||||||
previous_hazard_set: set, enemy_attack_map: dict, enemy_can_grow: dict,
|
hazard_count: dict, previous_hazard_set: set, enemy_attack_map: dict,
|
||||||
total_occupancy: float, width: int, height: int, deadline: float | None,
|
enemy_can_grow: dict, total_occupancy: float, width: int, height: int,
|
||||||
|
deadline: float | None,
|
||||||
) -> tuple[str, dict[str, float]]:
|
) -> tuple[str, dict[str, float]]:
|
||||||
scores: dict[str, float] = {}
|
scores: dict[str, float] = {}
|
||||||
safety: dict[str, dict] = {}
|
safety: dict[str, dict] = {}
|
||||||
@@ -488,7 +517,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
sc, info = self._score_move(
|
sc, info = self._score_move(
|
||||||
move=move, pos=pos, my_body=my_body, my_len=my_len, my_health=my_health,
|
move=move, pos=pos, my_body=my_body, my_len=my_len, my_health=my_health,
|
||||||
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
other_snakes=other_snakes, food_set=food_set, hazard_set=hazard_set,
|
||||||
hazard_damage=hazard_damage, previous_hazard_set=previous_hazard_set,
|
hazard_damage=hazard_damage, hazard_count=hazard_count,
|
||||||
|
previous_hazard_set=previous_hazard_set,
|
||||||
is_constrictor=True, enemy_attack_map=enemy_attack_map,
|
is_constrictor=True, enemy_attack_map=enemy_attack_map,
|
||||||
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
|
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
|
||||||
width=width, height=height, deadline=deadline,
|
width=width, height=height, deadline=deadline,
|
||||||
@@ -513,7 +543,7 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
safety[move] = info
|
safety[move] = info
|
||||||
|
|
||||||
if not scores:
|
if not scores:
|
||||||
return random.choice(list(safe_moves)), {}
|
return self._deterministic_fallback(safe_moves, width, height), {}
|
||||||
|
|
||||||
# F2: survival tree for constrictor mode
|
# F2: survival tree for constrictor mode
|
||||||
survivable_const = [m for m in scores if safety.get(m, {}).get("is_survivable", False)]
|
survivable_const = [m for m in scores if safety.get(m, {}).get("is_survivable", False)]
|
||||||
@@ -554,8 +584,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
def _score_move(
|
def _score_move(
|
||||||
self, move: str, pos: Coord, my_body: list, my_len: int, my_health: int,
|
self, move: str, pos: Coord, my_body: list, my_len: int, my_health: int,
|
||||||
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
other_snakes: list, food_set: set, hazard_set: set, hazard_damage: int,
|
||||||
previous_hazard_set: set, is_constrictor: bool, enemy_attack_map: dict,
|
hazard_count: dict, previous_hazard_set: set, is_constrictor: bool,
|
||||||
enemy_can_grow: dict, total_occupancy: float,
|
enemy_attack_map: dict, enemy_can_grow: dict, total_occupancy: float,
|
||||||
width: int, height: int, deadline: float | None,
|
width: int, height: int, deadline: float | None,
|
||||||
) -> tuple[float, dict]:
|
) -> tuple[float, dict]:
|
||||||
point = (pos["x"], pos["y"])
|
point = (pos["x"], pos["y"])
|
||||||
@@ -650,15 +680,18 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
|
|
||||||
hunger = max(0.0, (60.0 - my_health) / 60.0)
|
hunger = max(0.0, (60.0 - my_health) / 60.0)
|
||||||
|
|
||||||
|
# C1: use per-tile stack depth for accurate health simulation
|
||||||
|
hazard_stack = hazard_count.get(point, 1) if point in hazard_set else 1
|
||||||
# Simulated health after move
|
# Simulated health after move
|
||||||
health_after = 100 if ate_food else my_health - 1
|
health_after = 100 if ate_food else my_health - 1
|
||||||
|
# C2: food eaten on hazard tile — no hazard penalty (rules fidelity)
|
||||||
if point in hazard_set and not ate_food and point in previous_hazard_set:
|
if point in hazard_set and not ate_food and point in previous_hazard_set:
|
||||||
health_after -= hazard_damage
|
health_after -= hazard_damage * hazard_stack
|
||||||
|
|
||||||
# Fix #11: hazard corridor death check
|
# Fix #11: hazard corridor death check
|
||||||
hazard_will_kill = (
|
hazard_will_kill = (
|
||||||
not ate_food and point in hazard_set and point in previous_hazard_set
|
not ate_food and point in hazard_set and point in previous_hazard_set
|
||||||
and self._hazard_will_kill(point, hazard_set, blocked, width, height, my_health, hazard_damage)
|
and self._hazard_will_kill(point, hazard_set, hazard_count, blocked, width, height, my_health, hazard_damage)
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Score assembly ────────────────────────────────────────────────────────
|
# ── Score assembly ────────────────────────────────────────────────────────
|
||||||
@@ -719,7 +752,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
score -= 45.0
|
score -= 45.0
|
||||||
|
|
||||||
if point in hazard_set:
|
if point in hazard_set:
|
||||||
scale = max(0.5, hazard_damage / 14.0)
|
# C1: scale penalty by stack depth (Snail Mode stacked tiles are more dangerous)
|
||||||
|
scale = max(0.5, hazard_damage * hazard_stack / 14.0)
|
||||||
if not ate_food:
|
if not ate_food:
|
||||||
score -= (80.0 if my_health > 35 else 270.0) * scale
|
score -= (80.0 if my_health > 35 else 270.0) * scale
|
||||||
if hazard_will_kill:
|
if hazard_will_kill:
|
||||||
@@ -791,6 +825,16 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
|
|
||||||
# ── Move selector ─────────────────────────────────────────────────────────────
|
# ── Move selector ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _deterministic_fallback(self, safe_moves: MoveMap, width: int = 11, height: int = 11) -> str:
|
||||||
|
"""D3: Deterministic degrade ladder — last_move > center proximity > lexical order."""
|
||||||
|
if self.last_move and self.last_move in safe_moves:
|
||||||
|
return self.last_move
|
||||||
|
cx, cy = (width - 1) / 2.0, (height - 1) / 2.0
|
||||||
|
return min(
|
||||||
|
safe_moves,
|
||||||
|
key=lambda m: (abs(safe_moves[m]["x"] - cx) + abs(safe_moves[m]["y"] - cy), m),
|
||||||
|
)
|
||||||
|
|
||||||
def _select_best(
|
def _select_best(
|
||||||
self, scores: dict[str, float], safety: dict[str, dict],
|
self, scores: dict[str, float], safety: dict[str, dict],
|
||||||
safe_moves: MoveMap, considered: list[str],
|
safe_moves: MoveMap, considered: list[str],
|
||||||
@@ -799,7 +843,7 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
survivable = [m for m in considered if safety.get(m, {}).get("is_survivable", False)]
|
survivable = [m for m in considered if safety.get(m, {}).get("is_survivable", False)]
|
||||||
pool = survivable if survivable else (considered if considered else list(scores))
|
pool = survivable if survivable else (considered if considered else list(scores))
|
||||||
if not pool:
|
if not pool:
|
||||||
return random.choice(list(safe_moves))
|
return self._deterministic_fallback(safe_moves)
|
||||||
|
|
||||||
best_sc = max(scores.get(m, -1e9) for m in pool)
|
best_sc = max(scores.get(m, -1e9) for m in pool)
|
||||||
|
|
||||||
@@ -816,10 +860,13 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
|
|
||||||
def _minimax_sim(
|
def _minimax_sim(
|
||||||
self, my_body: list, enemy_body: list, food_set: set, hazard_set: set,
|
self, my_body: list, enemy_body: list, food_set: set, hazard_set: set,
|
||||||
my_health: int, enemy_health: int, hazard_damage: int,
|
my_health: int, enemy_health: int, hazard_damage: int, hazard_count: dict,
|
||||||
width: int, height: int, depth: int,
|
width: int, height: int, depth: int,
|
||||||
alpha: float, beta: float, deadline: float | None,
|
alpha: float, beta: float, deadline: float | None,
|
||||||
|
previous_hazard_set: set | None = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
|
# D1: spawn-immunity — effective prev set; None means treat all current hazards as old
|
||||||
|
eff_prev = previous_hazard_set if previous_hazard_set is not None else hazard_set
|
||||||
if depth <= 0 or self._time_exceeded(deadline):
|
if depth <= 0 or self._time_exceeded(deadline):
|
||||||
return self._minimax_eval(my_body, enemy_body, width, height)
|
return self._minimax_eval(my_body, enemy_body, width, height)
|
||||||
|
|
||||||
@@ -877,10 +924,13 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
|
|
||||||
nmy_h = 100 if my_ate else my_health - 1
|
nmy_h = 100 if my_ate else my_health - 1
|
||||||
nen_h = 100 if en_ate else enemy_health - 1
|
nen_h = 100 if en_ate else enemy_health - 1
|
||||||
if my_pt in hazard_set:
|
# C2: food consumed on hazard tile = no hazard penalty (rules fidelity)
|
||||||
nmy_h -= hazard_damage
|
# C1: scale damage by stack depth for Snail Mode accuracy
|
||||||
if en_pt in hazard_set:
|
# D1: spawn-immunity — only charge damage if hazard existed before this move
|
||||||
nen_h -= hazard_damage
|
if my_pt in hazard_set and not my_ate and my_pt in eff_prev:
|
||||||
|
nmy_h -= hazard_damage * hazard_count.get(my_pt, 1)
|
||||||
|
if en_pt in hazard_set and not en_ate and en_pt in eff_prev:
|
||||||
|
nen_h -= hazard_damage * hazard_count.get(en_pt, 1)
|
||||||
|
|
||||||
if nmy_h <= 0 and nen_h <= 0:
|
if nmy_h <= 0 and nen_h <= 0:
|
||||||
val = -500.0
|
val = -500.0
|
||||||
@@ -891,8 +941,9 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
else:
|
else:
|
||||||
val = self._minimax_sim(
|
val = self._minimax_sim(
|
||||||
new_my, new_en, food_set, hazard_set,
|
new_my, new_en, food_set, hazard_set,
|
||||||
nmy_h, nen_h, hazard_damage, width, height,
|
nmy_h, nen_h, hazard_damage, hazard_count, width, height,
|
||||||
depth - 1, alpha, beta, deadline,
|
depth - 1, alpha, beta, deadline,
|
||||||
|
previous_hazard_set=hazard_set, # D1: after this turn, current hazards are old
|
||||||
)
|
)
|
||||||
|
|
||||||
worst = min(worst, val)
|
worst = min(worst, val)
|
||||||
@@ -956,7 +1007,7 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
if depth <= 0 or self._time_exceeded(deadline):
|
if depth <= 0 or self._time_exceeded(deadline):
|
||||||
return 0.0
|
return 0.0
|
||||||
my_head = my_body[0]
|
my_head = my_body[0]
|
||||||
moves = self._legal_moves(my_head, my_body, other_snakes, food_set, is_constrictor, width, height)
|
moves = self._legal_moves(my_head, my_body, other_snakes, food_set, is_constrictor, width, height, enemy_can_grow)
|
||||||
if not moves:
|
if not moves:
|
||||||
return -5000.0 # no legal moves = dead
|
return -5000.0 # no legal moves = dead
|
||||||
|
|
||||||
@@ -1093,24 +1144,35 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
# ── Hazard multi-step check (fix #11) ────────────────────────────────────────
|
# ── Hazard multi-step check (fix #11) ────────────────────────────────────────
|
||||||
|
|
||||||
def _hazard_will_kill(
|
def _hazard_will_kill(
|
||||||
self, point: tuple, hazard_set: set, blocked: set,
|
self, point: tuple, hazard_set: set, hazard_count: dict, blocked: set,
|
||||||
width: int, height: int, health: int, hazard_damage: int,
|
width: int, height: int, health: int, hazard_damage: int,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Return True if entering this hazard cell leads to death before reaching a safe cell."""
|
"""Return True if entering this hazard cell leads to death before reaching a safe cell.
|
||||||
|
C3: each turn in hazard costs 1 (baseline) + hazard_damage * stack; exit costs 1.
|
||||||
|
D2: Dijkstra with per-tile stack cost replaces fixed entry_stack for entire corridor.
|
||||||
|
"""
|
||||||
if hazard_damage <= 0:
|
if hazard_damage <= 0:
|
||||||
return False
|
return False
|
||||||
queue: deque[tuple[tuple, int]] = deque([(point, 0)])
|
# D2: Dijkstra — accumulate damage per tile to find minimum-cost path to any non-hazard cell
|
||||||
seen = {point}
|
# Entry cost includes this turn's hazard damage for landing on point
|
||||||
while queue:
|
entry_cost = 1 + hazard_damage * hazard_count.get(point, 1)
|
||||||
pt, steps = queue.popleft()
|
heap: list[tuple[int, tuple]] = [(entry_cost, point)]
|
||||||
|
best: dict[tuple, int] = {point: entry_cost}
|
||||||
|
while heap:
|
||||||
|
cost, pt = heapq.heappop(heap)
|
||||||
|
if cost > best.get(pt, 10**9):
|
||||||
|
continue
|
||||||
if pt not in hazard_set:
|
if pt not in hazard_set:
|
||||||
# Found an exit: will we survive the journey?
|
return health - cost <= 0
|
||||||
return health - steps * hazard_damage <= 0
|
|
||||||
for n in self._neighbors(pt):
|
for n in self._neighbors(pt):
|
||||||
if n not in seen and self._in_bounds(n, width, height) and n not in blocked:
|
if not self._in_bounds(n, width, height) or n in blocked:
|
||||||
seen.add(n)
|
continue
|
||||||
queue.append((n, steps + 1))
|
step = (1 + hazard_damage * hazard_count.get(n, 1)) if n in hazard_set else 1
|
||||||
return True # no exit found = fatal
|
nc = cost + step
|
||||||
|
if nc < best.get(n, 10**9):
|
||||||
|
best[n] = nc
|
||||||
|
heapq.heappush(heap, (nc, n))
|
||||||
|
return True # no exit reachable = fatal
|
||||||
|
|
||||||
# ── Duel + constrictor helpers ────────────────────────────────────────────────
|
# ── Duel + constrictor helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1203,6 +1265,7 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
def _legal_moves(
|
def _legal_moves(
|
||||||
self, my_head: Coord, my_body: list, other_snakes: list,
|
self, my_head: Coord, my_body: list, other_snakes: list,
|
||||||
food_set: set, is_constrictor: bool, width: int, height: int,
|
food_set: set, is_constrictor: bool, width: int, height: int,
|
||||||
|
enemy_can_grow: dict | None = None,
|
||||||
) -> MoveMap:
|
) -> MoveMap:
|
||||||
occupied = {(s["x"], s["y"]) for s in my_body}
|
occupied = {(s["x"], s["y"]) for s in my_body}
|
||||||
for snake in other_snakes:
|
for snake in other_snakes:
|
||||||
@@ -1210,6 +1273,20 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
occupied.add((seg["x"], seg["y"]))
|
occupied.add((seg["x"], seg["y"]))
|
||||||
own_tail = (my_body[-1]["x"], my_body[-1]["y"])
|
own_tail = (my_body[-1]["x"], my_body[-1]["y"])
|
||||||
own_tail_stacked = self._is_tail_stacked(my_body)
|
own_tail_stacked = self._is_tail_stacked(my_body)
|
||||||
|
# C4: collect enemy tails that will vacate this turn (enemy won't grow, not stacked)
|
||||||
|
enemy_vacating_tails: set[tuple[int, int]] = set()
|
||||||
|
if not is_constrictor:
|
||||||
|
for snake in other_snakes:
|
||||||
|
if self._is_tail_stacked(snake["body"]):
|
||||||
|
continue
|
||||||
|
snake_id = snake.get("id")
|
||||||
|
can_grow: bool | None = None
|
||||||
|
if enemy_can_grow is not None and snake_id is not None:
|
||||||
|
can_grow = enemy_can_grow.get(snake_id)
|
||||||
|
if can_grow is None:
|
||||||
|
can_grow = self._enemy_can_grow_this_turn(snake, food_set)
|
||||||
|
if not can_grow:
|
||||||
|
enemy_vacating_tails.add((snake["body"][-1]["x"], snake["body"][-1]["y"]))
|
||||||
safe: MoveMap = {}
|
safe: MoveMap = {}
|
||||||
for move, (dx, dy) in self.DIRECTIONS.items():
|
for move, (dx, dy) in self.DIRECTIONS.items():
|
||||||
pt = (my_head["x"] + dx, my_head["y"] + dy)
|
pt = (my_head["x"] + dx, my_head["y"] + dy)
|
||||||
@@ -1217,6 +1294,8 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
continue
|
continue
|
||||||
ate = pt in food_set
|
ate = pt in food_set
|
||||||
can_step = self._can_step_on_own_tail(pt, own_tail, own_tail_stacked, ate, is_constrictor)
|
can_step = self._can_step_on_own_tail(pt, own_tail, own_tail_stacked, ate, is_constrictor)
|
||||||
|
if not can_step and pt in enemy_vacating_tails:
|
||||||
|
can_step = True
|
||||||
if pt in occupied and not can_step:
|
if pt in occupied and not can_step:
|
||||||
continue
|
continue
|
||||||
safe[move] = {"x": pt[0], "y": pt[1]}
|
safe[move] = {"x": pt[0], "y": pt[1]}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ SNAKE_REGISTRY = {
|
|||||||
"BetterMasterSnake": "1.3.0",
|
"BetterMasterSnake": "1.3.0",
|
||||||
"BestBattleSnake": "2.6.0",
|
"BestBattleSnake": "2.6.0",
|
||||||
"TrainedBattleSnake": "0.1.0",
|
"TrainedBattleSnake": "0.1.0",
|
||||||
"UltimateBattleSnake": "4.2.0",
|
"UltimateBattleSnake": "4.4.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
def build_snake(selected_snake: str):
|
def build_snake(selected_snake: str):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user