Compare commits
89 Commits
dev
...
c458219125
| Author | SHA1 | Date | |
|---|---|---|---|
|
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,34 @@
|
|||||||
|
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
|
||||||
@@ -7,3 +7,4 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
data/
|
data/
|
||||||
.env
|
.env
|
||||||
|
dbschema/migrations/
|
||||||
|
|||||||
+3
-1
@@ -1,4 +1,6 @@
|
|||||||
FROM python:3.10.6-slim
|
FROM python:alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache build-base
|
||||||
|
|
||||||
# Install app
|
# Install app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
+9
-9
@@ -1,10 +1,10 @@
|
|||||||
blinker==1.7.0
|
asgiref==3.8.1
|
||||||
click==8.1.7
|
blinker==1.9.0
|
||||||
Flask==3.0.2
|
click==8.1.8
|
||||||
itsdangerous==2.1.2
|
Flask==3.1.0
|
||||||
Jinja2==3.1.3
|
gel==3.1.0
|
||||||
MarkupSafe==2.1.5
|
itsdangerous==2.2.0
|
||||||
numpy==1.26.4
|
Jinja2==3.1.5
|
||||||
|
MarkupSafe==3.0.2
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
scipy==1.12.0
|
Werkzeug==3.1.3
|
||||||
Werkzeug==3.0.1
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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"])
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
def save(self, store_class, **kwargs):
|
||||||
|
store = store_class(**kwargs)
|
||||||
|
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}"
|
|
||||||
+87
-37
@@ -1,69 +1,113 @@
|
|||||||
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 Flask, jsonify
|
||||||
from flask import request
|
from flask import request
|
||||||
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.running_games:dict[str, GameBoard] = {}
|
||||||
|
|
||||||
self.app = Flask("Battlesnake")
|
self.app = Flask("Battlesnake")
|
||||||
|
|
||||||
@self.app.get("/")
|
@self.app.get("/")
|
||||||
def on_info():
|
async def on_info():
|
||||||
return self._info()
|
return self._info()
|
||||||
|
|
||||||
@self.app.post("/start")
|
@self.app.post("/start")
|
||||||
def on_start():
|
async def on_start():
|
||||||
game_state = request.get_json()
|
game_state = request.get_json()
|
||||||
self._start(game_state)
|
self._start(game_state)
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
@self.app.post("/move")
|
@self.app.post("/move")
|
||||||
def on_move():
|
async def on_move():
|
||||||
game_state = request.get_json()
|
game_state = request.get_json()
|
||||||
return self._move(game_state)
|
return self._move(game_state)
|
||||||
|
|
||||||
@self.app.post("/end")
|
@self.app.post("/end")
|
||||||
def on_end():
|
async def on_end():
|
||||||
game_state = request.get_json()
|
game_state = request.get_json()
|
||||||
self._end(game_state)
|
self._end(game_state)
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
@self.app.after_request
|
@self.app.after_request
|
||||||
def identify_server(response):
|
async def identify_server(response):
|
||||||
response.headers.set(
|
response.headers.set(
|
||||||
"server", "battlesnake/github/starter-snake-python"
|
"server", "battlesnake/github/starter-snake-python"
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@self.app.get("/cleanup")
|
||||||
|
async def cleanup():
|
||||||
|
results = self._cleanup_database()
|
||||||
|
return jsonify(data=json.loads(results), status=200)
|
||||||
|
|
||||||
def run(self, host:str="0.0.0.0", port:str="8000", debug:bool=False):
|
def run(self, host:str="0.0.0.0", port:str="8000", debug:bool=False):
|
||||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||||
|
|
||||||
print(f"\nRunning Battlesnake at http://{host}:{port} with the {self.snake_type.replace('Snake', '')} Snake")
|
print(f"\nRunning Battlesnake at http://{host}:{port} with the {' '.join(re.findall('[A-Z][^A-Z]*', self.snake_type))}")
|
||||||
self.app.run(host=host, port=port, debug=debug)
|
self.app.run(host=host, port=port, debug=debug)
|
||||||
|
|
||||||
def _read_json_config_or_create(self):
|
def _read_json_config_or_create(self):
|
||||||
snake_config = read_file(self.config_file, json.load)
|
snake_config = read_file(self.config_file, json.load)
|
||||||
if not snake_config:
|
if not snake_config:
|
||||||
snake_config = self.default_snake_config
|
snake_config = self._override_snake_config_with_environment_variables(self.default_snake_config)
|
||||||
save_file(self.config_file, snake_config, callback=json.dump, indent=2, ensure_ascii=False)
|
|
||||||
return snake_config
|
return self._override_snake_config_with_environment_variables(snake_config)
|
||||||
|
|
||||||
|
def _override_snake_config_with_environment_variables(self, config:dict[str]):
|
||||||
|
for key in ["author", "color", "head", "tail"]:
|
||||||
|
if os.environ.get(f"SNAKE_{key.upper()}", None):
|
||||||
|
config[key.lower()] = os.environ.get(f"SNAKE_{key.upper()}")
|
||||||
|
return config
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
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"]]
|
||||||
|
|
||||||
|
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 = self._create_game_board(game_state)
|
||||||
|
|
||||||
|
game_board.read_game_data(game_state)
|
||||||
|
if end:
|
||||||
|
game_board.end_game(game_state)
|
||||||
|
|
||||||
|
return game_board
|
||||||
|
|
||||||
def enable_store_game_state(self):
|
def enable_store_game_state(self):
|
||||||
self.store_game_state = True
|
self.store_game_state = True
|
||||||
@@ -73,41 +117,47 @@ class Server:
|
|||||||
# 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:
|
def _info(self) -> dict:
|
||||||
snake_config = self._read_json_config_or_create()
|
snake_config = 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):
|
def _start(self, game_state:dict):
|
||||||
if self.store_game_state:
|
self._create_game_board(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"])
|
print("GAME START:", game_state["game"])
|
||||||
|
|
||||||
# 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:
|
def _move(self, game_state:dict) -> dict:
|
||||||
next_move = self.running_snake[game_state["game"]["id"]].choose_move(game_state)
|
game_board = 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):
|
def _end(self, game_state:dict):
|
||||||
if self.store_game_state:
|
if self.store_game_state:
|
||||||
snake = self.running_snake[game_state["game"]["id"]]
|
game_board = 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(
|
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:
|
||||||
|
game_board.save(
|
||||||
|
StorageLoader.build(self.storage_type),
|
||||||
|
file_path=os.path.join(self.data_path, 'data'),
|
||||||
|
database=os.getenv("EDGEDB_DATABASE", None),
|
||||||
)
|
)
|
||||||
del self.running_games[game_state["game"]["id"]]
|
|
||||||
|
|
||||||
print("GAME OVER:\n- Winner is", [ x["name"] for x in game_state["board"]['snakes']])
|
print("GAME ENDED: Winner is", [ x["name"] for x in game_state["board"]['snakes']])
|
||||||
del self.running_snake[game_state["game"]["id"]]
|
self._delete_game_board(game_state)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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__,
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, game_board:GameBoard):
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user