update snakes with new code
Build and Push Docker Container / build-and-push (push) Successful in 1m21s

This commit is contained in:
2026-04-05 12:20:10 +02:00
parent 00d55b5419
commit 3cb3517892
4 changed files with 1260 additions and 84 deletions
+127 -48
View File
@@ -2,7 +2,7 @@ from collections.abc import Iterator
from collections import deque
from typing import Any, cast
from time import perf_counter
import random, os
import heapq, os
from snakes.TemplateSnake import TemplateSnake
from server.GameBoard import GameBoard
@@ -10,7 +10,7 @@ from server.dataset.RLBootstrapDataset import RLBootstrapDataset
class UltimateBattleSnake(TemplateSnake):
"""
UltimateBattleSnake v4.2.0
UltimateBattleSnake v4.4.0
All improvements over BestBattleSnake:
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 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.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]
Coord = dict[str, int]
SnakeState = dict[str, Any]
@@ -148,10 +156,20 @@ class UltimateBattleSnake(TemplateSnake):
foods = game_data.get_food()
hazards = game_data.get_hazard()
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}
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)
hazard_damage = self._hazard_damage_per_turn(game_data)
current_head_pt = (my_head["x"], my_head["y"])
@@ -187,6 +205,7 @@ class UltimateBattleSnake(TemplateSnake):
safe_moves = self._legal_moves(
my_head=my_head, my_body=my_body, other_snakes=other_snakes,
food_set=food_set, is_constrictor=is_constrictor, width=width, height=height,
enemy_can_grow=enemy_can_grow,
)
if not safe_moves:
@@ -207,7 +226,8 @@ class UltimateBattleSnake(TemplateSnake):
best_move, scores = self._choose_constrictor_move(
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,
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,
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
)
@@ -216,7 +236,8 @@ class UltimateBattleSnake(TemplateSnake):
best_move, scores = self._choose_duel_move(
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,
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,
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
)
@@ -225,7 +246,8 @@ class UltimateBattleSnake(TemplateSnake):
best_move, scores = self._choose_multi_move(
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,
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,
total_occupancy=total_occupancy, width=width, height=height, deadline=deadline,
)
@@ -251,8 +273,9 @@ class UltimateBattleSnake(TemplateSnake):
def _choose_multi_move(
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,
previous_hazard_set: set, enemy_attack_map: dict, enemy_can_grow: dict,
total_occupancy: float, width: int, height: int, deadline: float | None,
hazard_count: dict, previous_hazard_set: set, enemy_attack_map: dict,
enemy_can_grow: dict, total_occupancy: float, width: int, height: int,
deadline: float | None,
) -> tuple[str, dict[str, float]]:
scores: dict[str, float] = {}
safety: dict[str, dict] = {}
@@ -263,7 +286,8 @@ class UltimateBattleSnake(TemplateSnake):
sc, info = self._score_move(
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,
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,
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
width=width, height=height, deadline=deadline,
@@ -275,7 +299,7 @@ class UltimateBattleSnake(TemplateSnake):
safety[move] = info
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({
"turn": self.game_board.get_turn(), "mode": "multi",
"move": quick, "reason": "timeout_budget",
@@ -324,8 +348,9 @@ class UltimateBattleSnake(TemplateSnake):
def _choose_duel_move(
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,
previous_hazard_set: set, enemy_attack_map: dict, enemy_can_grow: dict,
total_occupancy: float, width: int, height: int, deadline: float | None,
hazard_count: dict, previous_hazard_set: set, enemy_attack_map: dict,
enemy_can_grow: dict, total_occupancy: float, width: int, height: int,
deadline: float | None,
) -> tuple[str, dict[str, float]]:
enemy = other_snakes[0]
enemy_len = enemy.get("length", len(enemy["body"]))
@@ -344,7 +369,8 @@ class UltimateBattleSnake(TemplateSnake):
sc, info = self._score_move(
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,
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,
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
width=width, height=height, deadline=deadline,
@@ -399,7 +425,7 @@ class UltimateBattleSnake(TemplateSnake):
safety[move] = info
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))
@@ -416,15 +442,17 @@ class UltimateBattleSnake(TemplateSnake):
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:
nmy_h -= hazard_damage
# C2: food eaten on hazard tile — no hazard penalty this turn (rules fidelity)
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(
my_body=fb, enemy_body=enemy["body"],
food_set=food_set, hazard_set=hazard_set,
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,
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
if mm_scores:
@@ -476,8 +504,9 @@ class UltimateBattleSnake(TemplateSnake):
def _choose_constrictor_move(
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,
previous_hazard_set: set, enemy_attack_map: dict, enemy_can_grow: dict,
total_occupancy: float, width: int, height: int, deadline: float | None,
hazard_count: dict, previous_hazard_set: set, enemy_attack_map: dict,
enemy_can_grow: dict, total_occupancy: float, width: int, height: int,
deadline: float | None,
) -> tuple[str, dict[str, float]]:
scores: dict[str, float] = {}
safety: dict[str, dict] = {}
@@ -488,7 +517,8 @@ class UltimateBattleSnake(TemplateSnake):
sc, info = self._score_move(
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,
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,
enemy_can_grow=enemy_can_grow, total_occupancy=total_occupancy,
width=width, height=height, deadline=deadline,
@@ -513,7 +543,7 @@ class UltimateBattleSnake(TemplateSnake):
safety[move] = info
if not scores:
return random.choice(list(safe_moves)), {}
return self._deterministic_fallback(safe_moves, width, height), {}
# F2: survival tree for constrictor mode
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(
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,
previous_hazard_set: set, is_constrictor: bool, enemy_attack_map: dict,
enemy_can_grow: dict, total_occupancy: float,
hazard_count: dict, previous_hazard_set: set, is_constrictor: bool,
enemy_attack_map: dict, enemy_can_grow: dict, total_occupancy: float,
width: int, height: int, deadline: float | None,
) -> tuple[float, dict]:
point = (pos["x"], pos["y"])
@@ -650,15 +680,18 @@ class UltimateBattleSnake(TemplateSnake):
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
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:
health_after -= hazard_damage
health_after -= hazard_damage * hazard_stack
# Fix #11: hazard corridor death check
hazard_will_kill = (
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 ────────────────────────────────────────────────────────
@@ -719,7 +752,8 @@ class UltimateBattleSnake(TemplateSnake):
score -= 45.0
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:
score -= (80.0 if my_health > 35 else 270.0) * scale
if hazard_will_kill:
@@ -791,6 +825,16 @@ class UltimateBattleSnake(TemplateSnake):
# ── 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(
self, scores: dict[str, float], safety: dict[str, dict],
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)]
pool = survivable if survivable else (considered if considered else list(scores))
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)
@@ -816,10 +860,13 @@ class UltimateBattleSnake(TemplateSnake):
def _minimax_sim(
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,
alpha: float, beta: float, deadline: float | None,
previous_hazard_set: set | None = None,
) -> 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):
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
nen_h = 100 if en_ate else enemy_health - 1
if my_pt in hazard_set:
nmy_h -= hazard_damage
if en_pt in hazard_set:
nen_h -= hazard_damage
# C2: food consumed on hazard tile = no hazard penalty (rules fidelity)
# C1: scale damage by stack depth for Snail Mode accuracy
# D1: spawn-immunity — only charge damage if hazard existed before this move
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:
val = -500.0
@@ -891,8 +941,9 @@ class UltimateBattleSnake(TemplateSnake):
else:
val = self._minimax_sim(
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,
previous_hazard_set=hazard_set, # D1: after this turn, current hazards are old
)
worst = min(worst, val)
@@ -956,7 +1007,7 @@ class UltimateBattleSnake(TemplateSnake):
if depth <= 0 or self._time_exceeded(deadline):
return 0.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:
return -5000.0 # no legal moves = dead
@@ -1093,24 +1144,35 @@ class UltimateBattleSnake(TemplateSnake):
# ── Hazard multi-step check (fix #11) ────────────────────────────────────────
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,
) -> 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:
return False
queue: deque[tuple[tuple, int]] = deque([(point, 0)])
seen = {point}
while queue:
pt, steps = queue.popleft()
# D2: Dijkstra — accumulate damage per tile to find minimum-cost path to any non-hazard cell
# Entry cost includes this turn's hazard damage for landing on point
entry_cost = 1 + hazard_damage * hazard_count.get(point, 1)
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:
# Found an exit: will we survive the journey?
return health - steps * hazard_damage <= 0
return health - cost <= 0
for n in self._neighbors(pt):
if n not in seen and self._in_bounds(n, width, height) and n not in blocked:
seen.add(n)
queue.append((n, steps + 1))
return True # no exit found = fatal
if not self._in_bounds(n, width, height) or n in blocked:
continue
step = (1 + hazard_damage * hazard_count.get(n, 1)) if n in hazard_set else 1
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 ────────────────────────────────────────────────
@@ -1203,6 +1265,7 @@ class UltimateBattleSnake(TemplateSnake):
def _legal_moves(
self, my_head: Coord, my_body: list, other_snakes: list,
food_set: set, is_constrictor: bool, width: int, height: int,
enemy_can_grow: dict | None = None,
) -> MoveMap:
occupied = {(s["x"], s["y"]) for s in my_body}
for snake in other_snakes:
@@ -1210,6 +1273,20 @@ class UltimateBattleSnake(TemplateSnake):
occupied.add((seg["x"], seg["y"]))
own_tail = (my_body[-1]["x"], my_body[-1]["y"])
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 = {}
for move, (dx, dy) in self.DIRECTIONS.items():
pt = (my_head["x"] + dx, my_head["y"] + dy)
@@ -1217,6 +1294,8 @@ class UltimateBattleSnake(TemplateSnake):
continue
ate = pt in food_set
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:
continue
safe[move] = {"x": pt[0], "y": pt[1]}