1 Commits

Author SHA1 Message Date
daniel156161 156ccbaf10 Merge pull request 'main' (#3) from main into dev
Reviewed-on: #3
2024-04-13 11:48:40 +02:00
24 changed files with 585 additions and 1377 deletions
-34
View File
@@ -1,34 +0,0 @@
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@v4
with:
token: '${{ secrets.ACTION_ACCESS_TOKEN }}'
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
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@v6
with:
context: .
push: true
tags: ${{ vars.DOCKER_REGISTRY_URL }}/daniel156161/battlesnake:latest
platforms: linux/amd64
-1
View File
@@ -7,4 +7,3 @@
__pycache__/
data/
.env
dbschema/migrations/
-1
View File
@@ -1 +0,0 @@
3.13
+3 -5
View File
@@ -1,13 +1,11 @@
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim
# RUN apk add --no-cache build-base
FROM python:3.10.6-slim
# Install app
COPY . /app
WORKDIR /app
# Install dependencies
RUN uv sync --no-config --frozen --compile-bytecode
RUN pip install --upgrade pip && pip install -r requirements.txt
# Run Battlesnake
CMD ["uv", "run", "main.py"]
CMD [ "python", "main.py" ]
-95
View File
@@ -1,95 +0,0 @@
module default {
function is_winner_me(winner: str) -> bool
using (winner = "me");
function gameboard_url(id: uuid) -> str
using ("https://play.battlesnake.com/game/" ++ <str>id);
type GameBoard {
overloaded required id: uuid {
readonly := true;
constraint exclusive;
}
url := gameboard_url(.id);
required created_at: datetime {
readonly := true;
}
required turns: int32 {
readonly := true;
}
required map: str {
readonly := true;
default := "standard";
}
required single type: GameType {
readonly := true;
on source delete delete target if orphan;
}
required single ruleset: Ruleset {
readonly := true;
on source delete delete target if orphan;
}
required winner: str {
readonly := true;
}
multi moves: Moves {
default := <Moves>{};
on source delete delete target;
on target delete allow;
}
required single snake: Snake {
readonly := true;
on source delete delete target if orphan;
}
is_winner_me := is_winner_me(.winner);
has_moves := exists(.moves);
}
type GameType {
required name: str {
readonly := true;
}
required is_ladder: bool {
readonly := true;
}
constraint exclusive on ( (.name, .is_ladder) );
}
type Ruleset {
required name: str {
readonly := true;
}
required version: str {
readonly := true;
}
required settings: json {
readonly := true;
}
constraint exclusive on ( (.name, .version, .settings) );
}
type Snake {
required type: str {
readonly := true;
}
constraint exclusive on ( .type );
}
type Moves {
required turn: int32 {
readonly := true;
}
required snake_move: str {
readonly := true;
}
required game_board: json {
readonly := true;
}
calculations: array<json> {
readonly := true;
}
}
}
-5
View File
@@ -11,9 +11,4 @@ services:
build:
context: ./
dockerfile: Dockerfile
#environment:
# - SNAKE_COLOR=blue
# - SNAKE_HEAD=caffeine
# - SNAKE_TAIL=mlh-gene
# - STORE_GAME_HISTORY=True
restart: always
-2
View File
@@ -1,2 +0,0 @@
[edgedb]
server-version = "5.3"
+4 -9
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S uv run --script
#!/usr/bin/env python3
# Welcome to
# __________ __ __ .__ __
@@ -12,23 +12,18 @@
# To get you started we've included code to prevent your Battlesnake from moving backwards.
# For more info see docs.battlesnake.com
from server.CreateEnvironmentFile import CreateEnvironmentFile
from server.Server import Server
from dotenv import load_dotenv, find_dotenv
import os
# Start server when `python main.py` is run
if __name__ == "__main__":
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})
load_dotenv(find_dotenv())
server = Server(
data_path=os.path.dirname(__file__),
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
snake_type=os.environ.get("SNAKE", "DummSnake"),
)
if os.environ.get("STORE_GAME_HISTORY", None):
-11
View File
@@ -1,11 +0,0 @@
[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",
]
+10 -19
View File
@@ -1,19 +1,10 @@
aiofiles==25.1.0
blinker==1.9.0
click==8.3.1
dotenv==0.9.9
flask==3.1.2
gel==3.1.0
h11==0.16.0
h2==4.3.0
hpack==4.1.0
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
blinker==1.7.0
click==8.1.7
Flask==3.0.2
itsdangerous==2.1.2
Jinja2==3.1.3
MarkupSafe==2.1.5
numpy==1.26.4
python-dotenv==1.0.1
scipy==1.12.0
Werkzeug==3.0.1
-26
View File
@@ -1,26 +0,0 @@
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)
+11 -19
View File
@@ -1,24 +1,16 @@
import aiofiles.os
import aiofiles
import os
async def read_file(path: str, callback=None):
if not await aiofiles.os.path.exists(path):
def read_file(path, callback=None):
if os.path.exists(path):
with open(path, 'r') as f:
data = callback(f)
return data
else:
return None
async with aiofiles.open(path, "r") as f:
if callback:
return await callback(f)
return await f.read()
def save_file(path, data, callback=None, *args, **kwargs):
if not os.path.exists(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
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)
with open(path, 'w') as f:
callback(data, f, *args, **kwargs)
-140
View File
@@ -1,140 +0,0 @@
from datetime import datetime
class GameBoard:
def __init__(self, game_id:str, width:int, height:int, ruleset:dict, source:str, map:str, snake_class):
self.id = game_id
self.width = width
self.height = height
self.type = ruleset["name"]
self.snake_class = snake_class
# What will get Stored
self.winner_snake_names = None
self.now_date = datetime.now()
self.turns = []
self.is_ladder = True if source == "ladder" else False
self.ruleset = ruleset
self.map = map
self.url = self._get_game_url(True if ruleset["version"] == "cli" else False)
# Setter Functions
def _set_snakes(self, snakes:list[dict]):
self.other_snakes = [ x for x in snakes if x["id"] != self.my_snake["id"] ]
def _set_my_snake(self, my_snake:str):
self.my_snake = my_snake
def _set_food(self, food:list[dict]):
self.food = food
def _set_hazards(self, hazards:list[dict]):
self.hazards = hazards
def _set_turn(self, turn:int):
self.turn = turn
# Getter Functions
def get_other_snakes(self):
return self.other_snakes
def get_my_snake(self):
return self.my_snake
def get_food(self):
return self.food
def get_hazard(self):
return self.hazards
def get_turn(self):
return self.turn
def get_dimension(self):
return {"width": self.width, "height": self.height}
def get_width(self):
return self.width
def get_height(self):
return self.height
def get_type(self):
return self.type
def get_my_snake_head(self):
return self.my_snake["head"]
def get_my_snake_body(self):
return self.my_snake["body"]
def get_my_snake_tail(self):
return self.my_snake["body"][-1]
def get_game_board_as_dict(self):
snakes = [self.my_snake]
snakes.extend(self.other_snakes)
return {
"height": self.height,
"width": self.width,
"snakes": snakes,
"food": self.food,
"hazards": self.hazards
}
# Game Functions
def read_game_data(self, game_data:dict):
self._set_food(game_data['board']['food'])
self._set_hazards(game_data['board']['hazards'])
self._set_my_snake(game_data['you'])
self._set_snakes(game_data['board']['snakes'])
self._set_turn(game_data["turn"])
async def start_game(self, game_data:dict):
self.init_snakes = len(game_data['board']['snakes'])
def end_game(self, game_data:dict):
self._set_winner_snake_name(game_data['board']['snakes'])
self.get_type_of_game()
# Function get Called from Server
def snake_neat_make_a_move(self):
move = self.snake_class.choose_move(self)
self.turns.append({
"turn": self.turn,
"move": move,
"game_board": self.get_game_board_as_dict()
})
return move
# Save functions
def _get_game_url(self, local_game:bool):
if local_game:
return None
return f"https://play.battlesnake.com/game/{self.id}"
def _set_winner_snake_name(self, snakes:list[dict]):
if self.my_snake["id"] in [ x["id"] for x in snakes]:
self.winner_snake_names = ["me"]
else:
self.winner_snake_names = [ x["name"] for x in snakes]
if len(self.winner_snake_names) == 0:
self.winner_snake_names = None
def get_winner(self):
return self.winner_snake_names
def get_type_of_game(self):
if self.init_snakes == 2:
return {"name": "duel", "is_ladder": self.is_ladder}
return {"name": self.type, "is_ladder": self.is_ladder}
async def save(self, store_class, **kwargs):
store = store_class(**kwargs)
await store.save(self)
del store
+59
View File
@@ -0,0 +1,59 @@
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}"
+73 -110
View File
@@ -1,150 +1,113 @@
from server.Files import read_file
from server.GameBoard import GameBoard
from server.Files import read_file, save_file
from server.GameStorage import GameStorage
from snakes.TemplateSnake import TemplateSnake
from server.SnakeBuilder import SnakeBuilder
from server.storage.StorageLoader import StorageLoader
from quart import Quart, request, jsonify
import logging, json, os, re
from datetime import datetime
from flask import Flask
from flask import request
import logging, json, os
class Server:
default_snake_config = {"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"}
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):
def __init__(self, data_path:str, snake_type:str, debug:bool=False):
self.debug = debug
self.snake_type = snake_type
self.storage_type = storage_type
self.config_file = os.path.join(data_path, 'data', 'snake-config.json')
self.data_path = data_path
self.check_tls_security = check_tls_security
self.store_game_state = False
self.store_game_when_win_and_moves_are_bigger_as = store_game_when_win_and_moves_are_bigger_as
self.running_games:dict[str, GameStorage] = {}
self.running_snake:dict[str, TemplateSnake] = {}
self.running_games:dict[str, GameBoard] = {}
self.app = Flask("Battlesnake")
self.app = Quart("Battlesnake")
# info is called when you create your Battlesnake on play.battlesnake.com
# and controls your Battlesnake's appearance
# TIP: If you open your Battlesnake URL in a browser you should see this data
@self.app.get("/")
async def on_info():
snake_config = await self._read_json_config_or_create()
def on_info():
return self._info()
print("INFO Snake:", snake_config)
return snake_config
# start is called when your Battlesnake begins a game
@self.app.post("/start")
async def on_start():
game_state = await request.get_json()
await self._create_game_board(game_state)
print("GAME START:", game_state["game"])
def on_start():
game_state = request.get_json()
self._start(game_state)
return "ok"
# move is called when your Battlesnake game is running game
@self.app.post("/move")
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()
def on_move():
game_state = request.get_json()
return self._move(game_state)
if self.debug:
print("TURN:", f'{game_state["turn"]:3},', "MOVE:", f"{next_move:5}")
return {"move": next_move}
# end is called when your Battlesnake finishes a game
@self.app.post("/end")
async def on_end():
game_state = await request.get_json()
if self.store_game_state:
game_board = await self._get_game_board(game_state, end=True)
#if not game_board.get_winner() == "me" and not game_board.get_turn() <= self.store_game_when_win_and_moves_are_bigger_as:
if self.check_tls_security:
await game_board.save(
StorageLoader.build(self.storage_type),
file_path=os.path.join(self.data_path, 'data'),
database=os.getenv("EDGEDB_DATABASE", None),
tls_security=None
)
else:
await game_board.save(
StorageLoader.build(self.storage_type),
file_path=os.path.join(self.data_path, 'data'),
database=os.getenv("EDGEDB_DATABASE", None),
)
print("GAME ENDED: Winner is", [ x["name"] for x in game_state["board"]['snakes']])
self._delete_game_board(game_state)
def on_end():
game_state = request.get_json()
self._end(game_state)
return "ok"
@self.app.after_request
async def identify_server(response):
def identify_server(response):
response.headers.set(
"server", "battlesnake/gitea/snake-python"
"server", "battlesnake/github/starter-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))}")
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)
async def _read_json_config_or_create(self):
snake_config = await read_file(self.config_file, json.load)
def _read_json_config_or_create(self):
snake_config = 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"]]
async def _get_game_board(self, game_state:str, end:bool=False):
try:
game_board = self.running_games[game_state["game"]["id"]]
except KeyError:
game_board = await self._create_game_board(game_state)
game_board.read_game_data(game_state)
if end:
game_board.end_game(game_state)
return game_board
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
def _cleanup_database(self):
storage = StorageLoader.build(self.storage_type)()
return storage.cleanup()
# info is called when you create your Battlesnake on play.battlesnake.com
# and controls your Battlesnake's appearance
# TIP: If you open your Battlesnake URL in a browser you should see this data
def _info(self) -> dict:
snake_config = self._read_json_config_or_create()
print("INFO Snake:", snake_config)
return snake_config
# start is called when your Battlesnake begins a game
def _start(self, game_state:dict):
if self.store_game_state:
self.running_games[game_state["game"]["id"]] = GameStorage(self.snake.__class__.__name__, path=os.path.join(self.data_path, 'data', 'history'))
self.running_games[game_state["game"]["id"]].start_new_game(game_state["game"], game_state["board"], game_state["you"])
self.running_snake[game_state["game"]["id"]] = SnakeBuilder.build(self.snake_type)
print("GAME START:", game_state["game"])
# move is called when your Battlesnake game is running game
def _move(self, game_state:dict) -> dict:
next_move = self.running_snake[game_state["game"]["id"]].choose_move(game_state)
if self.store_game_state:
self.running_games[game_state["game"]["id"]].add_moves(game_state["board"], next_move)
if self.debug:
print(self.running_games[game_state["game"]["id"]])
print("MOVE:", f"{next_move:5},", "Me:", {"head": game_state["you"]["head"], "length": game_state["you"]["length"]})
return {"move": next_move}
# end is called when your Battlesnake finishes a game
def _end(self, game_state:dict):
if self.store_game_state:
snake = self.running_snake[game_state["game"]["id"]]
self.running_games[game_state["game"]["id"]].add_end_state(game_state["board"], snake.get_history(), game_state["turn"])
self.running_games[game_state["game"]["id"]].save(
f"{snake.__class__.__name__}_{datetime.now().strftime('%d.%m.%Y_%H%M%S')}_{game_state['game']['id']}.json",
callback=json.dump, indent=2, ensure_ascii=False
)
del self.running_games[game_state["game"]["id"]]
print("GAME OVER:\n- Winner is", [ x["name"] for x in game_state["board"]['snakes']])
del self.running_snake[game_state["game"]["id"]]
-123
View File
@@ -1,123 +0,0 @@
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;
"""
)
-59
View File
@@ -1,59 +0,0 @@
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
-7
View File
@@ -1,7 +0,0 @@
class StorageLoader:
@classmethod
def build(self, selected_storage:str):
storage_module = __import__(f'server.storage.{selected_storage}', fromlist=[selected_storage])
storage_class = getattr(storage_module, selected_storage)
return storage_class
+165
View File
@@ -0,0 +1,165 @@
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
-221
View File
@@ -1,221 +0,0 @@
from snakes.TemplateSnake import TemplateSnake
from server.GameBoard import GameBoard
from collections import deque
class BetterMasterSnake(TemplateSnake):
def __init__(self):
super().__init__()
self.name = "BetterMasterSnake"
# Definiere die möglichen Bewegungsrichtungen
self.min_safe_area = 2
def choose_move(self, game_data:GameBoard):
self.game_board = game_data
self.calculations = []
self.eat_the_snake_overwrite = False
self.safe_positions = self.find_safe_positions(add_to_calculations=True)
if self.eat_the_snake_overwrite:
return self.overwrite_eat_the_other_snake(game_data.get_turn())
if game_data.get_type() == "constrictor":
move = self.selected_move_constrictor()
else:
move = self.selected_move_standard()
self.add_to_history({"turn": game_data.get_turn(), "data": self.calculations})
return move if move else "up"
def overwrite_eat_the_other_snake(self, turn:int):
self.add_calculations({"function": "eat_the_snake_overwrite", "my_head": self.game_board.get_my_snake_head(), "move": self.kill_the_snake, "safe_positions": self.safe_positions})
self.add_to_history({"turn": turn, "data": self.calculations})
return self.kill_the_snake
#TODO: How to Fill the Gameboard best?
def selected_move_constrictor(self):
move = self.move_close_to_body()
self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})
move = self.ensure_escape_route(move)
self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
return move
def selected_move_standard(self, move=None):
# Finde den besten Weg zur Nahrung
path_to_food = self.find_path_to_food()
if path_to_food:
move = self.move_towards(path_to_food[0])
self.add_calculations({"function": "move_towards", "my_head": self.game_board.get_my_snake_head(), "path_to_food": path_to_food, "move": move})
if not move or self.would_eating_the_food_kill_the_snake(move):
move = self.move_close_to_body(move_close_to_tail=True)
self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})
# Überprfe, ob der Zug einen Ausweg lässt
move = self.ensure_escape_route(move)
self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
return move
def find_path_to_food(self):
# Exclude own snake's body from obstacles
obstacles = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
for snake in self.game_board.get_other_snakes():
for part in snake['body']:
obstacles.add((part['x'], part['y']))
other_snakes_other_snake_posible_moves_set = {(d['x'], d['y']) for d in self.other_snake_posible_moves}
removed_elements_set = set([(elem['x'], elem['y']) for elem in self.game_board.get_food() if (elem['x'], elem['y']) in other_snakes_other_snake_posible_moves_set])
obstacles |= removed_elements_set
self.food_positions = [elem for elem in self.game_board.get_food() if (elem['x'], elem['y']) not in other_snakes_other_snake_posible_moves_set]
if len(self.food_positions) > 0:
# Choose the closest food source based on the heuristic
closest_food = min(self.food_positions, key=lambda food: abs(food['x'] - self.game_board.get_my_snake_head()['x']) + abs(food['y'] - self.game_board.get_my_snake_head()['y']))
self.set_target_food(closest_food)
# Use A* to search for a safe path
return self.a_star_search(self.game_board.get_my_snake_head(), closest_food, obstacles)
return None
def find_path_to_tail(self):
# Exclude other snake's body from obstacles
obstacles = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
for snake in self.game_board.get_other_snakes():
for part in snake['body']:
obstacles.add((part['x'], part['y']))
my_snake_tail = {"x": self.game_board.get_my_snake_tail()['x'], "y": self.game_board.get_my_snake_tail()['y']}
# Use A* to search for a safe path
path = self.a_star_search(self.game_board.get_my_snake_head(), my_snake_tail, obstacles)
return path
def move_towards(self, target):
best_direction = None
min_distance = float('inf')
for direction, coords in self.safe_positions.items():
distance = abs(target['x'] - coords['x']) + abs(target['y'] - coords['y'])
if distance < min_distance:
min_distance = distance
best_direction = direction
return best_direction if best_direction else "up"
def move_close_to_body(self, move_close_to_tail=False):
# Heuristik, um Positionen nahe dem eigenen Körper zu bevorzugen
body_positions = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
tail_position = (self.game_board.get_my_snake_tail()['x'], self.game_board.get_my_snake_tail()['y'])
best_move = None
max_distance = -1 # Initialize maximum distance
for direction, pos in self.safe_positions.items():
next_position = (pos['x'], pos['y'])
if next_position in self.safe_positions:
# Berechne die Distanz zum eigenen Körper
distance_to_body = min(abs(next_position[0] - part[0]) + abs(next_position[1] - part[1]) for part in body_positions)
# Berechne die Distanz zum eigenen Schwanz
distance_to_tail = abs(next_position[0] - tail_position[0]) + abs(next_position[1] - tail_position[1])
# Wähle die maximale Distanz (Körper oder Schwanz)
if move_close_to_tail:
distance = min(next_position, distance_to_tail)
else:
distance = max(next_position, distance_to_body)
# Update max_distance if a larger distance is found
if distance > max_distance:
max_distance = distance
best_move = direction
return best_move if best_move else "up" # Standardbewegung, falls keine bessere gefunden wird
#TODO: Neat to Implement Function to check if eating the food would kill the snake?
def would_eating_the_food_kill_the_snake(self, move:str):
return False
def ensure_escape_route(self, move:str):
try:
future_position = self.safe_positions[move]
except KeyError:
for move, pos in self.safe_positions.items():
if self.is_near_tail(pos, (self.game_board.get_my_snake_tail()['x'], self.game_board.get_my_snake_tail()['y'])):
self.add_calculations({"function": "ensure_escape_route", "move": move, "is_near_tail": True})
move = self.move_towards(pos)
return move
else:
path_to_tail = self.find_path_to_tail()
if path_to_tail:
self.add_calculations({"function": "move_towards", "my_head": self.game_board.get_my_snake_head(), "path_to_tail": path_to_tail, "move": move})
move = self.move_towards(path_to_tail[0])
self.add_calculations({"function": "ensure_escape_route", "move": move, "KeyError": "Snake Coild itself up"})
#return move
# TODO: Fix - Snake Neat to find the best way - Close to the Tail and maybe fill most free cells as posible
return move
def is_near_tail(self, position, tail):
return abs(position["x"] - tail[0]) + abs(position["y"] - tail[1]) <= 2
def a_star_search(self, start, goal, obstacles):
# Helper functions
def is_position_safe(position):
return 0 <= position['x'] < self.game_board.get_width() and 0 <= position['y'] < self.game_board.get_height() and (position['x'], position['y']) not in obstacles
def get_neighbors(position):
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # links, rechts, oben, unten
neighbor = {'x': position['x'] + dx, 'y': position['y'] + dy}
if is_position_safe(neighbor):
neighbors.append(neighbor)
return neighbors
def heuristic(position, goal):
# Verwenden Sie eine Heuristik, die immer positiv ist, selbst wenn das Ziel in der Nähe ist
return max(abs(position['x'] - goal['x']), abs(position['y'] - goal['y']))
# Überprüfen, ob das Ziel direkt neben dem Startpunkt liegt
if start == goal or (abs(start['x'] - goal['x']) <= 1 and abs(start['y'] - goal['y']) <= 1):
# Wenn das Ziel neben dem Startpunkt liegt, ist der Pfad das Ziel selbst
return [goal]
# Initialize the open and closed list
open_set = set([(start['x'], start['y'])])
came_from = {}
g_score = {(start['x'], start['y']): 0}
f_score = {(start['x'], start['y']): heuristic(start, goal)}
while open_set:
current = min(open_set, key=lambda pos: f_score.get(pos, float('inf')))
current_dict = {'x': current[0], 'y': current[1]}
if current_dict == goal:
# Reconstruct the path
path = []
while current in came_from:
current = came_from[current]
path.append({'x': current[0], 'y': current[1]})
path.reverse()
if path and path[0] == start:
path.pop(0) # Entferne das erste Element, wenn es dem Start entspricht
return path # Return the path as a list of dicts
open_set.remove(current)
for neighbor in get_neighbors(current_dict):
neighbor_tuple = (neighbor['x'], neighbor['y'])
tentative_g_score = g_score[current] + 1 # Distance between neighbors is always 1
if tentative_g_score < g_score.get(neighbor_tuple, float('inf')):
came_from[neighbor_tuple] = current
g_score[neighbor_tuple] = tentative_g_score
f_score[neighbor_tuple] = g_score[neighbor_tuple] + heuristic(neighbor, goal)
if neighbor_tuple not in open_set:
open_set.add(neighbor_tuple)
return None # Kein Pfad gefunden
def find_direction(self):
# Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
for direction, pos in self.safe_positions.items():
next_position = (pos['x'], pos['y'])
# Konvertiere safe_positions in eine Liste von Tupeln für den Vergleich
safe_positions_tuples = [(pos['x'], pos['y']) for pos in self.safe_positions.values()]
if next_position in safe_positions_tuples:
return direction
return "up" # Standardbewegung, falls keine sichere Position gefunden wird
+256
View File
@@ -0,0 +1,256 @@
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
+2 -183
View File
@@ -1,10 +1,6 @@
from server.GameBoard import GameBoard
import random
class TemplateSnake:
def __init__(self):
self.history = []
self.target_food = None
def clear_history(self):
self.history = []
@@ -15,182 +11,5 @@ class TemplateSnake:
def get_history(self):
return self.history
def add_calculations(self, calculations:dict):
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
def choose_move(self, game_data:dict):
pass
+2 -5
View File
@@ -1,7 +1,4 @@
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 --seed 1713099635738952360
else
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g constrictor --browser --minimumFood 0
fi
$BATTLESNAKE_CLI play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser
Generated
-302
View File
@@ -1,302 +0,0 @@
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" },
]