Implemented a complete network multiplayer Snake game with the following features: Core Game: - Client-server architecture using asyncio for networking - Pygame-based rendering at 60 FPS - Server-authoritative game state with 10 TPS - Collision detection (walls, self, other players) - Food spawning and score tracking - Support for multiple players with color-coded snakes Server Discovery: - UDP multicast-based automatic server discovery (239.255.0.1:9999) - Server beacon broadcasts presence every 2 seconds - Client discovery with 3-second timeout - Server selection UI for multiple servers - Auto-connect for single server - Graceful fallback to manual connection Project Structure: - src/shared/ - Protocol, models, constants, discovery utilities - src/server/ - Game server, game logic, server beacon - src/client/ - Game client, renderer, discovery, server selector - tests/ - Unit tests for game logic, models, and discovery Command-line interface with argparse for both server and client. Comprehensive documentation in README.md and CLAUDE.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
149 lines
4.4 KiB
Python
149 lines
4.4 KiB
Python
"""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."""
|
|
logic = GameLogic()
|
|
snake = logic.create_snake("player1")
|
|
logic.state.snakes.append(snake)
|
|
|
|
# Valid direction change
|
|
logic.update_snake_direction("player1", UP)
|
|
assert snake.direction == UP
|
|
|
|
# Invalid 180-degree turn (should be ignored)
|
|
logic.update_snake_direction("player1", DOWN)
|
|
assert snake.direction == UP # Should remain UP
|
|
|
|
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."""
|
|
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)
|
|
|
|
logic.move_snakes()
|
|
logic.check_collisions()
|
|
|
|
assert snake.alive is False
|
|
|
|
def test_collision_with_self(self) -> None:
|
|
"""Test collision detection with self."""
|
|
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)
|
|
|
|
logic.move_snakes()
|
|
logic.check_collisions()
|
|
|
|
assert snake.alive is False
|
|
|
|
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_is_game_over(self) -> None:
|
|
"""Test game over detection."""
|
|
logic = GameLogic()
|
|
|
|
# No game over with multiple alive snakes
|
|
snake1 = Snake(player_id="player1", alive=True)
|
|
snake2 = Snake(player_id="player2", alive=True)
|
|
logic.state.snakes = [snake1, snake2]
|
|
|
|
assert logic.is_game_over() is False
|
|
|
|
# Game over when only one snake alive
|
|
snake2.alive = False
|
|
assert logic.is_game_over() is True
|
|
|
|
def test_get_winner(self) -> None:
|
|
"""Test winner determination."""
|
|
logic = GameLogic()
|
|
|
|
snake1 = Snake(player_id="player1", alive=True)
|
|
snake2 = Snake(player_id="player2", alive=False)
|
|
logic.state.snakes = [snake1, snake2]
|
|
|
|
winner = logic.get_winner()
|
|
assert winner == "player1"
|