322 lines
14 KiB
Python
322 lines
14 KiB
Python
from snakes.TemplateSnake import TemplateSnake
|
|
from collections import deque
|
|
|
|
class BetterMasterSnake(TemplateSnake):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.name = "CloseToBodySnake"
|
|
self.disabled_find_near_by_food = True
|
|
# Definiere die möglichen Bewegungsrichtungen
|
|
self.min_safe_area = 2
|
|
self.directions = {
|
|
'up': (0, -1),
|
|
'down': (0, 1),
|
|
'left': (-1, 0),
|
|
'right': (1, 0)
|
|
}
|
|
|
|
def get_possible_moves(self, my_head):
|
|
return {
|
|
"up": {
|
|
"x": my_head["x"],
|
|
"y": my_head["y"] + 1
|
|
},
|
|
"down": {
|
|
"x": my_head["x"],
|
|
"y": my_head["y"] - 1
|
|
},
|
|
"left": {
|
|
"x": my_head["x"] - 1,
|
|
"y": my_head["y"]
|
|
},
|
|
"right": {
|
|
"x": my_head["x"] + 1,
|
|
"y": my_head["y"]
|
|
}
|
|
}
|
|
|
|
def avoid_my_body(self, my_body, possible_moves:dict) -> 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 = []
|
|
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 list(possible_moves.items()):
|
|
x_out_range = (location["x"] < 0 or location["x"] == board_width)
|
|
y_out_range = (location["y"] < 0 or location["y"] == 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 find_safe_positions(self):
|
|
safe_positions = self.get_possible_moves(self.my_head)
|
|
safe_positions = self.avoid_my_body(self.my_body, safe_positions)
|
|
safe_positions = self.avoid_walls(self.board_width, self.board_height, safe_positions)
|
|
safe_positions = self.avoid_snakes(self.snakes, safe_positions)
|
|
|
|
return safe_positions
|
|
|
|
def choose_move(self, game_data):
|
|
self.calculations = []
|
|
self.board_width = game_data['board']['width']
|
|
self.board_height = game_data['board']['height']
|
|
self.snakes = game_data['board']['snakes']
|
|
self.my_snake = game_data['you']
|
|
self.my_head = self.my_snake['head']
|
|
self.my_body = self.my_snake["body"]
|
|
self.food_positions = game_data['board']['food']
|
|
move = None
|
|
|
|
safe_positions = self.find_safe_positions()
|
|
# Finde die nächstgelegene Nahrungsquelle, wenn Nahrung vorhanden ist
|
|
try:
|
|
# Finde den besten Weg zur Nahrung
|
|
if self.is_food_nearby() or self.disabled_find_near_by_food:
|
|
path_to_food = self.find_path_to_food()
|
|
if path_to_food:
|
|
move = self.move_towards(path_to_food[0], safe_positions)
|
|
self.add_calculations({"function": "move_towards", "my_head": self.my_head, "path_to_food": path_to_food, "move": move})
|
|
|
|
if not move:
|
|
move = self.move_close_to_body(safe_positions)
|
|
self.add_calculations({"function": "move_close_to_body", "my_head": self.my_head, "move": move, "safe_positions": safe_positions})
|
|
|
|
# Überprfe, ob der Zug einen Ausweg lässt
|
|
move = self.ensure_escape_route(move, safe_positions)
|
|
self.add_calculations({"function": "ensure_escape_route", "my_head": self.my_head, "move": move, "safe_positions": safe_positions})
|
|
except ValueError:
|
|
move = self.ensure_escape_route(self.find_direction(safe_positions), safe_positions)
|
|
self.add_calculations({"function": "ValueError - find_direction", "my_head": self.my_head, "move": move, "safe_positions": safe_positions})
|
|
|
|
self.add_to_history(self.calculations)
|
|
return move if move else "up"
|
|
|
|
def is_food_nearby(self):
|
|
for food in self.food_positions:
|
|
if abs(self.my_head['x'] - food['x']) <= 1 and abs(self.my_head['y'] - food['y']) <= 1:
|
|
self.add_calculations({"function": "is_food_nearby", "my_head": self.my_head, "food": food, "return": True})
|
|
return True
|
|
|
|
self.add_calculations({"function": "is_food_nearby", "my_head": self.my_head, "food_positions": self.food_positions, "return": False})
|
|
return False
|
|
|
|
def find_path_to_food(self):
|
|
# Exclude own snake's body from obstacles
|
|
obstacles = set((part['x'], part['y']) for part in self.my_body)
|
|
|
|
for snake in self.snakes:
|
|
if snake['id'] != self.my_snake['id']:
|
|
for part in snake['body']:
|
|
obstacles.add((part['x'], part['y']))
|
|
|
|
# Choose the closest food source based on the heuristic
|
|
closest_food = min(self.food_positions, key=lambda food: abs(food['x'] - self.my_head['x']) + abs(food['y'] - self.my_head['y']))
|
|
|
|
# Use A* to search for a safe path
|
|
path = self.a_star_search(self.my_head, closest_food, obstacles, self.board_width, self.board_height)
|
|
return path
|
|
|
|
def find_path_to_tail(self):
|
|
# Exclude other snake's body from obstacles
|
|
obstacles = set()
|
|
for snake in self.snakes:
|
|
if snake['id'] != self.my_snake['id']:
|
|
for part in snake['body']:
|
|
obstacles.add((part['x'], part['y']))
|
|
|
|
my_snake_tail = {"x": self.my_body[-1]['x'], "y": self.my_body[-1]['y']}
|
|
|
|
# Use A* to search for a safe path
|
|
path = self.a_star_search(self.my_head, my_snake_tail, obstacles, self.board_width, self.board_height)
|
|
return path
|
|
|
|
def move_towards(self, target, safe_positions):
|
|
best_direction = None
|
|
min_distance = float('inf')
|
|
for direction, coords in 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, safe_positions):
|
|
# Heuristik, um Positionen nahe dem eigenen Körper zu bevorzugen
|
|
body_positions = set((part['x'], part['y']) for part in self.my_body)
|
|
best_move = None
|
|
best_score = float('inf')
|
|
for direction, (dx, dy) in self.directions.items():
|
|
next_position = (self.my_head['x'] + dx, self.my_head['y'] + dy)
|
|
if next_position in 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)
|
|
if distance_to_body < best_score:
|
|
best_score = distance_to_body
|
|
best_move = direction
|
|
return best_move if best_move else "up" # Standardbewegung, falls keine bessere gefunden wird
|
|
|
|
def ensure_escape_route(self, move, safe_positions):
|
|
try:
|
|
future_position = safe_positions[move]
|
|
except KeyError:
|
|
for move, pos in safe_positions.items():
|
|
if self.is_near_tail((pos["x"], pos["y"]), (self.my_body[-1]['x'], self.my_body[-1]['y'])):
|
|
self.add_calculations({"function": "ensure_escape_route", "move": move, "is_near_tail": True})
|
|
move = self.move_towards(pos, safe_positions)
|
|
return move
|
|
else:
|
|
path_to_tail = self.find_path_to_tail()
|
|
if path_to_tail:
|
|
self.add_calculations({"function": "move_towards", "my_head": self.my_head, "path_to_tail": path_to_tail, "move": move})
|
|
move = self.move_towards(path_to_tail[0], safe_positions)
|
|
|
|
self.add_calculations({"function": "ensure_escape_route", "move": move, "KeyError": "Snake Coild itself up"})
|
|
return move
|
|
return move
|
|
|
|
future_coords = (future_position['x'], future_position['y'])
|
|
tail_position = (self.my_body[-1]['x'], self.my_body[-1]['y'])
|
|
|
|
accessible_area_count = self.flood_fill_count(future_coords, [(part['x'], part['y']) for part in self.my_body])
|
|
self.add_calculations({
|
|
"function": "ensure_escape_route",
|
|
"move": move,
|
|
"future_coords": future_coords,
|
|
"accessible_area_count": accessible_area_count,
|
|
})
|
|
|
|
# TODO: Fix - Snake Neat to find the best way - Close to the Tail and maybe fill most free cells as posible
|
|
#if accessible_area_count < self.min_safe_area:
|
|
# # Finde den nächstgelegenen Zug zum Schwanz
|
|
# closest_move = min(safe_positions.keys(), key=lambda m: self.distance_to_tail(safe_positions[m], tail_position))
|
|
# # Überprüfe, ob der nächstgelegene Zug eine größere zugängliche Fläche hat
|
|
# if self.flood_fill_count(safe_positions[closest_move], [(part['x'], part['y']) for part in self.my_body]) >= self.min_safe_area:
|
|
# return closest_move
|
|
|
|
return move
|
|
|
|
def flood_fill_count(self, start, body):
|
|
visited = set()
|
|
queue = deque([start])
|
|
body_set = set(body)
|
|
|
|
while queue:
|
|
current = queue.popleft()
|
|
if current not in visited:
|
|
visited.add(current)
|
|
for direction, pos in self.directions.items():
|
|
neighbor = (current[0] + pos[0], current[1] + pos[1])
|
|
if (neighbor not in visited and neighbor not in body_set and
|
|
0 <= neighbor[0] < self.board_width and
|
|
0 <= neighbor[1] < self.board_height):
|
|
queue.append(neighbor)
|
|
|
|
return len(visited)
|
|
|
|
def is_near_tail(self, position, tail):
|
|
return abs(position[0] - tail[0]) + abs(position[1] - tail[1]) <= 2
|
|
|
|
def distance_to_tail(self, position, tail):
|
|
return abs(position[0] - tail[0]) + abs(position[1] - tail[1])
|
|
|
|
def a_star_search(self, start, goal, obstacles, board_width, board_height):
|
|
# Helper functions
|
|
def is_position_safe(position):
|
|
return 0 <= position['x'] < board_width and 0 <= position['y'] < board_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, safe_positions):
|
|
# Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
|
|
for direction, (dx, dy) in self.directions.items():
|
|
next_position = (self.my_head['x'] + dx, self.my_head['y'] + dy)
|
|
# Konvertiere safe_positions in eine Liste von Tupeln für den Vergleich
|
|
safe_positions_tuples = [(pos['x'], pos['y']) for pos in safe_positions.values()]
|
|
if next_position in safe_positions_tuples:
|
|
return direction
|
|
return "up" # Standardbewegung, falls keine sichere Position gefunden wird
|
|
|
|
def add_calculations(self, calculations:dict):
|
|
self.calculations.append(calculations)
|