diff --git a/server/Server.py b/server/Server.py index 21191e0..9e0aa24 100644 --- a/server/Server.py +++ b/server/Server.py @@ -17,6 +17,7 @@ class Server: 'color': '#888888', 'head': 'default', 'tail': 'default', + 'version': '1.0.0', } def __init__(self, data_path:str, snake_type:str, storage_type:str, debug:bool=False, check_tls_security:bool=False): @@ -42,6 +43,7 @@ class Server: 'max_turn': 0, } self.logger = build_logger('Battlesnake', debug_env_var='DEBUG_SERVER') + self.snake_version = self._get_snake_version() self.app = Quart('Battlesnake') @@ -134,12 +136,36 @@ class Server: return await self._override_snake_config_with_environment_variables(snake_config) async def _override_snake_config_with_environment_variables(self, config:dict[str, str]) -> dict[str, str]: + config['version'] = self.snake_version + for key in ('author', 'color', 'head', 'tail'): value = os.environ.get(f'SNAKE_{key.upper()}') if value is not None: config[key] = value + + version_override = os.environ.get('SNAKE_VERSION') + if version_override is not None: + config['version'] = version_override + return config + def _get_snake_version(self) -> str: + configured_version = SnakeBuilder.get_version(self.snake_type) + if configured_version: + return configured_version + + try: + snake = SnakeBuilder.build(self.snake_type) + except Exception: + return self.default_snake_config['version'] + + version = getattr(snake, 'version', None) + if version is None: + version = getattr(snake, 'VERSION', None) + if not version: + return self.default_snake_config['version'] + return str(version) + async def _create_game_board(self, game_state:dict): game_id = game_state['game']['id'] new_game_board = GameBoard( diff --git a/server/SnakeBuilder.py b/server/SnakeBuilder.py index 2c86475..321b892 100644 --- a/server/SnakeBuilder.py +++ b/server/SnakeBuilder.py @@ -1,7 +1,19 @@ - class SnakeBuilder: + SNAKE_VERSIONS = { + "TemplateSnake": "1.0.0", + "DummSnake": "1.0.0", + "LogicSnake": "1.1.0", + "MasterSnake": "1.2.0", + "BetterMasterSnake": "1.3.0", + "BestBattleSnake": "2.5.0", + } + @classmethod def build(self, selected_snake:str): - snake_module = __import__(f'snakes.{selected_snake}', fromlist=[selected_snake]) + snake_module = __import__(f"snakes.{selected_snake}", fromlist=[selected_snake]) snake_class = getattr(snake_module, selected_snake) return snake_class() + + @classmethod + def get_version(self, selected_snake:str) -> str | None: + return self.SNAKE_VERSIONS.get(selected_snake) diff --git a/snakes/BestBattleSnake.py b/snakes/BestBattleSnake.py index b14856c..c2aa7cb 100644 --- a/snakes/BestBattleSnake.py +++ b/snakes/BestBattleSnake.py @@ -6,6 +6,8 @@ import os from snakes.TemplateSnake import TemplateSnake class BestBattleSnake(TemplateSnake): + VERSION = "2.5.0" + DIRECTIONS = { "up": (0, 1), "down": (0, -1), @@ -23,6 +25,7 @@ class BestBattleSnake(TemplateSnake): def __init__(self): super().__init__() self.name = "BestBattleSnake" + self.version = self.VERSION self.recent_heads = deque(maxlen=14) self.last_move = None self.last_game_id = None diff --git a/snakes/BetterMasterSnake.py b/snakes/BetterMasterSnake.py index a5f185e..14724a3 100644 --- a/snakes/BetterMasterSnake.py +++ b/snakes/BetterMasterSnake.py @@ -3,219 +3,222 @@ from server.GameBoard import GameBoard from collections import deque class BetterMasterSnake(TemplateSnake): - def __init__(self): - super().__init__() - self.name = "BetterMasterSnake" - # Definiere die möglichen Bewegungsrichtungen - self.min_safe_area = 2 + VERSION = "1.3.0" - def choose_move(self, game_data:GameBoard): - self.game_board = game_data - self.calculations = [] - self.eat_the_snake_overwrite = False + def __init__(self): + super().__init__() + self.name = "BetterMasterSnake" + self.version = self.VERSION + # Definiere die möglichen Bewegungsrichtungen + self.min_safe_area = 2 - 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()) + def choose_move(self, game_data:GameBoard): + self.game_board = game_data + self.calculations = [] + self.eat_the_snake_overwrite = False - if game_data.get_type() == "constrictor": - move = self.selected_move_constrictor() + 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: - move = self.selected_move_standard() + 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 - self.add_to_history({"turn": game_data.get_turn(), "data": self.calculations}) - return move if move else "up" + #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 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 + 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]) - #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 + self.add_calculations({"function": "ensure_escape_route", "move": move, "KeyError": "Snake Coild itself up"}) + #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}) + # TODO: Fix - Snake Neat to find the best way - Close to the Tail and maybe fill most free cells as posible + return 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}) + def is_near_tail(self, position, tail): + return abs(position["x"] - tail[0]) + abs(position["y"] - tail[1]) <= 2 - # Ü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 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 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()) + 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 - for snake in self.game_board.get_other_snakes(): - for part in snake['body']: - obstacles.add((part['x'], part['y'])) + 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'])) - 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 + # Ü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] - 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] + # 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)} - 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) + 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 - # 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 + 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) - 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'])) + return None # Kein Pfad gefunden - 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 + 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 diff --git a/snakes/DummSnake.py b/snakes/DummSnake.py index d274bf8..c9e616f 100644 --- a/snakes/DummSnake.py +++ b/snakes/DummSnake.py @@ -3,53 +3,55 @@ from snakes.TemplateSnake import TemplateSnake import random class DummSnake(TemplateSnake): - def choose_move(self, data: dict) -> str: - is_move_safe = {"up": True, "down": True, "left": True, "right": True} + VERSION = "1.0.0" - # 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" + def choose_move(self, data: dict) -> str: + is_move_safe = {"up": True, "down": True, "left": True, "right": True} - if my_neck["x"] < my_head["x"]: # Neck is left of head, don't move left - is_move_safe["left"] = False + # 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" - elif my_neck["x"] > my_head["x"]: # Neck is right of head, don't move right - is_move_safe["right"] = False + if my_neck["x"] < my_head["x"]: # Neck is left of head, don't move left + is_move_safe["left"] = False - elif my_neck["y"] < my_head["y"]: # Neck is below head, don't move down - is_move_safe["down"] = 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 above head, don't move up - is_move_safe["up"] = False + elif my_neck["y"] < my_head["y"]: # Neck is below head, don't move down + is_move_safe["down"] = False - # TODO: Step 1 - Prevent your Battlesnake from moving out of bounds - # board_width = game_state['board']['width'] - # board_height = game_state['board']['height'] + elif my_neck["y"] > my_head["y"]: # Neck is above head, don't move up + is_move_safe["up"] = False - # TODO: Step 2 - Prevent your Battlesnake from colliding with itself - # my_body = game_state['you']['body'] + # 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 3 - Prevent your Battlesnake from colliding with other Battlesnakes - # opponents = game_state['board']['snakes'] + # TODO: Step 2 - Prevent your Battlesnake from colliding with itself + # my_body = game_state['you']['body'] - # Are there any safe moves left? - safe_moves = [] - for move, isSafe in is_move_safe.items(): - if isSafe: - safe_moves.append(move) + # TODO: Step 3 - Prevent your Battlesnake from colliding with other Battlesnakes + # opponents = game_state['board']['snakes'] - 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"} + # Are there any safe moves left? + safe_moves = [] + for move, isSafe in is_move_safe.items(): + if isSafe: + safe_moves.append(move) - # Choose a random move from the safe ones - move = random.choice(safe_moves) + 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"} - # TODO: Step 4 - Move towards food instead of random, to regain health and survive longer - # food = game_state['board']['food'] + # Choose a random move from the safe ones + move = random.choice(safe_moves) - 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}") + # TODO: Step 4 - Move towards food instead of random, to regain health and survive longer + # food = game_state['board']['food'] - return move + 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 diff --git a/snakes/LogicSnake.py b/snakes/LogicSnake.py index 824c5cd..3f3f8ea 100644 --- a/snakes/LogicSnake.py +++ b/snakes/LogicSnake.py @@ -4,6 +4,8 @@ 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. diff --git a/snakes/MasterSnake.py b/snakes/MasterSnake.py index b359b85..9e60678 100644 --- a/snakes/MasterSnake.py +++ b/snakes/MasterSnake.py @@ -1,246 +1,249 @@ from snakes.TemplateSnake import TemplateSnake class MasterSnake(TemplateSnake): - def __init__(self): - super().__init__() - self.name = "MasterSnake" - self.disabled_find_near_by_food = True + VERSION = "1.2.0" - 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 __init__(self): + super().__init__() + self.name = "MasterSnake" + self.version = self.VERSION + self.disabled_find_near_by_food = True - 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'])) + 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 - # 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 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'])) - 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 + # 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 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'] + 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 - # Vermeide Schlangenkörper - safe_positions = self.avoid_snake_body(snakes, board_width, board_height) + 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'] - # 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}) + # Vermeide Schlangenkörper + safe_positions = self.avoid_snake_body(snakes, board_width, board_height) - # 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) + # 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}) - return 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) - 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 + # Ü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}) - 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 move - return best_direction if best_direction else "up" # Default to moving up if no safe direction found + 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 - 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 + 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 - 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 + return best_direction if best_direction else "up" # Default to moving up if no safe direction found - 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 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 heuristic(position, goal): - return abs(position[0] - goal[0]) + abs(position[1] - goal[1]) + 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 - # Initialize start and goal positions - start = (start['x'], start['y']) - goal = (goal['x'], goal['y']) + 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))] - # Initialize the open and closed list - open_set = set([start]) - came_from = {} - g_score = {start: 0} - f_score = {start: heuristic(start, goal)} + def heuristic(position, goal): + return abs(position[0] - goal[0]) + abs(position[1] - goal[1]) - 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 + # Initialize start and goal positions + start = (start['x'], start['y']) + goal = (goal['x'], goal['y']) - 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) + # Initialize the open and closed list + open_set = set([start]) + came_from = {} + g_score = {start: 0} + f_score = {start: heuristic(start, goal)} - return None # Kein Pfad gefunden + 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 - 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 + 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) - 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 + return None # Kein Pfad gefunden - 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']) + 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 - 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 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 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 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']) - 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 + 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 - fill_bool = len(reachable_positions) > len(safe_positions_set) * 0.25 - if fill_bool: - return fill_bool + 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 - return len(safe_positions_set) >= len(snakes[0]['body']) + 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 - def flood_fill(self, start, safe_positions): - stack = [start] - visited = set() - max_area = 0 - max_area_start = None + fill_bool = len(reachable_positions) > len(safe_positions_set) * 0.25 + if fill_bool: + return fill_bool - while stack: - position = stack.pop() - if isinstance(position, dict): - position = tuple(position.values()) - else: - position = tuple(position) + return len(safe_positions_set) >= len(snakes[0]['body']) - 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) + def flood_fill(self, start, safe_positions): + stack = [start] + visited = set() + max_area = 0 + max_area_start = None - # Ü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 + while stack: + position = stack.pop() + if isinstance(position, dict): + position = tuple(position.values()) + else: + position = tuple(position) - return max_area_start, visited + 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 diff --git a/snakes/TemplateSnake.py b/snakes/TemplateSnake.py index 77eebe2..c219e17 100644 --- a/snakes/TemplateSnake.py +++ b/snakes/TemplateSnake.py @@ -2,9 +2,13 @@ from server.GameBoard import GameBoard import random class TemplateSnake: + VERSION = "1.0.0" + def __init__(self): self.history = [] self.target_food = None + self.name = self.__class__.__name__ + self.version = getattr(self, "VERSION", "1.0.0") def clear_history(self): self.history = []