fix(master-snake): repair runtime integration and move safety
Build and Push Docker Container / build-and-push (push) Successful in 10m49s

- Read board state through the GameBoard interface used by the server.
- Record one turn-indexed calculation history entry per move.
- Try alternative food when nearer targets are blocked or unreachable.
- Handle empty food lists without relying on exception fallback.
- Normalize flood-fill coordinates and count reachable cells correctly.
- Reject blocked destinations before evaluating escape space.
- Prefer legal fallback moves when no region meets the space threshold.
- Remove a distance tie-breaker that mistook free cells for snake bodies.
- Align stale tests with current methods and tuple-based path results.
- Cover runtime moves, food fallback, coordinate order, and dead ends.
This commit is contained in:
2026-11-03 10:08:38 +01:00
parent 5efab77248
commit d4d6375e01
2 changed files with 110 additions and 85 deletions
+54 -74
View File
@@ -35,7 +35,12 @@ class MasterSnake(TemplateSnake):
safe_positions.append({'x': x, 'y': y})
return safe_positions
def choose_move(self, game_data):
def choose_move(self, game_board):
self.game_board = game_board
game_data = {
'board': game_board.get_game_board_as_dict(),
'you': game_board.get_my_snake(),
}
board_width = game_data['board']['width']
board_height = game_data['board']['height']
snakes = game_data['board']['snakes']
@@ -45,25 +50,11 @@ class MasterSnake(TemplateSnake):
# Vermeide Schlangenkörper
safe_positions = self.avoid_snake_body(snakes, board_width, board_height)
# Finde die nächstgelegene Nahrungsquelle, wenn Nahrung vorhanden ist
try:
if self.is_food_nearby(my_head, game_data['board']['food']) or self.disabled_find_near_by_food:
path_to_food = self.find_path_to_food(game_data)
if path_to_food:
# Implementiere Logik, um in Richtung der Nahrungsquelle zu bewegen, falls sicher
move = self.move_towards(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 keine Nahrung vorhanden ist
move = self.find_direction(my_head, safe_positions)
self.add_to_history({"my_head": my_head, "move": move})
else:
# Wenn keine Nahrung in der Nähe ist, bewege dich in eine Richtung, die dich nahe an deinem eigenen Körper hält
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})
move = self.find_direction(my_head, safe_positions)
if self.is_food_nearby(my_head, game_data['board']['food']) or self.disabled_find_near_by_food:
path_to_food = self.find_path_to_food(game_data)
if path_to_food:
move = self.move_towards(my_head, path_to_food[0], safe_positions)
# Finde den größten sicheren Bereich
max_area_start, max_area = self.flood_fill(my_head, safe_positions)
@@ -74,7 +65,10 @@ class MasterSnake(TemplateSnake):
# Überprüfe zukünftige Bewegungen, um Sackgassen zu vermeiden
move = self.avoid_dead_ends(my_head, move, safe_positions, snakes)
self.add_to_history({"my_head": my_head, "move": move})
self.add_to_history({
"turn": game_board.get_turn(),
"data": [{"function": "choose_move", "my_head": my_head, "move": move}],
})
return move
@@ -82,18 +76,14 @@ class MasterSnake(TemplateSnake):
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(target[0] - next_position['x']) + abs(target[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):
if distance < min_distance:
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
@@ -113,12 +103,15 @@ class MasterSnake(TemplateSnake):
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
# Skip blocked or unreachable food instead of giving up on all food.
for food in sorted(
food_positions,
key=lambda food: abs(food['x'] - my_head['x']) + abs(food['y'] - my_head['y']),
):
path = self.a_star_search(my_head, food, obstacles, board_width, board_height)
if path:
return path
return None
def a_star_search(self, start, goal, obstacles, board_width, board_height):
# Convert snake positions into a set of obstacles
@@ -182,17 +175,15 @@ class MasterSnake(TemplateSnake):
def avoid_dead_ends(self, head, move, safe_positions, 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}
body_positions = set((part['x'], part['y']) for part in snakes[0]['body'])
candidates = [move] + [direction for direction in directions if direction != move]
for direction in candidates:
dx, dy = directions[direction]
future_head = {'x': head['x'] + dx, 'y': head['y'] + dy}
if self.is_future_move_safe(future_head, safe_positions, snakes):
return direction
if not self.is_future_move_safe(future_head, safe_positions, snakes) or not self.avoid_self_collision(future_head, body_positions):
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, snakes) and self.avoid_self_collision(alternative_future_head, body_positions):
return alternative_move
return move
# When every region is too small, still prefer a legal move to a collision.
return self.find_direction(head, safe_positions)
def simulate_snake_movement(self, snakes):
future_body_positions = set()
@@ -209,41 +200,30 @@ class MasterSnake(TemplateSnake):
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
reachable_positions = self.flood_fill((future_head['x'], future_head['y']), safe_positions_set)
# Entscheide, ob die Bewegung sicher ist, basierend auf der Anzahl der erreichbaren Positionen
fill_bool = len(reachable_positions) > len(safe_positions_set) * 0.25
if fill_bool:
return fill_bool
return len(safe_positions_set) >= len(snakes[0]['body'])
start = (future_head['x'], future_head['y'])
if start not in safe_positions_set:
return False
_, reachable_positions = self.flood_fill(start, safe_positions_set)
return len(reachable_positions) >= max(
(len(snake['body']) for snake in snakes), default=1,
)
def flood_fill(self, start, safe_positions):
start = (start['x'], start['y']) if isinstance(start, dict) else tuple(start)
allowed = {
(position['x'], position['y']) if isinstance(position, dict) else tuple(position)
for position in safe_positions
}
# The current head may be occupied; it is only a traversal origin.
stack = [start]
visited = set()
max_area = 0
max_area_start = None
while stack:
position = stack.pop()
if isinstance(position, dict):
position = tuple(position.values())
else:
position = tuple(position)
if position not in visited:
visited.add(position)
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # links, rechts, oben, unten
next_position = tuple([position[0] + dx, position[1] + dy])
if next_position in safe_positions:
stack.append(next_position)
# Überprüfe, ob der aktuelle Bereich größer ist als der bisher größte Bereich
if len(visited) > max_area:
max_area = len(visited)
max_area_start = position
return max_area_start, visited
if position in visited:
continue
visited.add(position)
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
neighbor = (position[0] + dx, position[1] + dy)
if neighbor in allowed and neighbor not in visited:
stack.append(neighbor)
return start, visited