"""Tests for game logic.""" import pytest from src.server.game_logic import GameLogic from src.shared.models import Position, Snake from src.shared.constants import GRID_WIDTH, GRID_HEIGHT, RIGHT, LEFT, UP, DOWN class TestGameLogic: """Test suite for GameLogic class.""" def test_create_snake(self) -> None: """Test snake creation.""" logic = GameLogic() snake = logic.create_snake("player1") assert snake.player_id == "player1" assert len(snake.body) == 3 assert snake.alive is True assert snake.score == 0 assert snake.direction == RIGHT def test_spawn_food(self) -> None: """Test food spawning.""" logic = GameLogic() food = logic.spawn_food() assert 0 <= food.position.x < GRID_WIDTH assert 0 <= food.position.y < GRID_HEIGHT def test_update_snake_direction(self) -> None: """Test updating snake direction via input buffer.""" logic = GameLogic() snake = logic.create_snake("player1") logic.state.snakes.append(snake) # Direction changes go into buffer first logic.update_snake_direction("player1", UP) assert snake.input_buffer == [UP] assert snake.direction == RIGHT # Original direction unchanged # Moving consumes from buffer logic.move_snakes() assert snake.direction == UP # Now changed after movement assert len(snake.input_buffer) == 0 def test_move_snakes(self) -> None: """Test snake movement.""" logic = GameLogic() snake = Snake(player_id="player1", body=[ Position(5, 5), Position(4, 5), Position(3, 5), ], direction=RIGHT) logic.state.snakes.append(snake) initial_length = len(snake.body) logic.move_snakes() # Snake should have moved one cell to the right assert snake.get_head().x == 6 assert snake.get_head().y == 5 assert len(snake.body) == initial_length def test_collision_with_wall(self) -> None: """Test collision detection with walls - snake gets stuck.""" logic = GameLogic() # Snake at left wall snake = Snake(player_id="player1", body=[ Position(0, 5), Position(1, 5), Position(2, 5), ], direction=LEFT) logic.state.snakes.append(snake) initial_length = len(snake.body) head_pos = snake.get_head() logic.move_snakes() # Snake should be stuck, not dead assert snake.stuck is True assert snake.alive is True # Head position should not change assert snake.get_head().x == head_pos.x assert snake.get_head().y == head_pos.y # Tail should shrink by 1 assert len(snake.body) == initial_length - 1 def test_collision_with_self(self) -> None: """Test collision detection with self - snake gets stuck then auto-unsticks.""" logic = GameLogic() # Create a snake that will hit itself snake = Snake(player_id="player1", body=[ Position(5, 5), Position(5, 6), Position(6, 6), Position(6, 5), ], direction=DOWN) logic.state.snakes.append(snake) # First move: should get stuck logic.move_snakes() assert snake.stuck is True assert snake.alive is True assert len(snake.body) == 3 # Shrunk by 1 # Continue moving: tail shrinks, eventually unsticks logic.move_snakes() assert len(snake.body) == 2 # Shrunk again logic.move_snakes() # After enough shrinking, blocking segment is gone, snake unsticks assert len(snake.body) >= 1 def test_food_eating(self) -> None: """Test snake eating food and growing.""" logic = GameLogic() snake = Snake(player_id="player1", body=[ Position(5, 5), Position(4, 5), Position(3, 5), ], direction=RIGHT) logic.state.snakes.append(snake) # Place food in front of snake from src.shared.models import Food food = Food(position=Position(6, 5)) logic.state.food.append(food) initial_length = len(snake.body) initial_score = snake.score logic.move_snakes() # Snake should have grown and scored assert len(snake.body) == initial_length + 1 assert snake.score == initial_score + 10 assert food not in logic.state.food def test_stuck_minimum_length(self) -> None: """Test that snake cannot shrink below length 1.""" logic = GameLogic() # Create length-1 snake stuck against wall snake = Snake(player_id="player1", body=[Position(0, 5)], direction=LEFT) logic.state.snakes.append(snake) # Move multiple times - should stay stuck, not shrink below 1 for _ in range(5): logic.move_snakes() assert len(snake.body) == 1 assert snake.stuck is True assert snake.alive is True def test_other_snake_blocks(self) -> None: """Test snake getting stuck on another snake.""" logic = GameLogic() # Snake A trying to move into Snake B snake_a = Snake(player_id="player_a", body=[ Position(5, 5), Position(4, 5), ], direction=RIGHT) snake_b = Snake(player_id="player_b", body=[ Position(6, 5), # Blocking Snake A Position(7, 5), ], direction=RIGHT) logic.state.snakes.extend([snake_a, snake_b]) logic.move_snakes() # Snake A should be stuck assert snake_a.stuck is True assert len(snake_a.body) == 1 # Shrunk by 1 def test_other_snake_moves_away(self) -> None: """Test snake auto-unsticks when other snake moves away.""" logic = GameLogic() # Snake A stuck behind Snake B snake_a = Snake(player_id="player_a", body=[ Position(5, 5), Position(4, 5), Position(3, 5), ], direction=RIGHT) snake_b = Snake(player_id="player_b", body=[ Position(6, 5), # Blocking at (6,5) Position(6, 6), # Tail at (6,6) ], direction=UP) # Will move up logic.state.snakes.extend([snake_a, snake_b]) # Tick 1: Snake A gets stuck, Snake B moves to (6,4)-(6,5) logic.move_snakes() assert snake_a.stuck is True assert len(snake_a.body) == 2 # Shrunk by 1 # Tick 2: Snake B moves to (6,3)-(6,4), still stuck (shrunk to length 1) logic.move_snakes() assert snake_a.stuck is True assert len(snake_a.body) == 1 # Shrunk to minimum # Tick 3: Position (6,5) clear, Snake A finally unsticks! logic.move_snakes() assert snake_a.stuck is False assert snake_a.get_head().x == 6 # Moved forward to (6,5) def test_direction_change_unsticks(self) -> None: """Test changing direction to clear path unsticks snake.""" logic = GameLogic() # Snake stuck against wall facing left snake = Snake(player_id="player1", body=[ Position(0, 5), Position(1, 5), ], direction=LEFT) logic.state.snakes.append(snake) # Get stuck logic.move_snakes() assert snake.stuck is True # Change direction to up (clear path) logic.update_snake_direction("player1", UP) # Next move should unstick logic.move_snakes() assert snake.stuck is False assert snake.get_head().y == 4 # Moved up def test_two_snakes_head_to_head(self) -> None: """Test two snakes stuck head-to-head both shrink.""" logic = GameLogic() snake_a = Snake(player_id="player_a", body=[ Position(5, 5), Position(4, 5), ], direction=RIGHT) snake_b = Snake(player_id="player_b", body=[ Position(6, 5), Position(7, 5), ], direction=LEFT) logic.state.snakes.extend([snake_a, snake_b]) logic.move_snakes() # Both snakes should be stuck assert snake_a.stuck is True assert snake_b.stuck is True # Both should shrink assert len(snake_a.body) == 1 assert len(snake_b.body) == 1 def test_input_buffer_fills_to_three(self) -> None: """Test input buffer fills up to 3 directions.""" logic = GameLogic() snake = logic.create_snake("player1") logic.state.snakes.append(snake) # Add 3 different directions logic.update_snake_direction("player1", UP) logic.update_snake_direction("player1", LEFT) logic.update_snake_direction("player1", DOWN) assert len(snake.input_buffer) == 3 assert snake.input_buffer == [UP, LEFT, DOWN] def test_input_buffer_ignores_duplicates(self) -> None: """Test input buffer ignores duplicate inputs.""" logic = GameLogic() snake = logic.create_snake("player1") logic.state.snakes.append(snake) logic.update_snake_direction("player1", UP) logic.update_snake_direction("player1", UP) # Duplicate assert len(snake.input_buffer) == 1 assert snake.input_buffer == [UP] def test_input_buffer_opposite_replacement(self) -> None: """Test opposite direction replaces last in buffer.""" logic = GameLogic() snake = logic.create_snake("player1") logic.state.snakes.append(snake) logic.update_snake_direction("player1", UP) logic.update_snake_direction("player1", DOWN) # Opposite to UP # DOWN should replace UP assert len(snake.input_buffer) == 1 assert snake.input_buffer == [DOWN] def test_input_buffer_overflow_replacement(self) -> None: """Test 4th input replaces last slot when buffer is full.""" logic = GameLogic() snake = logic.create_snake("player1") logic.state.snakes.append(snake) # Fill buffer with 3 directions logic.update_snake_direction("player1", UP) logic.update_snake_direction("player1", LEFT) logic.update_snake_direction("player1", DOWN) # 4th input should replace last slot logic.update_snake_direction("player1", RIGHT) assert len(snake.input_buffer) == 3 assert snake.input_buffer == [UP, LEFT, RIGHT] # DOWN replaced by RIGHT def test_input_buffer_consumption(self) -> None: """Test buffer is consumed during movement.""" logic = GameLogic() snake = Snake(player_id="player1", body=[ Position(5, 5), Position(4, 5), Position(3, 5), ], direction=RIGHT) logic.state.snakes.append(snake) # Add direction to buffer snake.input_buffer = [UP] logic.move_snakes() # Buffer should be consumed and direction applied assert len(snake.input_buffer) == 0 assert snake.direction == UP assert snake.get_head().y == 4 # Moved up def test_input_buffer_skips_180_turn(self) -> None: """Test buffer skips 180-degree turns for multi-segment snakes.""" logic = GameLogic() snake = Snake(player_id="player1", body=[ Position(5, 5), Position(4, 5), Position(3, 5), ], direction=RIGHT) logic.state.snakes.append(snake) # Buffer has opposite direction then valid direction snake.input_buffer = [LEFT, UP] # LEFT is 180° from RIGHT logic.move_snakes() # LEFT should be skipped, UP should be applied assert len(snake.input_buffer) == 0 assert snake.direction == UP assert snake.get_head().y == 4 # Moved up def test_zero_players_spawns_three_apples(self) -> None: """Test field populates with 3 apples when no players.""" logic = GameLogic() logic.state.game_running = True # Start with no snakes and no food logic.state.snakes = [] logic.state.food = [] # Update should populate 3 apples logic.update() assert len(logic.state.food) == 3 # Subsequent updates should maintain 3 apples logic.update() assert len(logic.state.food) == 3