diff --git a/.gitignore b/.gitignore index 45992d2e1..368a85b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.DS_Store *.pyc -battlesnake -.idea \ No newline at end of file +*.venv +__pycache__/ +game_state_example.txt +run_local_game.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..8deae9929 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10.6-slim + +# Install app +COPY . /usr/app +WORKDIR /usr/app + +# Install dependencies +RUN pip install --upgrade pip && pip install -r requirements.txt + +# Run Battlesnake +CMD [ "python", "main.py" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..ea13dce2d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Battlesnake Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Procfile b/Procfile deleted file mode 100644 index b671479df..000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: gunicorn app.main:application --worker-class gevent diff --git a/README.md b/README.md index 3b905c8b7..35d90831c 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,59 @@ -# battlesnake-python +# Battlesnake Python Starter Project -A simple [BattleSnake AI](http://battlesnake.io) written in Python. +An official Battlesnake template written in Python. Get started at [play.battlesnake.com](https://play.battlesnake.com). -Visit [battlesnake.io/readme](http://battlesnake.io/readme) for API documentation and instructions for running your AI. +![Battlesnake Logo](https://media.battlesnake.com/social/StarterSnakeGitHubRepos_Python.png) -This AI client uses the [bottle web framework](http://bottlepy.org/docs/dev/index.html) to serve requests and the [gunicorn web server](http://gunicorn.org/) for running bottle on Heroku. Dependencies are listed in [requirements.txt](requirements.txt). +This project is a great starting point for anyone wanting to program their first Battlesnake in Python. It can be run locally or easily deployed to a cloud provider of your choosing. See the [Battlesnake API Docs](https://docs.battlesnake.com/api) for more detail. -[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) +[![Run on Replit](https://repl.it/badge/github/BattlesnakeOfficial/starter-snake-python)](https://replit.com/@Battlesnake/starter-snake-python) -#### You will need... +## Technologies Used -* a working Python 2.7 development environment ([getting started guide](http://hackercodex.com/guide/python-development-environment-on-mac-osx/)) -* experience [deploying Python apps to Heroku](https://devcenter.heroku.com/articles/getting-started-with-python#introduction) -* [pip](https://pip.pypa.io/en/latest/installing.html) to install Python dependencies +This project uses [Python 3](https://www.python.org/) and [Flask](https://flask.palletsprojects.com/). It also comes with an optional [Dockerfile](https://docs.docker.com/engine/reference/builder/) to help with deployment. -## Running the Snake Locally +## Run Your Battlesnake -1) [Fork this repo](https://github.com/sendwithus/battlesnake-python/fork). +Install dependencies using pip -2) Clone repo to your development environment: -``` -git clone git@github.com:username/battlesnake-python.git -``` - -3) Install dependencies using [pip](https://pip.pypa.io/en/latest/installing.html): -``` +```sh pip install -r requirements.txt ``` -4) Run local server: -``` -python app/main.py -``` +Start your Battlesnake -5) Test client in your browser: [http://localhost:8080](http://localhost:8080). +```sh +python main.py +``` -## Deploying to Heroku +You should see the following output once it is running -1) Create a new Heroku app: -``` -heroku create [APP_NAME] +```sh +Running your Battlesnake at http://0.0.0.0:8000 + * Serving Flask app 'My Battlesnake' + * Debug mode: off ``` -2) Deploy code to Heroku servers: -``` -git push heroku master -``` +Open [localhost:8000](http://localhost:8000) in your browser and you should see -3) Open Heroku app in browser: +```json +{"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"} ``` -heroku open -``` -or visit [http://APP_NAME.herokuapp.com](http://APP_NAME.herokuapp.com). -4) View server logs with the `heroku logs` command: -``` -heroku logs --tail +## Play a Game Locally + +Install the [Battlesnake CLI](https://github.com/BattlesnakeOfficial/rules/tree/main/cli) +* You can [download compiled binaries here](https://github.com/BattlesnakeOfficial/rules/releases) +* or [install as a go package](https://github.com/BattlesnakeOfficial/rules/tree/main/cli#installation) (requires Go 1.18 or higher) + +Command to run a local game + +```sh +battlesnake play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser ``` -## Questions? +## Next Steps + +Continue with the [Battlesnake Quickstart Guide](https://docs.battlesnake.com/quickstart) to customize and improve your Battlesnake's behavior. -Email [battlesnake@sendwithus.com](mailto:battlesnake@sendwithus.com), or tweet [@send_with_us](http://twitter.com/send_with_us). +**Note:** To play games on [play.battlesnake.com](https://play.battlesnake.com) you'll need to deploy your Battlesnake to a live web server OR use a port forwarding tool like [ngrok](https://ngrok.com/) to access your server locally. diff --git a/app.json b/app.json deleted file mode 100644 index e5bd04c36..000000000 --- a/app.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "BattleSnake Python", - "description": "A simple BattleSnake AI written in Python.", - "repository": "https://github.com/sendwithus/battlesnake-python", - "website": "http://battlesnake.io", - "keywords": ["battlesnake","sendwithus"] -} diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/main.py b/app/main.py deleted file mode 100755 index c8f1e0e59..000000000 --- a/app/main.py +++ /dev/null @@ -1,49 +0,0 @@ -import bottle -import os -import random - - -@bottle.route('/static/') -def static(path): - return bottle.static_file(path, root='static/') - - -@bottle.post('/start') -def start(): - data = bottle.request.json - game_id = data['game_id'] - board_width = data['width'] - board_height = data['height'] - - head_url = '%s://%s/static/head.png' % ( - bottle.request.urlparts.scheme, - bottle.request.urlparts.netloc - ) - - # TODO: Do things with data - - return { - 'color': '#00FF00', - 'taunt': '{} ({}x{})'.format(game_id, board_width, board_height), - 'head_url': head_url, - 'name': 'battlesnake-python' - } - - -@bottle.post('/move') -def move(): - data = bottle.request.json - - # TODO: Do things with data - directions = ['up', 'down', 'left', 'right'] - - return { - 'move': random.choice(directions), - 'taunt': 'battlesnake-python!' - } - - -# Expose WSGI app (so gunicorn can find it) -application = bottle.default_app() -if __name__ == '__main__': - bottle.run(application, host=os.getenv('IP', '0.0.0.0'), port=os.getenv('PORT', '8080')) diff --git a/cloud9/run.sh b/cloud9/run.sh deleted file mode 100644 index 70f7cf78b..000000000 --- a/cloud9/run.sh +++ /dev/null @@ -1,3 +0,0 @@ -cd .. -echo -e "\n===== https://$C9_HOSTNAME =====\n" -gunicorn app.main:application --access-logfile - --worker-class gevent --bind "$IP:$PORT" diff --git a/main.py b/main.py new file mode 100644 index 000000000..9e5e99a5d --- /dev/null +++ b/main.py @@ -0,0 +1,56 @@ +# Welcome to +# __________ __ __ .__ __ +# \______ \_____ _/ |__/ |_| | ____ ______ ____ _____ | | __ ____ +# | | _/\__ \\ __\ __\ | _/ __ \ / ___// \\__ \ | |/ // __ \ +# | | \ / __ \| | | | | |_\ ___/ \___ \| | \/ __ \| <\ ___/ +# |________/(______/__| |__| |____/\_____>______>___|__(______/__|__\\_____> +# +# This file can be a nice home for your Battlesnake logic and helper functions. +# +# To get you started we've included code to prevent your Battlesnake from moving backwards. +# For more info see docs.battlesnake.com + +import random +import typing +from move import Move + +# 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() -> 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 + } + + +# start is called when your Battlesnake begins a game +def start(game_state: typing.Dict): + print("GAME START") + + +# end is called when your Battlesnake finishes a game +def end(game_state: typing.Dict): + print("GAME OVER\n") + + +# 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: + + bot = Move() + + return bot.choose_move(game_state) + + +# Start server when `python main.py` is run +if __name__ == "__main__": + from server import run_server + + run_server({"info": info, "start": start, "move": move, "end": end}) diff --git a/move.py b/move.py new file mode 100644 index 000000000..fd7f5fbea --- /dev/null +++ b/move.py @@ -0,0 +1,62 @@ +import random + +class Move(): + + def __init__(self): + + self.is_move_safe = {"up": {"is_safe": True}, + "down": {"is_safe": True}, + "left": {"is_safe": True}, + "right": {"is_safe": True}} + + def not_backward(self, game_state): + + # 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 + self.is_move_safe["left"]["is_safe"] = False + + elif my_neck["x"] > my_head["x"]: # Neck is right of head, don't move right + self.is_move_safe["right"]["is_safe"] = False + + elif my_neck["y"] < my_head["y"]: # Neck is below head, don't move down + self.is_move_safe["down"]["is_safe"] = False + + elif my_neck["y"] > my_head["y"]: # Neck is above head, don't move up + self.is_move_safe["up"]["is_safe"] = False + + def choose_move(self, game_state): + + self.not_backward(game_state) + + # Are there any safe moves left? + safe_moves = [] + for move , data in self.is_move_safe.items(): + + if data["is_safe"] == True: + + 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) + + return {"move": next_move} + +# 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'] + +# TODO: Step 4 - Move towards food instead of random, to regain health and survive longer +# food = game_state['board']['food'] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e24b73d21..5aad892a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1 @@ -bottle==0.12.9 -gevent==1.0.2 -greenlet==0.4.9 -gunicorn==19.4.5 +Flask==2.3.2 diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index fdf79660d..000000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-2.7.12 diff --git a/server.py b/server.py new file mode 100644 index 000000000..93f85adbb --- /dev/null +++ b/server.py @@ -0,0 +1,46 @@ +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/static/head.png b/static/head.png deleted file mode 100644 index 5ee3456ac..000000000 Binary files a/static/head.png and /dev/null differ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..090eecd7e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ + +#__init__.py \ No newline at end of file diff --git a/tests/test_not_backward.py b/tests/test_not_backward.py new file mode 100644 index 000000000..2b4c0eeca --- /dev/null +++ b/tests/test_not_backward.py @@ -0,0 +1,40 @@ + +import unittest +from unittest.mock import patch +from move import Move + +class TestNotBackward(unittest.TestCase): + + def test_not_down(self): + + game_state = {"you": {"id": "my", "head": {"x": 2, "y": 2} ,"body": [{"x": 2, "y": 2}, {"x": 2, "y": 3}, {"x": 2, "y": 4}],"length": 3}, + "board": {"snakes": [], "food": [{"x": 10, "y": 10}]}, + "turn": 1 + } + + bot = Move() + + bot.choose_move(game_state) + + is_safe = bot.is_move_safe["up"]["is_safe"] + + self.assertFalse(is_safe) + + def test_not_left(self): + + game_state = {"you": {"id": "my", "head": {"x": 2, "y": 2} ,"body": [{"x": 2, "y": 2}, {"x": 1, "y": 2}, {"x": 0, "y": 2}],"length": 3}, + "board": {"snakes": [], "food": [{"x": 10, "y": 10}]}, + "turn": 1 + } + + bot = Move() + + bot.choose_move(game_state) + + is_safe = bot.is_move_safe["left"]["is_safe"] + + self.assertFalse(is_safe) + +if __name__ == "__main__": + + unittest.main() \ No newline at end of file