Initial commit: Multiplayer Snake game with server discovery
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>
This commit is contained in:
148
tests/test_game_logic.py
Normal file
148
tests/test_game_logic.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user