fix(master-snake): repair runtime integration and move safety
Build and Push Docker Container / build-and-push (push) Successful in 10m49s
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:
@@ -35,7 +35,12 @@ class MasterSnake(TemplateSnake):
|
|||||||
safe_positions.append({'x': x, 'y': y})
|
safe_positions.append({'x': x, 'y': y})
|
||||||
return safe_positions
|
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_width = game_data['board']['width']
|
||||||
board_height = game_data['board']['height']
|
board_height = game_data['board']['height']
|
||||||
snakes = game_data['board']['snakes']
|
snakes = game_data['board']['snakes']
|
||||||
@@ -45,25 +50,11 @@ class MasterSnake(TemplateSnake):
|
|||||||
# Vermeide Schlangenkörper
|
# Vermeide Schlangenkörper
|
||||||
safe_positions = self.avoid_snake_body(snakes, board_width, board_height)
|
safe_positions = self.avoid_snake_body(snakes, board_width, board_height)
|
||||||
|
|
||||||
# Finde die nächstgelegene Nahrungsquelle, wenn Nahrung vorhanden ist
|
move = self.find_direction(my_head, safe_positions)
|
||||||
try:
|
|
||||||
if self.is_food_nearby(my_head, game_data['board']['food']) or self.disabled_find_near_by_food:
|
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)
|
path_to_food = self.find_path_to_food(game_data)
|
||||||
if path_to_food:
|
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)
|
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})
|
|
||||||
|
|
||||||
# Finde den größten sicheren Bereich
|
# Finde den größten sicheren Bereich
|
||||||
max_area_start, max_area = self.flood_fill(my_head, safe_positions)
|
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
|
# Überprüfe zukünftige Bewegungen, um Sackgassen zu vermeiden
|
||||||
move = self.avoid_dead_ends(my_head, move, safe_positions, snakes)
|
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
|
return move
|
||||||
|
|
||||||
@@ -82,18 +76,14 @@ class MasterSnake(TemplateSnake):
|
|||||||
directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
|
directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
|
||||||
best_direction = None
|
best_direction = None
|
||||||
min_distance = float('inf')
|
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():
|
for direction, (dx, dy) in directions.items():
|
||||||
next_position = {'x': head['x'] + dx, 'y': head['y'] + dy}
|
next_position = {'x': head['x'] + dx, 'y': head['y'] + dy}
|
||||||
if next_position in safe_positions:
|
if next_position in safe_positions:
|
||||||
distance = abs(target[0] - next_position['x']) + abs(target[1] - next_position['y'])
|
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:
|
||||||
if distance < min_distance or (distance == min_distance and distance_to_body < min_distance_to_body):
|
|
||||||
best_direction = direction
|
best_direction = direction
|
||||||
min_distance = distance
|
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
|
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']:
|
for part in snake['body']:
|
||||||
obstacles.add((part['x'], part['y']))
|
obstacles.add((part['x'], part['y']))
|
||||||
|
|
||||||
# Choose the closest food source based on the heuristic
|
# Skip blocked or unreachable food instead of giving up on all food.
|
||||||
closest_food = min(food_positions, key=lambda food: abs(food['x'] - my_head['x']) + abs(food['y'] - my_head['y']))
|
for food in sorted(
|
||||||
|
food_positions,
|
||||||
# Use A* to search for a safe path
|
key=lambda food: abs(food['x'] - my_head['x']) + abs(food['y'] - my_head['y']),
|
||||||
path = self.a_star_search(my_head, closest_food, obstacles, board_width, board_height)
|
):
|
||||||
|
path = self.a_star_search(my_head, food, obstacles, board_width, board_height)
|
||||||
|
if path:
|
||||||
return path
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
def a_star_search(self, start, goal, obstacles, board_width, board_height):
|
def a_star_search(self, start, goal, obstacles, board_width, board_height):
|
||||||
# Convert snake positions into a set of obstacles
|
# 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):
|
def avoid_dead_ends(self, head, move, safe_positions, snakes):
|
||||||
directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
|
directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
|
||||||
dx, dy = directions[move]
|
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}
|
future_head = {'x': head['x'] + dx, 'y': head['y'] + dy}
|
||||||
body_positions = set((part['x'], part['y']) for part in snakes[0]['body'])
|
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):
|
# When every region is too small, still prefer a legal move to a collision.
|
||||||
for alternative_move in directions.keys():
|
return self.find_direction(head, safe_positions)
|
||||||
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
|
|
||||||
|
|
||||||
def simulate_snake_movement(self, snakes):
|
def simulate_snake_movement(self, snakes):
|
||||||
future_body_positions = set()
|
future_body_positions = set()
|
||||||
@@ -209,41 +200,30 @@ class MasterSnake(TemplateSnake):
|
|||||||
safe_positions_set = set((pos['x'], pos['y']) for pos in safe_positions)
|
safe_positions_set = set((pos['x'], pos['y']) for pos in safe_positions)
|
||||||
# Entferne die zukünftigen Körperpositionen aus den sicheren Positionen
|
# Entferne die zukünftigen Körperpositionen aus den sicheren Positionen
|
||||||
safe_positions_set = safe_positions_set - future_body_positions
|
safe_positions_set = safe_positions_set - future_body_positions
|
||||||
# Füge die zukünftige Kopfposition hinzu, um sie als Startpunkt zu verwenden
|
start = (future_head['x'], future_head['y'])
|
||||||
safe_positions_set.add((future_head['x'], future_head['y']))
|
if start not in safe_positions_set:
|
||||||
# Berechne die Anzahl der erreichbaren sicheren Positionen von der zukünftigen Kopfposition aus
|
return False
|
||||||
reachable_positions = self.flood_fill((future_head['x'], future_head['y']), safe_positions_set)
|
_, reachable_positions = self.flood_fill(start, safe_positions_set)
|
||||||
# Entscheide, ob die Bewegung sicher ist, basierend auf der Anzahl der erreichbaren Positionen
|
return len(reachable_positions) >= max(
|
||||||
|
(len(snake['body']) for snake in snakes), default=1,
|
||||||
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'])
|
|
||||||
|
|
||||||
def flood_fill(self, start, safe_positions):
|
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]
|
stack = [start]
|
||||||
visited = set()
|
visited = set()
|
||||||
max_area = 0
|
|
||||||
max_area_start = None
|
|
||||||
|
|
||||||
while stack:
|
while stack:
|
||||||
position = stack.pop()
|
position = stack.pop()
|
||||||
if isinstance(position, dict):
|
if position in visited:
|
||||||
position = tuple(position.values())
|
continue
|
||||||
else:
|
|
||||||
position = tuple(position)
|
|
||||||
|
|
||||||
if position not in visited:
|
|
||||||
visited.add(position)
|
visited.add(position)
|
||||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # links, rechts, oben, unten
|
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
|
||||||
next_position = tuple([position[0] + dx, position[1] + dy])
|
neighbor = (position[0] + dx, position[1] + dy)
|
||||||
if next_position in safe_positions:
|
if neighbor in allowed and neighbor not in visited:
|
||||||
stack.append(next_position)
|
stack.append(neighbor)
|
||||||
|
return start, visited
|
||||||
# Ü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
|
|
||||||
|
|||||||
+56
-11
@@ -1,5 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from snakes.legacy.MasterSnake import MasterSnake
|
from snakes.legacy.MasterSnake import MasterSnake
|
||||||
|
from server.GameBoard import GameBoard
|
||||||
|
|
||||||
class TestMasterSnake(unittest.TestCase):
|
class TestMasterSnake(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -71,22 +72,68 @@ class TestMasterSnake(unittest.TestCase):
|
|||||||
self.assertIn({'x': 10, 'y': 10}, safe_positions)
|
self.assertIn({'x': 10, 'y': 10}, safe_positions)
|
||||||
|
|
||||||
def test_choose_move(self):
|
def test_choose_move(self):
|
||||||
move = self.snake.choose_move(self.game_data)
|
board = GameBoard(
|
||||||
self.assertIn(move, ["up", "down", "left", "right"])
|
game_id='test-game', width=11, height=11,
|
||||||
|
ruleset={'name': 'standard', 'version': 'v1'},
|
||||||
|
source='test', map='standard', snake_class=self.snake,
|
||||||
|
)
|
||||||
|
state = {**self.game_data, 'turn': 0}
|
||||||
|
state['board'] = {**state['board'], 'hazards': []}
|
||||||
|
board.read_game_data(state)
|
||||||
|
move = board.snake_neat_make_a_move()
|
||||||
|
self.assertEqual(move, 'up')
|
||||||
|
self.assertEqual(len(self.snake.get_history()), 1)
|
||||||
|
self.assertEqual(self.snake.get_history()[0]['turn'], 0)
|
||||||
|
self.assertIsInstance(self.snake.get_history()[0]['data'], list)
|
||||||
|
|
||||||
def test_move_towards_food(self):
|
def test_move_towards(self):
|
||||||
head = {'x': 0, 'y': 0}
|
head = {'x': 0, 'y': 0}
|
||||||
food = (5, 5)
|
food = (5, 5)
|
||||||
safe_positions = [{'x': 1, 'y': 0}, {'x': 0, 'y': 1}, {'x': 1, 'y': 1}]
|
safe_positions = [{'x': 1, 'y': 0}, {'x': 0, 'y': 1}, {'x': 1, 'y': 1}]
|
||||||
direction = self.snake.move_towards_food(head, food, safe_positions)
|
direction = self.snake.move_towards(head, food, safe_positions)
|
||||||
self.assertIn(direction, ["up", "down", "left", "right"])
|
self.assertIn(direction, ["up", "down", "left", "right"])
|
||||||
|
|
||||||
def test_find_path_to_food(self):
|
def test_find_path_to_food(self):
|
||||||
path = self.snake.find_path_to_food(self.game_data)
|
path = self.snake.find_path_to_food(self.game_data)
|
||||||
print(path)
|
|
||||||
self.assertIsInstance(path, list)
|
self.assertIsInstance(path, list)
|
||||||
self.assertTrue(all(isinstance(pos, dict) for pos in path))
|
self.assertEqual(path[-1], (5, 5))
|
||||||
self.assertTrue(all('x' in pos and 'y' in pos for pos in path))
|
obstacles = {
|
||||||
|
(part['x'], part['y'])
|
||||||
|
for snake in self.game_data['board']['snakes']
|
||||||
|
for part in snake['body']
|
||||||
|
}
|
||||||
|
previous = (0, 0)
|
||||||
|
for position in path:
|
||||||
|
self.assertNotIn(position, obstacles)
|
||||||
|
self.assertEqual(abs(position[0] - previous[0]) + abs(position[1] - previous[1]), 1)
|
||||||
|
previous = position
|
||||||
|
|
||||||
|
def test_no_reachable_food(self):
|
||||||
|
self.game_data['board']['food'] = []
|
||||||
|
self.assertIsNone(self.snake.find_path_to_food(self.game_data))
|
||||||
|
self.game_data['board']['food'] = [{'x': 1, 'y': 0}]
|
||||||
|
self.assertIsNone(self.snake.find_path_to_food(self.game_data))
|
||||||
|
|
||||||
|
def test_flood_fill_normalizes_coordinate_dicts(self):
|
||||||
|
start, reachable = self.snake.flood_fill(
|
||||||
|
{'y': 0, 'x': 0},
|
||||||
|
[{'x': 0, 'y': 1}, {'x': 0, 'y': 2}, {'x': 4, 'y': 4}],
|
||||||
|
)
|
||||||
|
self.assertEqual(start, (0, 0))
|
||||||
|
self.assertEqual(reachable, {(0, 0), (0, 1), (0, 2)})
|
||||||
|
|
||||||
|
def test_future_move_rejects_walls_bodies_and_small_regions(self):
|
||||||
|
snakes = [{'body': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 2, 'y': 0}]}]
|
||||||
|
safe = [{'x': 0, 'y': 1}, {'x': 0, 'y': 2}, {'x': 4, 'y': 4}]
|
||||||
|
for position in ({'x': -1, 'y': 0}, {'x': 1, 'y': 0}, {'x': 4, 'y': 4}):
|
||||||
|
with self.subTest(position=position):
|
||||||
|
self.assertFalse(self.snake.is_future_move_safe(position, safe, snakes))
|
||||||
|
|
||||||
|
def test_avoid_dead_ends_prefers_connected_escape_region(self):
|
||||||
|
head = {'x': 2, 'y': 2}
|
||||||
|
safe = [{'x': 3, 'y': 2}, {'x': 2, 'y': 3}, {'x': 2, 'y': 4}, {'x': 2, 'y': 5}]
|
||||||
|
snakes = [{'body': [{'x': 2, 'y': 2}, {'x': 2, 'y': 1}, {'x': 2, 'y': 0}]}]
|
||||||
|
self.assertEqual(self.snake.avoid_dead_ends(head, 'right', safe, snakes), 'up')
|
||||||
|
|
||||||
def test_find_direction(self):
|
def test_find_direction(self):
|
||||||
head = {'x': 0, 'y': 0}
|
head = {'x': 0, 'y': 0}
|
||||||
@@ -97,12 +144,10 @@ class TestMasterSnake(unittest.TestCase):
|
|||||||
def test_avoid_dead_ends(self):
|
def test_avoid_dead_ends(self):
|
||||||
head = {'x': 0, 'y': 0}
|
head = {'x': 0, 'y': 0}
|
||||||
move = "right"
|
move = "right"
|
||||||
board_width = 11
|
|
||||||
board_height = 11
|
|
||||||
snakes = self.game_data['board']['snakes']
|
snakes = self.game_data['board']['snakes']
|
||||||
safe_positions = [{'x': 1, 'y': 0}, {'x': 0, 'y': 1}, {'x': 1, 'y': 1}]
|
safe_positions = [{'x': 1, 'y': 0}, {'x': 0, 'y': 1}, {'x': 1, 'y': 1}]
|
||||||
new_move = self.snake.avoid_dead_ends(head, move, safe_positions, board_width, board_height, snakes)
|
new_move = self.snake.avoid_dead_ends(head, move, safe_positions, snakes)
|
||||||
self.assertIn(new_move, ["up", "down", "left", "right"])
|
self.assertEqual(new_move, 'up')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user