update UltimateBattleSnake to 4.5.0
This commit is contained in:
@@ -10,7 +10,7 @@ from server.dataset.RLBootstrapDataset import RLBootstrapDataset
|
||||
|
||||
class UltimateBattleSnake(TemplateSnake):
|
||||
"""
|
||||
UltimateBattleSnake v4.4.0
|
||||
UltimateBattleSnake v4.5.0
|
||||
|
||||
All improvements over BestBattleSnake:
|
||||
v3: #1+#9 Simultaneous minimax (both snakes move at once) with hazard/health tracking
|
||||
@@ -50,9 +50,12 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
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)
|
||||
v4.5 E1 _enemy_can_grow_this_turn: health-urgency heuristic + occupied-set check (food blocked = no eat)
|
||||
v4.5 E2 _flood_fill_count: per-turn frozenset-keyed transposition cache; resets each turn
|
||||
v4.5 E3 Snail-specific trail scoring: adjacent hazard density + stack-risk penalty via self._is_snail
|
||||
"""
|
||||
|
||||
VERSION = "4.4.0"
|
||||
VERSION = "4.5.0"
|
||||
Point = tuple[int, int]
|
||||
Coord = dict[str, int]
|
||||
SnakeState = dict[str, Any]
|
||||
@@ -84,6 +87,10 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
self._enemy_dmaps: list[dict] = []
|
||||
self._enemy_heads: list[tuple[int, int]] = []
|
||||
self._base_blocked: set[tuple[int, int]] = set()
|
||||
self._is_snail: bool = False
|
||||
# E2: per-turn transposition cache for flood-fill (reset each turn)
|
||||
self._bfs_cache: dict[tuple, int] = {}
|
||||
self._bfs_cache_turn: int = -1
|
||||
# Config
|
||||
self._planning_depth = max(1, min(4, self._env_int("BATTLE_FUTURE_PLANNING_DEPTH", 2)))
|
||||
self._planning_branch = max(1, min(3, self._env_int("BATTLE_FUTURE_PLANNING_BRANCH", 2)))
|
||||
@@ -161,6 +168,12 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
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"
|
||||
self._is_snail = is_snail # E3: store for use in _score_move
|
||||
|
||||
# E2: reset per-turn BFS transposition cache
|
||||
if turn != self._bfs_cache_turn:
|
||||
self._bfs_cache = {}
|
||||
self._bfs_cache_turn = turn
|
||||
|
||||
food_set: set[tuple[int, int]] = {(f["x"], f["y"]) for f in foods}
|
||||
# C1: track hazard stack depth (Snail Mode can stack multiple hazards on one tile)
|
||||
@@ -178,8 +191,13 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
total_body_cells = len(my_body) + sum(len(s["body"]) for s in other_snakes)
|
||||
total_occupancy = total_body_cells / board_area
|
||||
|
||||
# E1: build occupied set once so _enemy_can_grow_this_turn can skip food tiles under bodies
|
||||
all_occupied: set[tuple[int, int]] = {(s["x"], s["y"]) for s in my_body}
|
||||
for _s in other_snakes:
|
||||
for _seg in _s["body"]:
|
||||
all_occupied.add((_seg["x"], _seg["y"]))
|
||||
enemy_can_grow = {
|
||||
s["id"]: self._enemy_can_grow_this_turn(s, food_set)
|
||||
s["id"]: self._enemy_can_grow_this_turn(s, food_set, all_occupied)
|
||||
for s in other_snakes if "id" in s
|
||||
}
|
||||
|
||||
@@ -759,6 +777,25 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
if hazard_will_kill:
|
||||
score -= 10000.0
|
||||
|
||||
# E3: Snail Mode trail scoring
|
||||
# Penalise moves that place us in hazard-dense neighbourhoods (future stack risk),
|
||||
# reward moves toward hazard-free space (safer continuation).
|
||||
if self._is_snail and hazard_set:
|
||||
adjacent_hazard_stack = sum(
|
||||
hazard_count.get(n, 1)
|
||||
for n in self._neighbors(point)
|
||||
if n in hazard_set
|
||||
)
|
||||
# Each unit of adjacent total stack costs health faster next turn
|
||||
if adjacent_hazard_stack > 0:
|
||||
score -= adjacent_hazard_stack * 6.0
|
||||
# Bonus for having hazard-free neighbours (escape routes)
|
||||
hazard_free_neighbors = sum(
|
||||
1 for n in self._neighbors(point)
|
||||
if self._in_bounds(n, width, height) and n not in hazard_set and n not in blocked
|
||||
)
|
||||
score += hazard_free_neighbors * 8.0
|
||||
|
||||
# F12: territory call REMOVED from here — callers (_choose_*_move) apply it after _score_move
|
||||
|
||||
score -= self._revisit_penalty(point)
|
||||
@@ -1385,11 +1422,26 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
def _is_tail_stacked(self, body: list) -> bool:
|
||||
return len(body) >= 2 and body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"]
|
||||
|
||||
def _enemy_can_grow_this_turn(self, snake: dict, food_set: set) -> bool:
|
||||
def _enemy_can_grow_this_turn(self, snake:dict, food_set:set, all_occupied:set|None=None) -> bool:
|
||||
"""E1: Estimate if enemy will eat food this turn (tail won't vacate).
|
||||
- Hungry enemies (health < 40) always assumed to eat accessible adjacent food.
|
||||
- Healthy enemies assumed to eat unless the food tile is blocked by a body segment.
|
||||
- all_occupied: full set of body tiles; food under a body can't be eaten this turn.
|
||||
"""
|
||||
head = snake["head"]
|
||||
health = snake.get("health", 100)
|
||||
for dx, dy in self.DIRECTIONS.values():
|
||||
if (head["x"] + dx, head["y"] + dy) in food_set:
|
||||
pt = (head["x"] + dx, head["y"] + dy)
|
||||
if pt not in food_set:
|
||||
continue
|
||||
# Food blocked by a body segment: snake can't step there, so tail will still vacate
|
||||
if all_occupied is not None and pt in all_occupied:
|
||||
continue
|
||||
# Hungry snakes (health < 40) will eat regardless of other factors
|
||||
if health < 40:
|
||||
return True
|
||||
# Healthy snake with accessible adjacent food: conservative assumption → will eat
|
||||
return True
|
||||
return False
|
||||
|
||||
def _hazard_damage_per_turn(self, game_data: GameBoard) -> int:
|
||||
@@ -1400,6 +1452,11 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
# ── Pathfinding primitives ────────────────────────────────────────────────────
|
||||
|
||||
def _flood_fill_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||
# E2: transposition cache — frozenset key deduplicates identical blocked sets across branches
|
||||
cache_key = (start, frozenset(blocked))
|
||||
cached = self._bfs_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
queue = deque([start])
|
||||
seen = {start}
|
||||
while queue:
|
||||
@@ -1408,7 +1465,9 @@ class UltimateBattleSnake(TemplateSnake):
|
||||
if n not in seen and self._in_bounds(n, width, height) and n not in blocked:
|
||||
seen.add(n)
|
||||
queue.append(n)
|
||||
return len(seen)
|
||||
result = len(seen)
|
||||
self._bfs_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
def _open_neighbor_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||
return sum(
|
||||
|
||||
Reference in New Issue
Block a user