implement royale game mode and tighten spaces in duel mode

This commit is contained in:
2026-04-03 19:26:56 +02:00
parent a3fe386198
commit 013ac98821
5 changed files with 461 additions and 81 deletions
+115 -79
View File
@@ -26,6 +26,7 @@ class BestBattleSnake(TemplateSnake):
self.recent_heads = deque(maxlen=14)
self.last_move = None
self.last_game_id = None
self.previous_hazards = set()
self.duel_style = self._get_duel_style()
def _get_duel_style(self):
@@ -67,6 +68,7 @@ class BestBattleSnake(TemplateSnake):
if game_id != self.last_game_id or turn <= 1:
self.recent_heads.clear()
self.last_move = None
self.previous_hazards = set()
self.last_game_id = game_id
my_snake = cast(dict[str, Any], game_data.get_my_snake())
@@ -87,6 +89,16 @@ class BestBattleSnake(TemplateSnake):
food_set = {(food["x"], food["y"]) for food in foods}
hazard_set = {(hazard["x"], hazard["y"]) for hazard in hazards}
previous_hazard_set = set(self.previous_hazards)
hazard_damage = self._hazard_damage_per_turn(game_data)
enemy_heads = [
(snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes
]
enemy_can_grow_cache = {
snake["id"]: self._enemy_can_grow_this_turn(snake, food_set)
for snake in other_snakes
if "id" in snake
}
current_head_point = (my_head["x"], my_head["y"])
safe_moves = self._legal_moves(
@@ -110,6 +122,7 @@ class BestBattleSnake(TemplateSnake):
"reason": "no_safe_moves",
}
)
self.previous_hazards = set(hazard_set)
return fallback
enemy_attack_map = self._build_enemy_attack_map(
@@ -119,6 +132,7 @@ class BestBattleSnake(TemplateSnake):
is_constrictor=is_constrictor,
width=width,
height=height,
enemy_can_grow_cache=enemy_can_grow_cache,
)
if is_constrictor:
@@ -129,12 +143,15 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes,
food_set=food_set,
enemy_attack_map=enemy_attack_map,
enemy_heads=enemy_heads,
enemy_can_grow_cache=enemy_can_grow_cache,
width=width,
height=height,
)
self.recent_heads.append(current_head_point)
self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
self.previous_hazards = set(hazard_set)
return best_move
if len(other_snakes) == 1:
@@ -143,18 +160,20 @@ class BestBattleSnake(TemplateSnake):
my_body=my_body,
my_len=my_len,
my_health=my_health,
foods=foods,
food_set=food_set,
hazards=hazards,
hazard_set=hazard_set,
other_snakes=other_snakes,
enemy_attack_map=enemy_attack_map,
enemy_can_grow_cache=enemy_can_grow_cache,
previous_hazard_set=previous_hazard_set,
hazard_damage=hazard_damage,
width=width,
height=height,
)
self.recent_heads.append(current_head_point)
self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
self.previous_hazards = set(hazard_set)
return best_move
scores: dict[str, float] = {}
@@ -169,6 +188,7 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=is_constrictor,
enemy_can_grow_cache=enemy_can_grow_cache,
)
blocked.discard(point)
@@ -180,17 +200,13 @@ class BestBattleSnake(TemplateSnake):
)
territory = self._territory_control_score(
my_start=point,
enemy_starts=[
(snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes
],
enemy_starts=enemy_heads,
blocked=blocked,
width=width,
height=height,
)
nearest_food_dist = self._nearest_food_distance(
point, foods, blocked, width, height
)
nearest_food_dist = self._nearest_food_distance(point, food_set, blocked, width, height)
future_tail = future_body[-1]
tail_point = (future_tail["x"], future_tail["y"])
tail_dist = self._path_distance(
@@ -242,7 +258,9 @@ class BestBattleSnake(TemplateSnake):
score -= 40.0
if point in hazard_set:
score -= 70.0 if my_health > 35 else 250.0
hazard_scale = max(0.5, hazard_damage / 14.0)
if not ate_food:
score -= (70.0 if my_health > 35 else 250.0) * hazard_scale
score -= self._revisit_penalty(point)
@@ -256,8 +274,9 @@ class BestBattleSnake(TemplateSnake):
score -= 20.0
health_after_move = 100 if ate_food else my_health - 1
if point in hazard_set:
health_after_move -= 15
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:
score -= 10000.0
@@ -305,27 +324,15 @@ class BestBattleSnake(TemplateSnake):
self.recent_heads.append(current_head_point)
self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
self.previous_hazards = set(hazard_set)
return best_move
def _choose_duel_move(
self,
safe_moves,
my_body,
my_len,
my_health,
foods,
food_set,
hazards,
hazard_set,
other_snakes,
enemy_attack_map,
width,
height,
):
def _choose_duel_move(self, safe_moves, my_body, my_len, my_health, food_set, hazard_set, other_snakes, enemy_attack_map, enemy_can_grow_cache, previous_hazard_set, hazard_damage, width, height):
duel_weights = self._duel_weights(self.duel_style)
enemy = other_snakes[0]
enemy_head = (enemy["head"]["x"], enemy["head"]["y"])
enemy_len = enemy.get("length", len(enemy["body"]))
encase_target_space = max(8, enemy_len * 2)
scores: dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {}
@@ -342,6 +349,7 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=False,
enemy_can_grow_cache=enemy_can_grow_cache,
)
blocked.discard(point)
@@ -352,9 +360,7 @@ class BestBattleSnake(TemplateSnake):
future_body, blocked, width, height
)
nearest_food_dist = self._nearest_food_distance(
point, foods, blocked, width, height
)
nearest_food_dist = self._nearest_food_distance(point, food_set, blocked, width, height)
future_tail = future_body[-1]
tail_point = (future_tail["x"], future_tail["y"])
tail_dist = self._path_distance(
@@ -380,6 +386,7 @@ class BestBattleSnake(TemplateSnake):
enemy_attack_len is not None and enemy_attack_len >= my_len
)
direct_head_distance = self._manhattan(point, enemy_head)
enemy_space, enemy_options = self._enemy_confinement_metrics(enemy_head, blocked, width, height)
score = 0.0
score += reachable_space * 2.8
@@ -393,6 +400,15 @@ class BestBattleSnake(TemplateSnake):
if losing_head_to_head:
score -= 1500.0
is_safe_tightening_move = not likely_dead_end and not losing_head_to_head
if is_safe_tightening_move and enemy_space <= encase_target_space:
score += (encase_target_space - enemy_space) * 42.0
score += max(0, 3 - enemy_options) * 95.0
if reachable_space > enemy_space:
score += 120.0
if direct_head_distance <= 2:
score += 40.0
if my_len > enemy_len:
if direct_head_distance == 1:
score += 220.0 * duel_weights["head_pressure"]
@@ -420,7 +436,9 @@ class BestBattleSnake(TemplateSnake):
score -= 50.0
if point in hazard_set:
score -= 70.0 if my_health > 35 else 250.0
hazard_scale = max(0.5, hazard_damage / 14.0)
if not ate_food:
score -= (70.0 if my_health > 35 else 250.0) * hazard_scale
score -= self._revisit_penalty(point)
@@ -434,8 +452,9 @@ class BestBattleSnake(TemplateSnake):
score -= 20.0
health_after_move = 100 if ate_food else my_health - 1
if point in hazard_set:
health_after_move -= 15
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:
score -= 10000.0
@@ -478,17 +497,7 @@ class BestBattleSnake(TemplateSnake):
]
return random.choice(top_moves), scores
def _choose_constrictor_move(
self,
safe_moves,
my_body,
my_len,
other_snakes,
food_set,
enemy_attack_map,
width,
height,
):
def _choose_constrictor_move(self, safe_moves, my_body, my_len, other_snakes, food_set, enemy_attack_map, enemy_heads, enemy_can_grow_cache, width, height):
scores: dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {}
@@ -502,6 +511,7 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=True,
enemy_can_grow_cache=enemy_can_grow_cache,
)
blocked.discard(point)
@@ -513,9 +523,7 @@ class BestBattleSnake(TemplateSnake):
)
territory = self._territory_control_score(
my_start=point,
enemy_starts=[
(snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes
],
enemy_starts=enemy_heads,
blocked=blocked,
width=width,
height=height,
@@ -583,16 +591,14 @@ class BestBattleSnake(TemplateSnake):
]
return random.choice(top_moves), scores
def _legal_moves(
self, my_head, my_body, other_snakes, food_set, is_constrictor, width, height
):
def _legal_moves(self, my_head, my_body, other_snakes, food_set, is_constrictor, width, height):
occupied = self._occupied_cells(my_body, other_snakes)
own_tail = (my_body[-1]["x"], my_body[-1]["y"])
own_tail_stacked = self._is_tail_stacked(my_body)
safe_moves = {}
for move, pos in self.get_possible_moves(my_head).items():
point = (pos["x"], pos["y"])
for move, (dx, dy) in self.DIRECTIONS.items():
point = (my_head["x"] + dx, my_head["y"] + dy)
if not self._in_bounds(point, width, height):
continue
@@ -608,7 +614,7 @@ class BestBattleSnake(TemplateSnake):
if point in occupied and not can_step_on_tail:
continue
safe_moves[move] = pos
safe_moves[move] = {"x": point[0], "y": point[1]}
return safe_moves
@@ -618,7 +624,7 @@ class BestBattleSnake(TemplateSnake):
occupied |= {(segment["x"], segment["y"]) for segment in snake["body"]}
return occupied
def _simulation_blocked(self, future_body, other_snakes, food_set, is_constrictor):
def _simulation_blocked(self, future_body, other_snakes, food_set, is_constrictor, enemy_can_grow_cache=None):
blocked = {(segment["x"], segment["y"]) for segment in future_body}
if not is_constrictor and not self._is_tail_stacked(future_body):
@@ -632,7 +638,13 @@ class BestBattleSnake(TemplateSnake):
if is_constrictor:
continue
if self._enemy_can_grow_this_turn(snake, food_set):
snake_id = snake.get("id")
enemy_can_grow = (
enemy_can_grow_cache.get(snake_id)
if enemy_can_grow_cache and snake_id is not None
else self._enemy_can_grow_this_turn(snake, food_set)
)
if enemy_can_grow:
continue
if self._is_tail_stacked(snake["body"]):
@@ -643,20 +655,25 @@ class BestBattleSnake(TemplateSnake):
return blocked
def _build_enemy_attack_map(
self, my_snake, other_snakes, food_set, is_constrictor, width, height
):
def _build_enemy_attack_map(self, my_snake, other_snakes, food_set, is_constrictor, width, height, enemy_can_grow_cache=None):
occupied = self._occupied_cells(my_snake["body"], other_snakes)
my_id = my_snake["id"]
my_body_points = {(segment["x"], segment["y"]) for segment in my_snake["body"]}
attack_map = {}
for enemy in other_snakes:
enemy_len = enemy.get("length", len(enemy["body"]))
enemy_tail = (enemy["body"][-1]["x"], enemy["body"][-1]["y"])
enemy_tail_stacked = self._is_tail_stacked(enemy["body"])
snake_id = enemy.get("id")
enemy_can_grow = (
enemy_can_grow_cache.get(snake_id)
if enemy_can_grow_cache and snake_id is not None
else self._enemy_can_grow_this_turn(enemy, food_set)
)
for pos in self.get_possible_moves(enemy["head"]).values():
point = (pos["x"], pos["y"])
enemy_head = enemy["head"]
for dx, dy in self.DIRECTIONS.values():
point = (enemy_head["x"] + dx, enemy_head["y"] + dy)
if not self._in_bounds(point, width, height):
continue
@@ -664,18 +681,14 @@ class BestBattleSnake(TemplateSnake):
not is_constrictor
and point == enemy_tail
and not enemy_tail_stacked
and not self._enemy_can_grow_this_turn(enemy, food_set)
and not enemy_can_grow
)
if point in occupied and not can_step_on_enemy_tail:
continue
# Do not consider impossible overlap directly into my own occupied body except head swap possibilities.
if point in {
(segment["x"], segment["y"])
for segment in my_snake["body"]
if my_snake["id"] == my_id
}:
if point in my_body_points:
continue
previous = attack_map.get(point)
@@ -694,9 +707,7 @@ class BestBattleSnake(TemplateSnake):
next_body.pop()
return next_body
def _can_step_on_own_tail(
self, point, own_tail, own_tail_is_stacked, ate_food, is_constrictor
):
def _can_step_on_own_tail(self, point, own_tail, own_tail_is_stacked, ate_food, is_constrictor):
if is_constrictor:
return False
if ate_food:
@@ -717,17 +728,31 @@ class BestBattleSnake(TemplateSnake):
return True
return False
def _nearest_food_distance(self, start, foods, blocked, width, height):
if not foods:
return None
def _hazard_damage_per_turn(self, game_data):
ruleset = {}
if hasattr(game_data, "get_ruleset"):
ruleset = game_data.get_ruleset() or {}
elif hasattr(game_data, "ruleset"):
ruleset = game_data.ruleset or {}
settings = ruleset.get("settings", {}) if isinstance(ruleset, dict) else {}
return int(settings.get("hazardDamagePerTurn", 15))
targets = {(food["x"], food["y"]) for food in foods}
def _hazard_is_active(self, point, ate_food, hazard_set, previous_hazard_set):
if point not in hazard_set:
return False
if ate_food:
return False
return point in previous_hazard_set
def _nearest_food_distance(self, start, food_set, blocked, width, height):
if not food_set:
return None
queue = deque([(start, 0)])
seen = {start}
while queue:
point, distance = queue.popleft()
if point in targets:
if point in food_set:
return distance
for neighbor in self._neighbors(point):
@@ -735,7 +760,7 @@ class BestBattleSnake(TemplateSnake):
continue
if not self._in_bounds(neighbor, width, height):
continue
if neighbor in blocked and neighbor not in targets:
if neighbor in blocked and neighbor not in food_set:
continue
seen.add(neighbor)
queue.append((neighbor, distance + 1))
@@ -797,8 +822,8 @@ class BestBattleSnake(TemplateSnake):
next_head = future_body[0]
count = 0
for pos in self.get_possible_moves(next_head).values():
point = (pos["x"], pos["y"])
for dx, dy in self.DIRECTIONS.values():
point = (next_head["x"] + dx, next_head["y"] + dy)
if not self._in_bounds(point, width, height):
continue
if point in blocked:
@@ -807,6 +832,8 @@ class BestBattleSnake(TemplateSnake):
return count
def _revisit_penalty(self, point):
if not self.recent_heads:
return 0.0
penalty = 0.0
for index, old_point in enumerate(reversed(self.recent_heads), start=1):
if old_point != point:
@@ -867,6 +894,15 @@ class BestBattleSnake(TemplateSnake):
return distances
def _enemy_confinement_metrics(self, enemy_head, blocked, width, height):
enemy_blocked = set(blocked)
enemy_blocked.discard(enemy_head)
enemy_space = self._flood_fill_count(enemy_head, enemy_blocked, width, height)
enemy_options = self._open_neighbor_count(
enemy_head, enemy_blocked, width, height
)
return enemy_space, enemy_options
def _neighbors(self, point):
for dx, dy in self.DIRECTIONS.values():
yield (point[0] + dx, point[1] + dy)
@@ -878,8 +914,8 @@ class BestBattleSnake(TemplateSnake):
return 0 <= point[0] < width and 0 <= point[1] < height
def _fallback_move(self, head, width, height):
for move, pos in self.get_possible_moves(head).items():
point = (pos["x"], pos["y"])
for move, (dx, dy) in self.DIRECTIONS.items():
point = (head["x"] + dx, head["y"] + dy)
if self._in_bounds(point, width, height):
return move
return "up"