From 7acc269df4feff6a1e5aa3c6c21953f2f331595e Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Sat, 13 Apr 2024 03:03:09 +0200 Subject: [PATCH] main (#1) Reviewed-on: https://git.yiprawr.dev/daniel156161/snake-python/pulls/1 Co-authored-by: Daniel Dolezal Co-committed-by: Daniel Dolezal --- .gitignore | 1 + config.py | 6 - main.py | 68 +++----- requirements.txt | 1 + server/GameStorage.py | 15 +- server/Server.py | 103 ++++++++++++ server/SnakeBuilder.py | 7 + server/server.py | 45 ------ snakes/MasterSnake.py | 87 ++++++++-- snakes/MasterSnake_FoodBySpace.py | 256 ++++++++++++++++++++++++++++++ snakes/TemplateSnake.py | 3 + snakes/__init__.py | 0 12 files changed, 478 insertions(+), 114 deletions(-) delete mode 100644 config.py mode change 100644 => 100755 main.py create mode 100644 server/Server.py create mode 100644 server/SnakeBuilder.py delete mode 100644 server/server.py create mode 100644 snakes/MasterSnake_FoodBySpace.py create mode 100644 snakes/__init__.py diff --git a/.gitignore b/.gitignore index 1aee748..ffc7316 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .venv/ __pycache__/ data/ +.env diff --git a/config.py b/config.py deleted file mode 100644 index ea6c0e3..0000000 --- a/config.py +++ /dev/null @@ -1,6 +0,0 @@ -from snakes.DummSnake import DummSnake -from snakes.LogicSnake import LogicSnake -from snakes.AStarSnake import AStarSnake -from snakes.MasterSnake import MasterSnake - -SNAKE = MasterSnake() diff --git a/main.py b/main.py old mode 100644 new mode 100755 index ee745c3..06e52a7 --- a/main.py +++ b/main.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + # Welcome to # __________ __ __ .__ __ # \______ \_____ _/ |__/ |_| | ____ ______ ____ _____ | | __ ____ @@ -10,54 +12,26 @@ # To get you started we've included code to prevent your Battlesnake from moving backwards. # For more info see docs.battlesnake.com -from server.Files import read_file, save_file -from server.GameStorage import GameStorage -from config import SNAKE +from server.SnakeBuilder import SnakeBuilder +from server.Server import Server -from datetime import datetime -import typing -import json, os - -# info is called when you create your Battlesnake on play.battlesnake.com -# and controls your Battlesnake's appearance -# TIP: If you open your Battlesnake URL in a browser you should see this data -game_state_storage = GameStorage(SNAKE.__class__.__name__) - -def info() -> typing.Dict: - CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'data', 'snake-config.json') - snake = read_file(CONFIG_PATH, json.load) - if not snake: - snake = {"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"} - save_file(CONFIG_PATH, snake, callback=json.dump, indent=2, ensure_ascii=False) - print("INFO", snake) - return snake - - print("INFO Snake:", snake) - return snake - -# start is called when your Battlesnake begins a game -def start(game_state: typing.Dict): - game_state_storage.start_new_game(game_state["game"], game_state["board"], game_state["you"]) - SNAKE.clear_history() - print("GAME START:", game_state["game"]) - -# move is called when your Battlesnake game is running game -def move(game_state: typing.Dict) -> typing.Dict: - next_move = SNAKE.choose_move(game_state) - game_state_storage.add_moves(game_state["board"], next_move) - print("MOVE:", f"{next_move},", "Me:", {"body": game_state["you"]["body"], "head": game_state["you"]["head"], "length": game_state["you"]["length"]}) - return {"move": next_move} - -# end is called when your Battlesnake finishes a game -def end(game_state: typing.Dict): - HISTORY_PATH = os.path.join(os.path.dirname(__file__), 'data', 'history') - - game_state_storage.add_end_state(game_state["board"], SNAKE.get_history()) - game_state_storage.save(os.path.join(HISTORY_PATH, f"{SNAKE.__class__.__name__}_{datetime.now().strftime('%d.%m.%Y_%H%M%S')}_{game_state['game']['id']}.json"), callback=json.dump, indent=2, ensure_ascii=False) - - print("GAME OVER\n") +from dotenv import load_dotenv, find_dotenv +import os # Start server when `python main.py` is run if __name__ == "__main__": - from server.server import run_server - run_server({"info": info, "start": start, "move": move, "end": end}) + load_dotenv(find_dotenv()) + + server = Server( + data_path=os.path.dirname(__file__), + snake=SnakeBuilder.build(os.environ.get("SNAKE", "DummSnake")), + ) + + if os.environ.get("STORE_GAME_HISTORY", None): + server.enable_store_game_state() + + server.run( + host=os.environ.get("HOST", "0.0.0.0"), + port=int(os.environ.get("PORT", "8000")), + debug=bool(os.environ.get("DEBUG", False)) + ) diff --git a/requirements.txt b/requirements.txt index c016b06..fc879ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,6 @@ itsdangerous==2.1.2 Jinja2==3.1.3 MarkupSafe==2.1.5 numpy==1.26.4 +python-dotenv==1.0.1 scipy==1.12.0 Werkzeug==3.0.1 diff --git a/server/GameStorage.py b/server/GameStorage.py index 11c8818..fdb93b1 100644 --- a/server/GameStorage.py +++ b/server/GameStorage.py @@ -1,8 +1,10 @@ from server.Files import save_file +import os class GameStorage: - def __init__(self, snake:str): + def __init__(self, snake:str, path:str): self.snake_type = snake + self.folder = path def start_new_game(self, game_type:dict, game_board:dict, snake:dict): self.game_type = game_type @@ -18,8 +20,14 @@ class GameStorage: self.game_board.append(game_board) self.snake_history = snake_history_state + def set_winner_snake_name(self, snakes:list[dict]): + if self.start_position["id"] in [ x["id"] for x in snakes]: + self.winner_snake_names = "me" + else: + self.winner_snake_names = [ x["name"] for x in snakes] + def save(self, path:str, callback=None, **kwargs): - save_file(path, { + save_file(os.path.join(self.folder, path), { "snake": { "type": self.snake_type, "choices": self.snake_history, @@ -29,5 +37,6 @@ class GameStorage: "snake_start": self.start_position, "gameboard": self.game_board, "my_moves": self.moves, - } + }, + "winner": self.winner_snake_names, }, callback=callback, **kwargs) diff --git a/server/Server.py b/server/Server.py new file mode 100644 index 0000000..f3919a0 --- /dev/null +++ b/server/Server.py @@ -0,0 +1,103 @@ +from server.Files import read_file, save_file +from server.GameStorage import GameStorage +from snakes.TemplateSnake import TemplateSnake + +from datetime import datetime +from flask import Flask +from flask import request +import logging, json, os + +class Server: + default_snake_config = {"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"} + + def __init__(self, data_path:str, snake:TemplateSnake, debug:bool=False): + self.debug = debug + self.snake = snake + + self.config_file = os.path.join(data_path, 'data', 'snake-config.json') + self.game_state_storage = GameStorage(snake.__class__.__name__, path=os.path.join(data_path, 'data', 'history')) + self.store_game_state = False + + self.app = Flask("Battlesnake") + + @self.app.get("/") + def on_info(): + return self._info() + + @self.app.post("/start") + def on_start(): + game_state = request.get_json() + self._start(game_state) + return "ok" + + @self.app.post("/move") + def on_move(): + game_state = request.get_json() + return self._move(game_state) + + @self.app.post("/end") + def on_end(): + game_state = request.get_json() + self._end(game_state) + return "ok" + + @self.app.after_request + def identify_server(response): + response.headers.set( + "server", "battlesnake/github/starter-snake-python" + ) + return response + + def run(self, host:str="0.0.0.0", port:str="8000", debug:bool=False): + logging.getLogger("werkzeug").setLevel(logging.ERROR) + + print(f"\nRunning Battlesnake at http://{host}:{port} with the {self.snake.__class__.__name__.replace('Snake', '')} Snake") + self.app.run(host=host, port=port, debug=debug) + + def _read_json_config_or_create(self): + snake_config = read_file(self.config_file, json.load) + if not snake_config: + snake_config = self.default_snake_config + save_file(self.config_file, snake_config, callback=json.dump, indent=2, ensure_ascii=False) + return snake_config + + def enable_store_game_state(self): + self.store_game_state = True + + # info is called when you create your Battlesnake on play.battlesnake.com + # and controls your Battlesnake's appearance + # TIP: If you open your Battlesnake URL in a browser you should see this data + def _info(self) -> dict: + snake_config = self._read_json_config_or_create() + print("INFO Snake:", snake_config) + return snake_config + + # start is called when your Battlesnake begins a game + def _start(self, game_state:dict): + if self.store_game_state: + self.game_state_storage.start_new_game(game_state["game"], game_state["board"], game_state["you"]) + + self.snake.clear_history() + print("GAME START:", game_state["game"]) + + # move is called when your Battlesnake game is running game + def _move(self, game_state:dict) -> dict: + next_move = self.snake.choose_move(game_state) + + if self.store_game_state: + self.game_state_storage.add_moves(game_state["board"], next_move) + + print("MOVE:", f"{next_move:5},", "Me:", {"head": game_state["you"]["head"], "length": game_state["you"]["length"]}) + return {"move": next_move} + + # end is called when your Battlesnake finishes a game + def _end(self, game_state:dict): + if self.store_game_state: + self.game_state_storage.add_end_state(game_state["board"], self.snake.get_history()) + self.game_state_storage.set_winner_snake_name(game_state["board"]['snakes']) + self.game_state_storage.save( + f"{self.snake.__class__.__name__}_{datetime.now().strftime('%d.%m.%Y_%H%M%S')}_{game_state['game']['id']}.json", + callback=json.dump, indent=2, ensure_ascii=False + ) + + print("GAME OVER:\n- Winner is", [ x["name"] for x in game_state["board"]['snakes']]) diff --git a/server/SnakeBuilder.py b/server/SnakeBuilder.py new file mode 100644 index 0000000..2c86475 --- /dev/null +++ b/server/SnakeBuilder.py @@ -0,0 +1,7 @@ + +class SnakeBuilder: + @classmethod + def build(self, selected_snake:str): + snake_module = __import__(f'snakes.{selected_snake}', fromlist=[selected_snake]) + snake_class = getattr(snake_module, selected_snake) + return snake_class() diff --git a/server/server.py b/server/server.py deleted file mode 100644 index 94bb169..0000000 --- a/server/server.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging -import os -import typing - -from flask import Flask -from flask import request - -def run_server(handlers: typing.Dict): - app = Flask("Battlesnake") - - @app.get("/") - def on_info(): - return handlers["info"]() - - @app.post("/start") - def on_start(): - game_state = request.get_json() - handlers["start"](game_state) - return "ok" - - @app.post("/move") - def on_move(): - game_state = request.get_json() - return handlers["move"](game_state) - - @app.post("/end") - def on_end(): - game_state = request.get_json() - handlers["end"](game_state) - return "ok" - - @app.after_request - def identify_server(response): - response.headers.set( - "server", "battlesnake/github/starter-snake-python" - ) - return response - - host = "0.0.0.0" - port = int(os.environ.get("PORT", "8000")) - - logging.getLogger("werkzeug").setLevel(logging.ERROR) - - print(f"\nRunning Battlesnake at http://{host}:{port}") - app.run(host=host, port=port) diff --git a/snakes/MasterSnake.py b/snakes/MasterSnake.py index 1a01690..b359b85 100644 --- a/snakes/MasterSnake.py +++ b/snakes/MasterSnake.py @@ -4,6 +4,13 @@ class MasterSnake(TemplateSnake): def __init__(self): super().__init__() self.name = "MasterSnake" + 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 @@ -37,22 +44,38 @@ class MasterSnake(TemplateSnake): # Finde die nächstgelegene Nahrungsquelle, wenn Nahrung vorhanden ist try: - 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_food(my_head, path_to_food[0], safe_positions) + 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: - # Einfache Logik, um eine Bewegungsrichtung zu wählen, wenn keine Nahrung vorhanden ist + # 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, board_width, board_height, snakes) + 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_food(self, head, food, 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') @@ -62,7 +85,7 @@ class MasterSnake(TemplateSnake): 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(food[0] - next_position['x']) + abs(food[1] - next_position['y']) + 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 @@ -150,16 +173,21 @@ class MasterSnake(TemplateSnake): return direction return "up" # Standardbewegung, falls keine sichere Position gefunden wird - def avoid_dead_ends(self, head, move, safe_positions, board_width, board_height, snakes): + 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, board_width, board_height, snakes): + 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, board_width, board_height, snakes): + 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 @@ -171,7 +199,7 @@ class MasterSnake(TemplateSnake): future_body_positions.add((part['x'], part['y'])) return future_body_positions - def is_future_move_safe(self, future_head, safe_positions, board_width, board_height, snakes): + 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 @@ -181,5 +209,38 @@ class MasterSnake(TemplateSnake): # 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 - return safe_positions_set # oder wähle einen anderen Schwellenwert + + 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 diff --git a/snakes/MasterSnake_FoodBySpace.py b/snakes/MasterSnake_FoodBySpace.py new file mode 100644 index 0000000..49df991 --- /dev/null +++ b/snakes/MasterSnake_FoodBySpace.py @@ -0,0 +1,256 @@ +from snakes.TemplateSnake import TemplateSnake + +class MasterSnake(TemplateSnake): + def __init__(self): + super().__init__() + self.name = "MasterSnake" + self.history_head = [] + + 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) + + # Wähle Nahrung basierend auf verfügbarem Platz + try: + chosen_food = self.choose_food_based_on_space(game_data) + if chosen_food: + path_to_food = self.a_star_search(my_head, chosen_food, self.get_obstacles(game_data), board_width, board_height) + if path_to_food: + # Implementiere Logik, um in Richtung der Nahrungsquelle zu bewegen, falls sicher + move = self.move_towards_food(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 kein Pfad zur Nahrung vorhanden ist + move = self.find_direction(my_head, safe_positions) + self.add_to_history({"my_head": my_head, "move": move}) + else: + # Einfache Logik, um eine Bewegungsrichtung zu wählen, wenn keine geeignete Nahrung gefunden wird + 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}) + + # Überprüfe zukünftige Bewegungen, um Sackgassen zu vermeiden + move = self.avoid_dead_ends_and_circles(my_head, move, safe_positions, board_width, board_height, snakes) + self.add_to_history({"my_head": my_head, "move": move}) + self.add_to_history_head({"my_head": my_head, "move": move}) + + return move + + def move_towards_food(self, head, food, 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(food[0] - next_position['x']) + abs(food[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 choose_food_based_on_space(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'] + my_length = game_data['you']['length'] + + # Sortiere die Nahrungsquellen basierend auf ihrer Entfernung + sorted_food = sorted(food_positions, key=lambda food: abs(food['x'] - my_head['x']) + abs(food['y'] - my_head['y'])) + + for food in sorted_food: + path = self.a_star_search(my_head, food, self.get_obstacles(game_data), board_width, board_height) + if path and self.will_fit_in_space(path, my_length, board_width, board_height): + return food # Diese Nahrung ist erreichbar und es gibt genug Platz + + # Wenn keine geeignete Nahrung gefunden wird, gib ein Standard-Nahrungsobjekt zurück oder löse eine Ausnahme aus + if food_positions: + return food_positions[0] # Gib das erste Nahrungsobjekt zurück + else: + raise ValueError("Keine Nahrung gefunden") # Oder löse eine Ausnahme aus + + def will_fit_in_space(self, path, snake_length, board_width, board_height): + # Überprüfe, ob die Länge des Pfades größer oder gleich der Länge der Schlange ist + if len(path) >= snake_length: + return True + + # Überprüfe, ob es genügend Platz um den Endpunkt des Pfades gibt + end_of_path = path[-1] + space_count = self.count_space_around(end_of_path, board_width, board_height) + return space_count >= snake_length + + def count_space_around(self, position, board_width, board_height): + # Zähle die Anzahl der erreichbaren Positionen um einen Punkt herum + x, y = position + count = 0 + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if (dx != 0 or dy != 0) and 0 <= x + dx < board_width and 0 <= y + dy < board_height: + count += 1 + return count + + def get_obstacles(self, game_data): + # Erstelle ein Set von Hindernissen für die A* Suche + obstacles = set() + for snake in game_data['board']['snakes']: + for part in snake['body']: + obstacles.add((part['x'], part['y'])) + return obstacles + + 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 is_in_history(self, future_head): + # Überprüfe, ob die zukünftige Kopfposition in den letzten N Bewegungen vorkommt + return any(future_head == move_data["my_head"] for move_data in self.history_head[-10:]) + + def avoid_dead_ends_and_circles(self, head, move, safe_positions, board_width, board_height, 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} + + if not self.is_future_move_safe(future_head, safe_positions, board_width, board_height, snakes) or self.is_in_history(future_head): + 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, board_width, board_height, snakes) and not self.is_in_history(alternative_future_head): + return alternative_move + return move + + def add_to_history_head(self, move_data): + # Füge die aktuelle Kopfposition zur Historie hinzu und behalte nur die letzten 10 Positionen + self.history_head.append(move_data) + self.history_head = self.history_head[-10:] + + 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, board_width, board_height, 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 + # Entscheide, ob die Bewegung sicher ist, basierend auf der Anzahl der erreichbaren Positionen + return safe_positions_set # oder wähle einen anderen Schwellenwert diff --git a/snakes/TemplateSnake.py b/snakes/TemplateSnake.py index f77a9e6..70cc70e 100644 --- a/snakes/TemplateSnake.py +++ b/snakes/TemplateSnake.py @@ -10,3 +10,6 @@ class TemplateSnake: def get_history(self): return self.history + + def choose_move(self, game_data:dict): + pass diff --git a/snakes/__init__.py b/snakes/__init__.py new file mode 100644 index 0000000..e69de29