add types to function args

This commit is contained in:
2026-04-03 20:44:03 +02:00
parent d7bd89eae9
commit 6ab0161b49
+42 -37
View File
@@ -1,12 +1,17 @@
from collections.abc import Iterator
from collections import deque from collections import deque
from typing import Any, cast from typing import Any, cast
import random import random, os
import os
from snakes.TemplateSnake import TemplateSnake from snakes.TemplateSnake import TemplateSnake
class BestBattleSnake(TemplateSnake): class BestBattleSnake(TemplateSnake):
VERSION = "2.5.0" VERSION = "2.5.0"
Point = tuple[int, int]
Coord = dict[str, int]
SnakeState = dict[str, Any]
MoveMap = dict[str, Coord]
AttackMap = dict[Point, int]
DIRECTIONS = { DIRECTIONS = {
"up": (0, 1), "up": (0, 1),
@@ -32,7 +37,7 @@ class BestBattleSnake(TemplateSnake):
self.previous_hazards = set() self.previous_hazards = set()
self.duel_style = self._get_duel_style() self.duel_style = self._get_duel_style()
def _get_duel_style(self): def _get_duel_style(self) -> str:
"""Resolve duel tuning style from `BATTLE_SNAKE_DUEL_STYLE` or `DUEL_STYLE`.""" """Resolve duel tuning style from `BATTLE_SNAKE_DUEL_STYLE` or `DUEL_STYLE`."""
value = os.getenv("BATTLE_SNAKE_DUEL_STYLE") value = os.getenv("BATTLE_SNAKE_DUEL_STYLE")
if value is None: if value is None:
@@ -43,7 +48,7 @@ class BestBattleSnake(TemplateSnake):
return "balanced" return "balanced"
return style return style
def _duel_weights(self, style): def _duel_weights(self, style:str) -> dict[str, float]:
"""Return score multipliers for the selected duel style preset.""" """Return score multipliers for the selected duel style preset."""
if style == "safe": if style == "safe":
return { return {
@@ -63,7 +68,7 @@ class BestBattleSnake(TemplateSnake):
"food_bias": 1.00, "food_bias": 1.00,
} }
def choose_move(self, game_data): def choose_move(self, game_data:dict) -> str:
"""Pick the next move from a Battlesnake move request. """Pick the next move from a Battlesnake move request.
Docs: https://docs.battlesnake.com/api/example-move Docs: https://docs.battlesnake.com/api/example-move
@@ -185,8 +190,8 @@ class BestBattleSnake(TemplateSnake):
self.previous_hazards = set(hazard_set) self.previous_hazards = set(hazard_set)
return best_move return best_move
scores: dict[str, float] = {} scores:dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {} move_safety:dict[str, dict[str, Any]] = {}
for move, pos in safe_moves.items(): for move, pos in safe_moves.items():
point = (pos["x"], pos["y"]) point = (pos["x"], pos["y"])
ate_food = point in food_set ate_food = point in food_set
@@ -336,7 +341,7 @@ class BestBattleSnake(TemplateSnake):
self.previous_hazards = set(hazard_set) self.previous_hazards = set(hazard_set)
return best_move return best_move
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): def _choose_duel_move(self, safe_moves:MoveMap, my_body:list[Coord], my_len:int, my_health:int, food_set:set[Point], hazard_set:set[Point], other_snakes:list[SnakeState],enemy_attack_map:AttackMap, enemy_can_grow_cache:dict[Any, bool], previous_hazard_set:set[Point], hazard_damage:int, width:int, height:int) -> tuple[str, dict[str, float]]:
"""Score and select a move for one-vs-one games.""" """Score and select a move for one-vs-one games."""
duel_weights = self._duel_weights(self.duel_style) duel_weights = self._duel_weights(self.duel_style)
enemy = other_snakes[0] enemy = other_snakes[0]
@@ -344,8 +349,8 @@ class BestBattleSnake(TemplateSnake):
enemy_len = enemy.get("length", len(enemy["body"])) enemy_len = enemy.get("length", len(enemy["body"]))
encase_target_space = max(8, enemy_len * 2) encase_target_space = max(8, enemy_len * 2)
scores: dict[str, float] = {} scores:dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {} move_safety:dict[str, dict[str, Any]] = {}
for move, pos in safe_moves.items(): for move, pos in safe_moves.items():
point = (pos["x"], pos["y"]) point = (pos["x"], pos["y"])
@@ -507,10 +512,10 @@ class BestBattleSnake(TemplateSnake):
] ]
return random.choice(top_moves), scores return random.choice(top_moves), scores
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): def _choose_constrictor_move(self, safe_moves:MoveMap, my_body:list[Coord], my_len:int, other_snakes:list[SnakeState], food_set:set[Point], enemy_attack_map:AttackMap, enemy_heads:list[Point], enemy_can_grow_cache:dict[Any, bool], width:int, height:int) -> tuple[str, dict[str, float]]:
"""Score and select a move for constrictor games.""" """Score and select a move for constrictor games."""
scores: dict[str, float] = {} scores:dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {} move_safety:dict[str, dict[str, Any]] = {}
for move, pos in safe_moves.items(): for move, pos in safe_moves.items():
point = (pos["x"], pos["y"]) point = (pos["x"], pos["y"])
@@ -602,7 +607,7 @@ class BestBattleSnake(TemplateSnake):
] ]
return random.choice(top_moves), scores 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:Coord, my_body:list[Coord], other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, width:int, height:int) -> MoveMap:
"""Return legal immediate moves after body, wall, and tail checks.""" """Return legal immediate moves after body, wall, and tail checks."""
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"])
@@ -630,14 +635,14 @@ class BestBattleSnake(TemplateSnake):
return safe_moves return safe_moves
def _occupied_cells(self, my_body, other_snakes): def _occupied_cells(self, my_body:list[Coord], other_snakes:list[SnakeState]) -> set[Point]:
"""Build a set of occupied coordinates for all snake bodies.""" """Build a set of occupied coordinates for all snake bodies."""
occupied = {(segment["x"], segment["y"]) for segment in my_body} occupied = {(segment["x"], segment["y"]) for segment in my_body}
for snake in other_snakes: for snake in other_snakes:
occupied |= {(segment["x"], segment["y"]) for segment in snake["body"]} occupied |= {(segment["x"], segment["y"]) for segment in snake["body"]}
return occupied return occupied
def _simulation_blocked(self, future_body, other_snakes, food_set, is_constrictor, enemy_can_grow_cache=None): def _simulation_blocked(self, future_body:list[Coord], other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, enemy_can_grow_cache:dict[Any, bool]|None=None) -> set[Point]:
"""Build blocked cells for evaluating the board one turn ahead.""" """Build blocked cells for evaluating the board one turn ahead."""
blocked = {(segment["x"], segment["y"]) for segment in future_body} blocked = {(segment["x"], segment["y"]) for segment in future_body}
@@ -669,7 +674,7 @@ class BestBattleSnake(TemplateSnake):
return blocked return blocked
def _build_enemy_attack_map(self, my_snake, other_snakes, food_set, is_constrictor, width, height, enemy_can_grow_cache=None): def _build_enemy_attack_map(self, my_snake:SnakeState, other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, width:int, height:int, enemy_can_grow_cache:dict[Any, bool]|None=None) -> AttackMap:
"""Map cells enemies can contest next turn to their effective length.""" """Map cells enemies can contest next turn to their effective length."""
occupied = self._occupied_cells(my_snake["body"], other_snakes) occupied = self._occupied_cells(my_snake["body"], other_snakes)
my_body_points = {(segment["x"], segment["y"]) for segment in my_snake["body"]} my_body_points = {(segment["x"], segment["y"]) for segment in my_snake["body"]}
@@ -712,7 +717,7 @@ class BestBattleSnake(TemplateSnake):
return attack_map return attack_map
def _future_body(self, current_body, next_head, ate_food, is_constrictor): def _future_body(self, current_body:list[Coord], next_head:Coord, ate_food:bool, is_constrictor:bool) -> list[Coord]:
"""Simulate future body segments after a candidate move.""" """Simulate future body segments after a candidate move."""
next_body = [next_head] next_body = [next_head]
next_body.extend(current_body) next_body.extend(current_body)
@@ -723,7 +728,7 @@ class BestBattleSnake(TemplateSnake):
next_body.pop() next_body.pop()
return next_body 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:Point, own_tail:Point, own_tail_is_stacked:bool, ate_food:bool, is_constrictor:bool) -> bool:
"""Return whether stepping onto our tail is allowed this turn.""" """Return whether stepping onto our tail is allowed this turn."""
if is_constrictor: if is_constrictor:
return False return False
@@ -733,13 +738,13 @@ class BestBattleSnake(TemplateSnake):
return False return False
return point == own_tail return point == own_tail
def _is_tail_stacked(self, body): def _is_tail_stacked(self, body:list[Coord]) -> bool:
"""Check whether tail overlaps the previous body segment.""" """Check whether tail overlaps the previous body segment."""
if len(body) < 2: if len(body) < 2:
return False return False
return body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"] return body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"]
def _enemy_can_grow_this_turn(self, snake, food_set): def _enemy_can_grow_this_turn(self, snake:SnakeState, food_set:set[Point]) -> bool:
"""Return True if an enemy can eat food in one move.""" """Return True if an enemy can eat food in one move."""
head = snake["head"] head = snake["head"]
for dx, dy in self.DIRECTIONS.values(): for dx, dy in self.DIRECTIONS.values():
@@ -747,7 +752,7 @@ class BestBattleSnake(TemplateSnake):
return True return True
return False return False
def _hazard_damage_per_turn(self, game_data): def _hazard_damage_per_turn(self, game_data:dict) -> int:
"""Read royale hazard damage from ruleset settings. """Read royale hazard damage from ruleset settings.
Docs: https://docs.battlesnake.com/maps/royale Docs: https://docs.battlesnake.com/maps/royale
@@ -760,7 +765,7 @@ 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_is_active(self, point, ate_food, hazard_set, previous_hazard_set): 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.
Docs: https://docs.battlesnake.com/maps/royale Docs: https://docs.battlesnake.com/maps/royale
@@ -771,7 +776,7 @@ class BestBattleSnake(TemplateSnake):
return False return False
return point in previous_hazard_set return point in previous_hazard_set
def _nearest_food_distance(self, start, food_set, blocked, width, height): def _nearest_food_distance(self, start:Point, food_set:set[Point], blocked:set[Point], width:int, height:int) -> int|None:
"""Compute shortest reachable distance to any food using BFS.""" """Compute shortest reachable distance to any food using BFS."""
if not food_set: if not food_set:
return None return None
@@ -795,7 +800,7 @@ class BestBattleSnake(TemplateSnake):
return None return None
def _path_distance(self, start, goal, blocked, width, height): def _path_distance( self, start:Point, goal:Point, blocked:set[Point], width:int, height:int) -> int|None:
"""Compute shortest path distance between two cells.""" """Compute shortest path distance between two cells."""
queue = deque([(start, 0)]) queue = deque([(start, 0)])
seen = {start} seen = {start}
@@ -817,7 +822,7 @@ class BestBattleSnake(TemplateSnake):
return None return None
def _flood_fill_count(self, start, blocked, width, height): def _flood_fill_count(self, start:Point, blocked:set[Point], width:int, height:int) -> int:
"""Count reachable cells from `start` using flood fill.""" """Count reachable cells from `start` using flood fill."""
queue = deque([start]) queue = deque([start])
seen = {start} seen = {start}
@@ -836,7 +841,7 @@ class BestBattleSnake(TemplateSnake):
return len(seen) return len(seen)
def _open_neighbor_count(self, start, blocked, width, height): def _open_neighbor_count(self, start:Point, blocked:set[Point], width:int, height:int) -> int:
"""Count walkable orthogonal neighbors around `start`.""" """Count walkable orthogonal neighbors around `start`."""
count = 0 count = 0
for neighbor in self._neighbors(start): for neighbor in self._neighbors(start):
@@ -847,7 +852,7 @@ class BestBattleSnake(TemplateSnake):
count += 1 count += 1
return count return count
def _next_turn_option_count(self, future_body, blocked, width, height): def _next_turn_option_count(self, future_body:list[Coord], blocked:set[Point], width:int, height:int) -> int:
"""Estimate options available after the next simulated turn.""" """Estimate options available after the next simulated turn."""
if not future_body: if not future_body:
return 0 return 0
@@ -863,7 +868,7 @@ class BestBattleSnake(TemplateSnake):
count += 1 count += 1
return count return count
def _revisit_penalty(self, point): def _revisit_penalty(self, point:Point) -> float:
"""Return penalty for revisiting recent head positions.""" """Return penalty for revisiting recent head positions."""
if not self.recent_heads: if not self.recent_heads:
return 0.0 return 0.0
@@ -874,7 +879,7 @@ class BestBattleSnake(TemplateSnake):
penalty += max(0.0, 18.0 - index * 2.0) penalty += max(0.0, 18.0 - index * 2.0)
return penalty return penalty
def _territory_control_score(self, my_start, enemy_starts, blocked, width, height): def _territory_control_score(self, my_start:Point, enemy_starts:list[Point], blocked:set[Point], width:int, height:int) -> int:
"""Estimate territorial advantage versus enemy start positions.""" """Estimate territorial advantage versus enemy start positions."""
if not enemy_starts: if not enemy_starts:
return 0 return 0
@@ -910,10 +915,10 @@ class BestBattleSnake(TemplateSnake):
return score return score
def _distance_map(self, start, blocked, width, height): def _distance_map(self, start:Point, blocked:set[Point], width:int, height:int) -> dict[Point, int]:
"""Build a BFS distance map from the given start cell.""" """Build a BFS distance map from the given start cell."""
queue = deque([(start, 0)]) queue = deque([(start, 0)])
distances = {start: 0} distances = {start:0}
while queue: while queue:
point, distance = queue.popleft() point, distance = queue.popleft()
@@ -929,7 +934,7 @@ class BestBattleSnake(TemplateSnake):
return distances return distances
def _enemy_confinement_metrics(self, enemy_head, blocked, width, height): def _enemy_confinement_metrics(self, enemy_head:Point, blocked:set[Point], width:int, height:int) -> tuple[int, int]:
"""Return enemy reachable space and immediate exit count.""" """Return enemy reachable space and immediate exit count."""
enemy_blocked = set(blocked) enemy_blocked = set(blocked)
enemy_blocked.discard(enemy_head) enemy_blocked.discard(enemy_head)
@@ -939,20 +944,20 @@ class BestBattleSnake(TemplateSnake):
) )
return enemy_space, enemy_options return enemy_space, enemy_options
def _neighbors(self, point): def _neighbors(self, point:Point) -> Iterator[Point]:
"""Yield orthogonal neighbor coordinates for a point.""" """Yield orthogonal neighbor coordinates for a point."""
for dx, dy in self.DIRECTIONS.values(): for dx, dy in self.DIRECTIONS.values():
yield (point[0] + dx, point[1] + dy) yield (point[0] + dx, point[1] + dy)
def _manhattan(self, a, b): def _manhattan(self, a:Point, b:Point) -> int:
"""Return Manhattan distance between two points.""" """Return Manhattan distance between two points."""
return abs(a[0] - b[0]) + abs(a[1] - b[1]) return abs(a[0] - b[0]) + abs(a[1] - b[1])
def _in_bounds(self, point, width, height): def _in_bounds(self, point:Point, width:int, height:int) -> bool:
"""Return True when a point is inside board boundaries.""" """Return True when a point is inside board boundaries."""
return 0 <= point[0] < width and 0 <= point[1] < height return 0 <= point[0] < width and 0 <= point[1] < height
def _fallback_move(self, head, width, height): def _fallback_move(self, head:Coord, width:int, height:int) -> str:
"""Pick the first in-bounds move as emergency fallback.""" """Pick the first in-bounds move as emergency fallback."""
for move, (dx, dy) in self.DIRECTIONS.items(): for move, (dx, dy) in self.DIRECTIONS.items():
point = (head["x"] + dx, head["y"] + dy) point = (head["x"] + dx, head["y"] + dy)