101 Commits

Author SHA1 Message Date
daniel156161 2d7a2505d4 create payload of json string and then save it
Build and Push Docker Container / build-and-push (push) Successful in 1m33s
2026-04-03 14:31:15 +02:00
daniel156161 a82eaaaec5 allow sync and async function calls 2026-04-03 14:10:38 +02:00
daniel156161 51de53d01c add dataset updates with doc updates
Build and Push Docker Container / build-and-push (push) Failing after 12m18s
2026-04-03 11:40:47 +02:00
daniel156161 2e1f91355b add Dataset Class and Tests 2026-04-03 10:35:21 +02:00
daniel156161 6b69d133b6 fix line spacing and make it look better 2026-04-03 10:30:35 +02:00
daniel156161 7d52d7dca8 add justfile for testing 2026-04-03 10:30:07 +02:00
daniel156161 a885b624f9 add new BestBattleSnake 2026-04-03 10:29:48 +02:00
daniel156161 9e3a62d8e8 update actions to new version:
Build and Push Docker Container / build-and-push (push) Successful in 1m27s
- checkout: v6
- setup-buildx-action: v4
- login-action: v4
- build-push-action: v7
2026-03-10 09:00:30 +01:00
daniel156161 9093ca0512 add calling of portainer Stack Webhook to update conteiner
Build and Push Docker Container / build-and-push (push) Successful in 1m30s
2026-01-06 13:41:06 +01:00
daniel156161 6e74b5fb57 change from sync to async and from flask to quart
Build and Push Docker Container / build-and-push (push) Successful in 1m35s
2026-01-06 13:36:43 +01:00
daniel156161 962d8b1043 update requirements 2026-01-06 13:36:06 +01:00
daniel156161 8ea9cbdcee add uv package manager and use it to build container 2026-01-06 13:35:55 +01:00
daniel156161 c458219125 remove migrations and add them into ignore file list
Build and Push Docker Container / build-and-push (push) Successful in 7m44s
2025-10-13 16:38:24 +02:00
daniel156161 a7a463ed91 add workflow action to build docker image when pushing onto main 2025-10-13 16:30:36 +02:00
daniel156161 5c1ef7f05f have largest turns first 2025-06-03 11:36:55 +02:00
daniel156161 d32568cdf2 order cleanup by turns 2025-06-03 11:11:33 +02:00
daniel156161 61721a7eb6 change request endpoints to async 2025-05-27 13:28:08 +02:00
daniel156161 bcc9c71c30 change from edgedb to gel 2025-05-15 09:56:01 +02:00
daniel156161 58bbbf3cbd allow to return cleaned up values 2025-05-15 09:55:12 +02:00
daniel156161 4d515f0784 init class when cleaning up 2025-04-28 17:06:49 +02:00
daniel156161 8424c324e8 fix function nameing 2025-04-28 16:14:04 +02:00
daniel156161 c5c2652f3a add cleanup code for storage classes 2025-04-28 13:36:34 +02:00
daniel156161 0768e7f254 not check tls certificat when connection to server by default 2025-03-08 12:13:46 +01:00
daniel156161 31d2e7ea55 add code to reconnect to database if connection is gettring broken or database getting a reboot 2025-03-08 11:32:19 +01:00
daniel156161 4b51ddc84d update requirements 2025-01-22 10:05:21 +01:00
daniel156161 31f5225100 update requirements 2024-11-29 11:26:30 +01:00
daniel156161 deb95c6246 update requirments 2024-06-27 12:21:04 +02:00
daniel156161 600cde4a3e add EdgeDB Migrations 2024-05-12 12:44:22 +02:00
daniel156161 e068fb8614 add function when not eat food to remove snake tail 2024-05-08 23:45:07 +02:00
daniel156161 80b7c4df89 fix typo in LocalStorage file path 2024-05-08 23:43:01 +02:00
daniel156161 da0347731c make deletion easyer when linked with other objects and make gameboard url into a function 2024-05-08 19:04:58 +02:00
daniel156161 a09c05b6ec only store one object of GameType and Ruleset when they already exists in the database 2024-05-08 16:11:44 +02:00
daniel156161 aba457423e only store one snake with the same type and move Calculations into moves as the array<json> type 2024-05-08 15:38:14 +02:00
daniel156161 bb92715de1 marke spots where to add constraint 2024-05-08 14:44:48 +02:00
daniel156161 cf45aa60aa remove calculations from snake and move it into moves 2024-05-08 14:40:47 +02:00
daniel156161 a58e9695dd fix single mode local storage 2024-05-08 14:39:17 +02:00
daniel156161 b57ae5eab2 store all games and set standard when gameboard map is empty 2024-05-07 19:00:33 +02:00
daniel156161 817b970623 game game_type and ruleset exclusive 2024-05-07 18:59:59 +02:00
daniel156161 c9e6947758 change only output when snake is in a duel 2024-05-06 11:23:13 +02:00
daniel156161 4a1fbf2752 fix nameing of type and rename function name 2024-05-06 11:17:44 +02:00
daniel156161 5b8bf0da31 move storage classes into server folder and fix error in localStorage when winner_snake is none 2024-05-06 09:13:43 +02:00
daniel156161 4a8cb40bde make winner snake into list 2024-05-06 02:55:14 +02:00
daniel156161 c5342c1f4d add seperator when more winners when useing edgedb 2024-05-06 00:50:56 +02:00
daniel156161 ac7c397093 add c to build the module edgedb 2024-05-05 22:50:02 +02:00
daniel156161 7dd46dd72b add env argument for StorageLoader to load correct module and use it when store game history is enabled 2024-05-05 22:12:14 +02:00
daniel156161 10c7f2656c edgedb: move Calculations into own type 2024-05-05 22:05:10 +02:00
daniel156161 f00efe607f remove Moves when GameBoard get removed 2024-05-05 21:00:29 +02:00
daniel156161 c333706b75 add class to store the data in the EdgeDB 2024-05-05 20:54:56 +02:00
daniel156161 917bd3f6bd add EdgeDB dataschema and module 2024-05-05 20:46:15 +02:00
daniel156161 83bcf4f194 make new LocalStorage Class and move save functions into it 2024-05-05 17:02:18 +02:00
daniel156161 ef4dca447f fix error where my snake got not found in avoid_get_eaten_by_other_snakes 2024-04-18 23:56:49 +02:00
daniel156161 db3a353090 change over to new Game Board class from old Game Storage class 2024-04-18 23:41:53 +02:00
daniel156161 1f4d17d42f add store functions to game board 2024-04-18 23:41:17 +02:00
daniel156161 f98430462b use new game board functions 2024-04-18 22:21:57 +02:00
daniel156161 c26824aeaf create a game board class 2024-04-18 22:20:07 +02:00
daniel156161 87690177a5 make find_safe_positions function be useable for more posisions 2024-04-18 19:52:19 +02:00
daniel156161 8a2a62ef57 add env variable for the server class debug 2024-04-18 17:16:23 +02:00
daniel156161 5796ce0a6e add function to calculate where the new snake body is with or with no tail based on the move that is taken 2024-04-18 17:14:49 +02:00
daniel156161 5522a52227 set TemplateSnake as default loaded snake 2024-04-18 16:59:28 +02:00
daniel156161 5743f5c111 print moves only if debug is on and better error handeling if running game into found in dict 2024-04-18 08:23:10 +02:00
daniel156161 9950fa1952 create env variable that allow to create a .env file if neaded 2024-04-17 20:09:57 +02:00
daniel156161 4620ee31eb split class name and print out with spaces 2024-04-17 20:09:34 +02:00
daniel156161 9103e3e139 store history into data folder 2024-04-17 19:57:39 +02:00
daniel156161 04eef9229c not store config file just read if exist or overwrite default_snake_config 2024-04-17 19:53:02 +02:00
daniel156161 7cb1fdc57d add sample env config to docker file 2024-04-17 19:29:49 +02:00
daniel156161 cceded8468 add function to overwrite snake config when its set as env and use smaller python alpine container as base 2024-04-17 19:29:18 +02:00
daniel156161 8c57e48f60 if got Key Error in move function create a new snake and calculate next best move 2024-04-17 15:55:37 +02:00
daniel156161 5ce12d70c1 remove pos where other snake head is to not go into a dead end or use the pos where the snake head could be to find new food 2024-04-17 15:17:09 +02:00
daniel156161 950351b407 add code to not get eaten when head is by food 2024-04-17 15:15:25 +02:00
daniel156161 12ac257d19 add option to overwrite color with env param 2024-04-17 13:43:58 +02:00
daniel156161 d4b54d48b9 when both posisions are not save keep them in safe posisions - remove other useless MasterSnake 2024-04-17 13:35:42 +02:00
daniel156161 034b0e361a save direction to kill the snake use the correct kill direction in overwrite_eat_the_other_snake can end in a draw when both heads are by the food 2024-04-17 12:22:54 +02:00
daniel156161 a57536b7cb remove functions that are in the TemplateSnake now and at to do if eating the food would kill the snake 2024-04-17 11:54:24 +02:00
daniel156161 a606ae6f94 add template code for a smart random snake 2024-04-17 11:53:21 +02:00
daniel156161 b364c6454e optimise game type constrictor to fill up most of the game board 2024-04-17 11:15:06 +02:00
daniel156161 b9a0bca4c6 remove not used function is_food_nearby 2024-04-17 10:54:18 +02:00
daniel156161 ea36d60b4d change code to not have a big choose_move function and add todos to try fix some problems in game 2024-04-17 09:33:35 +02:00
daniel156161 0fe4e6ac83 fix a error where self.safe_positions[0] can't be accest 2024-04-17 08:19:32 +02:00
daniel156161 93b2c8ba99 cleanup the code a bit more to use the self params 2024-04-16 21:47:40 +02:00
daniel156161 f6db5cb96a fix error in eat_the_snake_overwrite when more moves are posible 2024-04-16 00:52:18 +02:00
daniel156161 e3d7cccb64 remove my_snake output into server console 2024-04-16 00:35:46 +02:00
daniel156161 0eecbb774b store game turn in history file and add safe_positions output to ensure_escape_route calculation 2024-04-15 23:56:03 +02:00
daniel156161 9e0a919233 fix not used code for flood_fill_count 2024-04-15 23:44:47 +02:00
daniel156161 6d9df32076 add more debug outputs and use all the self.args when neaded in a specific function, make own variable to only store other snakes 2024-04-15 23:16:15 +02:00
daniel156161 863ca1b277 fix calculations and not run in snake tail when in constrictor game mode 2024-04-15 22:20:15 +02:00
daniel156161 16cab3a9ca change code to store safe_positions in class and remove self.directions 2024-04-15 21:56:53 +02:00
daniel156161 281b52e71d remove moving tail of snake when begin moves are done and not calculate own body in avoid_snakes because already avoid own body 2024-04-15 14:51:28 +02:00
daniel156161 38ba576de9 add .env var to set no save when win and turns are less or the same as the var 2024-04-15 03:29:20 +02:00
daniel156161 7457e66339 add in init to not store when win and moves are smaller or the param 2024-04-15 03:17:21 +02:00
daniel156161 b601b378c8 add game url in GameStorage if its not local 2024-04-15 03:06:01 +02:00
daniel156161 24e744f705 remove print in _get_correct_folder_for_save_file because its not neaded 2024-04-15 02:21:36 +02:00
daniel156161 39b16a1702 add function to avoid get eaten by head to head collisons when my snake is smaller else eat the other snake 2024-04-15 02:13:11 +02:00
daniel156161 87fe6550b2 change folder names to add numbers an the beginning of the folder name 2024-04-14 23:56:25 +02:00
daniel156161 c854b5fec9 remove prints and add a note that i neat to fix later 2024-04-14 23:33:11 +02:00
daniel156161 51108ce21c change game storage to save it into a nice folder structure 2024-04-14 23:32:49 +02:00
daniel156161 2f8e35aced create a better Master Snake that try to not hit itself 2024-04-14 21:32:43 +02:00
daniel156161 9869f14bbe Output Turns in server logs when doing moves 2024-04-14 21:32:10 +02:00
daniel156161 1154127a40 change history of GameStorage to calculations and remove snake_start add constrictor test in test_run scripts 2024-04-14 21:31:32 +02:00
daniel156161 ae1489240d use a specific seed 2024-04-14 16:39:28 +02:00
daniel156161 0af6d862d4 create .env file if its not exists 2024-04-14 13:36:59 +02:00
daniel156161 f472ddd0d9 fix error when creating GameStorage object 2024-04-13 12:06:51 +02:00
32 changed files with 3153 additions and 597 deletions
+40
View File
@@ -0,0 +1,40 @@
name: Build and Push Docker Container
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: '${{ secrets.ACTION_ACCESS_TOKEN }}'
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
registry: ${{ vars.DOCKER_REGISTRY_URL }}
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.ACTION_ACCESS_TOKEN }}
- name: Build and push Docker image for latest tag
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ vars.DOCKER_REGISTRY_URL }}/daniel156161/battlesnake:latest
platforms: linux/amd64
- name: Invoke Portainer Stack Deployment
if: ${{ vars.PORTAINER_STACK_WEBHOOK_URL && vars.PORTAINER_STACK_WEBHOOK_URL != '' }}
uses: distributhor/workflow-webhook@v3
with:
webhook_url: ${{ vars.PORTAINER_STACK_WEBHOOK_URL }}
+1
View File
@@ -7,3 +7,4 @@
__pycache__/ __pycache__/
data/ data/
.env .env
dbschema/migrations/
+1
View File
@@ -0,0 +1 @@
3.13
+5 -3
View File
@@ -1,11 +1,13 @@
FROM python:3.10.6-slim FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim
# RUN apk add --no-cache build-base
# Install app # Install app
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
# Install dependencies # Install dependencies
RUN pip install --upgrade pip && pip install -r requirements.txt RUN uv sync --no-config --frozen --compile-bytecode
# Run Battlesnake # Run Battlesnake
CMD [ "python", "main.py" ] CMD ["uv", "run", "main.py"]
+47
View File
@@ -54,4 +54,51 @@ battlesnake play -W 11 -H 11 --name 'Python Starter Project' --url http://localh
Continue with the [Battlesnake Quickstart Guide](https://docs.battlesnake.com/quickstart) to customize and improve your Battlesnake's behavior. Continue with the [Battlesnake Quickstart Guide](https://docs.battlesnake.com/quickstart) to customize and improve your Battlesnake's behavior.
## Included Competitive Snake
This repo now includes `snakes/BestBattleSnake.py`, a stronger default snake that combines:
- collision and head-to-head risk checks
- flood-fill space evaluation to avoid traps
- food routing that gets more aggressive as health drops
- tail access checks for better long-term survival
Run it explicitly with:
```sh
SNAKE=BestBattleSnake python main.py
```
Optional duel tuning (when only 2 snakes are alive):
```sh
BATTLE_SNAKE_DUEL_STYLE=balanced python main.py
```
Allowed values: `safe`, `balanced`, `aggressive`.
## Export Training Dataset
Game saves now include a `dataset` section with labeled move samples.
Export all stored samples to JSONL:
```sh
python -m server.DatasetExporter --input data --output data/dataset/good_moves.jsonl
```
Or with `just`:
```sh
just export-dataset
```
To store compact dataset-only records (JSONL) and skip full per-game JSON files:
```sh
STORE_DATASET_ONLY=true DATASET_JSONL_PATH=data/dataset/good_moves.jsonl python main.py
```
Optional compact storage tuning:
- `DATASET_ROTATE_DAILY=true` creates one JSONL file per day (default: `true`)
- `DATASET_JSONL_MAX_MB=50` rotates when file reaches max size in MB (default: `50`)
- `DATASET_COMPRESS_ROTATED=true` gzip-compresses rotated/old JSONL files (default: `true`)
**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. **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.
+95
View File
@@ -0,0 +1,95 @@
module default {
function is_winner_me(winner: str) -> bool
using (winner = "me");
function gameboard_url(id: uuid) -> str
using ("https://play.battlesnake.com/game/" ++ <str>id);
type GameBoard {
overloaded required id: uuid {
readonly := true;
constraint exclusive;
}
url := gameboard_url(.id);
required created_at: datetime {
readonly := true;
}
required turns: int32 {
readonly := true;
}
required map: str {
readonly := true;
default := "standard";
}
required single type: GameType {
readonly := true;
on source delete delete target if orphan;
}
required single ruleset: Ruleset {
readonly := true;
on source delete delete target if orphan;
}
required winner: str {
readonly := true;
}
multi moves: Moves {
default := <Moves>{};
on source delete delete target;
on target delete allow;
}
required single snake: Snake {
readonly := true;
on source delete delete target if orphan;
}
is_winner_me := is_winner_me(.winner);
has_moves := exists(.moves);
}
type GameType {
required name: str {
readonly := true;
}
required is_ladder: bool {
readonly := true;
}
constraint exclusive on ( (.name, .is_ladder) );
}
type Ruleset {
required name: str {
readonly := true;
}
required version: str {
readonly := true;
}
required settings: json {
readonly := true;
}
constraint exclusive on ( (.name, .version, .settings) );
}
type Snake {
required type: str {
readonly := true;
}
constraint exclusive on ( .type );
}
type Moves {
required turn: int32 {
readonly := true;
}
required snake_move: str {
readonly := true;
}
required game_board: json {
readonly := true;
}
calculations: array<json> {
readonly := true;
}
}
}
+5
View File
@@ -11,4 +11,9 @@ services:
build: build:
context: ./ context: ./
dockerfile: Dockerfile dockerfile: Dockerfile
#environment:
# - SNAKE_COLOR=blue
# - SNAKE_HEAD=caffeine
# - SNAKE_TAIL=mlh-gene
# - STORE_GAME_HISTORY=True
restart: always restart: always
+2
View File
@@ -0,0 +1,2 @@
[edgedb]
server-version = "5.3"
+54
View File
@@ -0,0 +1,54 @@
# Justfile for Migrate Database Changes Workflow
# Docs: https://just.systems/man/en/
# ------------------------------------------------------------------------------
# Global settings
# ------------------------------------------------------------------------------
# Load Env
set dotenv-load
set dotenv-required := true
# Use zsh
set shell := ["bash", "-cu"]
# ------------------------------------------------------------------------------
# Default
# ------------------------------------------------------------------------------
# List all Available recipes
[private]
default:
@just --list --unsorted
# ------------------------------------------------------------------------------
# Snake Script helpers
# ------------------------------------------------------------------------------
run:
"{{justfile_directory()}}/main.py"
# ------------------------------------------------------------------------------
# Testing helpers
# ------------------------------------------------------------------------------
test-constrictor:
#!/usr/bin/env bash
set -euo pipefail
BATTLESNAKE_CLI=battlesnake_cli_1.2.3_Linux_x86_64/battlesnake
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g constrictor --browser --minimumFood 0
test-seed:
#!/usr/bin/env bash
set -euo pipefail
BATTLESNAKE_CLI=battlesnake_cli_1.2.3_Linux_x86_64/battlesnake
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser --seed 1713099635738952360
# ------------------------------------------------------------------------------
# Fataset helpers
# ------------------------------------------------------------------------------
export-dataset input="data" output="data/dataset/good_moves.jsonl":
python -m server.DatasetExporter --input "{{input}}" --output "{{output}}"
+24 -14
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env -S uv run --script
# Welcome to # Welcome to
# __________ __ __ .__ __ # __________ __ __ .__ __
@@ -12,25 +12,35 @@
# To get you started we've included code to prevent your Battlesnake from moving backwards. # To get you started we've included code to prevent your Battlesnake from moving backwards.
# For more info see docs.battlesnake.com # For more info see docs.battlesnake.com
from server.CreateEnvironmentFile import CreateEnvironmentFile
from server.Server import Server from server.Server import Server
from dotenv import load_dotenv, find_dotenv
import os import os
# Start server when `python main.py` is run # Start server when `python main.py` is run
if __name__ == "__main__": if __name__ == "__main__":
load_dotenv(find_dotenv()) if os.environ.get("CREATE_ENV_FILE", None):
CreateEnvironmentFile.load_dotenv({
"STORE_GAME_HISTORY": True,
"DEBUG": True,
"SNAKE": "TemplateSnake",
"STORE_IF_WIN_AND_MOVES_ARE_BIGGER_AS": 10,
})
server = Server( server = Server(
data_path=os.path.dirname(__file__), data_path=os.path.dirname(__file__),
snake_type=os.environ.get("SNAKE", "DummSnake"), snake_type=os.environ.get("SNAKE", "TemplateSnake"),
) storage_type=os.environ.get("STORAGE", "LocalStorage"),
store_game_when_win_and_moves_are_bigger_as=int(os.environ.get("STORE_IF_WIN_AND_MOVES_ARE_BIGGER_AS", 10)),
debug=os.environ.get("DEBUG_SERVER", False),
check_tls_security=False,
)
if os.environ.get("STORE_GAME_HISTORY", None): if os.environ.get("STORE_GAME_HISTORY", None):
server.enable_store_game_state() server.enable_store_game_state()
server.run( server.run(
host=os.environ.get("HOST", "0.0.0.0"), host=os.environ.get("HOST", "0.0.0.0"),
port=int(os.environ.get("PORT", "8000")), port=int(os.environ.get("PORT", "8000")),
debug=bool(os.environ.get("DEBUG", False)) debug=bool(os.environ.get("DEBUG", False)),
) )
+11
View File
@@ -0,0 +1,11 @@
[project]
name = "snake-python"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"dotenv>=0.9.9",
"gel>=3.1.0",
"quart>=0.20.0",
]
+19 -10
View File
@@ -1,10 +1,19 @@
blinker==1.7.0 aiofiles==25.1.0
click==8.1.7 blinker==1.9.0
Flask==3.0.2 click==8.3.1
itsdangerous==2.1.2 dotenv==0.9.9
Jinja2==3.1.3 flask==3.1.2
MarkupSafe==2.1.5 gel==3.1.0
numpy==1.26.4 h11==0.16.0
python-dotenv==1.0.1 h2==4.3.0
scipy==1.12.0 hpack==4.1.0
Werkzeug==3.0.1 hypercorn==0.18.0
hyperframe==6.1.0
itsdangerous==2.2.0
jinja2==3.1.6
markupsafe==3.0.3
priority==2.0.0
python-dotenv==1.2.1
quart==0.20.0
werkzeug==3.1.4
wsproto==1.3.2
+26
View File
@@ -0,0 +1,26 @@
from dotenv import load_dotenv, find_dotenv
import os
class CreateEnvironmentFile:
def __init__(self):
self.path = find_dotenv()
def create_file(self, environment_vars:dict[str], path:str="./.env"):
if environment_vars:
data = self.convert_dict_to_list(environment_vars)
with open(path, 'w') as f:
f.writelines(data)
def convert_dict_to_list(self, data_dict:dict):
data = []
for k, v in data_dict.items():
data.append(f"{k}={v}\n")
return data
@classmethod
def load_dotenv(cls, environment_vars:dict[str]=None):
new_class = cls()
if os.path.exists(new_class.path):
return load_dotenv(new_class.path)
else:
return new_class.create_file(environment_vars)
+56
View File
@@ -0,0 +1,56 @@
from server.GameBoard import GameBoard
class Dataset:
VALID_MOVES = {"up", "down", "left", "right"}
def __init__(self, game_board: GameBoard):
self.game_board = game_board
def _did_we_win(self):
winners = self.game_board.winner_snake_names or []
return "me" in winners
def _is_good_move(self, move: str):
return move in self.VALID_MOVES
def build(self, only_good_moves: bool = True):
game_type = self.game_board.get_type_of_game()
did_win = self._did_we_win()
samples = []
history = self.game_board.snake_class.get_history()
for index, turn in enumerate(self.game_board.turns):
move = turn.get("move")
is_good_move = did_win and self._is_good_move(move)
if only_good_moves and not is_good_move:
continue
samples.append({
"turn": turn.get("turn"),
"move": move,
"game_board": turn.get("game_board"),
"is_good_move": is_good_move,
"history": history[index] if index < len(history) else {},
})
return {
"game": {
"id": self.game_board.id,
"map": self.game_board.map,
"type": game_type,
},
"snake": {
"type": self.game_board.snake_class.__class__.__name__,
},
"did_win": did_win,
"total_samples": len(samples),
"samples": samples,
}
def labels_by_turn(self):
did_win = self._did_we_win()
labels = {}
for turn in self.game_board.turns:
move = turn.get("move")
labels[turn.get("turn")] = did_win and self._is_good_move(move)
return labels
+64
View File
@@ -0,0 +1,64 @@
import argparse
import json
from pathlib import Path
class DatasetExporter:
def __init__(self, input_dir:str, output_file:str):
self.input_dir = Path(input_dir)
self.output_file = Path(output_file)
def _iter_game_files(self):
if not self.input_dir.exists():
return []
return sorted(self.input_dir.rglob("*.json"))
def _extract_samples(self, payload:dict, source_file:Path):
dataset = payload.get("dataset", {})
game_info = dataset.get("game", payload.get("game", {}))
snake_info = dataset.get("snake", payload.get("snake", {}))
samples = []
for sample in dataset.get("samples", []):
samples.append({
"game_id": game_info.get("id"),
"game_map": game_info.get("map"),
"game_type": game_info.get("type"),
"snake_type": snake_info.get("type"),
"turn": sample.get("turn"),
"move": sample.get("move"),
"is_good_move": sample.get("is_good_move", False),
"game_board": sample.get("game_board"),
"history": sample.get("history"),
"source_file": str(source_file),
})
return samples
def export_jsonl(self):
game_files = self._iter_game_files()
self.output_file.parent.mkdir(parents=True, exist_ok=True)
sample_count = 0
with self.output_file.open("w", encoding="utf-8") as output:
for game_file in game_files:
with game_file.open("r", encoding="utf-8") as source:
payload = json.load(source)
for sample in self._extract_samples(payload, game_file):
output.write(json.dumps(sample, ensure_ascii=False) + "\n")
sample_count += 1
return {
"games_scanned": len(game_files),
"samples_exported": sample_count,
"output_file": str(self.output_file),
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Export Battlesnake dataset to JSONL")
parser.add_argument("--input", default="data", help="Input directory with stored game JSON files")
parser.add_argument("--output", default="data/dataset/good_moves.jsonl", help="Output JSONL file")
args = parser.parse_args()
report = DatasetExporter(args.input, args.output).export_jsonl()
print(json.dumps(report, indent=2))
+25 -11
View File
@@ -1,16 +1,30 @@
import aiofiles.os
import aiofiles
import os import os
import inspect
def read_file(path, callback=None): async def read_file(path: str, callback=None):
if os.path.exists(path): if not await aiofiles.os.path.exists(path):
with open(path, 'r') as f:
data = callback(f)
return data
else:
return None return None
def save_file(path, data, callback=None, *args, **kwargs): async with aiofiles.open(path, "r") as f:
if not os.path.exists(path): if callback:
os.makedirs(os.path.dirname(path), exist_ok=True) result = callback(f)
if inspect.isawaitable(result):
return await result
return result
return await f.read()
with open(path, 'w') as f:
callback(data, f, *args, **kwargs) async def save_file(path: str, data, callback=None, *args, **kwargs):
dir_path = os.path.dirname(path)
if dir_path:
await aiofiles.os.makedirs(dir_path, exist_ok=True)
async with aiofiles.open(path, "w") as f:
if callback:
result = callback(data, f, *args, **kwargs)
if inspect.isawaitable(result):
await result
else:
await f.write(data)
+140
View File
@@ -0,0 +1,140 @@
from datetime import datetime
class GameBoard:
def __init__(self, game_id:str, width:int, height:int, ruleset:dict, source:str, map:str, snake_class):
self.id = game_id
self.width = width
self.height = height
self.type = ruleset["name"]
self.snake_class = snake_class
# What will get Stored
self.winner_snake_names = None
self.now_date = datetime.now()
self.turns = []
self.is_ladder = True if source == "ladder" else False
self.ruleset = ruleset
self.map = map
self.url = self._get_game_url(True if ruleset["version"] == "cli" else False)
# Setter Functions
def _set_snakes(self, snakes:list[dict]):
self.other_snakes = [ x for x in snakes if x["id"] != self.my_snake["id"] ]
def _set_my_snake(self, my_snake:str):
self.my_snake = my_snake
def _set_food(self, food:list[dict]):
self.food = food
def _set_hazards(self, hazards:list[dict]):
self.hazards = hazards
def _set_turn(self, turn:int):
self.turn = turn
# Getter Functions
def get_other_snakes(self):
return self.other_snakes
def get_my_snake(self):
return self.my_snake
def get_food(self):
return self.food
def get_hazard(self):
return self.hazards
def get_turn(self):
return self.turn
def get_dimension(self):
return {"width": self.width, "height": self.height}
def get_width(self):
return self.width
def get_height(self):
return self.height
def get_type(self):
return self.type
def get_my_snake_head(self):
return self.my_snake["head"]
def get_my_snake_body(self):
return self.my_snake["body"]
def get_my_snake_tail(self):
return self.my_snake["body"][-1]
def get_game_board_as_dict(self):
snakes = [self.my_snake]
snakes.extend(self.other_snakes)
return {
"height": self.height,
"width": self.width,
"snakes": snakes,
"food": self.food,
"hazards": self.hazards
}
# Game Functions
def read_game_data(self, game_data:dict):
self._set_food(game_data['board']['food'])
self._set_hazards(game_data['board']['hazards'])
self._set_my_snake(game_data['you'])
self._set_snakes(game_data['board']['snakes'])
self._set_turn(game_data["turn"])
async def start_game(self, game_data:dict):
self.init_snakes = len(game_data['board']['snakes'])
def end_game(self, game_data:dict):
self._set_winner_snake_name(game_data['board']['snakes'])
self.get_type_of_game()
# Function get Called from Server
def snake_neat_make_a_move(self):
move = self.snake_class.choose_move(self)
self.turns.append({
"turn": self.turn,
"move": move,
"game_board": self.get_game_board_as_dict()
})
return move
# Save functions
def _get_game_url(self, local_game:bool):
if local_game:
return None
return f"https://play.battlesnake.com/game/{self.id}"
def _set_winner_snake_name(self, snakes:list[dict]):
if self.my_snake["id"] in [ x["id"] for x in snakes]:
self.winner_snake_names = ["me"]
else:
self.winner_snake_names = [ x["name"] for x in snakes]
if len(self.winner_snake_names) == 0:
self.winner_snake_names = None
def get_winner(self):
return self.winner_snake_names
def get_type_of_game(self):
if self.init_snakes == 2:
return {"name": "duel", "is_ladder": self.is_ladder}
return {"name": self.type, "is_ladder": self.is_ladder}
async def save(self, store_class, **kwargs):
store = store_class(**kwargs)
await store.save(self)
del store
-59
View File
@@ -1,59 +0,0 @@
from server.Files import save_file
import os
class GameStorage:
def __init__(self, snake:str, path:str):
self.snake_type = snake
self.folder = path
self.winner_snake_names = None
def start_new_game(self, game_type:dict, game_board:dict, snake:dict):
self.game_type = game_type
self.start_position = snake
self.game_board = [game_board]
self.moves = []
def add_moves(self, game_board:dict, my_move:str):
self.game_board.append(game_board)
self.moves.append(my_move)
def add_end_state(self, game_board:dict, snake_history_state:list[dict], final_turns:int):
self.game_board.append(game_board)
self.snake_history = snake_history_state
self._set_winner_snake_name(game_board['snakes'])
self.final_turns = final_turns
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 _get_type_of_gameboard(self):
if len(self.game_board[0]["snakes"]) == 2:
return "duel"
return "standart"
def save(self, path:str, callback=None, **kwargs):
if self.winner_snake_names == "me" and self.final_turns <= 10:
return None
save_file(os.path.join(self.folder, path), {
"snake": {
"type": self.snake_type,
"choices": self.snake_history,
},
"game": {
"type": self._get_type_of_gameboard(),
"infos": self.game_type,
"snake_start": self.start_position,
"final_turns": self.final_turns,
"gameboard": self.game_board,
"my_moves": self.moves,
},
"winner": self.winner_snake_names,
}, callback=callback, **kwargs)
def __str__(self):
return f"<{self.__class__.__name__}> Snake: {self.snake_type}, Folder: {self.folder}, Winner: {self.winner_snake_names}, Old Moves: {self.moves}"
+110 -73
View File
@@ -1,113 +1,150 @@
from server.Files import read_file, save_file from server.Files import read_file
from server.GameStorage import GameStorage from server.GameBoard import GameBoard
from snakes.TemplateSnake import TemplateSnake
from server.SnakeBuilder import SnakeBuilder from server.SnakeBuilder import SnakeBuilder
from datetime import datetime from server.storage.StorageLoader import StorageLoader
from flask import Flask
from flask import request from quart import Quart, request, jsonify
import logging, json, os import logging, json, os, re
class Server: class Server:
default_snake_config = {"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"} default_snake_config = {"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"}
def __init__(self, data_path:str, snake_type:str, debug:bool=False): def __init__(self, data_path:str, snake_type:str, storage_type:str, debug:bool=False, store_game_when_win_and_moves_are_bigger_as:int=10, check_tls_security:bool=False):
self.debug = debug self.debug = debug
self.snake_type = snake_type self.snake_type = snake_type
self.storage_type = storage_type
self.config_file = os.path.join(data_path, 'data', 'snake-config.json') self.config_file = os.path.join(data_path, 'data', 'snake-config.json')
self.data_path = data_path self.data_path = data_path
self.check_tls_security = check_tls_security
self.store_game_state = False self.store_game_state = False
self.running_games:dict[str, GameStorage] = {} self.store_game_when_win_and_moves_are_bigger_as = store_game_when_win_and_moves_are_bigger_as
self.running_snake:dict[str, TemplateSnake] = {}
self.app = Flask("Battlesnake") self.running_games:dict[str, GameBoard] = {}
self.app = Quart("Battlesnake")
# 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
@self.app.get("/") @self.app.get("/")
def on_info(): async def on_info():
return self._info() snake_config = await self._read_json_config_or_create()
print("INFO Snake:", snake_config)
return snake_config
# start is called when your Battlesnake begins a game
@self.app.post("/start") @self.app.post("/start")
def on_start(): async def on_start():
game_state = request.get_json() game_state = await request.get_json()
self._start(game_state) await self._create_game_board(game_state)
print("GAME START:", game_state["game"])
return "ok" return "ok"
# move is called when your Battlesnake game is running game
@self.app.post("/move") @self.app.post("/move")
def on_move(): async def on_move():
game_state = request.get_json() game_state = await request.get_json()
return self._move(game_state) game_board = await self._get_game_board(game_state)
next_move = game_board.snake_neat_make_a_move()
if self.debug:
print("TURN:", f'{game_state["turn"]:3},', "MOVE:", f"{next_move:5}")
return {"move": next_move}
# end is called when your Battlesnake finishes a game
@self.app.post("/end") @self.app.post("/end")
def on_end(): async def on_end():
game_state = request.get_json() game_state = await request.get_json()
self._end(game_state) if self.store_game_state:
game_board = await self._get_game_board(game_state, end=True)
#if not game_board.get_winner() == "me" and not game_board.get_turn() <= self.store_game_when_win_and_moves_are_bigger_as:
if self.check_tls_security:
await game_board.save(
StorageLoader.build(self.storage_type),
file_path=os.path.join(self.data_path, 'data'),
database=os.getenv("EDGEDB_DATABASE", None),
tls_security=None
)
else:
await game_board.save(
StorageLoader.build(self.storage_type),
file_path=os.path.join(self.data_path, 'data'),
database=os.getenv("EDGEDB_DATABASE", None),
)
print("GAME ENDED: Winner is", [ x["name"] for x in game_state["board"]['snakes']])
self._delete_game_board(game_state)
return "ok" return "ok"
@self.app.after_request @self.app.after_request
def identify_server(response): async def identify_server(response):
response.headers.set( response.headers.set(
"server", "battlesnake/github/starter-snake-python" "server", "battlesnake/gitea/snake-python"
) )
return response return response
@self.app.get("/cleanup")
async def cleanup():
results = self._cleanup_database()
return jsonify(data=json.loads(results), status=200)
def run(self, host:str="0.0.0.0", port:str="8000", debug:bool=False): def run(self, host:str="0.0.0.0", port:str="8000", debug:bool=False):
logging.getLogger("werkzeug").setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
print(f"\nRunning Battlesnake at http://{host}:{port} with the {self.snake_type.replace('Snake', '')} Snake") print(f"\nRunning Battlesnake at http://{host}:{port} with the {' '.join(re.findall('[A-Z][^A-Z]*', self.snake_type))}")
self.app.run(host=host, port=port, debug=debug) self.app.run(host=host, port=port, debug=debug)
def _read_json_config_or_create(self): async def _read_json_config_or_create(self):
snake_config = read_file(self.config_file, json.load) snake_config = await read_file(self.config_file, json.load)
if not snake_config: if not snake_config:
snake_config = self.default_snake_config return await self._override_snake_config_with_environment_variables(self.default_snake_config)
save_file(self.config_file, snake_config, callback=json.dump, indent=2, ensure_ascii=False) return await self._override_snake_config_with_environment_variables(snake_config)
return snake_config
async def _override_snake_config_with_environment_variables(self, config: dict[str, str]) -> dict[str, str]:
for key in ("author", "color", "head", "tail"):
value = os.environ.get(f"SNAKE_{key.upper()}")
if value is not None:
config[key] = value
return config
async def _create_game_board(self, game_state:dict):
new_game_board = GameBoard(
game_id=game_state["game"]["id"],
width=game_state['board']['width'],
height=game_state['board']['height'],
ruleset=game_state['game']["ruleset"],
source=game_state['game']['source'],
map=game_state['game']['map'],
snake_class=SnakeBuilder.build(self.snake_type)
)
await new_game_board.start_game(game_state)
self.running_games[game_state["game"]["id"]] = new_game_board
return new_game_board
def _delete_game_board(self, game_state):
del self.running_games[game_state["game"]["id"]]
async def _get_game_board(self, game_state:str, end:bool=False):
try:
game_board = self.running_games[game_state["game"]["id"]]
except KeyError:
game_board = await self._create_game_board(game_state)
game_board.read_game_data(game_state)
if end:
game_board.end_game(game_state)
return game_board
def enable_store_game_state(self): def enable_store_game_state(self):
self.store_game_state = True self.store_game_state = True
# info is called when you create your Battlesnake on play.battlesnake.com def _cleanup_database(self):
# and controls your Battlesnake's appearance storage = StorageLoader.build(self.storage_type)()
# TIP: If you open your Battlesnake URL in a browser you should see this data return storage.cleanup()
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.running_games[game_state["game"]["id"]] = GameStorage(self.snake.__class__.__name__, path=os.path.join(self.data_path, 'data', 'history'))
self.running_games[game_state["game"]["id"]].start_new_game(game_state["game"], game_state["board"], game_state["you"])
self.running_snake[game_state["game"]["id"]] = SnakeBuilder.build(self.snake_type)
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.running_snake[game_state["game"]["id"]].choose_move(game_state)
if self.store_game_state:
self.running_games[game_state["game"]["id"]].add_moves(game_state["board"], next_move)
if self.debug:
print(self.running_games[game_state["game"]["id"]])
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:
snake = self.running_snake[game_state["game"]["id"]]
self.running_games[game_state["game"]["id"]].add_end_state(game_state["board"], snake.get_history(), game_state["turn"])
self.running_games[game_state["game"]["id"]].save(
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
)
del self.running_games[game_state["game"]["id"]]
print("GAME OVER:\n- Winner is", [ x["name"] for x in game_state["board"]['snakes']])
del self.running_snake[game_state["game"]["id"]]
+156
View File
@@ -0,0 +1,156 @@
from server.GameBoard import GameBoard
from server.Dataset import Dataset
from datetime import datetime
import json, time
try:
import gel as _gel # type: ignore[import-not-found]
except ImportError: # pragma: no cover
_gel = None
class EdgeDB:
def __init__(self, database:str=None, tls_security:str='insecure', **kwargs):
self.database = database
self.tls_security = tls_security
self._connect()
def _connect(self):
if _gel is None:
raise ImportError("The 'gel' package is required to use EdgeDB storage")
self.client = _gel.create_client(
tls_security=self.tls_security, database=self.database
)
def run_query_with_reconnection(self, function, *args, **kwargs):
while True:
try:
return function(*args, **kwargs)
except Exception as error:
if error.__class__.__name__ != "ClientConnectionFailedError":
raise
self._connect()
time.sleep(0.5)
def insert_game_type(self, name:str, is_ladder:bool):
return self.run_query_with_reconnection(
self.client.query_required_single,
"""
insert GameType {
name := <str>$name,
is_ladder := <bool>$is_ladder
}""",
name=name,
is_ladder=is_ladder
)
def create_moves_with_calculations(self, game_board:GameBoard):
data = []
moves = game_board.turns
snake_calulations = [[calc for calc in ele["data"]] for ele in game_board.snake_class.get_history() ]
labels_by_turn = Dataset(game_board).labels_by_turn()
for i in range(len(moves)):
calculations = snake_calulations[i] if i < len(snake_calulations) else []
calculations.append({
"dataset": {
"is_good_move": labels_by_turn.get(moves[i]["turn"], False)
}
})
data.append({
"turn": moves[i]["turn"],
"move": moves[i]["move"],
"game_board": moves[i]["game_board"],
"calculations": calculations,
})
return data
async def insert(self, game_board:GameBoard):
game_type = game_board.get_type_of_game()
self.run_query_with_reconnection(
self.client.query,
"""
insert GameBoard {
id := <uuid>$id,
created_at := <datetime>$created_at,
turns := <int32>$turns,
map := <str>$map,
winner := <str>$winner,
moves := (
with input_data := <array <tuple <turn: int32, `move`: str, game_board: json, calculations: array<json> >>>$moves
for data in array_unpack(input_data)
insert Moves {
turn := data.turn,
snake_move := data.`move`,
game_board := data.game_board,
calculations := data.calculations
}
),
type := (
insert GameType {
name := <str>$game_type,
is_ladder := <bool>$is_ladder
} unless conflict on (.name, .is_ladder) else GameType
),
ruleset := (
insert Ruleset {
name := <str>$ruleset,
version := <str>$version,
settings := to_json(<str>$settings)
} unless conflict on (.name, .version, .settings) else Ruleset
),
snake := (
insert Snake {
type := <str>$snake_type
} unless conflict on .type else Snake
)
}""",
id=game_board.id,
created_at=datetime.fromtimestamp(game_board.now_date.timestamp(), game_board.now_date.astimezone().tzinfo),
turns=game_board.turn,
map=game_board.map if game_board.map else "standard",
winner=', '.join(game_board.winner_snake_names)
if game_board.winner_snake_names
else "",
moves=[
tuple(
[
x["turn"],
x["move"],
json.dumps(x["game_board"]),
[json.dumps(ele) for ele in x["calculations"]],
]
)
for x in self.create_moves_with_calculations(game_board)
],
game_type=game_type["name"],
is_ladder=game_type["is_ladder"],
ruleset=game_board.ruleset["name"],
version=game_board.ruleset["version"],
settings=json.dumps(game_board.ruleset["settings"]),
snake_type=game_board.snake_class.__class__.__name__,
)
async def save(self, game_board:GameBoard):
await self.insert(game_board)
def __del__(self):
self.client.close()
def cleanup(self):
return self.run_query_with_reconnection(
self.client.query_json,
"""
delete Moves { };
with gameboard := (delete GameBoard filter .turns < <std::int32>"200" or .is_winner_me = <std::bool>"false")
select gameboard {id, url, winner, turns, type: { is_ladder, name } } order by .turns desc;
"""
)
+177
View File
@@ -0,0 +1,177 @@
from server.GameBoard import GameBoard
from server.Dataset import Dataset
from server.Files import save_file
import aiofiles
import aiofiles.os
import gzip
import json, os
class LocalStorage:
def __init__(self, file_path:str, **kwargs):
self.save_folder_dict = {
"standard": "01_Standard",
"duel": "02_Duels",
"constrictor": "04_Constrictor",
"solo": "05_Solo",
}
self.file_path = file_path
self.dataset_only = os.getenv("STORE_DATASET_ONLY", "false").strip().lower() in ("1", "true", "yes", "on")
self.dataset_jsonl_path = os.getenv("DATASET_JSONL_PATH", os.path.join(self.file_path, "dataset", "good_moves.jsonl"))
self.dataset_rotate_daily = os.getenv("DATASET_ROTATE_DAILY", "true").strip().lower() in ("1", "true", "yes", "on")
self.dataset_compress_rotated = os.getenv("DATASET_COMPRESS_ROTATED", "true").strip().lower() in ("1", "true", "yes", "on")
self.dataset_max_bytes = int(float(os.getenv("DATASET_JSONL_MAX_MB", "50")) * 1024 * 1024)
def _get_active_dataset_path(self, game_board:GameBoard):
if not self.dataset_rotate_daily:
return self.dataset_jsonl_path
base, ext = os.path.splitext(self.dataset_jsonl_path)
if ext == "":
ext = ".jsonl"
return f"{base}-{game_board.now_date.strftime('%Y-%m-%d')}{ext}"
def _gzip_file(self, file_path:str):
gz_path = f"{file_path}.gz"
with open(file_path, "rb") as src:
with gzip.open(gz_path, "wb") as dst:
dst.writelines(src)
os.remove(file_path)
async def _compress_old_daily_files(self, active_path:str):
if not self.dataset_compress_rotated:
return
folder = os.path.dirname(active_path)
base_name = os.path.basename(self.dataset_jsonl_path)
base_stem, _ = os.path.splitext(base_name)
prefix = f"{base_stem}-"
active_name = os.path.basename(active_path)
if folder == "" or not await aiofiles.os.path.exists(folder):
return
for name in os.listdir(folder):
if name == active_name:
continue
if not name.startswith(prefix):
continue
if not name.endswith(".jsonl"):
continue
self._gzip_file(os.path.join(folder, name))
async def _rotate_if_needed(self, active_path:str, game_board:GameBoard):
if self.dataset_max_bytes <= 0:
return
if not await aiofiles.os.path.exists(active_path):
return
file_size = (await aiofiles.os.stat(active_path)).st_size
if file_size < self.dataset_max_bytes:
return
timestamp = game_board.now_date.strftime("%Y%m%d-%H%M%S")
rotated_path = f"{active_path}.{timestamp}.jsonl"
suffix = 1
while await aiofiles.os.path.exists(rotated_path):
suffix += 1
rotated_path = f"{active_path}.{timestamp}.{suffix}.jsonl"
await aiofiles.os.rename(active_path, rotated_path)
if self.dataset_compress_rotated:
self._gzip_file(rotated_path)
def _build_dataset_rows(self, dataset_payload:dict, game_board:GameBoard):
game_info = dataset_payload.get("game", {})
snake_info = dataset_payload.get("snake", {})
rows = []
for sample in dataset_payload.get("samples", []):
rows.append({
"game_id": game_info.get("id", game_board.id),
"game_map": game_info.get("map", game_board.map),
"game_type": game_info.get("type", game_board.get_type_of_game()),
"snake_type": snake_info.get(
"type", game_board.snake_class.__class__.__name__
),
"turn": sample.get("turn"),
"move": sample.get("move"),
"is_good_move": sample.get("is_good_move", False),
"game_board": sample.get("game_board"),
"history": sample.get("history"),
})
return rows
async def _append_dataset_jsonl(self, dataset_payload:dict, game_board:GameBoard):
rows = self._build_dataset_rows(dataset_payload, game_board)
if len(rows) == 0:
return
active_path = self._get_active_dataset_path(game_board)
await aiofiles.os.makedirs(os.path.dirname(active_path), exist_ok=True)
await self._compress_old_daily_files(active_path)
await self._rotate_if_needed(active_path, game_board)
async with aiofiles.open(active_path, "a") as f:
for row in rows:
await f.write(json.dumps(row, ensure_ascii=False) + "\n")
def _get_correct_folder_for_save_file(self, game_board:GameBoard, file_name:str, game_type:str, leader_board:bool, winner:bool):
storage_folder = self.file_path
if leader_board:
storage_folder = os.path.join(storage_folder, "00_Leaderboards")
storage_folder = os.path.join(storage_folder, self.save_folder_dict[game_type])
storage_folder = os.path.join(
storage_folder,
game_board.now_date.strftime("%Y"),
game_board.now_date.strftime("%m_%B"),
game_board.now_date.strftime("%d"),
)
if winner:
storage_folder = os.path.join(storage_folder, "Winner")
else:
storage_folder = os.path.join(storage_folder, "Lost")
return os.path.join(storage_folder, file_name)
async def save(self, game_board:GameBoard):
game_type = game_board.get_type_of_game()
dataset = Dataset(game_board).build(only_good_moves=True)
await self._append_dataset_jsonl(dataset, game_board)
if self.dataset_only:
return
save_file_path = self._get_correct_folder_for_save_file(
game_board,
f"{game_board.snake_class.__class__.__name__}_{game_board.now_date.strftime('%H-%M-%S')}_{game_board.id}.json",
game_type["name"],
game_type["is_ladder"],
True if game_board.winner_snake_names and "me" in game_board.winner_snake_names else False
)
payload = {
"winner": game_board.winner_snake_names,
"game": {
"url": game_board.url,
"id": game_board.id,
"final_turns": game_board.turn,
"map": game_board.map,
"type": game_type,
"ruleset": game_board.ruleset,
},
"moves": game_board.turns,
"snake": {
"type": game_board.snake_class.__class__.__name__,
"calculations": game_board.snake_class.get_history(),
},
"dataset": dataset,
}
await save_file(save_file_path, json.dumps(payload, indent=2, ensure_ascii=False))
def cleanup(self):
pass
+7
View File
@@ -0,0 +1,7 @@
class StorageLoader:
@classmethod
def build(self, selected_storage:str):
storage_module = __import__(f'server.storage.{selected_storage}', fromlist=[selected_storage])
storage_class = getattr(storage_module, selected_storage)
return storage_class
-165
View File
@@ -1,165 +0,0 @@
from snakes.TemplateSnake import TemplateSnake
from queue import PriorityQueue, Queue
import random
class AStarSnake(TemplateSnake):
def avoid_my_body(self, my_body, possible_moves: dict) -> list:
"""
my_body: Set of tuples representing x/y coordinates for every segment of a Battlesnake.
e.g. {(0, 0), (1, 0), (2, 0)}
possible_moves: Dictionary of moves to pick from, with coordinates as tuples.
e.g. {"up": (0, 1), "down": (0, -1), "left": (-1, 0), "right": (1, 0)}
return: The dictionary of remaining possible_moves, with the moves leading to self-collision removed
"""
remove = []
for direction, location in possible_moves.items():
if location in my_body:
remove.append(direction)
for direction in remove:
del possible_moves[direction]
return possible_moves
def avoid_walls(self, board_width: int, board_height: int, possible_moves: dict):
remove = []
for direction, location in possible_moves.items():
x_out_range = (location[0] < 0 or location[0] == board_width)
y_out_range = (location[1] < 0 or location[1] == board_height)
if x_out_range or y_out_range:
remove.append(direction)
for direction in remove:
del possible_moves[direction]
return possible_moves
def avoid_snakes(self, snakes: list, possible_moves: dict):
remove = []
for snake in snakes:
for direction, location in possible_moves.items():
if location in snake["body"]:
remove.append(direction)
remove = set(remove)
for direction in remove:
del possible_moves[direction]
return possible_moves
def get_target_close(self, foods: list, my_head: tuple):
if len(foods) == 0:
return None
return min(foods, key=lambda food: abs(food["x"] - my_head[0]) + abs(food["y"] - my_head[1]))
def flood_fill(self, board_width: int, board_height: int, my_body: set):
"""
Perform Flood Fill to identify safe areas on the board.
"""
visited = set()
safe_cells = set()
# Define directions (up, down, left, right)
directions = [(0, 1), (0, -1), (-1, 0), (1, 0)]
# Perform Flood Fill from each cell not occupied by the snake's body
for x in range(board_width):
for y in range(board_height):
if (x, y) not in my_body and (x, y) not in visited:
# Start Flood Fill from this cell
q = Queue()
q.put((x, y))
visited.add((x, y))
safe_cells.add((x, y))
# Continue Flood Fill until the queue is empty
while not q.empty():
current_cell = q.get()
for dx, dy in directions:
new_cell = (current_cell[0] + dx, current_cell[1] + dy)
if 0 <= new_cell[0] < board_width and 0 <= new_cell[1] < board_height:
if new_cell not in my_body and new_cell not in visited:
q.put(new_cell)
visited.add(new_cell)
safe_cells.add(new_cell)
return safe_cells
def choose_move(self, data: dict) -> str:
my_head = (data["you"]["head"]["x"], data["you"]["head"]["y"])
my_body = {(part["x"], part["y"]) for part in data["you"]["body"]}
board_height = data["board"]["height"]
board_width = data["board"]["width"]
foods = data["board"]["food"]
# Perform Flood Fill to identify safe areas on the board
safe_cells = self.flood_fill(board_width, board_height, my_body)
# Find the nearest food located in a safe area using A* algorithm
def heuristic(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def a_star(start, goal:set=()):
open_set = Queue()
open_set.put(start)
came_from = {}
g_score = {start: 0}
while not open_set.empty():
current = open_set.get()
if current == goal:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
return path[::-1][0]
for dx, dy in [(0, 1), (0, -1), (-1, 0), (1, 0)]:
new_cell = (current[0] + dx, current[1] + dy)
if new_cell in safe_cells:
tentative_g_score = g_score[current] + 1
if new_cell not in g_score or tentative_g_score < g_score[new_cell]:
came_from[new_cell] = current
g_score[new_cell] = tentative_g_score
open_set.put(new_cell)
return None
try:
nearest_food = min(foods, key=lambda food: heuristic(my_head, (food["x"], food["y"])))
target_position = (nearest_food["x"], nearest_food["y"])
move_target = a_star(my_head, target_position)
except ValueError:
# TODO: What to do when no food is available?
# - Avoid own body and other snakes
# - Flut fill?
move_target = a_star(my_head, safe_cells)
print(move_target)
# Choose the next move based on the path obtained from A* algorithm
if move_target:
dx = move_target[0] - my_head[0]
dy = move_target[1] - my_head[1]
self.add_to_history({"my_head": my_head, "my_body": tuple(my_body), "target": move_target, "target_position": target_position, "nearest_food": nearest_food, "dx": dx, "dy": dy})
if dx == 1:
return "right"
elif dx == -1:
return "left"
elif dy == 1:
return "up"
elif dy == -1:
return "down"
# If no safe path to food is found, choose a random move
random_move = random.choice(["up", "down", "left", "right"])
try:
self.add_to_history({"my_head": my_head, "my_body": tuple(my_body), "target": move_target, "target_position": target_position, "nearest_food": nearest_food, "random_move": random_move})
except UnboundLocalError:
self.add_to_history({"my_head": my_head, "my_body": tuple(my_body), "target": move_target, "random_move": random_move})
return random_move
+885
View File
@@ -0,0 +1,885 @@
from collections import deque
from typing import Any, cast
import random
import os
from snakes.TemplateSnake import TemplateSnake
class BestBattleSnake(TemplateSnake):
DIRECTIONS = {
"up": (0, 1),
"down": (0, -1),
"left": (-1, 0),
"right": (1, 0),
}
OPPOSITE = {
"up": "down",
"down": "up",
"left": "right",
"right": "left",
}
def __init__(self):
super().__init__()
self.name = "BestBattleSnake"
self.recent_heads = deque(maxlen=14)
self.last_move = None
self.last_game_id = None
self.duel_style = self._get_duel_style()
def _get_duel_style(self):
value = os.getenv("BATTLE_SNAKE_DUEL_STYLE")
if value is None:
value = os.getenv("DUEL_STYLE", "balanced")
style = value.strip().lower()
if style not in {"safe", "balanced", "aggressive"}:
return "balanced"
return style
def _duel_weights(self, style):
if style == "safe":
return {
"head_pressure": 0.65,
"distance_safety": 1.30,
"food_bias": 1.00,
}
if style == "aggressive":
return {
"head_pressure": 1.35,
"distance_safety": 0.75,
"food_bias": 0.85,
}
return {
"head_pressure": 1.00,
"distance_safety": 1.00,
"food_bias": 1.00,
}
def choose_move(self, game_data):
self.game_board = game_data
self.calculations = []
self.duel_style = self._get_duel_style()
game_id = getattr(game_data, "id", None)
turn = game_data.get_turn()
if game_id != self.last_game_id or turn <= 1:
self.recent_heads.clear()
self.last_move = None
self.last_game_id = game_id
my_snake = cast(dict[str, Any], game_data.get_my_snake())
my_head = my_snake["head"]
my_body = my_snake["body"]
my_len = my_snake.get("length", len(my_body))
my_health = my_snake.get("health", 100)
width = game_data.get_width()
height = game_data.get_height()
board_area = max(1, width * height)
occupancy_ratio = my_len / board_area
preserve_space_mode = occupancy_ratio >= 0.34 and my_health > 35
foods = game_data.get_food()
hazards = game_data.get_hazard()
other_snakes = game_data.get_other_snakes()
is_constrictor = game_data.get_type() == "constrictor"
food_set = {(food["x"], food["y"]) for food in foods}
hazard_set = {(hazard["x"], hazard["y"]) for hazard in hazards}
current_head_point = (my_head["x"], my_head["y"])
safe_moves = self._legal_moves(
my_head=my_head,
my_body=my_body,
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=is_constrictor,
width=width,
height=height,
)
if not safe_moves:
fallback = self._fallback_move(my_head, width, height)
self.recent_heads.append(current_head_point)
self.last_move = fallback
self.add_to_history(
{
"turn": turn,
"move": fallback,
"reason": "no_safe_moves",
}
)
return fallback
enemy_attack_map = self._build_enemy_attack_map(
my_snake=my_snake,
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=is_constrictor,
width=width,
height=height,
)
if is_constrictor:
best_move, scores = self._choose_constrictor_move(
safe_moves=safe_moves,
my_body=my_body,
my_len=my_len,
other_snakes=other_snakes,
food_set=food_set,
enemy_attack_map=enemy_attack_map,
width=width,
height=height,
)
self.recent_heads.append(current_head_point)
self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
return best_move
if len(other_snakes) == 1:
best_move, scores = self._choose_duel_move(
safe_moves=safe_moves,
my_body=my_body,
my_len=my_len,
my_health=my_health,
foods=foods,
food_set=food_set,
hazards=hazards,
hazard_set=hazard_set,
other_snakes=other_snakes,
enemy_attack_map=enemy_attack_map,
width=width,
height=height,
)
self.recent_heads.append(current_head_point)
self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
return best_move
scores: dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {}
for move, pos in safe_moves.items():
point = (pos["x"], pos["y"])
ate_food = point in food_set
future_body = self._future_body(my_body, pos, ate_food, is_constrictor)
blocked = self._simulation_blocked(
future_body=future_body,
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=is_constrictor,
)
blocked.discard(point)
reachable_space = self._flood_fill_count(point, blocked, width, height)
required_space = len(future_body)
liberties = self._open_neighbor_count(point, blocked, width, height)
next_options = self._next_turn_option_count(
future_body, blocked, width, height
)
territory = self._territory_control_score(
my_start=point,
enemy_starts=[
(snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes
],
blocked=blocked,
width=width,
height=height,
)
nearest_food_dist = self._nearest_food_distance(
point, foods, blocked, width, height
)
future_tail = future_body[-1]
tail_point = (future_tail["x"], future_tail["y"])
tail_dist = self._path_distance(
point, tail_point, blocked - {tail_point}, width, height
)
has_tail_escape = tail_dist is not None
likely_dead_end = (
(reachable_space < required_space and not has_tail_escape)
or (liberties == 0 and not has_tail_escape)
or (next_options == 0 and not has_tail_escape)
)
score = 0.0
score += reachable_space * 2.6
score += liberties * 18.0
score += next_options * 10.0
score += territory * 0.35
if reachable_space < required_space:
score -= 1200.0
if liberties == 0:
score -= 900.0
enemy_len = enemy_attack_map.get(point)
if enemy_len is not None:
if enemy_len >= my_len:
score -= 1200.0
else:
score += 70.0
hunger_urgency = max(0.0, (60.0 - my_health) / 60.0)
if nearest_food_dist is not None:
score += (28.0 + 70.0 * hunger_urgency) / (nearest_food_dist + 1)
elif my_health < 30:
score -= 150.0
if ate_food:
if likely_dead_end:
score -= 1800.0
else:
score += 260.0 + 220.0 * hunger_urgency
if preserve_space_mode and ate_food and my_health > 45:
score -= 280.0
if tail_dist is not None:
score += 12.0 / (tail_dist + 1)
else:
score -= 40.0
if point in hazard_set:
score -= 70.0 if my_health > 35 else 250.0
score -= self._revisit_penalty(point)
if self.last_move == move:
score += 6.0
elif (
self.last_move
and self.OPPOSITE[self.last_move] == move
and len(safe_moves) > 1
):
score -= 20.0
health_after_move = 100 if ate_food else my_health - 1
if point in hazard_set:
health_after_move -= 15
if health_after_move <= 0:
score -= 10000.0
is_losing_head_to_head = enemy_len is not None and enemy_len >= my_len
is_dead_end = likely_dead_end
move_safety[move] = {
"is_survivable": (not is_dead_end)
and (not is_losing_head_to_head)
and health_after_move > 0,
"reachable_space": reachable_space,
"next_options": next_options,
"tail_escape": has_tail_escape,
}
scores[move] = round(score, 5)
survivable_moves = [
move for move, data in move_safety.items() if data["is_survivable"]
]
if survivable_moves:
best_space = max(
move_safety[move]["reachable_space"] for move in survivable_moves
)
roomy_moves = [
move
for move in survivable_moves
if move_safety[move]["reachable_space"]
>= max(1, int(best_space * 0.60))
]
tail_escape_moves = [
move for move in survivable_moves if move_safety[move]["tail_escape"]
]
if tail_escape_moves:
considered_moves = tail_escape_moves
else:
considered_moves = roomy_moves if roomy_moves else survivable_moves
else:
considered_moves = list(scores.keys())
best_score = max(scores[move] for move in considered_moves)
top_moves = [
move for move in considered_moves if best_score - scores[move] <= 1.5
]
best_move = random.choice(top_moves)
self.recent_heads.append(current_head_point)
self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
return best_move
def _choose_duel_move(
self,
safe_moves,
my_body,
my_len,
my_health,
foods,
food_set,
hazards,
hazard_set,
other_snakes,
enemy_attack_map,
width,
height,
):
duel_weights = self._duel_weights(self.duel_style)
enemy = other_snakes[0]
enemy_head = (enemy["head"]["x"], enemy["head"]["y"])
enemy_len = enemy.get("length", len(enemy["body"]))
scores: dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {}
for move, pos in safe_moves.items():
point = (pos["x"], pos["y"])
ate_food = point in food_set
future_body = self._future_body(
my_body, pos, ate_food, is_constrictor=False
)
blocked = self._simulation_blocked(
future_body=future_body,
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=False,
)
blocked.discard(point)
reachable_space = self._flood_fill_count(point, blocked, width, height)
required_space = len(future_body)
liberties = self._open_neighbor_count(point, blocked, width, height)
next_options = self._next_turn_option_count(
future_body, blocked, width, height
)
nearest_food_dist = self._nearest_food_distance(
point, foods, blocked, width, height
)
future_tail = future_body[-1]
tail_point = (future_tail["x"], future_tail["y"])
tail_dist = self._path_distance(
point, tail_point, blocked - {tail_point}, width, height
)
territory = self._territory_control_score(
my_start=point,
enemy_starts=[enemy_head],
blocked=blocked,
width=width,
height=height,
)
has_tail_escape = tail_dist is not None
likely_dead_end = (
(reachable_space < required_space and not has_tail_escape)
or (liberties == 0 and not has_tail_escape)
or (next_options == 0 and not has_tail_escape)
)
enemy_attack_len = enemy_attack_map.get(point)
losing_head_to_head = (
enemy_attack_len is not None and enemy_attack_len >= my_len
)
direct_head_distance = self._manhattan(point, enemy_head)
score = 0.0
score += reachable_space * 2.8
score += liberties * 18.0
score += next_options * 10.0
score += territory * 0.50
if likely_dead_end:
score -= 1400.0
if losing_head_to_head:
score -= 1500.0
if my_len > enemy_len:
if direct_head_distance == 1:
score += 220.0 * duel_weights["head_pressure"]
elif direct_head_distance == 2:
score += 80.0 * duel_weights["head_pressure"]
else:
if direct_head_distance <= 2:
score -= 120.0 * duel_weights["distance_safety"]
hunger_urgency = max(0.0, (65.0 - my_health) / 65.0)
if nearest_food_dist is not None:
score += (
(25.0 + 90.0 * hunger_urgency) * duel_weights["food_bias"]
) / (nearest_food_dist + 1)
if ate_food:
if likely_dead_end:
score -= 1700.0
else:
score += 260.0 + 250.0 * hunger_urgency
if tail_dist is not None:
score += 14.0 / (tail_dist + 1)
else:
score -= 50.0
if point in hazard_set:
score -= 70.0 if my_health > 35 else 250.0
score -= self._revisit_penalty(point)
if self.last_move == move:
score += 6.0
elif (
self.last_move
and self.OPPOSITE[self.last_move] == move
and len(safe_moves) > 1
):
score -= 20.0
health_after_move = 100 if ate_food else my_health - 1
if point in hazard_set:
health_after_move -= 15
if health_after_move <= 0:
score -= 10000.0
move_safety[move] = {
"is_survivable": (not likely_dead_end)
and (not losing_head_to_head)
and health_after_move > 0,
"reachable_space": reachable_space,
"tail_escape": has_tail_escape,
}
scores[move] = round(score, 5)
survivable_moves = [
move for move, data in move_safety.items() if data["is_survivable"]
]
if survivable_moves:
tail_escape_moves = [
move for move in survivable_moves if move_safety[move]["tail_escape"]
]
if tail_escape_moves:
considered_moves = tail_escape_moves
else:
best_space = max(
move_safety[move]["reachable_space"] for move in survivable_moves
)
considered_moves = [
move
for move in survivable_moves
if move_safety[move]["reachable_space"]
>= max(1, int(best_space * 0.60))
]
if not considered_moves:
considered_moves = survivable_moves
else:
considered_moves = list(scores.keys())
best_score = max(scores[move] for move in considered_moves)
top_moves = [
move for move in considered_moves if best_score - scores[move] <= 1.5
]
return random.choice(top_moves), scores
def _choose_constrictor_move(
self,
safe_moves,
my_body,
my_len,
other_snakes,
food_set,
enemy_attack_map,
width,
height,
):
scores: dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {}
for move, pos in safe_moves.items():
point = (pos["x"], pos["y"])
future_body = self._future_body(
my_body, pos, ate_food=False, is_constrictor=True
)
blocked = self._simulation_blocked(
future_body=future_body,
other_snakes=other_snakes,
food_set=food_set,
is_constrictor=True,
)
blocked.discard(point)
reachable_space = self._flood_fill_count(point, blocked, width, height)
required_space = len(future_body) + 1
liberties = self._open_neighbor_count(point, blocked, width, height)
next_options = self._next_turn_option_count(
future_body, blocked, width, height
)
territory = self._territory_control_score(
my_start=point,
enemy_starts=[
(snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes
],
blocked=blocked,
width=width,
height=height,
)
enemy_len = enemy_attack_map.get(point)
is_losing_head_to_head = enemy_len is not None and enemy_len >= my_len
is_dead_end = (
reachable_space < required_space or liberties == 0 or next_options == 0
)
score = 0.0
score += reachable_space * 3.8
score += liberties * 24.0
score += next_options * 16.0
score += territory * 0.65
if is_dead_end:
score -= 2600.0
if is_losing_head_to_head:
score -= 2400.0
elif enemy_len is not None:
score += 90.0
score -= self._revisit_penalty(point)
if self.last_move == move:
score += 10.0
elif (
self.last_move
and self.OPPOSITE[self.last_move] == move
and len(safe_moves) > 1
):
score -= 35.0
move_safety[move] = {
"is_survivable": (not is_dead_end) and (not is_losing_head_to_head),
"reachable_space": reachable_space,
}
scores[move] = round(score, 5)
survivable_moves = [
move for move, data in move_safety.items() if data["is_survivable"]
]
if survivable_moves:
best_space = max(
move_safety[move]["reachable_space"] for move in survivable_moves
)
considered_moves = [
move
for move in survivable_moves
if move_safety[move]["reachable_space"]
>= max(1, int(best_space * 0.70))
]
if not considered_moves:
considered_moves = survivable_moves
else:
considered_moves = list(scores.keys())
best_score = max(scores[move] for move in considered_moves)
top_moves = [
move for move in considered_moves if best_score - scores[move] <= 2.0
]
return random.choice(top_moves), scores
def _legal_moves(
self, my_head, my_body, other_snakes, food_set, is_constrictor, width, height
):
occupied = self._occupied_cells(my_body, other_snakes)
own_tail = (my_body[-1]["x"], my_body[-1]["y"])
own_tail_stacked = self._is_tail_stacked(my_body)
safe_moves = {}
for move, pos in self.get_possible_moves(my_head).items():
point = (pos["x"], pos["y"])
if not self._in_bounds(point, width, height):
continue
ate_food = point in food_set
can_step_on_tail = self._can_step_on_own_tail(
point=point,
own_tail=own_tail,
own_tail_is_stacked=own_tail_stacked,
ate_food=ate_food,
is_constrictor=is_constrictor,
)
if point in occupied and not can_step_on_tail:
continue
safe_moves[move] = pos
return safe_moves
def _occupied_cells(self, my_body, other_snakes):
occupied = {(segment["x"], segment["y"]) for segment in my_body}
for snake in other_snakes:
occupied |= {(segment["x"], segment["y"]) for segment in snake["body"]}
return occupied
def _simulation_blocked(self, future_body, other_snakes, food_set, is_constrictor):
blocked = {(segment["x"], segment["y"]) for segment in future_body}
if not is_constrictor and not self._is_tail_stacked(future_body):
my_tail = future_body[-1]
blocked.discard((my_tail["x"], my_tail["y"]))
for snake in other_snakes:
for segment in snake["body"]:
blocked.add((segment["x"], segment["y"]))
if is_constrictor:
continue
if self._enemy_can_grow_this_turn(snake, food_set):
continue
if self._is_tail_stacked(snake["body"]):
continue
enemy_tail = snake["body"][-1]
blocked.discard((enemy_tail["x"], enemy_tail["y"]))
return blocked
def _build_enemy_attack_map(
self, my_snake, other_snakes, food_set, is_constrictor, width, height
):
occupied = self._occupied_cells(my_snake["body"], other_snakes)
my_id = my_snake["id"]
attack_map = {}
for enemy in other_snakes:
enemy_len = enemy.get("length", len(enemy["body"]))
enemy_tail = (enemy["body"][-1]["x"], enemy["body"][-1]["y"])
enemy_tail_stacked = self._is_tail_stacked(enemy["body"])
for pos in self.get_possible_moves(enemy["head"]).values():
point = (pos["x"], pos["y"])
if not self._in_bounds(point, width, height):
continue
can_step_on_enemy_tail = (
not is_constrictor
and point == enemy_tail
and not enemy_tail_stacked
and not self._enemy_can_grow_this_turn(enemy, food_set)
)
if point in occupied and not can_step_on_enemy_tail:
continue
# Do not consider impossible overlap directly into my own occupied body except head swap possibilities.
if point in {
(segment["x"], segment["y"])
for segment in my_snake["body"]
if my_snake["id"] == my_id
}:
continue
previous = attack_map.get(point)
if previous is None or enemy_len > previous:
attack_map[point] = enemy_len
return attack_map
def _future_body(self, current_body, next_head, ate_food, is_constrictor):
next_body = [next_head]
next_body.extend(current_body)
if is_constrictor or ate_food:
return next_body
next_body.pop()
return next_body
def _can_step_on_own_tail(
self, point, own_tail, own_tail_is_stacked, ate_food, is_constrictor
):
if is_constrictor:
return False
if ate_food:
return False
if own_tail_is_stacked:
return False
return point == own_tail
def _is_tail_stacked(self, body):
if len(body) < 2:
return False
return body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"]
def _enemy_can_grow_this_turn(self, snake, food_set):
head = snake["head"]
for dx, dy in self.DIRECTIONS.values():
if (head["x"] + dx, head["y"] + dy) in food_set:
return True
return False
def _nearest_food_distance(self, start, foods, blocked, width, height):
if not foods:
return None
targets = {(food["x"], food["y"]) for food in foods}
queue = deque([(start, 0)])
seen = {start}
while queue:
point, distance = queue.popleft()
if point in targets:
return distance
for neighbor in self._neighbors(point):
if neighbor in seen:
continue
if not self._in_bounds(neighbor, width, height):
continue
if neighbor in blocked and neighbor not in targets:
continue
seen.add(neighbor)
queue.append((neighbor, distance + 1))
return None
def _path_distance(self, start, goal, blocked, width, height):
queue = deque([(start, 0)])
seen = {start}
while queue:
point, distance = queue.popleft()
if point == goal:
return distance
for neighbor in self._neighbors(point):
if neighbor in seen:
continue
if not self._in_bounds(neighbor, width, height):
continue
if neighbor in blocked and neighbor != goal:
continue
seen.add(neighbor)
queue.append((neighbor, distance + 1))
return None
def _flood_fill_count(self, start, blocked, width, height):
queue = deque([start])
seen = {start}
while queue:
point = queue.popleft()
for neighbor in self._neighbors(point):
if neighbor in seen:
continue
if not self._in_bounds(neighbor, width, height):
continue
if neighbor in blocked:
continue
seen.add(neighbor)
queue.append(neighbor)
return len(seen)
def _open_neighbor_count(self, start, blocked, width, height):
count = 0
for neighbor in self._neighbors(start):
if not self._in_bounds(neighbor, width, height):
continue
if neighbor in blocked:
continue
count += 1
return count
def _next_turn_option_count(self, future_body, blocked, width, height):
if not future_body:
return 0
next_head = future_body[0]
count = 0
for pos in self.get_possible_moves(next_head).values():
point = (pos["x"], pos["y"])
if not self._in_bounds(point, width, height):
continue
if point in blocked:
continue
count += 1
return count
def _revisit_penalty(self, point):
penalty = 0.0
for index, old_point in enumerate(reversed(self.recent_heads), start=1):
if old_point != point:
continue
penalty += max(0.0, 18.0 - index * 2.0)
return penalty
def _territory_control_score(self, my_start, enemy_starts, blocked, width, height):
if not enemy_starts:
return 0
my_distances = self._distance_map(my_start, blocked, width, height)
enemy_maps = [
self._distance_map(start, blocked, width, height) for start in enemy_starts
]
score = 0
for x in range(width):
for y in range(height):
point = (x, y)
if point in blocked:
continue
my_distance = my_distances.get(point)
if my_distance is None:
continue
enemy_best = None
for enemy_map in enemy_maps:
enemy_distance = enemy_map.get(point)
if enemy_distance is None:
continue
if enemy_best is None or enemy_distance < enemy_best:
enemy_best = enemy_distance
if enemy_best is None or my_distance < enemy_best:
score += 1
elif enemy_best < my_distance:
score -= 1
return score
def _distance_map(self, start, blocked, width, height):
queue = deque([(start, 0)])
distances = {start: 0}
while queue:
point, distance = queue.popleft()
for neighbor in self._neighbors(point):
if neighbor in distances:
continue
if not self._in_bounds(neighbor, width, height):
continue
if neighbor in blocked:
continue
distances[neighbor] = distance + 1
queue.append((neighbor, distance + 1))
return distances
def _neighbors(self, point):
for dx, dy in self.DIRECTIONS.values():
yield (point[0] + dx, point[1] + dy)
def _manhattan(self, a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def _in_bounds(self, point, width, height):
return 0 <= point[0] < width and 0 <= point[1] < height
def _fallback_move(self, head, width, height):
for move, pos in self.get_possible_moves(head).items():
point = (pos["x"], pos["y"])
if self._in_bounds(point, width, height):
return move
return "up"
+221
View File
@@ -0,0 +1,221 @@
from snakes.TemplateSnake import TemplateSnake
from server.GameBoard import GameBoard
from collections import deque
class BetterMasterSnake(TemplateSnake):
def __init__(self):
super().__init__()
self.name = "BetterMasterSnake"
# Definiere die möglichen Bewegungsrichtungen
self.min_safe_area = 2
def choose_move(self, game_data:GameBoard):
self.game_board = game_data
self.calculations = []
self.eat_the_snake_overwrite = False
self.safe_positions = self.find_safe_positions(add_to_calculations=True)
if self.eat_the_snake_overwrite:
return self.overwrite_eat_the_other_snake(game_data.get_turn())
if game_data.get_type() == "constrictor":
move = self.selected_move_constrictor()
else:
move = self.selected_move_standard()
self.add_to_history({"turn": game_data.get_turn(), "data": self.calculations})
return move if move else "up"
def overwrite_eat_the_other_snake(self, turn:int):
self.add_calculations({"function": "eat_the_snake_overwrite", "my_head": self.game_board.get_my_snake_head(), "move": self.kill_the_snake, "safe_positions": self.safe_positions})
self.add_to_history({"turn": turn, "data": self.calculations})
return self.kill_the_snake
#TODO: How to Fill the Gameboard best?
def selected_move_constrictor(self):
move = self.move_close_to_body()
self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})
move = self.ensure_escape_route(move)
self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
return move
def selected_move_standard(self, move=None):
# Finde den besten Weg zur Nahrung
path_to_food = self.find_path_to_food()
if path_to_food:
move = self.move_towards(path_to_food[0])
self.add_calculations({"function": "move_towards", "my_head": self.game_board.get_my_snake_head(), "path_to_food": path_to_food, "move": move})
if not move or self.would_eating_the_food_kill_the_snake(move):
move = self.move_close_to_body(move_close_to_tail=True)
self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})
# Überprfe, ob der Zug einen Ausweg lässt
move = self.ensure_escape_route(move)
self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
return move
def find_path_to_food(self):
# Exclude own snake's body from obstacles
obstacles = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
for snake in self.game_board.get_other_snakes():
for part in snake['body']:
obstacles.add((part['x'], part['y']))
other_snakes_other_snake_posible_moves_set = {(d['x'], d['y']) for d in self.other_snake_posible_moves}
removed_elements_set = set([(elem['x'], elem['y']) for elem in self.game_board.get_food() if (elem['x'], elem['y']) in other_snakes_other_snake_posible_moves_set])
obstacles |= removed_elements_set
self.food_positions = [elem for elem in self.game_board.get_food() if (elem['x'], elem['y']) not in other_snakes_other_snake_posible_moves_set]
if len(self.food_positions) > 0:
# Choose the closest food source based on the heuristic
closest_food = min(self.food_positions, key=lambda food: abs(food['x'] - self.game_board.get_my_snake_head()['x']) + abs(food['y'] - self.game_board.get_my_snake_head()['y']))
self.set_target_food(closest_food)
# Use A* to search for a safe path
return self.a_star_search(self.game_board.get_my_snake_head(), closest_food, obstacles)
return None
def find_path_to_tail(self):
# Exclude other snake's body from obstacles
obstacles = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
for snake in self.game_board.get_other_snakes():
for part in snake['body']:
obstacles.add((part['x'], part['y']))
my_snake_tail = {"x": self.game_board.get_my_snake_tail()['x'], "y": self.game_board.get_my_snake_tail()['y']}
# Use A* to search for a safe path
path = self.a_star_search(self.game_board.get_my_snake_head(), my_snake_tail, obstacles)
return path
def move_towards(self, target):
best_direction = None
min_distance = float('inf')
for direction, coords in self.safe_positions.items():
distance = abs(target['x'] - coords['x']) + abs(target['y'] - coords['y'])
if distance < min_distance:
min_distance = distance
best_direction = direction
return best_direction if best_direction else "up"
def move_close_to_body(self, move_close_to_tail=False):
# Heuristik, um Positionen nahe dem eigenen Körper zu bevorzugen
body_positions = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
tail_position = (self.game_board.get_my_snake_tail()['x'], self.game_board.get_my_snake_tail()['y'])
best_move = None
max_distance = -1 # Initialize maximum distance
for direction, pos in self.safe_positions.items():
next_position = (pos['x'], pos['y'])
if next_position in self.safe_positions:
# Berechne die Distanz zum eigenen Körper
distance_to_body = min(abs(next_position[0] - part[0]) + abs(next_position[1] - part[1]) for part in body_positions)
# Berechne die Distanz zum eigenen Schwanz
distance_to_tail = abs(next_position[0] - tail_position[0]) + abs(next_position[1] - tail_position[1])
# Wähle die maximale Distanz (Körper oder Schwanz)
if move_close_to_tail:
distance = min(next_position, distance_to_tail)
else:
distance = max(next_position, distance_to_body)
# Update max_distance if a larger distance is found
if distance > max_distance:
max_distance = distance
best_move = direction
return best_move if best_move else "up" # Standardbewegung, falls keine bessere gefunden wird
#TODO: Neat to Implement Function to check if eating the food would kill the snake?
def would_eating_the_food_kill_the_snake(self, move:str):
return False
def ensure_escape_route(self, move:str):
try:
future_position = self.safe_positions[move]
except KeyError:
for move, pos in self.safe_positions.items():
if self.is_near_tail(pos, (self.game_board.get_my_snake_tail()['x'], self.game_board.get_my_snake_tail()['y'])):
self.add_calculations({"function": "ensure_escape_route", "move": move, "is_near_tail": True})
move = self.move_towards(pos)
return move
else:
path_to_tail = self.find_path_to_tail()
if path_to_tail:
self.add_calculations({"function": "move_towards", "my_head": self.game_board.get_my_snake_head(), "path_to_tail": path_to_tail, "move": move})
move = self.move_towards(path_to_tail[0])
self.add_calculations({"function": "ensure_escape_route", "move": move, "KeyError": "Snake Coild itself up"})
#return move
# TODO: Fix - Snake Neat to find the best way - Close to the Tail and maybe fill most free cells as posible
return move
def is_near_tail(self, position, tail):
return abs(position["x"] - tail[0]) + abs(position["y"] - tail[1]) <= 2
def a_star_search(self, start, goal, obstacles):
# Helper functions
def is_position_safe(position):
return 0 <= position['x'] < self.game_board.get_width() and 0 <= position['y'] < self.game_board.get_height() and (position['x'], position['y']) not in obstacles
def get_neighbors(position):
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # links, rechts, oben, unten
neighbor = {'x': position['x'] + dx, 'y': position['y'] + dy}
if is_position_safe(neighbor):
neighbors.append(neighbor)
return neighbors
def heuristic(position, goal):
# Verwenden Sie eine Heuristik, die immer positiv ist, selbst wenn das Ziel in der Nähe ist
return max(abs(position['x'] - goal['x']), abs(position['y'] - goal['y']))
# Überprüfen, ob das Ziel direkt neben dem Startpunkt liegt
if start == goal or (abs(start['x'] - goal['x']) <= 1 and abs(start['y'] - goal['y']) <= 1):
# Wenn das Ziel neben dem Startpunkt liegt, ist der Pfad das Ziel selbst
return [goal]
# Initialize the open and closed list
open_set = set([(start['x'], start['y'])])
came_from = {}
g_score = {(start['x'], start['y']): 0}
f_score = {(start['x'], start['y']): heuristic(start, goal)}
while open_set:
current = min(open_set, key=lambda pos: f_score.get(pos, float('inf')))
current_dict = {'x': current[0], 'y': current[1]}
if current_dict == goal:
# Reconstruct the path
path = []
while current in came_from:
current = came_from[current]
path.append({'x': current[0], 'y': current[1]})
path.reverse()
if path and path[0] == start:
path.pop(0) # Entferne das erste Element, wenn es dem Start entspricht
return path # Return the path as a list of dicts
open_set.remove(current)
for neighbor in get_neighbors(current_dict):
neighbor_tuple = (neighbor['x'], neighbor['y'])
tentative_g_score = g_score[current] + 1 # Distance between neighbors is always 1
if tentative_g_score < g_score.get(neighbor_tuple, float('inf')):
came_from[neighbor_tuple] = current
g_score[neighbor_tuple] = tentative_g_score
f_score[neighbor_tuple] = g_score[neighbor_tuple] + heuristic(neighbor, goal)
if neighbor_tuple not in open_set:
open_set.add(neighbor_tuple)
return None # Kein Pfad gefunden
def find_direction(self):
# Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
for direction, pos in self.safe_positions.items():
next_position = (pos['x'], pos['y'])
# Konvertiere safe_positions in eine Liste von Tupeln für den Vergleich
safe_positions_tuples = [(pos['x'], pos['y']) for pos in self.safe_positions.values()]
if next_position in safe_positions_tuples:
return direction
return "up" # Standardbewegung, falls keine sichere Position gefunden wird
-256
View File
@@ -1,256 +0,0 @@
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
+183 -2
View File
@@ -1,6 +1,10 @@
from server.GameBoard import GameBoard
import random
class TemplateSnake: class TemplateSnake:
def __init__(self): def __init__(self):
self.history = [] self.history = []
self.target_food = None
def clear_history(self): def clear_history(self):
self.history = [] self.history = []
@@ -11,5 +15,182 @@ class TemplateSnake:
def get_history(self): def get_history(self):
return self.history return self.history
def choose_move(self, game_data:dict): def add_calculations(self, calculations:dict):
pass self.calculations.append(calculations)
def choose_move(self, game_data:GameBoard):
self.game_board = game_data
self.calculations = []
self.eat_the_snake_overwrite = False
self.safe_positions = self.find_safe_positions(add_to_calculations=True)
moves = list(self.safe_positions.keys())
if len(moves) > 0:
move = random.choice(moves)
else:
print("No safe positions left - Going to Die")
move = None
self.add_to_history({"turn": game_data.get_turn(), "data": self.calculations})
return move if move else "up"
def get_possible_moves(self, snake_head):
return {
"up": {
"x": snake_head["x"],
"y": snake_head["y"] + 1
},
"down": {
"x": snake_head["x"],
"y": snake_head["y"] - 1
},
"left": {
"x": snake_head["x"] - 1,
"y": snake_head["y"]
},
"right": {
"x": snake_head["x"] + 1,
"y": snake_head["y"]
}
}
def get_snake_body_without_snake_tail(self, snake:list[dict]):
if len(set((pos["x"], pos["y"]) for pos in snake)) < 3:
return snake
snake.pop()
return snake
def avoid_my_body(self, my_body:list[dict], my_head:dict, safe_positions:dict[str, dict], add_to_calculations:bool=False) -> list:
"""
my_body: List of dictionaries of x/y coordinates for every segment of a Battlesnake.
e.g. [ {"x": 0, "y": 0}, {"x": 1, "y": 0}, {"x": 2, "y": 0} ]
possible_moves: List of strings. Moves to pick from.
e.g. ["up", "down", "left", "right"]
return: The list of remaining possible_moves, with the 'neck' direction removed
"""
remove = []
my_body = self.did_snake_eat_food(my_body, my_head, add_to_calculations)
for direction, location in safe_positions.items():
if location in my_body:
remove.append(direction)
for direction in remove:
del safe_positions[direction]
if add_to_calculations:
self.add_calculations({"function": "avoid_my_body", "my_body": my_body, "safe_positions": safe_positions})
return safe_positions
def avoid_walls(self, safe_positions:dict[str, dict], add_to_calculations:bool=False):
remove = []
for direction, location in list(safe_positions.items()):
x_out_range = (location["x"] < 0 or location["x"] == self.game_board.get_width())
y_out_range = (location["y"] < 0 or location["y"] == self.game_board.get_height())
if x_out_range or y_out_range:
remove.append(direction)
for direction in remove:
del safe_positions[direction]
if add_to_calculations:
self.add_calculations({"function": "avoid_walls", "board_width": self.game_board.get_width(), "board_height": self.game_board.get_height(), "safe_positions": safe_positions})
return safe_positions
def avoid_snakes(self, other_snakes:list[dict], safe_positions:dict[str, dict], add_to_calculations:bool=False):
remove = []
for snake in other_snakes:
for direction, location in safe_positions.items():
#if self.game_type == "constrictor":
if location in snake["body"]:
remove.append(direction)
#else:
# if location in self.get_snake_body_without_snake_tail(snake["body"]):
# remove.append(direction)
remove = set(remove)
for direction in remove:
del safe_positions[direction]
if add_to_calculations:
self.add_calculations({"function": "avoid_snakes", "other_snakes": other_snakes, "safe_positions": safe_positions})
return safe_positions
def avoid_get_eaten_by_other_snakes(self, other_snakes:list[dict], safe_positions:dict[str, dict], add_to_calculations:bool=False):
remove = []
no_way_out = {}
self.other_snake_posible_moves = []
for snake in other_snakes:
for direction, location in safe_positions.items():
if len(safe_positions) > 1:
self.other_snake_posible_moves = [{"x": v["x"], "y": v["y"]} for k, v in self.get_possible_moves(snake["head"]).items()]
if snake["length"] < self.game_board.get_my_snake()["length"] and location in self.other_snake_posible_moves:
self.eat_the_snake_overwrite = True
self.kill_the_snake = direction
#TODO: Testing - Check if snake on the way to the food here and only remove this pos
elif location in self.other_snake_posible_moves and location in [{"x": food["x"], "y": food["y"]} for food in self.game_board.get_food()]:
remove.append(direction)
elif location in self.other_snake_posible_moves:
no_way_out[direction] = location
remove.append(direction)
remove = set(remove)
for direction in remove:
del safe_positions[direction]
if len(safe_positions) == 0:
safe_positions = no_way_out
if add_to_calculations:
self.add_calculations({"function": "avoid_get_eaten_by_other_snakes", "other_snakes": other_snakes, "safe_positions": safe_positions})
return safe_positions
def find_safe_positions(self, add_to_calculations:bool=False):
safe_positions = self.get_possible_moves(self.game_board.get_my_snake_head())
if add_to_calculations:
self.add_calculations({"function": "get_possible_moves", "safe_positions": safe_positions})
safe_positions = self.avoid_my_body(self.game_board.get_my_snake_body(), self.game_board.get_my_snake_head(), safe_positions, add_to_calculations)
safe_positions = self.avoid_walls(safe_positions, add_to_calculations)
safe_positions = self.avoid_snakes(self.game_board.get_other_snakes(), safe_positions, add_to_calculations)
safe_positions = self.avoid_get_eaten_by_other_snakes(self.game_board.get_other_snakes(), safe_positions, add_to_calculations)
return safe_positions
def calculate_new_body_position(self, move:str=None, with_tail:bool=False):
if move:
head = self.get_possible_moves(self.game_board.get_my_snake_head())[move]
body = [head]
body.extend(self.game_board.get_my_snake_body())
body.pop()
if not with_tail:
body.pop()
return body
return move
def did_snake_eat_food(self, my_body:list[dict], my_head:dict, add_to_calculations:bool=False):
if self.target_food is None:
if add_to_calculations:
self.add_calculations({"function": "did_snake_eat_food", "my_body": my_body, "my_head": my_head, "target_food": self.target_food, "action": "No Target Food"})
return my_body
if self.target_food["x"] == my_head["x"] and self.target_food["y"] == my_head["y"]:
if add_to_calculations:
self.add_calculations({"function": "did_snake_eat_food", "my_body": my_body, "my_head": my_head, "target_food": self.target_food, "action": "Snake Eat no food"})
return my_body
if add_to_calculations:
self.add_calculations({"function": "did_snake_eat_food", "my_body": my_body[:-1], "my_head": my_head, "target_food": self.target_food, "action": "Remove Tail from Body"})
return my_body[:-1]
def set_target_food(self, target_food:dict):
self.target_food = target_food
return True
-4
View File
@@ -1,4 +0,0 @@
BATTLESNAKE_CLI=battlesnake_cli_1.2.3_Linux_x86_64/battlesnake
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser
+398
View File
@@ -0,0 +1,398 @@
import unittest
from snakes.BestBattleSnake import BestBattleSnake
from server.GameBoard import GameBoard
def make_board(game_state):
board = GameBoard(
game_id=game_state["game"]["id"],
width=game_state["board"]["width"],
height=game_state["board"]["height"],
ruleset=game_state["game"]["ruleset"],
source=game_state["game"]["source"],
map=game_state["game"]["map"],
snake_class=BestBattleSnake(),
)
board.read_game_data(game_state)
return board
class TestBestBattleSnake(unittest.TestCase):
def test_avoids_walls_and_body(self):
game_state = {
"game": {
"id": "test-wall-body",
"ruleset": {"name": "standard", "version": "v1.0.0"},
"source": "custom",
"map": "standard",
},
"turn": 20,
"board": {
"height": 7,
"width": 7,
"food": [{"x": 5, "y": 5}],
"hazards": [],
"snakes": [
{
"id": "me",
"name": "me",
"health": 90,
"length": 4,
"head": {"x": 0, "y": 0},
"body": [
{"x": 0, "y": 0},
{"x": 0, "y": 1},
{"x": 1, "y": 1},
{"x": 1, "y": 0},
],
}
],
},
"you": {
"id": "me",
"name": "me",
"health": 90,
"length": 4,
"head": {"x": 0, "y": 0},
"body": [
{"x": 0, "y": 0},
{"x": 0, "y": 1},
{"x": 1, "y": 1},
{"x": 1, "y": 0},
],
},
}
move = make_board(game_state).snake_neat_make_a_move()
self.assertEqual(move, "right")
def test_prioritizes_food_when_low_health(self):
game_state = {
"game": {
"id": "test-food-low-health",
"ruleset": {"name": "standard", "version": "v1.0.0"},
"source": "custom",
"map": "standard",
},
"turn": 32,
"board": {
"height": 11,
"width": 11,
"food": [{"x": 6, "y": 5}],
"hazards": [],
"snakes": [
{
"id": "me",
"name": "me",
"health": 10,
"length": 3,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 5, "y": 3},
],
}
],
},
"you": {
"id": "me",
"name": "me",
"health": 10,
"length": 3,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 5, "y": 3},
],
},
}
move = make_board(game_state).snake_neat_make_a_move()
self.assertEqual(move, "right")
def test_prioritizes_food_when_safe(self):
game_state = {
"game": {
"id": "test-food-safe",
"ruleset": {"name": "standard", "version": "v1.0.0"},
"source": "custom",
"map": "standard",
},
"turn": 8,
"board": {
"height": 11,
"width": 11,
"food": [{"x": 6, "y": 5}],
"hazards": [],
"snakes": [
{
"id": "me",
"name": "me",
"health": 95,
"length": 3,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 5, "y": 3},
],
}
],
},
"you": {
"id": "me",
"name": "me",
"health": 95,
"length": 3,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 5, "y": 3},
],
},
}
move = make_board(game_state).snake_neat_make_a_move()
self.assertEqual(move, "right")
def test_avoids_losing_head_to_head(self):
game_state = {
"game": {
"id": "test-head-to-head",
"ruleset": {"name": "standard", "version": "v1.0.0"},
"source": "custom",
"map": "standard",
},
"turn": 44,
"board": {
"height": 11,
"width": 11,
"food": [{"x": 1, "y": 1}],
"hazards": [],
"snakes": [
{
"id": "me",
"name": "me",
"health": 90,
"length": 3,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 5, "y": 3},
],
},
{
"id": "enemy",
"name": "enemy",
"health": 90,
"length": 6,
"head": {"x": 7, "y": 5},
"body": [
{"x": 7, "y": 5},
{"x": 7, "y": 4},
{"x": 7, "y": 3},
{"x": 7, "y": 2},
{"x": 7, "y": 1},
{"x": 6, "y": 1},
],
},
],
},
"you": {
"id": "me",
"name": "me",
"health": 90,
"length": 3,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 5, "y": 3},
],
},
}
move = make_board(game_state).snake_neat_make_a_move()
self.assertNotEqual(move, "right")
def test_does_not_step_into_stacked_tail(self):
game_state = {
"game": {
"id": "test-stacked-tail",
"ruleset": {"name": "standard", "version": "v1.0.0"},
"source": "custom",
"map": "standard",
},
"turn": 15,
"board": {
"height": 11,
"width": 11,
"food": [{"x": 10, "y": 10}],
"hazards": [],
"snakes": [
{
"id": "me",
"name": "me",
"health": 90,
"length": 5,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 4, "y": 4},
{"x": 4, "y": 5},
{"x": 4, "y": 5},
],
}
],
},
"you": {
"id": "me",
"name": "me",
"health": 90,
"length": 5,
"head": {"x": 5, "y": 5},
"body": [
{"x": 5, "y": 5},
{"x": 5, "y": 4},
{"x": 4, "y": 4},
{"x": 4, "y": 5},
{"x": 4, "y": 5},
],
},
}
move = make_board(game_state).snake_neat_make_a_move()
self.assertNotEqual(move, "left")
def test_avoids_food_if_it_is_a_dead_end(self):
game_state = {
"game": {
"id": "test-food-dead-end",
"ruleset": {"name": "standard", "version": "v1.0.0"},
"source": "custom",
"map": "standard",
},
"turn": 30,
"board": {
"height": 7,
"width": 7,
"food": [{"x": 3, "y": 4}],
"hazards": [],
"snakes": [
{
"id": "me",
"name": "me",
"health": 70,
"length": 3,
"head": {"x": 3, "y": 3},
"body": [
{"x": 3, "y": 3},
{"x": 3, "y": 2},
{"x": 3, "y": 1},
],
},
{
"id": "enemy",
"name": "enemy",
"health": 90,
"length": 5,
"head": {"x": 2, "y": 4},
"body": [
{"x": 2, "y": 4},
{"x": 2, "y": 5},
{"x": 3, "y": 5},
{"x": 4, "y": 5},
{"x": 4, "y": 4},
],
},
],
},
"you": {
"id": "me",
"name": "me",
"health": 70,
"length": 3,
"head": {"x": 3, "y": 3},
"body": [
{"x": 3, "y": 3},
{"x": 3, "y": 2},
{"x": 3, "y": 1},
],
},
}
move = make_board(game_state).snake_neat_make_a_move()
self.assertNotEqual(move, "up")
def test_constrictor_avoids_growth_dead_end(self):
game_state = {
"game": {
"id": "test-constrictor-dead-end",
"ruleset": {"name": "constrictor", "version": "v1.0.0"},
"source": "custom",
"map": "standard",
},
"turn": 12,
"board": {
"height": 7,
"width": 7,
"food": [],
"hazards": [],
"snakes": [
{
"id": "me",
"name": "me",
"health": 100,
"length": 4,
"head": {"x": 1, "y": 1},
"body": [
{"x": 1, "y": 1},
{"x": 1, "y": 0},
{"x": 0, "y": 0},
{"x": 0, "y": 1},
],
},
{
"id": "enemy",
"name": "enemy",
"health": 100,
"length": 8,
"head": {"x": 4, "y": 4},
"body": [
{"x": 4, "y": 4},
{"x": 3, "y": 4},
{"x": 3, "y": 3},
{"x": 2, "y": 3},
{"x": 2, "y": 2},
{"x": 2, "y": 0},
{"x": 2, "y": 2},
{"x": 3, "y": 1},
],
},
],
},
"you": {
"id": "me",
"name": "me",
"health": 100,
"length": 4,
"head": {"x": 1, "y": 1},
"body": [
{"x": 1, "y": 1},
{"x": 1, "y": 0},
{"x": 0, "y": 0},
{"x": 0, "y": 1},
],
},
}
move = make_board(game_state).snake_neat_make_a_move()
self.assertEqual(move, "up")
if __name__ == "__main__":
unittest.main()
+52
View File
@@ -0,0 +1,52 @@
import unittest
from typing import cast
from server.Dataset import Dataset
from server.GameBoard import GameBoard
class DummySnake:
def get_history(self):
return [
{"turn": 1, "data": [{"score": 1}]},
{"turn": 2, "data": [{"score": 2}]},
]
class DummyGameBoard:
def __init__(self, winners):
self.id = "game-1"
self.map = "standard"
self.winner_snake_names = winners
self.snake_class = DummySnake()
self.turns = [
{"turn": 1, "move": "up", "game_board": {"width": 11, "height": 11}},
{"turn": 2, "move": "left", "game_board": {"width": 11, "height": 11}},
]
def get_type_of_game(self):
return {"name": "standard", "is_ladder": False}
class TestDataset(unittest.TestCase):
def test_build_only_good_moves_for_wins(self):
dataset = Dataset(cast(GameBoard, DummyGameBoard(["me"])))
payload = dataset.build(only_good_moves=True)
self.assertTrue(payload["did_win"])
self.assertEqual(payload["total_samples"], 2)
self.assertTrue(all(sample["is_good_move"] for sample in payload["samples"]))
def test_build_returns_no_samples_for_losses_when_only_good(self):
dataset = Dataset(cast(GameBoard, DummyGameBoard(["enemy"])))
payload = dataset.build(only_good_moves=True)
self.assertFalse(payload["did_win"])
self.assertEqual(payload["total_samples"], 0)
def test_labels_by_turn(self):
winner_labels = Dataset(cast(GameBoard, DummyGameBoard(["me"]))).labels_by_turn()
loser_labels = Dataset(cast(GameBoard, DummyGameBoard(["enemy"]))).labels_by_turn()
self.assertEqual(winner_labels, {1: True, 2: True})
self.assertEqual(loser_labels, {1: False, 2: False})
if __name__ == "__main__":
unittest.main()
+47
View File
@@ -0,0 +1,47 @@
import json
import tempfile
import unittest
from pathlib import Path
from server.DatasetExporter import DatasetExporter
class TestDatasetExporter(unittest.TestCase):
def test_export_jsonl(self):
with tempfile.TemporaryDirectory() as tmp:
input_dir = Path(tmp) / "data"
output_file = Path(tmp) / "out" / "dataset.jsonl"
game_file = input_dir / "game-1.json"
game_file.parent.mkdir(parents=True, exist_ok=True)
game_payload = {
"dataset": {
"game": {"id": "g-1", "map": "standard", "type": {"name": "duel"}},
"snake": {"type": "BestBattleSnake"},
"samples": [
{
"turn": 1,
"move": "up",
"is_good_move": True,
"game_board": {"width": 11, "height": 11},
"history": {"data": []},
}
],
}
}
game_file.write_text(json.dumps(game_payload), encoding="utf-8")
report = DatasetExporter(str(input_dir), str(output_file)).export_jsonl()
self.assertEqual(report["games_scanned"], 1)
self.assertEqual(report["samples_exported"], 1)
self.assertTrue(output_file.exists())
lines = output_file.read_text(encoding="utf-8").strip().splitlines()
self.assertEqual(len(lines), 1)
first = json.loads(lines[0])
self.assertEqual(first["game_id"], "g-1")
self.assertEqual(first["move"], "up")
self.assertTrue(first["is_good_move"])
if __name__ == "__main__":
unittest.main()
Generated
+302
View File
@@ -0,0 +1,302 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "aiofiles"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "certifi"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "dotenv"
version = "0.9.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dotenv" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
]
[[package]]
name = "flask"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
[[package]]
name = "gel"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/70/84cee8eb48a2760893e60e8267502bc16ddef2197dd41f20c103a3f04e01/gel-3.1.0.tar.gz", hash = "sha256:3bb0b21167e00c976675a7d5dd73c21002e659651bf9de7fa46cd418bfa3eb85", size = 1331332, upload-time = "2025-04-29T20:44:49.659Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/f4/69cf0d0753e93526659d4810d7816d33a6c72f2934b26d14da283602d175/gel-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5e41f4ddc86e054256e81302905e671647577c1aed7249e1781aee1e0cb2f7c", size = 907956, upload-time = "2025-04-29T20:44:28.285Z" },
{ url = "https://files.pythonhosted.org/packages/4f/1b/88cc28e5c58e912b9d74f5b27bb41203af88cac9ea6cf329ccb9f4a23fab/gel-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd20c45beec358197293ad5c1fcf7fd383153e0d8d7038322085cf27436dc975", size = 863463, upload-time = "2025-04-29T20:44:29.906Z" },
{ url = "https://files.pythonhosted.org/packages/e2/52/57ce33203a606dac51abf16aaa2388d31a4efac434bb3730dc22494cea13/gel-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a526e11ba696d1cf6c876c08afe6ab5f762a227faaa3b78523ebc2511567609", size = 4619168, upload-time = "2025-04-29T20:44:32.895Z" },
{ url = "https://files.pythonhosted.org/packages/eb/ad/eb7958f8197ba763054a706892c4948753f210c6f82ce46f7de9d13d4f94/gel-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06f11fcde231d6264dbe0b19d7b2942767a196e9c6f2cabf22fbb04094134af", size = 4729625, upload-time = "2025-04-29T20:44:34.747Z" },
{ url = "https://files.pythonhosted.org/packages/cd/62/7b89eb81430f5714bf56f257679998a9a09f327f60d3c061dece5b5b2797/gel-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:48235a5082f03d7eccbcaa4255260c8a3cdb2cfd123f121dd6b0d3ebc51421ac", size = 831743, upload-time = "2025-04-29T20:44:36.992Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "h2"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
[[package]]
name = "hpack"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
]
[[package]]
name = "hypercorn"
version = "0.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
{ name = "h2" },
{ name = "priority" },
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/01/39f41a014b83dd5c795217362f2ca9071cf243e6a75bdcd6cd5b944658cc/hypercorn-0.18.0.tar.gz", hash = "sha256:d63267548939c46b0247dc8e5b45a9947590e35e64ee73a23c074aa3cf88e9da", size = 68420, upload-time = "2025-11-08T13:54:04.78Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/35/850277d1b17b206bd10874c8a9a3f52e059452fb49bb0d22cbb908f6038b/hypercorn-0.18.0-py3-none-any.whl", hash = "sha256:225e268f2c1c2f28f6d8f6db8f40cb8c992963610c5725e13ccfcddccb24b1cd", size = 61640, upload-time = "2025-11-08T13:54:03.202Z" },
]
[[package]]
name = "hyperframe"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "priority"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792, upload-time = "2021-06-27T10:15:05.487Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946, upload-time = "2021-06-27T10:15:03.856Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "quart"
version = "0.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
{ name = "blinker" },
{ name = "click" },
{ name = "flask" },
{ name = "hypercorn" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/9d/12e1143a5bd2ccc05c293a6f5ae1df8fd94a8fc1440ecc6c344b2b30ce13/quart-0.20.0.tar.gz", hash = "sha256:08793c206ff832483586f5ae47018c7e40bdd75d886fee3fabbdaa70c2cf505d", size = 63874, upload-time = "2024-12-23T13:53:05.664Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/e9/cc28f21f52913adf333f653b9e0a3bf9cb223f5083a26422968ba73edd8d/quart-0.20.0-py3-none-any.whl", hash = "sha256:003c08f551746710acb757de49d9b768986fd431517d0eb127380b656b98b8f1", size = 77960, upload-time = "2024-12-23T13:53:02.842Z" },
]
[[package]]
name = "snake-python"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "dotenv" },
{ name = "gel" },
{ name = "quart" },
]
[package.metadata]
requires-dist = [
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "gel", specifier = ">=3.1.0" },
{ name = "quart", specifier = ">=0.20.0" },
]
[[package]]
name = "werkzeug"
version = "3.1.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" },
]
[[package]]
name = "wsproto"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
]