feat(snake): modularize engine and add tournament tools
- Split active strategies, reusable engine code, core classes, and legacy snakes. - Replace implicit snake imports with explicit module registrations. - Extract Prism duel, spatial, and survival behavior into focused mixins. - Improve duel scoring with food races, pressure, caches, and depth metrics. - Add deterministic arena scenarios and paired seeded engine tournaments. - Expand benchmark telemetry and bump Prism to version 1.3.0. - Update documentation and tests for the new package layout and tooling.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,224 @@
|
||||
from snakes.core.template import TemplateSnake
|
||||
from server.GameBoard import GameBoard
|
||||
from collections import deque
|
||||
|
||||
class BetterMasterSnake(TemplateSnake):
|
||||
VERSION = "1.3.0"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "BetterMasterSnake"
|
||||
self.version = self.VERSION
|
||||
# Definiere die möglichen Bewegungsrichtungen
|
||||
self.min_safe_area = 2
|
||||
|
||||
def choose_move(self, game_data:GameBoard):
|
||||
self.game_board = game_data
|
||||
self.calculations = []
|
||||
self.eat_the_snake_overwrite = False
|
||||
|
||||
self.safe_positions = self.find_safe_positions(add_to_calculations=True)
|
||||
if self.eat_the_snake_overwrite:
|
||||
return self.overwrite_eat_the_other_snake(game_data.get_turn())
|
||||
|
||||
if game_data.get_type() == "constrictor":
|
||||
move = self.selected_move_constrictor()
|
||||
else:
|
||||
move = self.selected_move_standard()
|
||||
|
||||
self.add_to_history({"turn": game_data.get_turn(), "data": self.calculations})
|
||||
return move if move else "up"
|
||||
|
||||
def overwrite_eat_the_other_snake(self, turn:int):
|
||||
self.add_calculations({"function": "eat_the_snake_overwrite", "my_head": self.game_board.get_my_snake_head(), "move": self.kill_the_snake, "safe_positions": self.safe_positions})
|
||||
self.add_to_history({"turn": turn, "data": self.calculations})
|
||||
return self.kill_the_snake
|
||||
|
||||
#TODO: How to Fill the Gameboard best?
|
||||
def selected_move_constrictor(self):
|
||||
move = self.move_close_to_body()
|
||||
self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})
|
||||
move = self.ensure_escape_route(move)
|
||||
self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
|
||||
return move
|
||||
|
||||
def selected_move_standard(self, move=None):
|
||||
# Finde den besten Weg zur Nahrung
|
||||
path_to_food = self.find_path_to_food()
|
||||
if path_to_food:
|
||||
move = self.move_towards(path_to_food[0])
|
||||
self.add_calculations({"function": "move_towards", "my_head": self.game_board.get_my_snake_head(), "path_to_food": path_to_food, "move": move})
|
||||
|
||||
if not move or self.would_eating_the_food_kill_the_snake(move):
|
||||
move = self.move_close_to_body(move_close_to_tail=True)
|
||||
self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})
|
||||
|
||||
# Überprfe, ob der Zug einen Ausweg lässt
|
||||
move = self.ensure_escape_route(move)
|
||||
self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
|
||||
return move
|
||||
|
||||
def find_path_to_food(self):
|
||||
# Exclude own snake's body from obstacles
|
||||
obstacles = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
|
||||
|
||||
for snake in self.game_board.get_other_snakes():
|
||||
for part in snake['body']:
|
||||
obstacles.add((part['x'], part['y']))
|
||||
|
||||
other_snakes_other_snake_posible_moves_set = {(d['x'], d['y']) for d in self.other_snake_posible_moves}
|
||||
removed_elements_set = set([(elem['x'], elem['y']) for elem in self.game_board.get_food() if (elem['x'], elem['y']) in other_snakes_other_snake_posible_moves_set])
|
||||
obstacles |= removed_elements_set
|
||||
|
||||
self.food_positions = [elem for elem in self.game_board.get_food() if (elem['x'], elem['y']) not in other_snakes_other_snake_posible_moves_set]
|
||||
|
||||
if len(self.food_positions) > 0:
|
||||
# Choose the closest food source based on the heuristic
|
||||
closest_food = min(self.food_positions, key=lambda food: abs(food['x'] - self.game_board.get_my_snake_head()['x']) + abs(food['y'] - self.game_board.get_my_snake_head()['y']))
|
||||
self.set_target_food(closest_food)
|
||||
|
||||
# Use A* to search for a safe path
|
||||
return self.a_star_search(self.game_board.get_my_snake_head(), closest_food, obstacles)
|
||||
return None
|
||||
|
||||
def find_path_to_tail(self):
|
||||
# Exclude other snake's body from obstacles
|
||||
obstacles = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
|
||||
for snake in self.game_board.get_other_snakes():
|
||||
for part in snake['body']:
|
||||
obstacles.add((part['x'], part['y']))
|
||||
|
||||
my_snake_tail = {"x": self.game_board.get_my_snake_tail()['x'], "y": self.game_board.get_my_snake_tail()['y']}
|
||||
|
||||
# Use A* to search for a safe path
|
||||
path = self.a_star_search(self.game_board.get_my_snake_head(), my_snake_tail, obstacles)
|
||||
return path
|
||||
|
||||
def move_towards(self, target):
|
||||
best_direction = None
|
||||
min_distance = float('inf')
|
||||
for direction, coords in self.safe_positions.items():
|
||||
distance = abs(target['x'] - coords['x']) + abs(target['y'] - coords['y'])
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
best_direction = direction
|
||||
|
||||
return best_direction if best_direction else "up"
|
||||
|
||||
def move_close_to_body(self, move_close_to_tail=False):
|
||||
# Heuristik, um Positionen nahe dem eigenen Körper zu bevorzugen
|
||||
body_positions = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
|
||||
tail_position = (self.game_board.get_my_snake_tail()['x'], self.game_board.get_my_snake_tail()['y'])
|
||||
|
||||
best_move = None
|
||||
max_distance = -1 # Initialize maximum distance
|
||||
for direction, pos in self.safe_positions.items():
|
||||
next_position = (pos['x'], pos['y'])
|
||||
if next_position in self.safe_positions:
|
||||
# Berechne die Distanz zum eigenen Körper
|
||||
distance_to_body = min(abs(next_position[0] - part[0]) + abs(next_position[1] - part[1]) for part in body_positions)
|
||||
# Berechne die Distanz zum eigenen Schwanz
|
||||
distance_to_tail = abs(next_position[0] - tail_position[0]) + abs(next_position[1] - tail_position[1])
|
||||
# Wähle die maximale Distanz (Körper oder Schwanz)
|
||||
if move_close_to_tail:
|
||||
distance = min(next_position, distance_to_tail)
|
||||
else:
|
||||
distance = max(next_position, distance_to_body)
|
||||
# Update max_distance if a larger distance is found
|
||||
if distance > max_distance:
|
||||
max_distance = distance
|
||||
best_move = direction
|
||||
return best_move if best_move else "up" # Standardbewegung, falls keine bessere gefunden wird
|
||||
|
||||
#TODO: Neat to Implement Function to check if eating the food would kill the snake?
|
||||
def would_eating_the_food_kill_the_snake(self, move:str):
|
||||
return False
|
||||
|
||||
def ensure_escape_route(self, move:str):
|
||||
try:
|
||||
future_position = self.safe_positions[move]
|
||||
except KeyError:
|
||||
for move, pos in self.safe_positions.items():
|
||||
if self.is_near_tail(pos, (self.game_board.get_my_snake_tail()['x'], self.game_board.get_my_snake_tail()['y'])):
|
||||
self.add_calculations({"function": "ensure_escape_route", "move": move, "is_near_tail": True})
|
||||
move = self.move_towards(pos)
|
||||
return move
|
||||
else:
|
||||
path_to_tail = self.find_path_to_tail()
|
||||
if path_to_tail:
|
||||
self.add_calculations({"function": "move_towards", "my_head": self.game_board.get_my_snake_head(), "path_to_tail": path_to_tail, "move": move})
|
||||
move = self.move_towards(path_to_tail[0])
|
||||
|
||||
self.add_calculations({"function": "ensure_escape_route", "move": move, "KeyError": "Snake Coild itself up"})
|
||||
#return move
|
||||
|
||||
# TODO: Fix - Snake Neat to find the best way - Close to the Tail and maybe fill most free cells as posible
|
||||
return move
|
||||
|
||||
def is_near_tail(self, position, tail):
|
||||
return abs(position["x"] - tail[0]) + abs(position["y"] - tail[1]) <= 2
|
||||
|
||||
def a_star_search(self, start, goal, obstacles):
|
||||
# Helper functions
|
||||
def is_position_safe(position):
|
||||
return 0 <= position['x'] < self.game_board.get_width() and 0 <= position['y'] < self.game_board.get_height() and (position['x'], position['y']) not in obstacles
|
||||
|
||||
def get_neighbors(position):
|
||||
neighbors = []
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # links, rechts, oben, unten
|
||||
neighbor = {'x': position['x'] + dx, 'y': position['y'] + dy}
|
||||
if is_position_safe(neighbor):
|
||||
neighbors.append(neighbor)
|
||||
return neighbors
|
||||
|
||||
def heuristic(position, goal):
|
||||
# Verwenden Sie eine Heuristik, die immer positiv ist, selbst wenn das Ziel in der Nähe ist
|
||||
return max(abs(position['x'] - goal['x']), abs(position['y'] - goal['y']))
|
||||
|
||||
# Überprüfen, ob das Ziel direkt neben dem Startpunkt liegt
|
||||
if start == goal or (abs(start['x'] - goal['x']) <= 1 and abs(start['y'] - goal['y']) <= 1):
|
||||
# Wenn das Ziel neben dem Startpunkt liegt, ist der Pfad das Ziel selbst
|
||||
return [goal]
|
||||
|
||||
# Initialize the open and closed list
|
||||
open_set = set([(start['x'], start['y'])])
|
||||
came_from = {}
|
||||
g_score = {(start['x'], start['y']): 0}
|
||||
f_score = {(start['x'], start['y']): heuristic(start, goal)}
|
||||
|
||||
while open_set:
|
||||
current = min(open_set, key=lambda pos: f_score.get(pos, float('inf')))
|
||||
current_dict = {'x': current[0], 'y': current[1]}
|
||||
if current_dict == goal:
|
||||
# Reconstruct the path
|
||||
path = []
|
||||
while current in came_from:
|
||||
current = came_from[current]
|
||||
path.append({'x': current[0], 'y': current[1]})
|
||||
path.reverse()
|
||||
if path and path[0] == start:
|
||||
path.pop(0) # Entferne das erste Element, wenn es dem Start entspricht
|
||||
return path # Return the path as a list of dicts
|
||||
|
||||
open_set.remove(current)
|
||||
for neighbor in get_neighbors(current_dict):
|
||||
neighbor_tuple = (neighbor['x'], neighbor['y'])
|
||||
tentative_g_score = g_score[current] + 1 # Distance between neighbors is always 1
|
||||
if tentative_g_score < g_score.get(neighbor_tuple, float('inf')):
|
||||
came_from[neighbor_tuple] = current
|
||||
g_score[neighbor_tuple] = tentative_g_score
|
||||
f_score[neighbor_tuple] = g_score[neighbor_tuple] + heuristic(neighbor, goal)
|
||||
if neighbor_tuple not in open_set:
|
||||
open_set.add(neighbor_tuple)
|
||||
|
||||
return None # Kein Pfad gefunden
|
||||
|
||||
def find_direction(self):
|
||||
# Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
|
||||
for direction, pos in self.safe_positions.items():
|
||||
next_position = (pos['x'], pos['y'])
|
||||
# Konvertiere safe_positions in eine Liste von Tupeln für den Vergleich
|
||||
safe_positions_tuples = [(pos['x'], pos['y']) for pos in self.safe_positions.values()]
|
||||
if next_position in safe_positions_tuples:
|
||||
return direction
|
||||
return "up" # Standardbewegung, falls keine sichere Position gefunden wird
|
||||
@@ -0,0 +1,57 @@
|
||||
from snakes.core.template import TemplateSnake
|
||||
|
||||
import random
|
||||
|
||||
class DummSnake(TemplateSnake):
|
||||
VERSION = "1.0.0"
|
||||
|
||||
def choose_move(self, data: dict) -> str:
|
||||
is_move_safe = {"up": True, "down": True, "left": True, "right": True}
|
||||
|
||||
# We've included code to prevent your Battlesnake from moving backwards
|
||||
my_head = data["you"]["body"][0] # Coordinates of your head
|
||||
my_neck = data["you"]["body"][1] # Coordinates of your "neck"
|
||||
|
||||
if my_neck["x"] < my_head["x"]: # Neck is left of head, don't move left
|
||||
is_move_safe["left"] = False
|
||||
|
||||
elif my_neck["x"] > my_head["x"]: # Neck is right of head, don't move right
|
||||
is_move_safe["right"] = False
|
||||
|
||||
elif my_neck["y"] < my_head["y"]: # Neck is below head, don't move down
|
||||
is_move_safe["down"] = False
|
||||
|
||||
elif my_neck["y"] > my_head["y"]: # Neck is above head, don't move up
|
||||
is_move_safe["up"] = False
|
||||
|
||||
# TODO: Step 1 - Prevent your Battlesnake from moving out of bounds
|
||||
# board_width = game_state['board']['width']
|
||||
# board_height = game_state['board']['height']
|
||||
|
||||
# TODO: Step 2 - Prevent your Battlesnake from colliding with itself
|
||||
# my_body = game_state['you']['body']
|
||||
|
||||
# TODO: Step 3 - Prevent your Battlesnake from colliding with other Battlesnakes
|
||||
# opponents = game_state['board']['snakes']
|
||||
|
||||
# Are there any safe moves left?
|
||||
safe_moves = []
|
||||
for move, isSafe in is_move_safe.items():
|
||||
if isSafe:
|
||||
safe_moves.append(move)
|
||||
|
||||
if len(safe_moves) == 0:
|
||||
print(f"MOVE {data['turn']}: No safe moves detected! Moving down")
|
||||
self.add_to_history({"my_head": my_head, "my_neck": my_neck, "move": move, "safe_moves": safe_moves, "is_move_safe": is_move_safe})
|
||||
return {"move": "down"}
|
||||
|
||||
# Choose a random move from the safe ones
|
||||
move = random.choice(safe_moves)
|
||||
|
||||
# TODO: Step 4 - Move towards food instead of random, to regain health and survive longer
|
||||
# food = game_state['board']['food']
|
||||
|
||||
self.add_to_history({"my_head": my_head, "my_neck": my_neck, "move": move, "safe_moves": safe_moves, "is_move_safe": is_move_safe})
|
||||
print(f"{data['game']['id']} MOVE {data['turn']}: {move} picked from all valid options in {is_move_safe}")
|
||||
|
||||
return move
|
||||
@@ -0,0 +1,148 @@
|
||||
from snakes.core.template import TemplateSnake
|
||||
|
||||
import random
|
||||
from scipy import spatial
|
||||
|
||||
class LogicSnake(TemplateSnake):
|
||||
VERSION = "1.1.0"
|
||||
|
||||
def avoid_my_body(self, my_body, possible_moves: dict) -> list:
|
||||
"""
|
||||
my_body: List of dictionaries of x/y coordinates for every segment of a Battlesnake.
|
||||
e.g. [ {"x": 0, "y": 0}, {"x": 1, "y": 0}, {"x": 2, "y": 0} ]
|
||||
possible_moves: List of strings. Moves to pick from.
|
||||
e.g. ["up", "down", "left", "right"]
|
||||
|
||||
return: The list of remaining possible_moves, with the 'neck' direction removed
|
||||
"""
|
||||
remove = []
|
||||
for direction, location in possible_moves.items():
|
||||
if location in my_body:
|
||||
remove.append(direction)
|
||||
|
||||
for direction in remove:
|
||||
del possible_moves[direction]
|
||||
|
||||
return possible_moves
|
||||
|
||||
def avoid_walls(self, board_width: int, board_height: int, possible_moves: dict):
|
||||
remove = []
|
||||
for direction, location in possible_moves.items():
|
||||
x_out_range = (location["x"] < 0 or location["x"] == board_width)
|
||||
y_out_range = (location["y"] < 0 or location["y"] == board_height)
|
||||
if x_out_range or y_out_range:
|
||||
remove.append(direction)
|
||||
|
||||
for direction in remove:
|
||||
del possible_moves[direction]
|
||||
|
||||
return possible_moves
|
||||
|
||||
def avoid_snakes(self, snakes: list, possible_moves: dict):
|
||||
remove = []
|
||||
for snake in snakes:
|
||||
for direction, location in possible_moves.items():
|
||||
if location in snake["body"]:
|
||||
remove.append(direction)
|
||||
|
||||
remove = set(remove)
|
||||
for direction in remove:
|
||||
del possible_moves[direction]
|
||||
|
||||
return possible_moves
|
||||
|
||||
def get_rarget_close(self, foods: list, my_head: dict):
|
||||
coordinates = []
|
||||
|
||||
if len(foods) == 0:
|
||||
return None
|
||||
|
||||
for food in foods:
|
||||
coordinates.append((food["x"], food["y"]))
|
||||
|
||||
tree = spatial.KDTree(coordinates)
|
||||
results = tree.query([(my_head["x"], my_head["y"])])[1]
|
||||
|
||||
return foods[results[0]]
|
||||
|
||||
def move_target(self, possible_moves: list, my_head: dict, target:dict):
|
||||
distance_x = abs(my_head["x"] - target["x"])
|
||||
distance_y = abs(my_head["y"] - target["y"])
|
||||
|
||||
for direction, location in possible_moves.items():
|
||||
new_distance_x = abs(location["x"] - target["x"])
|
||||
new_distance_y = abs(location["y"] - target["y"])
|
||||
|
||||
if new_distance_x < distance_x or new_distance_y < distance_y:
|
||||
return direction
|
||||
|
||||
return list(possible_moves.keys())[0]
|
||||
|
||||
def choose_move(self, data: dict) -> str:
|
||||
"""
|
||||
data: Dictionary of all Game Board data as received from the Battlesnake Engine.
|
||||
For a full example of 'data', see https://docs.battlesnake.com/references/api/sample-move-request
|
||||
|
||||
return: A String, the single move to make. One of "up", "down", "left" or "right".
|
||||
|
||||
Use the information in 'data' to decide your next move. The 'data' variable can be interacted
|
||||
with as a Python Dictionary, and contains all of the information about the Battlesnake board
|
||||
for each move of the game.
|
||||
|
||||
"""
|
||||
my_head = data["you"]["head"] # A dictionary of x/y coordinates like {"x": 0, "y": 0}
|
||||
my_body = data["you"]["body"] # A list of x/y coordinate dictionaries like [ {"x": 0, "y": 0}, {"x": 1, "y": 0}, {"x": 2, "y": 0} ]
|
||||
board_height = data["board"]["height"]
|
||||
board_width = data["board"]["width"]
|
||||
snakes = data["board"]["snakes"]
|
||||
foods = data["board"]["food"]
|
||||
|
||||
# TODO: uncomment the lines below so you can see what this data looks like in your output!
|
||||
# print(f"~~~ Turn: {data['turn']} Game Mode: {data['game']['ruleset']['name']} ~~~")
|
||||
# print(f"All board data this turn: {data}")
|
||||
# print(f"My Battlesnakes head this turn is: {my_head}")
|
||||
# print(f"My Battlesnakes body this turn is: {my_body}")
|
||||
|
||||
#possible_moves = ["up", "down", "left", "right"]
|
||||
|
||||
possible_moves = {
|
||||
"up": {
|
||||
"x": my_head["x"],
|
||||
"y": my_head["y"] + 1
|
||||
},
|
||||
"down": {
|
||||
"x": my_head["x"],
|
||||
"y": my_head["y"] - 1
|
||||
},
|
||||
"left": {
|
||||
"x": my_head["x"] - 1,
|
||||
"y": my_head["y"]
|
||||
},
|
||||
"right": {
|
||||
"x": my_head["x"] + 1,
|
||||
"y": my_head["y"]
|
||||
}
|
||||
}
|
||||
|
||||
# Don't allow your Battlesnake to move back in on it's own neck
|
||||
possible_moves = self.avoid_my_body(my_body, possible_moves)
|
||||
possible_moves = self.avoid_walls(board_width, board_height, possible_moves)
|
||||
possible_moves = self.avoid_snakes(snakes, possible_moves)
|
||||
|
||||
target = self.get_rarget_close(foods, my_head)
|
||||
|
||||
# TODO: Explore new strategies for picking a move that are better than random
|
||||
if len(possible_moves) > 0:
|
||||
if target is not None:
|
||||
move = self.move_target(possible_moves, my_head, target)
|
||||
else:
|
||||
possible_moves = list(possible_moves.keys())
|
||||
move = random.choice(possible_moves)
|
||||
else:
|
||||
move = "up"
|
||||
print("GOING TO LOSE!!")
|
||||
|
||||
self.add_to_history({"my_head": my_head, "my_body": tuple(my_body), "target": target, "possible_moves": possible_moves, "move": move})
|
||||
print(f"{data['game']['id']} MOVE {data['turn']}: {move} picked from all valid options in {possible_moves}")
|
||||
|
||||
return move
|
||||
@@ -0,0 +1,249 @@
|
||||
from snakes.core.template import TemplateSnake
|
||||
|
||||
class MasterSnake(TemplateSnake):
|
||||
VERSION = "1.2.0"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "MasterSnake"
|
||||
self.version = self.VERSION
|
||||
self.disabled_find_near_by_food = True
|
||||
|
||||
def is_food_nearby(self, head, food_positions):
|
||||
for food in food_positions:
|
||||
if abs(head['x'] - food['x']) <= 1 and abs(head['y'] - food['y']) <= 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def avoid_snake_body(self, snakes, board_width, board_height):
|
||||
# Konvertiere die Körperpositionen der Schlangen in ein Set von Tupeln für schnellen Zugriff
|
||||
body_positions = set()
|
||||
for snake in snakes:
|
||||
for part in snake['body']:
|
||||
body_positions.add((part['x'], part['y']))
|
||||
|
||||
# Implementiere die Logik, um Positionen zu finden, die nicht von Schlangenkörpern belegt sind
|
||||
safe_positions = self.find_safe_positions(body_positions, board_width, board_height)
|
||||
return safe_positions
|
||||
|
||||
def find_safe_positions(self, body_positions, board_width, board_height):
|
||||
# Finde sichere Positionen basierend auf den Körperpositionen und der Größe des Spielbretts
|
||||
safe_positions = []
|
||||
for x in range(board_width): # Nutze die tatsächliche Breite des Spielbretts
|
||||
for y in range(board_height): # Nutze die tatsächliche Höhe des Spielbretts
|
||||
if (x, y) not in body_positions:
|
||||
safe_positions.append({'x': x, 'y': y})
|
||||
return safe_positions
|
||||
|
||||
def choose_move(self, game_data):
|
||||
board_width = game_data['board']['width']
|
||||
board_height = game_data['board']['height']
|
||||
snakes = game_data['board']['snakes']
|
||||
my_snake = game_data['you']
|
||||
my_head = my_snake['head']
|
||||
|
||||
# Vermeide Schlangenkörper
|
||||
safe_positions = self.avoid_snake_body(snakes, board_width, board_height)
|
||||
|
||||
# Finde die nächstgelegene Nahrungsquelle, wenn Nahrung vorhanden ist
|
||||
try:
|
||||
if self.is_food_nearby(my_head, game_data['board']['food']) or self.disabled_find_near_by_food:
|
||||
path_to_food = self.find_path_to_food(game_data)
|
||||
if path_to_food:
|
||||
# Implementiere Logik, um in Richtung der Nahrungsquelle zu bewegen, falls sicher
|
||||
move = self.move_towards(my_head, path_to_food[0], safe_positions)
|
||||
self.add_to_history({"my_head": my_head, "path_to_food": path_to_food, "move": move})
|
||||
else:
|
||||
# Einfache Logik, um eine Bewegungsrichtung zu wählen, wenn keine Nahrung vorhanden ist
|
||||
move = self.find_direction(my_head, safe_positions)
|
||||
self.add_to_history({"my_head": my_head, "move": move})
|
||||
else:
|
||||
# Wenn keine Nahrung in der Nähe ist, bewege dich in eine Richtung, die dich nahe an deinem eigenen Körper hält
|
||||
move = self.find_direction(my_head, safe_positions)
|
||||
self.add_to_history({"my_head": my_head, "move": move})
|
||||
except ValueError:
|
||||
move = self.find_direction(my_head, safe_positions)
|
||||
self.add_to_history({"my_head": my_head, "move": move})
|
||||
|
||||
# Finde den größten sicheren Bereich
|
||||
max_area_start, max_area = self.flood_fill(my_head, safe_positions)
|
||||
# Wenn der Schwanz der Schlange im größten sicheren Bereich liegt, bewege dich in Richtung des Schwanzes
|
||||
my_tail = (my_snake['body'][-1]['x'], my_snake['body'][-1]['y']) # Convert to tuple
|
||||
if my_tail in max_area:
|
||||
move = self.move_towards(my_head, my_tail, safe_positions)
|
||||
|
||||
# Überprüfe zukünftige Bewegungen, um Sackgassen zu vermeiden
|
||||
move = self.avoid_dead_ends(my_head, move, safe_positions, snakes)
|
||||
self.add_to_history({"my_head": my_head, "move": move})
|
||||
|
||||
return move
|
||||
|
||||
def move_towards(self, head, target, safe_positions):
|
||||
directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
|
||||
best_direction = None
|
||||
min_distance = float('inf')
|
||||
min_distance_to_body = float('inf')
|
||||
body_positions = set((pos['x'], pos['y']) for pos in safe_positions[:-1]) # Exclude the head from body positions
|
||||
|
||||
for direction, (dx, dy) in directions.items():
|
||||
next_position = {'x': head['x'] + dx, 'y': head['y'] + dy}
|
||||
if next_position in safe_positions:
|
||||
distance = abs(target[0] - next_position['x']) + abs(target[1] - next_position['y'])
|
||||
distance_to_body = sum(abs(part[0] - next_position['x']) + abs(part[1] - next_position['y']) for part in body_positions)
|
||||
if distance < min_distance or (distance == min_distance and distance_to_body < min_distance_to_body):
|
||||
best_direction = direction
|
||||
min_distance = distance
|
||||
min_distance_to_body = distance_to_body
|
||||
|
||||
return best_direction if best_direction else "up" # Default to moving up if no safe direction found
|
||||
|
||||
def find_path_to_food(self, game_data):
|
||||
my_head = game_data['you']['head']
|
||||
food_positions = game_data['board']['food']
|
||||
snakes = game_data['board']['snakes']
|
||||
board_width = game_data['board']['width']
|
||||
board_height = game_data['board']['height']
|
||||
|
||||
# Exclude own snake's body from obstacles
|
||||
own_snake_body = game_data['you']['body']
|
||||
obstacles = set((part['x'], part['y']) for part in own_snake_body)
|
||||
|
||||
for snake in snakes:
|
||||
if snake['id'] != game_data['you']['id']:
|
||||
for part in snake['body']:
|
||||
obstacles.add((part['x'], part['y']))
|
||||
|
||||
# Choose the closest food source based on the heuristic
|
||||
closest_food = min(food_positions, key=lambda food: abs(food['x'] - my_head['x']) + abs(food['y'] - my_head['y']))
|
||||
|
||||
# Use A* to search for a safe path
|
||||
path = self.a_star_search(my_head, closest_food, obstacles, board_width, board_height)
|
||||
return path
|
||||
|
||||
def a_star_search(self, start, goal, obstacles, board_width, board_height):
|
||||
# Convert snake positions into a set of obstacles
|
||||
# Helper functions
|
||||
def is_position_safe(position):
|
||||
x, y = position
|
||||
return 0 <= x < board_width and 0 <= y < board_height and position not in obstacles
|
||||
|
||||
def get_neighbors(position):
|
||||
x, y = position
|
||||
return [(nx, ny) for nx, ny in [(x-1, y), (x+1, y), (x, y-1), (x, y+1)] if is_position_safe((nx, ny))]
|
||||
|
||||
def heuristic(position, goal):
|
||||
return abs(position[0] - goal[0]) + abs(position[1] - goal[1])
|
||||
|
||||
# Initialize start and goal positions
|
||||
start = (start['x'], start['y'])
|
||||
goal = (goal['x'], goal['y'])
|
||||
|
||||
# Initialize the open and closed list
|
||||
open_set = set([start])
|
||||
came_from = {}
|
||||
g_score = {start: 0}
|
||||
f_score = {start: heuristic(start, goal)}
|
||||
|
||||
while open_set:
|
||||
current = min(open_set, key=lambda pos: f_score.get(pos, float('inf')))
|
||||
if current == goal:
|
||||
# Reconstruct the path
|
||||
path = []
|
||||
while current in came_from:
|
||||
path.append(current)
|
||||
current = came_from[current]
|
||||
path.reverse()
|
||||
return path # Return the path as a list of tuples
|
||||
|
||||
open_set.remove(current)
|
||||
for neighbor in get_neighbors(current):
|
||||
tentative_g_score = g_score[current] + 1 # Distance between neighbors is always 1
|
||||
if tentative_g_score < g_score.get(neighbor, float('inf')):
|
||||
came_from[neighbor] = current
|
||||
g_score[neighbor] = tentative_g_score
|
||||
f_score[neighbor] = g_score[neighbor] + heuristic(neighbor, goal)
|
||||
if neighbor not in open_set:
|
||||
open_set.add(neighbor)
|
||||
|
||||
return None # Kein Pfad gefunden
|
||||
|
||||
def find_direction(self, head, safe_positions):
|
||||
# Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
|
||||
directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
|
||||
for direction, (dx, dy) in directions.items():
|
||||
next_position = {'x': head['x'] + dx, 'y': head['y'] + dy}
|
||||
if next_position in safe_positions:
|
||||
return direction
|
||||
return "up" # Standardbewegung, falls keine sichere Position gefunden wird
|
||||
|
||||
def avoid_self_collision(self, future_head, body_positions):
|
||||
# Überprüft, ob die zukünftige Kopfposition im Körper der Schlange liegt
|
||||
return (future_head['x'], future_head['y']) not in body_positions
|
||||
|
||||
def avoid_dead_ends(self, head, move, safe_positions, snakes):
|
||||
directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
|
||||
dx, dy = directions[move]
|
||||
future_head = {'x': head['x'] + dx, 'y': head['y'] + dy}
|
||||
body_positions = set((part['x'], part['y']) for part in snakes[0]['body'])
|
||||
|
||||
if not self.is_future_move_safe(future_head, safe_positions, snakes) or not self.avoid_self_collision(future_head, body_positions):
|
||||
for alternative_move in directions.keys():
|
||||
dx, dy = directions[alternative_move]
|
||||
alternative_future_head = {'x': head['x'] + dx, 'y': head['y'] + dy}
|
||||
if self.is_future_move_safe(alternative_future_head, safe_positions, snakes) and self.avoid_self_collision(alternative_future_head, body_positions):
|
||||
return alternative_move
|
||||
return move
|
||||
|
||||
def simulate_snake_movement(self, snakes):
|
||||
future_body_positions = set()
|
||||
for snake in snakes:
|
||||
# Beachte, dass dies nur ein Beispiel ist und angepasst werden muss, um deine spezifische Spiellogik zu berücksichtigen
|
||||
for part in snake['body'][:-1]: # Ignoriere den letzten Teil des Körpers, da er sich bewegt
|
||||
future_body_positions.add((part['x'], part['y']))
|
||||
return future_body_positions
|
||||
|
||||
def is_future_move_safe(self, future_head, safe_positions, snakes):
|
||||
# Simuliere die Bewegung der Schlange und aktualisiere die Positionen des eigenen Körpers
|
||||
future_body_positions = self.simulate_snake_movement(snakes)
|
||||
# Konvertiere safe_positions in ein Set von Tupeln für den Flood Fill Algorithmus
|
||||
safe_positions_set = set((pos['x'], pos['y']) for pos in safe_positions)
|
||||
# Entferne die zukünftigen Körperpositionen aus den sicheren Positionen
|
||||
safe_positions_set = safe_positions_set - future_body_positions
|
||||
# Füge die zukünftige Kopfposition hinzu, um sie als Startpunkt zu verwenden
|
||||
safe_positions_set.add((future_head['x'], future_head['y']))
|
||||
# Berechne die Anzahl der erreichbaren sicheren Positionen von der zukünftigen Kopfposition aus
|
||||
reachable_positions = self.flood_fill((future_head['x'], future_head['y']), safe_positions_set)
|
||||
# Entscheide, ob die Bewegung sicher ist, basierend auf der Anzahl der erreichbaren Positionen
|
||||
|
||||
fill_bool = len(reachable_positions) > len(safe_positions_set) * 0.25
|
||||
if fill_bool:
|
||||
return fill_bool
|
||||
|
||||
return len(safe_positions_set) >= len(snakes[0]['body'])
|
||||
|
||||
def flood_fill(self, start, safe_positions):
|
||||
stack = [start]
|
||||
visited = set()
|
||||
max_area = 0
|
||||
max_area_start = None
|
||||
|
||||
while stack:
|
||||
position = stack.pop()
|
||||
if isinstance(position, dict):
|
||||
position = tuple(position.values())
|
||||
else:
|
||||
position = tuple(position)
|
||||
|
||||
if position not in visited:
|
||||
visited.add(position)
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # links, rechts, oben, unten
|
||||
next_position = tuple([position[0] + dx, position[1] + dy])
|
||||
if next_position in safe_positions:
|
||||
stack.append(next_position)
|
||||
|
||||
# Überprüfe, ob der aktuelle Bereich größer ist als der bisher größte Bereich
|
||||
if len(visited) > max_area:
|
||||
max_area = len(visited)
|
||||
max_area_start = position
|
||||
|
||||
return max_area_start, visited
|
||||
@@ -0,0 +1,513 @@
|
||||
"""SupremeBattleSnake v1.0.0
|
||||
|
||||
Built on ApexBattleSnake v1.0.0. All strategic logic is inherited.
|
||||
Performance improvement: all spatial primitives (flood fill, territory,
|
||||
articulation detection, distance maps, pathfinding) replaced by a
|
||||
bitboard engine that uses integer arithmetic instead of Python sets/deques.
|
||||
|
||||
Key speedups:
|
||||
S1: Bitboard flood fill — replaces BFS deque+set with integer bit-expansion.
|
||||
~60× faster per call, eliminates _neighbors() generator overhead.
|
||||
S2: Bitboard territory — dual-BFS expansion on ints replaces per-cell
|
||||
distance-map comparison loop.
|
||||
S3: Bitboard articulation — partition sizes via bit-flood instead of
|
||||
_bounded_bfs with sets.
|
||||
S4: Bitboard distance map — BFS via bit-expansion + bit-extract.
|
||||
S5: Bitboard path distance — early-exit BFS on ints.
|
||||
S6: Bitboard nearest food — BFS food search on ints.
|
||||
S7: Per-turn BitBoard instance cached for board dimensions.
|
||||
S8: Blocked-set → bitboard conversion cached within a turn to avoid
|
||||
redundant O(n) conversions for the same frozen set.
|
||||
S9: Survival-tree uses bitboards natively — enemy body/attack bits
|
||||
precomputed once at tree root, no per-node set/dict rebuilds.
|
||||
S10: _legal_moves override uses bitboard neighbour mask instead of
|
||||
per-direction Python loop + _in_bounds calls.
|
||||
S11: _future_survival_tree inlines legal-move check with bitboard ops.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from time import perf_counter
|
||||
|
||||
from snakes.strategies.apex import ApexBattleSnake
|
||||
from snakes.engine.bitboard import BitBoard
|
||||
from server.GameBoard import GameBoard
|
||||
|
||||
# Direction offsets for coord-dict → tuple conversion
|
||||
_DIR_DELTAS = ((0, 1), (0, -1), (-1, 0), (1, 0))
|
||||
_DIR_NAMES = ("up", "down", "left", "right")
|
||||
|
||||
class SupremeBattleSnake_ClaudeOpus4_6(ApexBattleSnake):
|
||||
VERSION = "1.0.0"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.name = "SupremeBattleSnake"
|
||||
self.version = self.VERSION
|
||||
|
||||
# S7: cached BitBoard instance (reused while board dimensions stay the same)
|
||||
self._bb: BitBoard | None = None
|
||||
self._bb_w: int = 0
|
||||
self._bb_h: int = 0
|
||||
|
||||
# S8: per-turn frozenset → bitboard conversion cache
|
||||
self._bits_cache: dict[int, int] = {}
|
||||
self._bits_cache_turn: int = -1
|
||||
|
||||
# S9: precomputed enemy state for survival tree (set per turn in choose_move)
|
||||
self._enemy_body_bits: int = 0 # all enemy body cells as bitboard
|
||||
self._enemy_tail_bits: int = 0 # enemy tails that will vacate
|
||||
self._enemy_attack_danger: int = 0 # tiles where enemy len >= our len
|
||||
self._enemy_attack_opportunity: int = 0 # tiles where enemy len < our len
|
||||
|
||||
# ── BitBoard accessor ────────────────────────────────────────────────────
|
||||
|
||||
def _get_bb(self, width: int, height: int) -> BitBoard:
|
||||
"""Return (possibly cached) BitBoard for the current dimensions."""
|
||||
if self._bb is None or width != self._bb_w or height != self._bb_h:
|
||||
self._bb = BitBoard(width, height)
|
||||
self._bb_w = width
|
||||
self._bb_h = height
|
||||
return self._bb
|
||||
|
||||
def _blocked_to_bits(self, blocked: set[tuple[int, int]], width: int, height: int) -> int:
|
||||
"""Convert a blocked set to a bitboard, with per-turn caching."""
|
||||
bb = self._get_bb(width, height)
|
||||
sid = id(blocked)
|
||||
cached = self._bits_cache.get(sid)
|
||||
if cached is not None:
|
||||
return cached
|
||||
bits = bb.set_to_bits(blocked)
|
||||
self._bits_cache[sid] = bits
|
||||
return bits
|
||||
|
||||
# ── choose_move override: reset caches + precompute enemy bits ───────────
|
||||
|
||||
def choose_move(self, game_data: GameBoard) -> str:
|
||||
turn = game_data.get_turn()
|
||||
if turn != self._bits_cache_turn:
|
||||
self._bits_cache = {}
|
||||
self._bits_cache_turn = turn
|
||||
|
||||
bb = self._get_bb(game_data.get_width(), game_data.get_height())
|
||||
|
||||
# S9: precompute enemy body / tail / attack bitboards for survival tree
|
||||
other_snakes = game_data.get_other_snakes()
|
||||
my_snake = game_data.get_my_snake()
|
||||
my_len = my_snake.get("length", len(my_snake["body"]))
|
||||
food_set = {(f["x"], f["y"]) for f in game_data.get_food()}
|
||||
game_type = game_data.get_type()
|
||||
is_constrictor = game_type == "constrictor"
|
||||
w = bb.width
|
||||
|
||||
enemy_body_bits = 0
|
||||
enemy_tail_bits = 0
|
||||
enemy_attack_danger = 0
|
||||
enemy_attack_opportunity = 0
|
||||
|
||||
for snake in other_snakes:
|
||||
for seg in snake["body"]:
|
||||
enemy_body_bits |= 1 << (seg["y"] * w + seg["x"])
|
||||
body = snake["body"]
|
||||
# Check if tail will vacate
|
||||
if not is_constrictor and len(body) >= 2:
|
||||
tail_stacked = (body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"])
|
||||
if not tail_stacked:
|
||||
can_grow = self._enemy_can_grow_this_turn(snake, food_set)
|
||||
if not can_grow:
|
||||
enemy_tail_bits |= 1 << (body[-1]["y"] * w + body[-1]["x"])
|
||||
|
||||
# Attack map: tiles enemy head can reach in 1 move
|
||||
eh = snake["head"]
|
||||
e_len = snake.get("length", len(body))
|
||||
ehx, ehy = eh["x"], eh["y"]
|
||||
for dx, dy in _DIR_DELTAS:
|
||||
nx, ny = ehx + dx, ehy + dy
|
||||
if 0 <= nx < w and 0 <= ny < bb.height:
|
||||
bit = 1 << (ny * w + nx)
|
||||
if e_len >= my_len:
|
||||
enemy_attack_danger |= bit
|
||||
else:
|
||||
enemy_attack_opportunity |= bit
|
||||
|
||||
self._enemy_body_bits = enemy_body_bits
|
||||
self._enemy_tail_bits = enemy_tail_bits
|
||||
self._enemy_attack_danger = enemy_attack_danger
|
||||
self._enemy_attack_opportunity = enemy_attack_opportunity
|
||||
|
||||
return super().choose_move(game_data)
|
||||
|
||||
# ── S1: Bitboard flood fill ──────────────────────────────────────────────
|
||||
|
||||
def _flood_fill_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
|
||||
# A7/E2: per-turn transposition cache (kept from Apex)
|
||||
cache_key = (start, frozenset(blocked))
|
||||
cached = self._bfs_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
result = bb.flood_count(start_idx, blocked_bits)
|
||||
|
||||
if len(self._bfs_cache) < self._bfs_cache_max:
|
||||
self._bfs_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
# ── S2: Bitboard territory ──────────────────────────────────────────────
|
||||
|
||||
def _territory_fast(
|
||||
self, my_pos: tuple, blocked: set, width: int, height: int,
|
||||
deadline: float | None = None,
|
||||
) -> int:
|
||||
if not self._enemy_heads:
|
||||
return 0
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
my_idx = bb.idx(my_pos[0], my_pos[1])
|
||||
enemy_idxs = [bb.idx(eh[0], eh[1]) for eh in self._enemy_heads]
|
||||
return bb.territory(my_idx, enemy_idxs, blocked_bits)
|
||||
|
||||
# ── S3: Bitboard articulation penalty ────────────────────────────────────
|
||||
|
||||
def _articulation_penalty(
|
||||
self, point: tuple, blocked: set, width: int, height: int, required_space: int,
|
||||
) -> float:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
point_idx = bb.idx(point[0], point[1])
|
||||
|
||||
sizes = bb.partition_sizes(point_idx, blocked_bits)
|
||||
if not sizes:
|
||||
return 0.0
|
||||
|
||||
min_size = min(sizes)
|
||||
if min_size < required_space:
|
||||
return 1500.0
|
||||
elif min_size < required_space * 2:
|
||||
return 400.0
|
||||
else:
|
||||
return 85.0
|
||||
|
||||
def _bounded_bfs(self, start: tuple, blocked: set, width: int, height: int, limit: int) -> set:
|
||||
"""Bitboard-accelerated bounded BFS. Returns a set for API compatibility."""
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
reachable_bits = bb.flood_fill(start_idx, blocked_bits)
|
||||
|
||||
result: set[tuple[int, int]] = set()
|
||||
temp = reachable_bits
|
||||
w = bb.width
|
||||
while temp:
|
||||
bit = temp & (-temp)
|
||||
idx = bit.bit_length() - 1
|
||||
result.add((idx % w, idx // w))
|
||||
temp ^= bit
|
||||
if len(result) >= limit:
|
||||
break
|
||||
return result
|
||||
|
||||
# ── S4: Bitboard distance map ───────────────────────────────────────────
|
||||
|
||||
def _distance_map(self, start: tuple, blocked: set, width: int, height: int) -> dict:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
idx_dmap = bb.distance_map(start_idx, blocked_bits)
|
||||
w = bb.width
|
||||
return {(idx % w, idx // w): d for idx, d in idx_dmap.items()}
|
||||
|
||||
# ── S5: Bitboard path distance ──────────────────────────────────────────
|
||||
|
||||
def _path_distance(
|
||||
self, start: tuple, goal: tuple, blocked: set, width: int, height: int,
|
||||
) -> int | None:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
return bb.path_distance(
|
||||
bb.idx(start[0], start[1]),
|
||||
bb.idx(goal[0], goal[1]),
|
||||
blocked_bits,
|
||||
)
|
||||
|
||||
# ── S6: Bitboard nearest food ───────────────────────────────────────────
|
||||
|
||||
def _nearest_food_info(
|
||||
self, start: tuple, food_set: set, blocked: set, width: int, height: int,
|
||||
) -> tuple[int | None, tuple | None]:
|
||||
if not food_set:
|
||||
return None, None
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
food_bits = bb.set_to_bits(food_set)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
dist, cell_idx = bb.nearest_food(start_idx, food_bits, blocked_bits)
|
||||
if dist is None or cell_idx is None:
|
||||
return None, None
|
||||
return dist, bb.coord(cell_idx)
|
||||
|
||||
# ── Bitboard open-neighbour helpers ──────────────────────────────────────
|
||||
|
||||
def _open_neighbor_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
return bb.open_neighbor_count(bb.idx(start[0], start[1]), blocked_bits)
|
||||
|
||||
def _next_turn_options(self, head: dict, blocked: set, width: int, height: int) -> int:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
return bb.open_neighbor_count(bb.idx(head["x"], head["y"]), blocked_bits)
|
||||
|
||||
# ── S9: Optimised survival tree (bitboard-native) ────────────────────────
|
||||
|
||||
def _future_position_score(
|
||||
self, my_body: list, other_snakes: list, food_set: set, is_constrictor: bool,
|
||||
width: int, height: int, enemy_can_grow: dict, deadline: float | None,
|
||||
) -> float:
|
||||
"""S9: Bitboard-native position scoring for the survival tree.
|
||||
|
||||
Builds blocked bitboard directly from body lists (no intermediate set).
|
||||
Uses precomputed enemy bits instead of rebuilding attack map per node.
|
||||
"""
|
||||
if deadline is not None and perf_counter() >= deadline:
|
||||
return 0.0
|
||||
|
||||
bb = self._bb # already initialised in choose_move
|
||||
w = bb.width
|
||||
head = my_body[0]
|
||||
hx, hy = head["x"], head["y"]
|
||||
head_idx = hy * w + hx
|
||||
head_bit = 1 << head_idx
|
||||
body_len = len(my_body)
|
||||
|
||||
# ── Build blocked bitboard directly (no set) ──────────────────────
|
||||
my_bits = 0
|
||||
for seg in my_body:
|
||||
my_bits |= 1 << (seg["y"] * w + seg["x"])
|
||||
|
||||
# Own tail vacates unless stacked or constrictor
|
||||
if not is_constrictor and body_len >= 2:
|
||||
t, t2 = my_body[-1], my_body[-2]
|
||||
if not (t["x"] == t2["x"] and t["y"] == t2["y"]):
|
||||
my_bits &= ~(1 << (t["y"] * w + t["x"]))
|
||||
|
||||
# Enemy body (precomputed) minus vacating tails
|
||||
en_bits = self._enemy_body_bits & ~self._enemy_tail_bits
|
||||
|
||||
blocked_bits = (my_bits | en_bits) & ~head_bit
|
||||
|
||||
# ── Reachable space ───────────────────────────────────────────────
|
||||
reachable = bb.flood_count(head_idx, blocked_bits)
|
||||
required = body_len + max(3, body_len // 6) if is_constrictor else body_len
|
||||
if reachable < required:
|
||||
return -5000.0
|
||||
|
||||
# ── Open neighbours (liberties) ───────────────────────────────────
|
||||
nb_free = bb._neighbor_masks[head_idx] & ~blocked_bits & bb.board_mask
|
||||
liberties = nb_free.bit_count()
|
||||
if liberties == 0:
|
||||
return -5000.0
|
||||
|
||||
# ── Safe next options (enemy-attack aware) ────────────────────────
|
||||
# Remove tiles where an enemy of >= our length could head-to-head.
|
||||
# The danger bitboard was precomputed; filter out tiles blocked by
|
||||
# current body (enemy can't step there either).
|
||||
danger_here = self._enemy_attack_danger & ~blocked_bits
|
||||
safe_nb = nb_free & ~danger_here
|
||||
en_safe = safe_nb.bit_count()
|
||||
|
||||
if en_safe == 0:
|
||||
return -4000.0
|
||||
|
||||
sc = reachable * 1.9 + liberties * 14.0 + liberties * 11.0 + en_safe * 26.0
|
||||
if en_safe == 1:
|
||||
sc -= 420.0
|
||||
return sc
|
||||
|
||||
def _future_survival_tree(
|
||||
self, my_body: list, other_snakes: list, food_set: set, is_constrictor: bool,
|
||||
width: int, height: int, enemy_can_grow: dict,
|
||||
depth: int, branch: int, deadline: float | None,
|
||||
) -> float:
|
||||
"""S9/S11: Bitboard-accelerated survival tree.
|
||||
|
||||
Inlines legal-move check with bitboard ops instead of per-direction
|
||||
Python loops. Uses the bitboard-native _future_position_score.
|
||||
"""
|
||||
if depth <= 0 or (deadline is not None and perf_counter() >= deadline):
|
||||
return 0.0
|
||||
|
||||
bb = self._bb
|
||||
w = bb.width
|
||||
h = bb.height
|
||||
head = my_body[0]
|
||||
hx, hy = head["x"], head["y"]
|
||||
head_idx = hy * w + hx
|
||||
body_len = len(my_body)
|
||||
|
||||
# ── Build occupied bitboard for legal-move check ──────────────────
|
||||
occupied_bits = 0
|
||||
for seg in my_body:
|
||||
occupied_bits |= 1 << (seg["y"] * w + seg["x"])
|
||||
occupied_bits |= self._enemy_body_bits
|
||||
|
||||
# Own tail can be stepped on if not stacked/constrictor
|
||||
passable = 0
|
||||
if not is_constrictor and body_len >= 2:
|
||||
t, t2 = my_body[-1], my_body[-2]
|
||||
if not (t["x"] == t2["x"] and t["y"] == t2["y"]):
|
||||
passable |= 1 << (t["y"] * w + t["x"])
|
||||
|
||||
# Enemy vacating tails are also steppable
|
||||
passable |= self._enemy_tail_bits
|
||||
|
||||
# Legal moves: free neighbours OR passable tiles
|
||||
legal_bits = bb._neighbor_masks[head_idx] & ((~occupied_bits & bb.board_mask) | passable)
|
||||
|
||||
if not legal_bits:
|
||||
return -5000.0
|
||||
|
||||
# ── Precompute food bitboard once ─────────────────────────────────
|
||||
food_bits_local = 0
|
||||
for fx, fy in food_set:
|
||||
food_bits_local |= 1 << (fy * w + fx)
|
||||
|
||||
# ── Score each legal move ─────────────────────────────────────────
|
||||
scored: list[tuple[float, list]] = []
|
||||
temp = legal_bits
|
||||
while temp:
|
||||
if deadline is not None and perf_counter() >= deadline:
|
||||
break
|
||||
bit = temp & (-temp)
|
||||
temp ^= bit
|
||||
idx = bit.bit_length() - 1
|
||||
nx, ny = idx % w, idx // w
|
||||
pos = {"x": nx, "y": ny}
|
||||
ate = bool(bit & food_bits_local)
|
||||
fb = self._future_body(my_body, pos, ate, is_constrictor)
|
||||
sc = self._future_position_score(
|
||||
fb, other_snakes, food_set, is_constrictor,
|
||||
width, height, enemy_can_grow, deadline,
|
||||
)
|
||||
scored.append((sc, fb))
|
||||
|
||||
if not scored:
|
||||
return -5000.0
|
||||
|
||||
DEATH = self._TREE_DEATH_THRESHOLD
|
||||
viable = [(sc, fb) for sc, fb in scored if sc > DEATH]
|
||||
if not viable:
|
||||
return max(sc for sc, _ in scored)
|
||||
|
||||
viable.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
if depth == 1:
|
||||
return viable[0][0]
|
||||
|
||||
best = viable[0][0]
|
||||
for sc, fb in viable[:branch]:
|
||||
if deadline is not None and perf_counter() >= deadline:
|
||||
break
|
||||
cont = self._future_survival_tree(
|
||||
fb, other_snakes, food_set, is_constrictor,
|
||||
width, height, enemy_can_grow, depth - 1, branch, deadline,
|
||||
)
|
||||
total = sc + cont * 0.72
|
||||
if total > best:
|
||||
best = total
|
||||
return best
|
||||
|
||||
# ── S10: Bitboard legal moves ────────────────────────────────────────────
|
||||
|
||||
def _legal_moves(
|
||||
self, my_head, my_body: list, other_snakes: list,
|
||||
food_set: set, is_constrictor: bool, width: int, height: int,
|
||||
enemy_can_grow: dict | None = None,
|
||||
):
|
||||
"""S10: Bitboard-accelerated legal move generation."""
|
||||
bb = self._get_bb(width, height)
|
||||
w = bb.width
|
||||
|
||||
# Build occupied bitboard
|
||||
occupied = 0
|
||||
for seg in my_body:
|
||||
occupied |= 1 << (seg["y"] * w + seg["x"])
|
||||
for snake in other_snakes:
|
||||
for seg in snake["body"]:
|
||||
occupied |= 1 << (seg["y"] * w + seg["x"])
|
||||
|
||||
hx, hy = my_head["x"], my_head["y"]
|
||||
head_idx = hy * w + hx
|
||||
|
||||
# Own tail can be stepped on
|
||||
passable = 0
|
||||
if not is_constrictor and len(my_body) >= 2:
|
||||
t, t2 = my_body[-1], my_body[-2]
|
||||
if not (t["x"] == t2["x"] and t["y"] == t2["y"]):
|
||||
passable |= 1 << (t["y"] * w + t["x"])
|
||||
|
||||
# Enemy tails that will vacate
|
||||
if not is_constrictor:
|
||||
for snake in other_snakes:
|
||||
sbody = snake["body"]
|
||||
if len(sbody) < 2:
|
||||
continue
|
||||
st, st2 = sbody[-1], sbody[-2]
|
||||
if st["x"] == st2["x"] and st["y"] == st2["y"]:
|
||||
continue # stacked
|
||||
sid = snake.get("id")
|
||||
can_grow = None
|
||||
if enemy_can_grow is not None and sid is not None:
|
||||
can_grow = enemy_can_grow.get(sid)
|
||||
if can_grow is None:
|
||||
can_grow = self._enemy_can_grow_this_turn(snake, food_set)
|
||||
if not can_grow:
|
||||
passable |= 1 << (st["y"] * w + st["x"])
|
||||
|
||||
legal = bb._neighbor_masks[head_idx] & ((~occupied & bb.board_mask) | passable)
|
||||
|
||||
safe: dict[str, dict[str, int]] = {}
|
||||
for name, (dx, dy) in self.DIRECTIONS.items():
|
||||
nx, ny = hx + dx, hy + dy
|
||||
if 0 <= nx < w and 0 <= ny < bb.height:
|
||||
if (1 << (ny * w + nx)) & legal:
|
||||
safe[name] = {"x": nx, "y": ny}
|
||||
return safe
|
||||
|
||||
# ── Enemy confinement (uses bitboard flood) ──────────────────────────────
|
||||
|
||||
def _enemy_confinement_metrics(
|
||||
self, enemy_head: tuple, blocked: set, width: int, height: int,
|
||||
) -> tuple[int, int]:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
eh_idx = bb.idx(enemy_head[0], enemy_head[1])
|
||||
eb_bits = blocked_bits & ~(1 << eh_idx)
|
||||
space = bb.flood_count(eh_idx, eb_bits)
|
||||
options = bb.open_neighbor_count(eh_idx, eb_bits)
|
||||
return space, options
|
||||
|
||||
def _enemy_constrictor_projection(
|
||||
self, other_snakes: list, blocked: set, width: int, height: int,
|
||||
) -> tuple[int, int]:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
best_space = 0
|
||||
total_opts = 0
|
||||
for enemy in other_snakes:
|
||||
eh = (enemy["head"]["x"], enemy["head"]["y"])
|
||||
eh_idx = bb.idx(eh[0], eh[1])
|
||||
nb = bb.neighbors_of(eh_idx) & ~blocked_bits & bb.board_mask
|
||||
temp = nb
|
||||
while temp:
|
||||
total_opts += 1
|
||||
bit = temp & (-temp)
|
||||
n_idx = bit.bit_length() - 1
|
||||
sp = bb.flood_count(n_idx, blocked_bits | bit)
|
||||
if sp > best_space:
|
||||
best_space = sp
|
||||
temp ^= bit
|
||||
return best_space, total_opts
|
||||
@@ -0,0 +1,91 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import random, json, os
|
||||
|
||||
from server.TrainBattleSnakeAI import MOVES, extract_feature_values
|
||||
from snakes.core.template import TemplateSnake
|
||||
|
||||
class TrainedBattleSnake(TemplateSnake):
|
||||
VERSION = "0.1.0"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "TrainedBattleSnake"
|
||||
self.version = self.VERSION
|
||||
self._model_path:Path|None=None
|
||||
self._model_data:dict[str, Any]|None=None
|
||||
|
||||
def choose_move(self, game_data) -> str:
|
||||
self.game_board = game_data
|
||||
self.calculations = []
|
||||
|
||||
safe_positions = self.find_safe_positions(add_to_calculations=True)
|
||||
if not safe_positions:
|
||||
self.add_to_history({"turn": game_data.get_turn(), "reason": "no_safe_moves"})
|
||||
return "up"
|
||||
|
||||
model = self._load_model()
|
||||
if not model:
|
||||
move = random.choice(list(safe_positions.keys()))
|
||||
self.add_to_history({
|
||||
"turn": game_data.get_turn(),
|
||||
"move": move,
|
||||
"reason": "model_missing",
|
||||
"safe_moves": list(safe_positions.keys()),
|
||||
})
|
||||
return move
|
||||
|
||||
row = {
|
||||
"turn": game_data.get_turn(),
|
||||
"game_board": game_data.get_game_board_as_dict(),
|
||||
}
|
||||
scores = self._predict_scores(model, row)
|
||||
|
||||
best_safe_move = max(safe_positions.keys(), key=lambda move: scores.get(move, float("-inf")))
|
||||
self.add_to_history({
|
||||
"turn": game_data.get_turn(),
|
||||
"move": best_safe_move,
|
||||
"safe_moves": list(safe_positions.keys()),
|
||||
"scores": {move: round(scores.get(move, 0.0), 5) for move in MOVES},
|
||||
})
|
||||
return best_safe_move
|
||||
|
||||
def _load_model(self) -> dict[str, Any] | None:
|
||||
env_path = os.getenv("TRAINED_SNAKE_MODEL", "models/battlesnake_softmax_v2.json")
|
||||
path = Path(env_path)
|
||||
|
||||
if self._model_path == path and self._model_data is not None:
|
||||
return self._model_data
|
||||
|
||||
if not path.exists() or not path.is_file():
|
||||
self._model_path = path
|
||||
self._model_data = None
|
||||
return None
|
||||
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
model = payload.get("model")
|
||||
if not isinstance(model, dict):
|
||||
self._model_path = path
|
||||
self._model_data = None
|
||||
return None
|
||||
|
||||
self._model_path = path
|
||||
self._model_data = model
|
||||
return model
|
||||
|
||||
def _predict_scores(self, model:dict[str, Any], row:dict[str, Any]) -> dict[str, float]:
|
||||
return self._predict_scores_softmax_v2(model, row)
|
||||
|
||||
def _predict_scores_softmax_v2(self, model:dict[str, Any], row:dict[str, Any]) -> dict[str, float]:
|
||||
features = extract_feature_values(row)
|
||||
weights = model.get("weights", {})
|
||||
bias = model.get("bias", {})
|
||||
scores:dict[str, float] = {}
|
||||
|
||||
for move in MOVES:
|
||||
move_weights = weights.get(move, {})
|
||||
score = float(bias.get(move, 0.0))
|
||||
for name, value in features.items():
|
||||
score += float(move_weights.get(name, 0.0)) * float(value)
|
||||
scores[move] = score
|
||||
return scores
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
"""Historical snake strategies retained for replay and comparison."""
|
||||
Reference in New Issue
Block a user