From f1b738b50819b0067a521797564c5a2a9b102da8 Mon Sep 17 00:00:00 2001 From: Phu Le Date: Sat, 8 Jul 2023 01:07:09 +0000 Subject: [PATCH] add daddy2 snake --- __init__.py | 0 pathfinding/__init__.py | 0 pathfinding/astarpath.py | 31 ++-- pathfinding/findapath.py | 129 ++++++++++++++++ pathfinding/location.py | 16 ++ pathfinding/patrickpath.py | 113 -------------- pathfinding/peterpath.py | 24 +-- snake_daddy2.json | 306 +++++++++++++++++++++++++++++++++++++ snake_daddy2.py | 232 ++++++++++++++++++++++++++++ snake_peter.py | 2 +- 10 files changed, 703 insertions(+), 150 deletions(-) create mode 100644 __init__.py create mode 100644 pathfinding/__init__.py create mode 100644 pathfinding/findapath.py create mode 100644 pathfinding/location.py delete mode 100644 pathfinding/patrickpath.py create mode 100644 snake_daddy2.json create mode 100644 snake_daddy2.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pathfinding/__init__.py b/pathfinding/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pathfinding/astarpath.py b/pathfinding/astarpath.py index d9832f7..c829cf7 100644 --- a/pathfinding/astarpath.py +++ b/pathfinding/astarpath.py @@ -2,23 +2,9 @@ import logging from enum import Enum -class Location: - def __init__(self, x, y) -> None: - self.x = x - self.y = y - def __repr__(self) -> str: - retString = "(" + str(self.x) + ", " + str(self.y) + ")" - return retString - def __eq__(self, object) -> bool: - if self.x == object.x and self.y == object.y: - return True - else: - return False - def __hash__(self): - return self.x*1000 + self.y - def get_distance(self, location): - return abs(self.x - location.x) + abs(self.y - location.y) - +def find_astar_path(startSnake, endLoc, obstacles): + finder = AStarFinder(startSnake, endLoc, obstacles, 11, 11, 2) + return finder.findthepath() class PathEntry: def __init__(self, snake) -> None: @@ -143,9 +129,8 @@ def basicTest1(): destination = Location(9,9) obstacles = [ ] - finder = AStarFinder(snake, destination, obstacles, 11, 11, 1) - path = finder.findthepath() + path = find_astar_path(snake, destination, obstacles) logging.info(path) logging.info("elapsed time: %d", (time.time() - start_time)*1000) @@ -181,6 +166,12 @@ def circleSnake1(): logging.info("elapsed time: %d", (time.time() - start_time)*1000) if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) + # peterpath.py is being run directly + from location import Location + # run tests + logging.basicConfig(level=logging.INFO) circleSnake1() +else: + # peterpath.py is being imported into another script + from .location import Location \ No newline at end of file diff --git a/pathfinding/findapath.py b/pathfinding/findapath.py new file mode 100644 index 0000000..e165ab8 --- /dev/null +++ b/pathfinding/findapath.py @@ -0,0 +1,129 @@ +def findapath(startLoc, endLoc, obstacles): + finder = PathFinder(startLoc, endLoc, obstacles, 11, 11) + return finder.findthepath() + +class PathEntry: + def __init__(self, location) -> None: + self.location = location + self.history = [] + self.cost = 0 + +class PathFinder: + def __init__(self, startLoc, destination, obstacles, width, height) -> None: + self.data = [] + self.startLoc = startLoc + self.destination = destination + self.obstacles = obstacles + self.width = width + self.height = height + self.visited = [] + + def addPathData(self, oldEntry, newlocation): + if (self.isValidLocation(newlocation)): + newEntry = PathEntry(newlocation) + newEntry.history = oldEntry.history.copy() + newEntry.history.append(oldEntry.location) + self.data.append(newEntry) + + def findthepath(self): + startEntry = PathEntry(self.startLoc) + self.data.append(startEntry) + + while len(self.data) > 0: + current = self.data.pop(0) + if current.location == self.destination: + current.history.append(self.destination) + # print("Found a path") + return current.history + break + if not self.isVisited(current.location): + self.visited.append(current.location) + uplocation = Location(current.location.x, current.location.y+1) + downlocation = Location(current.location.x, current.location.y-1) + leftlocation = Location(current.location.x-1, current.location.y) + rightlocation = Location(current.location.x+1, current.location.y) + + self.addPathData(current, uplocation) + self.addPathData(current, downlocation) + self.addPathData(current, leftlocation) + self.addPathData(current, rightlocation) + + def isValidLocation(self, location): + if location.x < 0 or location.x > self.width-1: + return False + if location.y < 0 or location.y > self.height-1: + return False + for obstacles in self.obstacles: + if obstacles == location: + return False + return True + def isVisited(self, newlocation): + for location in self.visited: + if location == newlocation: + return True + return False + +def basicTest1(): + start = {"x": 0, "y": 0} + end = {"x": 8, "y": 8} + snakes = [ + { + "id": "snake-508e96ac-94ad-11ea-bb37", + "name": "My Snake", + "health": 54, + "body": [ + {"x": 0, "y": 0}, + {"x": 1, "y": 0}, + {"x": 2, "y": 0} + ], + "latency": "111", + "head": {"x": 0, "y": 0}, + "length": 3, + "shout": "why are we shouting??", + "customizations":{ + "color":"#FF0000", + "head":"pixel", + "tail":"pixel" + } + }, + { + "id": "snake-b67f4906-94ae-11ea-bb37", + "name": "Another Snake", + "health": 16, + "body": [ + {"x": 5, "y": 4}, + {"x": 5, "y": 3}, + {"x": 6, "y": 3}, + {"x": 6, "y": 2} + ], + "latency": "222", + "head": {"x": 5, "y": 4}, + "length": 4, + "shout": "I'm not really sure...", + "customizations":{ + "color":"#26CF04", + "head":"silly", + "tail":"curled" + } + } + ] + + obstacles = [] + for snake in snakes: + for position in snake["body"]: + obstacles.append(Location(position["x"],position["y"])) + startLoc = Location(start["x"], start["y"]) + endLoc = Location(end["x"], end["y"]) + + print(findapath(startLoc, endLoc, obstacles)) + + +if __name__ == "__main__": + # peterpath.py is being run directly + from location import Location + + # run tests + basicTest1() +else: + # peterpath.py is being imported into another script + from .location import Location \ No newline at end of file diff --git a/pathfinding/location.py b/pathfinding/location.py new file mode 100644 index 0000000..b89c80c --- /dev/null +++ b/pathfinding/location.py @@ -0,0 +1,16 @@ +class Location: + def __init__(self, x, y) -> None: + self.x = x + self.y = y + def __repr__(self) -> str: + retString = "(" + str(self.x) + ", " + str(self.y) + ")" + return retString + def __eq__(self, object) -> bool: + if self.x == object.x and self.y == object.y: + return True + else: + return False + def __hash__(self): + return self.x*1000 + self.y + def get_distance(self, location): + return abs(self.x - location.x) + abs(self.y - location.y) \ No newline at end of file diff --git a/pathfinding/patrickpath.py b/pathfinding/patrickpath.py deleted file mode 100644 index 3b24bc8..0000000 --- a/pathfinding/patrickpath.py +++ /dev/null @@ -1,113 +0,0 @@ -class Location: - def __init__(self, x, y) -> None: - self.x = x - self.y = y - def __repr__(self) -> str: - retString = "(" + str(self.x) + ", " + str(self.y) + ")" - return retString - def __eq__(self, object) -> bool: - if self.x == object.x and self.y == object.y: - return True - else: - return False - def get_distance(self, location): - return abs(self.x - location.x) + abs(self.y - location.y) - - -class PathEntry: - def __init__(self, snake, history) -> None: - self.snake = snake - self.history = history - def get_cost(self, destination): - return self.snake[0].get_distance(destination) + len(self.history) - -class PathFinder: - def __init__(self, snake, destination, obstacles, width, height) -> None: - self.data = [] - self.snake = snake - self.destination = destination - self.obstacles = obstacles - self.width = width - self.height = height - - def addPathData(self, oldEntry, newlocation): - if (self.isValidLocation(newlocation)): - visited = False - for oldLoc in oldEntry.history: - if newlocation == oldLoc: - visited = True - if not visited: - newSnake = oldEntry.snake.copy() - newSnake.insert(0, newlocation) - newSnake.pop(len(newSnake)-1) - newHistory = oldEntry.history.copy() - newHistory.append(oldEntry.snake[0]) - newEntry = PathEntry(newSnake, newHistory) - self.data.append(newEntry) - - def findthepath(self): - startEntry = PathEntry(self.snake, []) - self.data.append(startEntry) - - while len(self.data) > 0: - current = self.data.pop(0) - if current.snake[0] == self.destination: - current.history.append(self.destination) - # print("Found a path") - return current.history - break - - uplocation = Location(current.snake[0].x, current.snake[0].y+1) - downlocation = Location(current.snake[0].x, current.snake[0].y-1) - leftlocation = Location(current.snake[0].x-1, current.snake[0].y) - rightlocation = Location(current.snake[0].x+1, current.snake[0].y) - - self.addPathData(current, uplocation) - self.addPathData(current, downlocation) - self.addPathData(current, leftlocation) - self.addPathData(current, rightlocation) - - def isValidLocation(self, location): - if location.x < 0 or location.x > self.width-1: - return False - if location.y < 0 or location.y > self.height-1: - return False - for obstacles in self.obstacles: - if obstacles == location: - return False - for i in range(len(self.snake)-1): - if self.snake[i] == location: - return False - return True - def isVisited(self, newlocation): - for location in self.visited: - if location == newlocation: - return True - return False - -def basicTest1(): - snake = [ - Location(0,0), - Location(1,0), - Location(2,0), - Location(3,0) - ] - destination = Location(8,8) - obstacles = [ - Location(4, 0), - Location(4, 1), - Location(4, 2), - Location(4, 3), - Location(4, 4), - Location(4, 5), - Location(4, 6), - Location(4, 7), - Location(4, 8), - Location(4, 9) - ] - finder = PathFinder(snake, destination, obstacles, 11, 11) - - print(finder.findthepath()) - -if __name__ == "__main__": - basicTest1() diff --git a/pathfinding/peterpath.py b/pathfinding/peterpath.py index 7d23ea0..605a2cb 100644 --- a/pathfinding/peterpath.py +++ b/pathfinding/peterpath.py @@ -1,19 +1,4 @@ -class Location: - def __init__(self, x, y) -> None: - self.x = x - self.y = y - def __repr__(self) -> str: - retString = "(" + str(self.x) + ", " + str(self.y) + ")" - return retString - def __eq__(self, object) -> bool: - if self.x == object.x and self.y == object.y: - return True - else: - return False - def get_distance(self, location): - return abs(self.x - location.x) + abs(self.y - location.y) - - + class PathEntry: def __init__(self, location) -> None: self.location = location @@ -95,4 +80,11 @@ def basicTest1(): finder.findthepath() if __name__ == "__main__": + # peterpath.py is being run directly + from location import Location + + # run tests basicTest1() +else: + # peterpath.py is being imported into another script + from .location import Location \ No newline at end of file diff --git a/snake_daddy2.json b/snake_daddy2.json new file mode 100644 index 0000000..e5a2e68 --- /dev/null +++ b/snake_daddy2.json @@ -0,0 +1,306 @@ +{ + "game": { + "id": "424fda0c-600c-494b-8f06-89e65b14f558", + "ruleset": { + "name": "standard", + "version": "v1.2.3", + "settings": { + "foodSpawnChance": 15, + "minimumFood": 1, + "hazardDamagePerTurn": 0, + "hazardMap": "", + "hazardMapAuthor": "", + "royale": { + "shrinkEveryNTurns": 0 + }, + "squad": { + "allowBodyCollisions": false, + "sharedElimination": false, + "sharedHealth": false, + "sharedLength": false + } + } + }, + "map": "standard", + "timeout": 500, + "source": "custom" + }, + "turn": 150, + "board": { + "height": 11, + "width": 11, + "snakes": [ + { + "id": "gs_3gvjhdYcxpvpQ8BtVKBh4yq3", + "name": "DaddySnake", + "latency": "78", + "health": 100, + "body": [ + { + "x": 10, + "y": 0 + }, + { + "x": 10, + "y": 1 + }, + { + "x": 10, + "y": 2 + }, + { + "x": 10, + "y": 3 + }, + { + "x": 9, + "y": 3 + }, + { + "x": 9, + "y": 4 + }, + { + "x": 9, + "y": 5 + }, + { + "x": 9, + "y": 6 + }, + { + "x": 8, + "y": 6 + }, + { + "x": 8, + "y": 5 + }, + { + "x": 8, + "y": 4 + }, + { + "x": 8, + "y": 3 + }, + { + "x": 8, + "y": 3 + } + ], + "head": { + "x": 10, + "y": 0 + }, + "length": 13, + "shout": "", + "squad": "", + "customizations": { + "color": "#0066ff", + "head": "bendr", + "tail": "curled" + } + }, + { + "id": "gs_YQrrkbTbDTbHtcKcRx84QMtG", + "name": "test_daddy", + "latency": "287", + "health": 100, + "body": [ + { + "x": 0, + "y": 10 + }, + { + "x": 1, + "y": 10 + }, + { + "x": 2, + "y": 10 + }, + { + "x": 3, + "y": 10 + }, + { + "x": 4, + "y": 10 + }, + { + "x": 5, + "y": 10 + }, + { + "x": 5, + "y": 9 + }, + { + "x": 5, + "y": 8 + }, + { + "x": 5, + "y": 7 + }, + { + "x": 5, + "y": 6 + }, + { + "x": 4, + "y": 6 + }, + { + "x": 3, + "y": 6 + }, + { + "x": 2, + "y": 6 + }, + { + "x": 1, + "y": 6 + }, + { + "x": 1, + "y": 7 + }, + { + "x": 1, + "y": 8 + }, + { + "x": 1, + "y": 9 + }, + { + "x": 0, + "y": 9 + }, + { + "x": 0, + "y": 9 + } + ], + "head": { + "x": 0, + "y": 10 + }, + "length": 19, + "shout": "", + "squad": "", + "customizations": { + "color": "#0066ff", + "head": "bendr", + "tail": "curled" + } + } + ], + "food": [ + { + "x": 7, + "y": 1 + } + ], + "hazards": [] + }, + "you": { + "id": "gs_YQrrkbTbDTbHtcKcRx84QMtG", + "name": "test_daddy", + "latency": "287", + "health": 100, + "body": [ + { + "x": 0, + "y": 10 + }, + { + "x": 1, + "y": 10 + }, + { + "x": 2, + "y": 10 + }, + { + "x": 3, + "y": 10 + }, + { + "x": 4, + "y": 10 + }, + { + "x": 5, + "y": 10 + }, + { + "x": 5, + "y": 9 + }, + { + "x": 5, + "y": 8 + }, + { + "x": 5, + "y": 7 + }, + { + "x": 5, + "y": 6 + }, + { + "x": 4, + "y": 6 + }, + { + "x": 3, + "y": 6 + }, + { + "x": 2, + "y": 6 + }, + { + "x": 1, + "y": 6 + }, + { + "x": 1, + "y": 7 + }, + { + "x": 1, + "y": 8 + }, + { + "x": 1, + "y": 9 + }, + { + "x": 0, + "y": 9 + }, + { + "x": 0, + "y": 9 + } + ], + "head": { + "x": 0, + "y": 10 + }, + "length": 19, + "shout": "", + "squad": "", + "customizations": { + "color": "#0066ff", + "head": "bendr", + "tail": "curled" + } + } +} \ No newline at end of file diff --git a/snake_daddy2.py b/snake_daddy2.py new file mode 100644 index 0000000..5188b0d --- /dev/null +++ b/snake_daddy2.py @@ -0,0 +1,232 @@ +import random +import typing +from pathfinding.astarpath import Location +from pathfinding.astarpath import AStarFinder +from pathfinding.astarpath import find_astar_path +import json +from pathlib import Path +import time +import logging +import os + +author_name = "daddy2" + +def info(): + return { + "apiversion": "1", + "author": author_name, # TODO: Your Battlesnake Username + "color": "#9933ff", # TODO: Choose color + "head": "tongue", # TODO: Choose head + "tail": "round-bum", # TODO: Choose tail + } + +def get_next_move(current, next_loc): + if next_loc.x > current.x: + return "right" + elif next_loc.x < current.x: + return "left" + elif next_loc.y > current.y: + return "up" + else: + return "down" + +def get_longest_destination(): + global startLoc, board_width, board_height + # Get longest destination if there is no food + destinations = [ + Location(0, 0), + Location(board_width - 1, 0), + Location(board_width - 1, board_height - 1), + Location(0, board_height - 1) ] + + longest_index = 0 + for i in range(len(destinations)): + if startLoc.get_distance(destinations[i]) > startLoc.get_distance(destinations[shortest_index]): + longest_index = i + + return destinations[longest_index] + +def get_distance(dict_loc1, dict_loc2): + return abs(dict_loc1['x'] - dict_loc2['x']) + abs(dict_loc1['y'] - dict_loc2['y']) + +def is_adjacent_to_bigger_snakes(location): + global my_snake_name, my_body, opponents + adjacents = [ + Location(location.x, location.y+1), + Location(location.x, location.y-1), + Location(location.x-1, location.y), + Location(location.x+1, location.y) + ] + for snake in opponents: + if snake["name"] != my_snake_name: + head = Location(snake["body"][0]["x"], snake["body"][0]["y"]) + for adj in adjacents: + if adj == head and len(snake["body"]) >= len(my_body): + return True + return False + +# move is called on every turn and returns your next move +# Valid moves are "up", "down", "left", or "right" +# See https://docs.battlesnake.com/api/example-move for available data +def move(game_state: typing.Dict) -> typing.Dict: + start_time = time.time() + deployment_mode = os.environ.get("deployment_mode") + if deployment_mode != "production": + logFileName = "logs/turn_" + str(game_state["turn"]) + ".json" + logFilePath = Path(__file__).parent / logFileName + json_file = open(logFilePath, "w") + json.dump(game_state, json_file, indent=4) + json_file.close() + + global my_head, my_body, my_snake_name, startLoc + my_head = game_state["you"]["body"][0] # Coordinates of your head + my_snake_name = game_state["you"]["name"] + startLoc = Location(my_head["x"],my_head["y"]) + global board_width, board_height, opponents + board_width = game_state['board']['width'] + board_height = game_state['board']['height'] + my_body = game_state['you']['body'] + opponents = game_state['board']['snakes'] + + # move_rank will be used to pick the best move to return + # higher numer means better move + # 0 mean not safe at all + move_rank = { + "up": 5, + "down": 5, + "left": 5, + "right": 5 + } + + # Calculate obstacles + obstacles = [] + my_snake = [] + opponent_tails = [] + for snake in opponents: + # Only add the other snakes as Obstacles + if snake["name"] != my_snake_name: + # the reason for the "-1" is that + # the tail of the snakes are not really obstacles + for i in range(len(snake["body"]) - 1): + obstacles.append(Location(snake["body"][i]["x"],snake["body"][i]["y"])) + # add the tails of every other snakes + opponent_tails.append(Location(snake["body"][len(snake["body"]) - 1]["x"],snake["body"][len(snake["body"]) - 1]["y"])) + + for part in my_body: + my_snake.append(Location(part["x"], part["y"])) + + print("obstacles: ", obstacles) + + print("elapsed time: ", (time.time() - start_time) * 1000) + + # Find a possible path + print("Start path finding ...") + next_move = "unknown" + + # First try to make a run to the nearest food + foods = game_state['board']['food'] + destinations = [] + if (len(foods) > 0): + nearest_food = foods[0] + for food in foods: + if (get_distance(food, my_head) < get_distance(nearest_food, my_head)): + nearest_food = food + + food_destination = Location(nearest_food['x'], nearest_food['y']) + food_path = find_astar_path(my_snake, food_destination, obstacles) + elapsed_time = (time.time() - start_time) * 1000 + print("elapsed time: ", elapsed_time) + if (food_path != None and len(food_path) > 1): + print("Found food path: ", food_path) + # Build a future snake + future_snake = [] + future_snake_length = len(my_snake)+1 + for i in range (1, len(food_path)): + future_snake.append(food_path[len(food_path) - i]) + if len(future_snake) == future_snake_length: + break + + if len(future_snake) < future_snake_length: + remainder = future_snake_length - len(food_path) + for i in range(0, remainder): + future_snake.append(my_snake[i]) + logging.debug("Current snake: %s", my_snake) + logging.debug("Future snake: %s", future_snake) + future_tail_path = find_astar_path(future_snake, future_snake[len(future_snake)-1], obstacles) + if (future_tail_path != None and len(future_tail_path) > 1): + print("New path can chase tail") + # only set next_move when the found path can chase tail + next_move = get_next_move(food_path[0], food_path[1]) + + if next_move == "unknown": + # if we cannot get to a nearest food, then we will chase our tail + tail_destination = my_snake[len(my_snake)-1] + tail_finder = AStarFinder(my_snake, tail_destination, obstacles, board_width, board_height, 2) + tail_path = tail_finder.findthepath() + if (tail_path != None and len(tail_path) > 1): + print("Found tail path: ", tail_path) + next_move = get_next_move(tail_path[0], tail_path[1]) + + if next_move == "unknown": + # if we cannot chase our tail, then we will chase other snake's tail + for tail_destination in opponent_tails: + tail_finder = AStarFinder(my_snake, tail_destination, obstacles, board_width, board_height, 2) + tail_path = tail_finder.findthepath() + if (tail_path != None and len(tail_path) > 1): + print("Found opponent tail path: ", tail_path) + next_move = get_next_move(tail_path[0], tail_path[1]) + break + + if (next_move != "unknown"): + move_rank[next_move] += 1 + + up_location = Location(startLoc.x, startLoc.y + 1) + down_location = Location(startLoc.x, startLoc.y - 1) + left_location = Location(startLoc.x - 1, startLoc.y) + right_location = Location(startLoc.x + 1, startLoc.y) + # if no path is found then we fall back to a random safe move + if (my_head['x'] == 0): + move_rank["left"] = 0 + if (my_head['x'] == board_width-1): + move_rank["right"] = 0 + if (my_head['y'] == 0): + move_rank["down"] = 0 + if (my_head['y'] == board_height-1): + move_rank["up"] = 0 + + # Add my snake to the list of obstacle, but not the tail + for i in range(len(my_snake)-1): + obstacles.append(my_snake[i]) + for obs in obstacles: + if (obs == up_location): + move_rank["up"] = 0 + if (obs == down_location): + move_rank["down"] = 0 + if (obs == left_location): + move_rank["left"] = 0 + if (obs == right_location): + move_rank["right"] = 0 + + if is_adjacent_to_bigger_snakes(up_location): + move_rank["up"] -= 2 + if is_adjacent_to_bigger_snakes(down_location): + move_rank["down"] -= 2 + if is_adjacent_to_bigger_snakes(left_location): + move_rank["left"] -= 2 + if is_adjacent_to_bigger_snakes(right_location): + move_rank["right"] -= 2 + + # pick our final move + next_move = max(move_rank, key=move_rank.get) + + print("elapsed time: ", (time.time() - start_time) * 1000) + return {"move": next_move} + +if __name__ == "__main__": + dataFileName = "snake_" + author_name +".json" + dataFilePath = Path(__file__).parent / dataFileName + rhandle = open(dataFilePath, "r") + + gamestate = json.load(rhandle) + rhandle.close() + print(move(gamestate)) \ No newline at end of file diff --git a/snake_peter.py b/snake_peter.py index 0bcae56..525e223 100644 --- a/snake_peter.py +++ b/snake_peter.py @@ -1,6 +1,6 @@ import random import typing -from pathfinding.peterpath import Location +from pathfinding.location import Location from pathfinding.peterpath import PathFinder import json from pathlib import Path