diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c785aeb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11.4-slim +# FROM continuumio/conda-ci-linux-64-python3.8 +# FROM continuumio/miniconda3 + +# Install app +COPY . /app +WORKDIR /app + +# Install dependencies +RUN pip install --upgrade pip && pip install -r requirements.txt +# RUN conda env create -f environment.yaml +# ENV CONDA_DEFAULT_ENV battlesnake + +# Run Battlesnake +ENTRYPOINT [ "python", "server.py" ] +CMD [ "StarterSnake" ] \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..291d71b --- /dev/null +++ b/server.py @@ -0,0 +1,62 @@ +import argparse +import logging +import os +import typing + +from flask import Flask +from flask import request + +from snakes.snake import Snake + + +def run_server(snake: Snake, host: str, port: int): + app = Flask("Battlesnake") + + @app.get("/") + def on_info(): + return snake.info() + + @app.post("/start") + def on_start(): + game_state = request.get_json() + snake.start(game_state) + return "ok" + + @app.post("/move") + def on_move(): + game_state = request.get_json() + return snake.move(game_state) + + @app.post("/end") + def on_end(): + game_state = request.get_json() + snake.end(game_state) + return "ok" + + @app.after_request + def identify_server(response): + response.headers.set( + "server", "shillerben/gitea/starter-snake-python" + ) + return response + + logging.getLogger("werkzeug").setLevel(logging.ERROR) + + print(f"\nRunning Battlesnake at http://{host}:{port}") + app.run(host=host, port=port) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-b", "--bind", default="0.0.0.0", help="host ip to listen on") + parser.add_argument("-p", "--port", type=int, default=8000, help="host port to listen on") + parser.add_argument("snake", help="snake to run") + args = parser.parse_args() + + host = args.bind + port = args.port + snake_name = args.snake + + snake = Snake.SNAKES[snake_name]() + + run_server(snake, host, port) diff --git a/snakes/__init__.py b/snakes/__init__.py new file mode 100644 index 0000000..e9953d5 --- /dev/null +++ b/snakes/__init__.py @@ -0,0 +1,2 @@ +from .snake import Snake +from .starter_snake import StarterSnake \ No newline at end of file diff --git a/snakes/snake.py b/snakes/snake.py new file mode 100644 index 0000000..2b535ed --- /dev/null +++ b/snakes/snake.py @@ -0,0 +1,32 @@ +import typing + + +class Snake: + + SNAKES: typing.Dict[str, typing.Type["Snake"]] = {} + + def __init_subclass__(cls) -> None: + cls.SNAKES[cls.__name__] = cls + + def info(self) -> typing.Dict: + '''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 + ''' + raise NotImplementedError() + + def start(self, game_state: typing.Dict) -> None: + '''start is called when your Battlesnake begins a game''' + raise NotImplementedError() + + def end(self, game_state: typing.Dict) -> None: + '''end is called when your Battlesnake finishes a game''' + raise NotImplementedError() + + def move(self, game_state: typing.Dict) -> typing.Dict: + '''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 + ''' + raise NotImplementedError() diff --git a/snakes/starter_snake.py b/snakes/starter_snake.py new file mode 100644 index 0000000..e70438f --- /dev/null +++ b/snakes/starter_snake.py @@ -0,0 +1,74 @@ + +import random +import typing + +from .snake import Snake + + +class StarterSnake(Snake): + '''Snake from Battlesnake's starter snake repo''' + + def info(self) -> typing.Dict: + print("INFO") + + return { + "apiversion": "1", + "author": "", # TODO: Your Battlesnake Username + "color": "#888888", # TODO: Choose color + "head": "default", # TODO: Choose head + "tail": "default", # TODO: Choose tail + } + + def start(self, game_state: typing.Dict): + print("GAME START") + + def end(self, game_state: typing.Dict): + print("GAME OVER\n") + + def move(self, game_state: typing.Dict) -> typing.Dict: + is_move_safe = {"up": True, "down": True, "left": True, "right": True} + + # We've included code to prevent your Battlesnake from moving backwards + my_head = game_state["you"]["body"][0] # Coordinates of your head + my_neck = game_state["you"]["body"][1] # Coordinates of your "neck" + + if my_neck["x"] < my_head["x"]: # Neck is left of head, don't move left + is_move_safe["left"] = False + + elif my_neck["x"] > my_head["x"]: # Neck is right of head, don't move right + is_move_safe["right"] = False + + elif my_neck["y"] < my_head["y"]: # Neck is below head, don't move down + is_move_safe["down"] = False + + elif my_neck["y"] > my_head["y"]: # Neck is above head, don't move up + is_move_safe["up"] = False + + # TODO: Step 1 - Prevent your Battlesnake from moving out of bounds + # board_width = game_state['board']['width'] + # board_height = game_state['board']['height'] + + # TODO: Step 2 - Prevent your Battlesnake from colliding with itself + # my_body = game_state['you']['body'] + + # TODO: Step 3 - Prevent your Battlesnake from colliding with other Battlesnakes + # opponents = game_state['board']['snakes'] + + # Are there any safe moves left? + safe_moves = [] + for move, isSafe in is_move_safe.items(): + if isSafe: + safe_moves.append(move) + + if len(safe_moves) == 0: + print(f"MOVE {game_state['turn']}: No safe moves detected! Moving down") + return {"move": "down"} + + # Choose a random move from the safe ones + next_move = random.choice(safe_moves) + + # TODO: Step 4 - Move towards food instead of random, to regain health and survive longer + # food = game_state['board']['food'] + + print(f"MOVE {game_state['turn']}: {next_move}") + return {"move": next_move}