diff --git a/template_snakeapi_service/Dockerfile b/template_snakeapi_service/Dockerfile new file mode 100644 index 0000000..b4bd57f --- /dev/null +++ b/template_snakeapi_service/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11-slim +WORKDIR /app + +RUN python -m pip install --upgrade pip && \ + pip install --no-cache-dir jupyter flask awscli flask_cors nbconvert nbformat + + +COPY ./entrypoint.sh . +COPY ./notebooks ./notebooks +COPY ./snakeapi_server.py . + +RUN chmod +x entrypoint.sh + +ENV PORT=8000 +EXPOSE 8000 + +CMD ["./entrypoint.sh"] diff --git a/template_snakeapi_service/entrypoint.sh b/template_snakeapi_service/entrypoint.sh new file mode 100644 index 0000000..a66bab1 --- /dev/null +++ b/template_snakeapi_service/entrypoint.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +NOTEBOOK_DIR="notebooks" +echo "Creating notebook directory: ${NOTEBOOK_DIR}" +mkdir -p "${NOTEBOOK_DIR}" + +# fetch latest notebook +echo "Syncing notebooks from S3 bucket..." +aws --endpoint-url "$AWS_ENDPOINT_URL_S3" --region "$AWS_REGION" \ + s3 sync "s3://$BUCKET_NAME/$INSTANCE_PREFIX/notebooks/" "${NOTEBOOK_DIR}/" + +# convert to Python script for dynamic import +echo "Finding the latest notebook..." +latest_ipynb=$(ls -t "${NOTEBOOK_DIR}"/*.ipynb | head -1) +if [ -n "$latest_ipynb" ]; then + echo "Running the latest notebook ${latest_ipynb} with Jupyter..." + jupyter nbconvert --to notebook --execute "$latest_ipynb" --stdout + # echo "Converting notebook ${latest_ipynb} to Python script..." + # jupyter nbconvert --to script "$latest_ipynb" --output "${NOTEBOOK_DIR}/notebook.py" +else + echo "No notebooks found in ${NOTEBOOK_DIR}." +fi + +# # start the Flask server +# echo "Starting the Flask server..." +# python snakeapi_server.py diff --git a/template_snakeapi_service/fly.toml b/template_snakeapi_service/fly.toml new file mode 100644 index 0000000..178ffc1 --- /dev/null +++ b/template_snakeapi_service/fly.toml @@ -0,0 +1,39 @@ +# fly.toml app configuration file generated for template-snake-api on 2025-04-30T12:40:47-07:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'snake-api-template' +primary_region = 'sea' +kill_signal = 'SIGINT' +kill_timeout = '5s' + +[build] + dockerfile = 'Dockerfile' + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[services]] + protocol = 'tcp' + internal_port = 8000 + + [[services.ports]] + port = 443 + handlers = ['tls', 'http'] + + [[services.ports]] + port = 80 + handlers = ['http'] + +[[vm]] + memory = '512mb' + cpu_kind = 'shared' + cpus = 1 + +[[metrics]] diff --git a/template_snakeapi_service/notebooks/notebook.ipynb b/template_snakeapi_service/notebooks/notebook.ipynb new file mode 100644 index 0000000..3ecd903 --- /dev/null +++ b/template_snakeapi_service/notebooks/notebook.ipynb @@ -0,0 +1,363 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "a9683468", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defaulting to user installation because normal site-packages is not writeable\n", + "Requirement already satisfied: flask in /home/student/.local/lib/python3.10/site-packages (2.3.2)\n", + "Requirement already satisfied: Jinja2>=3.1.2 in /home/student/.local/lib/python3.10/site-packages (from flask) (3.1.4)\n", + "Requirement already satisfied: itsdangerous>=2.1.2 in /home/student/.local/lib/python3.10/site-packages (from flask) (2.2.0)\n", + "Requirement already satisfied: Werkzeug>=2.3.3 in /home/student/.local/lib/python3.10/site-packages (from flask) (3.0.3)\n", + "Requirement already satisfied: click>=8.1.3 in /home/student/.local/lib/python3.10/site-packages (from flask) (8.1.7)\n", + "Requirement already satisfied: blinker>=1.6.2 in /home/student/.local/lib/python3.10/site-packages (from flask) (1.8.2)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /home/student/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)\n" + ] + } + ], + "source": [ + "!pip install flask" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aeee415d", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import os\n", + "import typing\n", + "import flask\n", + "\n", + "from flask import Flask\n", + "from flask import request\n", + "\n", + "print(\"Starting Battlesnake Server...\")\n", + "\n", + "def run_server(handlers: typing.Dict):\n", + " app = Flask(__name__)\n", + "\n", + " @app.get(\"/\")\n", + " def on_info():\n", + " print(\"info\")\n", + " return handlers[\"info\"]()\n", + "\n", + " @app.post(\"/start\")\n", + " def on_start():\n", + " print(\"go!\")\n", + " game_state = request.get_json()\n", + " handlers[\"start\"](game_state)\n", + " return \"ok\"\n", + "\n", + " @app.post(\"/move\")\n", + " def on_move():\n", + " game_state = request.get_json()\n", + " return handlers[\"move\"](game_state)\n", + "\n", + " @app.post(\"/end\")\n", + " def on_end():\n", + "# This isn't being called by the CLI atm\n", + "# print(request.get_json())\n", + "# end_game = request.get_json()\n", + "# print(end_game[\"death\"])\n", + " handlers[\"end\"](end_game)\n", + " return \"ok\"\n", + "\n", + " @app.after_request\n", + " def identify_server(response):\n", + " response.headers.set(\n", + " \"server\", \"battlesnake/github/starter-snake-python\"\n", + " )\n", + " return response\n", + "\n", + "\n", + " host = \"::\"\n", + " port = int(os.environ.get(\"PORT\", \"8000\"))\n", + "\n", + " logging.getLogger(\"werkzeug\").setLevel(logging.ERROR)\n", + "\n", + " print(f\"\\nRunning Battlesnake at http://{host}:{port}\")\n", + " app.run(host=host, port=port)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11a8ff03", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Welcome to\n", + "# __________ __ __ .__ __\n", + "# \\______ \\_____ _/ |__/ |_| | ____ ______ ____ _____ | | __ ____\n", + "# | | _/\\__ \\\\ __\\ __\\ | _/ __ \\ / ___// \\\\__ \\ | |/ // __ \\\n", + "# | | \\ / __ \\| | | | | |_\\ ___/ \\___ \\| | \\/ __ \\| <\\ ___/\n", + "# |________/(______/__| |__| |____/\\_____>______>___|__(______/__|__\\\\_____>\n", + "#\n", + "# This file can be a nice home for your Battlesnake logic and helper functions.\n", + "#\n", + "# To get you started we've included code to prevent your Battlesnake from moving backwards.\n", + "# For more info see docs.battlesnake.com\n", + "\n", + "import random\n", + "import typing\n", + "\n", + "# info is called when you create your Battlesnake on play.battlesnake.com\n", + "# and controls your Battlesnake's appearance\n", + "# TIP: If you open your Battlesnake URL in a browser you should see this data\n", + "def info() -> typing.Dict:\n", + " print(\"INFO\")\n", + "#MMMM CUSTOMIZATIONS\n", + " return {\n", + " \"apiversion\": \"1\",\n", + " \"author\": \"MaybeMadelyn\", # TODO: Your Battlesnake Username\n", + " \"color\": \"#d5dbdb\", # TODO: Choose color\n", + " \"head\": \"default\", # TODO: Choose head\n", + " \"tail\": \"default\", # TODO: Choose tail\n", + " }\n", + "\n", + "\n", + "# start is called when your Battlesnake begins a game\n", + "def start(game_state: typing.Dict):\n", + " global start_snake_count\n", + " start_snake_count = len(game_state['board']['snakes'])\n", + " print(\"GAME START with \", start_snake_count,\" snakes\")\n", + "\n", + "\n", + "# end is called when your Battlesnake finishes a game\n", + "# this is not happening right now :/\n", + "def end(end_game: typing.Dict):\n", + "# ending = end_game['death']\n", + " print(\"GAME OVER. You died by: \",ending) \n", + "\n", + " \n", + "def search(targets, my_head, move_points, points):\n", + " if targets:\n", + " closest = targets[0]\n", + " distance = 100\n", + " \n", + " for target in targets:\n", + " x_distance = abs(my_head[\"x\"] - target[\"x\"])\n", + " y_distance = abs(my_head[\"y\"] - target[\"y\"])\n", + " \n", + " if x_distance + y_distance < distance:\n", + " closest = target\n", + " distance = x_distance + y_distance\n", + " \n", + " if closest[\"x\"] < my_head[\"x\"]:\n", + " move_points[\"left\"] += points\n", + " if closest[\"x\"] > my_head[\"x\"]:\n", + " move_points[\"right\"] += points\n", + " if closest[\"y\"] < my_head[\"y\"]:\n", + " move_points[\"down\"] += points\n", + " if closest[\"y\"] > my_head[\"y\"]:\n", + " move_points[\"up\"] += points\n", + " \n", + " \n", + "def search_head(targets, my_head, move_points, points):\n", + " if targets:\n", + " closest = targets[0]\n", + " distance = 100\n", + " \n", + " for target in targets:\n", + " x_distance = abs(my_head[\"x\"] - target[\"x\"])\n", + " y_distance = abs(my_head[\"y\"] - target[\"y\"])\n", + " \n", + " if x_distance + y_distance < distance:\n", + " closest = target\n", + " distance = x_distance + y_distance\n", + " \n", + " if closest[\"x\"] < my_head[\"x\"]:\n", + " move_points[\"left\"] += points\n", + " if closest[\"x\"] > my_head[\"x\"]:\n", + " move_points[\"right\"] += points\n", + " if closest[\"y\"] < my_head[\"y\"]:\n", + " move_points[\"down\"] += points\n", + " if closest[\"y\"] > my_head[\"y\"]:\n", + " move_points[\"up\"] += points\n", + " \n", + "\n", + " search(game_state[\"board\"][\"food\"], my_head, move_points, 20)\n", + " search(game_state[\"board\"][\"snakes\"], my_head, move_points, 20) \n", + "#mmmm FLOODFILL FUNCTION\n", + "def floodfill(snakes,start):\n", + " empty_spaces = 0\n", + " visited = []\n", + " to_visit = [start] \n", + " unsafe = []\n", + " for snake in snakes:\n", + " for body_part in snake[\"body\"]:\n", + " unsafe.append(body_part) #mmmm APPEND means adding something to the list\n", + " in_bounds = True\n", + " while to_visit: \n", + " in_bounds = not(to_visit[0][\"y\"] < 0 or to_visit[0][\"y\"] > 10 or to_visit[0][\"x\"] < 0 or to_visit[0][\"x\"] > 10)\n", + " if to_visit[0] not in unsafe and in_bounds and to_visit[0] not in visited:\n", + " empty_spaces += 1\n", + " to_visit.append({\"x\": to_visit[0][\"x\"] +1, \"y\": to_visit[0][\"y\"]})\n", + " to_visit.append({\"x\": to_visit[0][\"x\"] -1, \"y\": to_visit[0][\"y\"]})\n", + " to_visit.append({\"x\": to_visit[0][\"x\"], \"y\": to_visit[0][\"y\"] +1})\n", + " to_visit.append({\"x\": to_visit[0][\"x\"], \"y\": to_visit[0][\"y\"] -1})\n", + " \n", + " visited.append(to_visit[0])\n", + " to_visit.pop(0)\n", + " \n", + " return empty_spaces\n", + "\n", + " \n", + "# move is called on every turn and returns your next move\n", + "# Valid moves are \"up\", \"down\", \"left\", or \"right\"\n", + "# See https://docs.battlesnake.com/api/example-move for available data\n", + "def move(game_state: typing.Dict) -> typing.Dict:\n", + "\n", + " move_points = {\"up\": 10, \"down\": 10, \"left\": 10, \"right\": 10}\n", + " moves = [\"up\", \"down\", \"left\", \"right\"]\n", + "\n", + " # We've included code to prevent your Battlesnake from moving backwards\n", + " my_head = game_state[\"you\"][\"body\"][0] # Coordinates of your head\n", + " my_neck = game_state[\"you\"][\"body\"][1] # Coordinates of your \"neck\"\n", + " my_body = game_state['you']['body']\n", + "\n", + " if my_neck[\"x\"] < my_head[\"x\"]: # Neck is left of head, don't move left\n", + " move_points[\"left\"] = 0\n", + "\n", + " elif my_neck[\"x\"] > my_head[\"x\"]: # Neck is right of head, don't move right\n", + " move_points[\"right\"] = 0\n", + "\n", + " elif my_neck[\"y\"] < my_head[\"y\"]: # Neck is below head, don't move down\n", + " move_points[\"down\"] = 0\n", + "\n", + " elif my_neck[\"y\"] > my_head[\"y\"]: # Neck is above head, don't move up\n", + " move_points[\"up\"] = 0\n", + "\n", + " #MMMM MAKE SURE YOU DONT HIT THE WALL\n", + "\n", + " game_height = game_state[\"board\"][\"height\"]\n", + " game_width = game_state[\"board\"][\"width\"]\n", + " \n", + " \n", + " if my_head[\"y\"] == game_height - 1:\n", + " move_points[\"up\"] -= 1000\n", + " \n", + " if my_head[\"y\"] == 0:\n", + " move_points[\"down\"] -= 1000\n", + " \n", + " if my_head[\"x\"] == game_width - 1:\n", + " move_points[\"right\"] -= 1000\n", + " print(\"DONT GO RIGHT!\")\n", + " print(f\"Right points: {move_points['right']}\")\n", + "\n", + " if my_head[\"x\"] == 0:\n", + " move_points[\"left\"] -= 1000\n", + " \n", + " \n", + " #MMMM DONT RUN INTO YOURSELF\n", + " \n", + " for body_part in my_body:\n", + " if body_part[\"y\"] == my_head[\"y\"]+1 and body_part[\"x\"] == my_head[\"x\"]:\n", + " move_points[\"up\"] -= 1000\n", + " \n", + " if body_part[\"y\"] == my_head[\"y\"]-1 and body_part[\"x\"] == my_head[\"x\"]:\n", + " move_points[\"down\"] -= 1000\n", + " \n", + " if body_part[\"x\"] == my_head[\"x\"]+1 and body_part[\"y\"] == my_head[\"y\"]:\n", + " move_points[\"right\"] -= 1000\n", + " \n", + " if body_part[\"x\"] == my_head[\"x\"]-1 and body_part[\"y\"] == my_head[\"y\"]:\n", + " move_points[\"left\"] -= 1000\n", + " \n", + " #MMMM DONT RUN INTO OTHER SNAKES\n", + " \n", + " all_snakes = game_state[\"board\"][\"snakes\"]\n", + " for snakes in all_snakes:\n", + " for body_part in snakes[\"body\"]:\n", + " if body_part[\"y\"] == my_head[\"y\"]+1 and body_part[\"x\"] == my_head[\"x\"]:\n", + " move_points[\"up\"] -= 1000\n", + "\n", + " if body_part[\"y\"] == my_head[\"y\"]-1 and body_part[\"x\"] == my_head[\"x\"]:\n", + " move_points[\"down\"] -= 1000\n", + "\n", + " if body_part[\"x\"] == my_head[\"x\"]+1 and body_part[\"y\"] == my_head[\"y\"]:\n", + " move_points[\"right\"] -= 1000\n", + "\n", + " if body_part[\"x\"] == my_head[\"x\"]-1 and body_part[\"y\"] == my_head[\"y\"]:\n", + " move_points[\"left\"] -= 1000\n", + " \n", + " move_points[\"right\"] += floodfill(all_snakes, {\"x\": my_head[\"x\"] +1, \"y\": my_head[\"y\"]})\n", + " move_points[\"left\"] += floodfill(all_snakes, {\"x\": my_head[\"x\"] -1, \"y\": my_head[\"y\"]})\n", + " move_points[\"up\"] += floodfill(all_snakes, {\"x\": my_head[\"x\"] , \"y\": my_head[\"y\"] +1})\n", + " move_points[\"down\"] += floodfill(all_snakes, {\"x\": my_head[\"x\"] , \"y\": my_head[\"y\"] -1})\n", + "\n", + " \n", + " #mmmm EAT SOME FOOD \n", + " #bfs\n", + " \n", + " # Compare all of the moves to create a list of all moves with the highest score\n", + " print(f\"Right points: {move_points['right']}\")\n", + " best_moves = []\n", + " for move, points in move_points.items():\n", + " if len(best_moves) == 0:\n", + " best_moves.append(move)\n", + " else:\n", + " for good_move in best_moves:\n", + " if points > move_points[good_move]:\n", + " best_moves.clear()\n", + " best_moves.append(move)\n", + " elif points == move_points[good_move]:\n", + " best_moves.append(move)\n", + " break\n", + " \n", + " # Choose a random move from the best choices\n", + " print(f\"Right points: {move_points['right']}\")\n", + " next_move = random.choice(best_moves)\n", + " # TODO: Step 4 - Move towards food instead of random, to regain health and survive longer\n", + " # food = game_state['board']['food']\n", + "\n", + " print(f\"MOVE {game_state['turn']}: {next_move}\")\n", + " print(f\"MOVE {game_state['turn']} POINTS: UP - {move_points['up']}, DOWN - {move_points['down']}, LEFT - {move_points['left']}, RIGHT - {move_points['right']}\")\n", + " return {\"move\": next_move}\n", + "\n", + "run_server({\"info\": info, \"start\": start, \"move\": move, \"end\": end})\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "845e510c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/template_snakeapi_service/snakeapi_server.py b/template_snakeapi_service/snakeapi_server.py new file mode 100644 index 0000000..c81317a --- /dev/null +++ b/template_snakeapi_service/snakeapi_server.py @@ -0,0 +1,44 @@ +# snakeapi_service/snakeapi_server.py +import os +import importlib.util +from flask import Flask, request +from flask_cors import CORS + +# load notebook code as module +spec = importlib.util.spec_from_file_location( + "nb_module", os.path.join("notebooks", "notebook.py") +) +nb = importlib.util.module_from_spec(spec) +spec.loader.exec_module(nb) + +handlers = { + "info": nb.info, + "start": nb.start, + "move": nb.move, + "end": nb.end +} + +app = Flask(__name__) +CORS(app) + +@app.route("/", methods=["GET"]) +def on_info(): + return handlers["info"]() + +@app.route("/start", methods=["POST"]) +def on_start(): + handlers["start"](request.get_json()) + return "ok" + +@app.route("/move", methods=["POST"]) +def on_move(): + return handlers["move"](request.get_json()) + +@app.route("/end", methods=["POST"]) +def on_end(): + handlers["end"](request.get_json()) + return "ok" + +if __name__ == "__main__": + port = int(os.environ.get("PORT", "3006")) + app.run(host="0.0.0.0", port=port)