Files
snake-python/tests/test_MasterSnake.py
T
daniel156161 d4d6375e01
Build and Push Docker Container / build-and-push (push) Successful in 10m49s
fix(master-snake): repair runtime integration and move safety
- 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.
2026-11-03 10:08:38 +01:00

154 lines
6.1 KiB
Python

import unittest
from snakes.legacy.MasterSnake import MasterSnake
from server.GameBoard import GameBoard
class TestMasterSnake(unittest.TestCase):
def setUp(self):
self.snake = MasterSnake()
self.game_data = {
"board": {
"height": 11,
"width": 11,
"snakes": [
{
"id": "snake-1",
"body": [
{"x": 0, "y": 0},
{"x": 1, "y": 0},
{"x": 2, "y": 0}
],
"head": {"x": 2, "y": 1},
},
{
"id": "snake-2",
"body": [
{"x": 5, "y": 4},
{"x": 5, "y": 3},
{"x": 6, "y": 3},
{"x": 6, "y": 2}
],
"head": {"x": 6, "y": 3},
}
],
"food": [
{
"x": 2,
"y": 0
},
{
"x": 10,
"y": 8
},
{
"x": 5,
"y": 5
}
],
},
"you": {
"id": "snake-1",
"body": [
{"x": 0, "y": 0},
{"x": 1, "y": 0},
{"x": 2, "y": 0}
],
"head": {"x": 0, "y": 0},
}
}
def test_avoid_snake_body(self):
snakes = self.game_data['board']['snakes']
board_width = self.game_data['board']['width']
board_height = self.game_data['board']['height']
safe_positions = self.snake.avoid_snake_body(snakes, board_width, board_height)
self.assertIsInstance(safe_positions, list)
self.assertTrue(all(isinstance(pos, dict) for pos in safe_positions))
self.assertTrue(all('x' in pos and 'y' in pos for pos in safe_positions))
def test_find_safe_positions(self):
body_positions = {(0, 0), (1, 0), (2, 0), (5, 4), (5, 3), (6, 3), (6, 2)}
safe_positions = self.snake.find_safe_positions(body_positions, 11, 11)
self.assertNotIn({'x': 0, 'y': 0}, safe_positions)
self.assertIn({'x': 10, 'y': 10}, safe_positions)
def test_choose_move(self):
board = GameBoard(
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(self):
head = {'x': 0, 'y': 0}
food = (5, 5)
safe_positions = [{'x': 1, 'y': 0}, {'x': 0, 'y': 1}, {'x': 1, 'y': 1}]
direction = self.snake.move_towards(head, food, safe_positions)
self.assertIn(direction, ["up", "down", "left", "right"])
def test_find_path_to_food(self):
path = self.snake.find_path_to_food(self.game_data)
self.assertIsInstance(path, list)
self.assertEqual(path[-1], (5, 5))
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):
head = {'x': 0, 'y': 0}
safe_positions = [{'x': 1, 'y': 0}, {'x': 0, 'y': 1}, {'x': 1, 'y': 1}]
direction = self.snake.find_direction(head, safe_positions)
self.assertIn(direction, ["up", "down", "left", "right"])
def test_avoid_dead_ends(self):
head = {'x': 0, 'y': 0}
move = "right"
snakes = self.game_data['board']['snakes']
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, snakes)
self.assertEqual(new_move, 'up')
if __name__ == '__main__':
unittest.main()