Compare commits
94 Commits
v1.0
...
9e3a62d8e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
9e3a62d8e8
|
|||
|
9093ca0512
|
|||
|
6e74b5fb57
|
|||
|
962d8b1043
|
|||
|
8ea9cbdcee
|
|||
|
c458219125
|
|||
|
a7a463ed91
|
|||
|
5c1ef7f05f
|
|||
|
d32568cdf2
|
|||
|
61721a7eb6
|
|||
|
bcc9c71c30
|
|||
|
58bbbf3cbd
|
|||
|
4d515f0784
|
|||
|
8424c324e8
|
|||
|
c5c2652f3a
|
|||
|
0768e7f254
|
|||
|
31d2e7ea55
|
|||
|
4b51ddc84d
|
|||
|
31f5225100
|
|||
|
deb95c6246
|
|||
|
600cde4a3e
|
|||
|
e068fb8614
|
|||
|
80b7c4df89
|
|||
|
da0347731c
|
|||
|
a09c05b6ec
|
|||
|
aba457423e
|
|||
|
bb92715de1
|
|||
|
cf45aa60aa
|
|||
|
a58e9695dd
|
|||
|
b57ae5eab2
|
|||
|
817b970623
|
|||
|
c9e6947758
|
|||
|
4a1fbf2752
|
|||
|
5b8bf0da31
|
|||
|
4a8cb40bde
|
|||
|
c5342c1f4d
|
|||
|
ac7c397093
|
|||
|
7dd46dd72b
|
|||
|
10c7f2656c
|
|||
|
f00efe607f
|
|||
|
c333706b75
|
|||
|
917bd3f6bd
|
|||
|
83bcf4f194
|
|||
|
ef4dca447f
|
|||
|
db3a353090
|
|||
|
1f4d17d42f
|
|||
|
f98430462b
|
|||
|
c26824aeaf
|
|||
|
87690177a5
|
|||
|
8a2a62ef57
|
|||
|
5796ce0a6e
|
|||
|
5522a52227
|
|||
|
5743f5c111
|
|||
|
9950fa1952
|
|||
|
4620ee31eb
|
|||
|
9103e3e139
|
|||
|
04eef9229c
|
|||
|
7cb1fdc57d
|
|||
|
cceded8468
|
|||
|
8c57e48f60
|
|||
|
5ce12d70c1
|
|||
|
950351b407
|
|||
|
12ac257d19
|
|||
|
d4b54d48b9
|
|||
|
034b0e361a
|
|||
|
a57536b7cb
|
|||
|
a606ae6f94
|
|||
|
b364c6454e
|
|||
|
b9a0bca4c6
|
|||
|
ea36d60b4d
|
|||
|
0fe4e6ac83
|
|||
|
93b2c8ba99
|
|||
|
f6db5cb96a
|
|||
|
e3d7cccb64
|
|||
|
0eecbb774b
|
|||
|
9e0a919233
|
|||
|
6d9df32076
|
|||
|
863ca1b277
|
|||
|
16cab3a9ca
|
|||
|
281b52e71d
|
|||
|
38ba576de9
|
|||
|
7457e66339
|
|||
|
b601b378c8
|
|||
|
24e744f705
|
|||
|
39b16a1702
|
|||
|
87fe6550b2
|
|||
|
c854b5fec9
|
|||
|
51108ce21c
|
|||
|
2f8e35aced
|
|||
|
9869f14bbe
|
|||
|
1154127a40
|
|||
|
ae1489240d
|
|||
|
0af6d862d4
|
|||
|
f472ddd0d9
|
@@ -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 }}
|
||||||
@@ -7,3 +7,4 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
data/
|
data/
|
||||||
.env
|
.env
|
||||||
|
dbschema/migrations/
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
+5
-3
@@ -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"]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env -S uv run --script
|
||||||
|
|
||||||
# Welcome to
|
# Welcome to
|
||||||
# __________ __ __ .__ __
|
# __________ __ __ .__ __
|
||||||
@@ -12,18 +12,23 @@
|
|||||||
# 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):
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
+19
-11
@@ -1,16 +1,24 @@
|
|||||||
|
import aiofiles.os
|
||||||
|
import aiofiles
|
||||||
import os
|
import os
|
||||||
|
|
||||||
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)
|
return await callback(f)
|
||||||
|
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:
|
||||||
|
await callback(data, f, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
await f.write(data)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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}"
|
|
||||||
+114
-77
@@ -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.get("/")
|
self.app = Quart("Battlesnake")
|
||||||
def on_info():
|
|
||||||
return self._info()
|
|
||||||
|
|
||||||
@self.app.post("/start")
|
|
||||||
def on_start():
|
|
||||||
game_state = request.get_json()
|
|
||||||
self._start(game_state)
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
@self.app.post("/move")
|
|
||||||
def on_move():
|
|
||||||
game_state = request.get_json()
|
|
||||||
return self._move(game_state)
|
|
||||||
|
|
||||||
@self.app.post("/end")
|
|
||||||
def on_end():
|
|
||||||
game_state = request.get_json()
|
|
||||||
self._end(game_state)
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
@self.app.after_request
|
|
||||||
def identify_server(response):
|
|
||||||
response.headers.set(
|
|
||||||
"server", "battlesnake/github/starter-snake-python"
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def run(self, host:str="0.0.0.0", port:str="8000", debug:bool=False):
|
|
||||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
print(f"\nRunning Battlesnake at http://{host}:{port} with the {self.snake_type.replace('Snake', '')} Snake")
|
|
||||||
self.app.run(host=host, port=port, debug=debug)
|
|
||||||
|
|
||||||
def _read_json_config_or_create(self):
|
|
||||||
snake_config = read_file(self.config_file, json.load)
|
|
||||||
if not snake_config:
|
|
||||||
snake_config = self.default_snake_config
|
|
||||||
save_file(self.config_file, snake_config, callback=json.dump, indent=2, ensure_ascii=False)
|
|
||||||
return snake_config
|
|
||||||
|
|
||||||
def enable_store_game_state(self):
|
|
||||||
self.store_game_state = True
|
|
||||||
|
|
||||||
# info is called when you create your Battlesnake on play.battlesnake.com
|
# info is called when you create your Battlesnake on play.battlesnake.com
|
||||||
# and controls your Battlesnake's appearance
|
# and controls your Battlesnake's appearance
|
||||||
# TIP: If you open your Battlesnake URL in a browser you should see this data
|
# TIP: If you open your Battlesnake URL in a browser you should see this data
|
||||||
def _info(self) -> dict:
|
@self.app.get("/")
|
||||||
snake_config = self._read_json_config_or_create()
|
async def on_info():
|
||||||
|
snake_config = await self._read_json_config_or_create()
|
||||||
|
|
||||||
print("INFO Snake:", snake_config)
|
print("INFO Snake:", snake_config)
|
||||||
return snake_config
|
return snake_config
|
||||||
|
|
||||||
# start is called when your Battlesnake begins a game
|
# start is called when your Battlesnake begins a game
|
||||||
def _start(self, game_state:dict):
|
@self.app.post("/start")
|
||||||
if self.store_game_state:
|
async def on_start():
|
||||||
self.running_games[game_state["game"]["id"]] = GameStorage(self.snake.__class__.__name__, path=os.path.join(self.data_path, 'data', 'history'))
|
game_state = await request.get_json()
|
||||||
self.running_games[game_state["game"]["id"]].start_new_game(game_state["game"], game_state["board"], game_state["you"])
|
await self._create_game_board(game_state)
|
||||||
|
|
||||||
self.running_snake[game_state["game"]["id"]] = SnakeBuilder.build(self.snake_type)
|
|
||||||
print("GAME START:", game_state["game"])
|
print("GAME START:", game_state["game"])
|
||||||
|
return "ok"
|
||||||
|
|
||||||
# move is called when your Battlesnake game is running game
|
# move is called when your Battlesnake game is running game
|
||||||
def _move(self, game_state:dict) -> dict:
|
@self.app.post("/move")
|
||||||
next_move = self.running_snake[game_state["game"]["id"]].choose_move(game_state)
|
async def on_move():
|
||||||
|
game_state = await request.get_json()
|
||||||
|
game_board = await self._get_game_board(game_state)
|
||||||
|
next_move = game_board.snake_neat_make_a_move()
|
||||||
|
|
||||||
if self.store_game_state:
|
|
||||||
self.running_games[game_state["game"]["id"]].add_moves(game_state["board"], next_move)
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
print(self.running_games[game_state["game"]["id"]])
|
print("TURN:", f'{game_state["turn"]:3},', "MOVE:", f"{next_move:5}")
|
||||||
|
|
||||||
print("MOVE:", f"{next_move:5},", "Me:", {"head": game_state["you"]["head"], "length": game_state["you"]["length"]})
|
|
||||||
return {"move": next_move}
|
return {"move": next_move}
|
||||||
|
|
||||||
# end is called when your Battlesnake finishes a game
|
# end is called when your Battlesnake finishes a game
|
||||||
def _end(self, game_state:dict):
|
@self.app.post("/end")
|
||||||
|
async def on_end():
|
||||||
|
game_state = await request.get_json()
|
||||||
if self.store_game_state:
|
if self.store_game_state:
|
||||||
snake = self.running_snake[game_state["game"]["id"]]
|
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:
|
||||||
self.running_games[game_state["game"]["id"]].add_end_state(game_state["board"], snake.get_history(), game_state["turn"])
|
if self.check_tls_security:
|
||||||
self.running_games[game_state["game"]["id"]].save(
|
await game_board.save(
|
||||||
f"{snake.__class__.__name__}_{datetime.now().strftime('%d.%m.%Y_%H%M%S')}_{game_state['game']['id']}.json",
|
StorageLoader.build(self.storage_type),
|
||||||
callback=json.dump, indent=2, ensure_ascii=False
|
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"
|
||||||
|
|
||||||
|
@self.app.after_request
|
||||||
|
async def identify_server(response):
|
||||||
|
response.headers.set(
|
||||||
|
"server", "battlesnake/gitea/snake-python"
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
async def _read_json_config_or_create(self):
|
||||||
|
snake_config = await read_file(self.config_file, json.load)
|
||||||
|
if not snake_config:
|
||||||
|
return await self._override_snake_config_with_environment_variables(self.default_snake_config)
|
||||||
|
return await self._override_snake_config_with_environment_variables(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"]]
|
del self.running_games[game_state["game"]["id"]]
|
||||||
|
|
||||||
print("GAME OVER:\n- Winner is", [ x["name"] for x in game_state["board"]['snakes']])
|
async def _get_game_board(self, game_state:str, end:bool=False):
|
||||||
del self.running_snake[game_state["game"]["id"]]
|
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):
|
||||||
|
self.store_game_state = True
|
||||||
|
|
||||||
|
def _cleanup_database(self):
|
||||||
|
storage = StorageLoader.build(self.storage_type)()
|
||||||
|
return storage.cleanup()
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
from server.GameBoard import GameBoard
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import gel, json, time
|
||||||
|
|
||||||
|
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):
|
||||||
|
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 gel.errors.ClientConnectionFailedError:
|
||||||
|
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() ]
|
||||||
|
|
||||||
|
for i in range(len(moves)):
|
||||||
|
data.append({"turn": moves[i]["turn"], "move": moves[i]["move"], "game_board": moves[i]["game_board"], "calculations": snake_calulations[i]})
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from server.GameBoard import GameBoard
|
||||||
|
from server.Files import save_file
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
await save_file(save_file_path, {
|
||||||
|
"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(),
|
||||||
|
},
|
||||||
|
}, callback=json.dump, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
pass
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
+5
-2
@@ -1,4 +1,7 @@
|
|||||||
BATTLESNAKE_CLI=battlesnake_cli_1.2.3_Linux_x86_64/battlesnake
|
BATTLESNAKE_CLI=battlesnake_cli_1.2.3_Linux_x86_64/battlesnake
|
||||||
|
|
||||||
|
if [ -z $1 ]; then
|
||||||
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser
|
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser --seed 1713099635738952360
|
||||||
|
else
|
||||||
|
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g constrictor --browser --minimumFood 0
|
||||||
|
fi
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user