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>
136 lines
4.0 KiB
Python
136 lines
4.0 KiB
Python
"""Tests for data models."""
|
|
|
|
import pytest
|
|
from src.shared.models import Position, Snake, Food, GameState
|
|
|
|
|
|
class TestPosition:
|
|
"""Test suite for Position class."""
|
|
|
|
def test_position_creation(self) -> None:
|
|
"""Test creating a position."""
|
|
pos = Position(5, 10)
|
|
assert pos.x == 5
|
|
assert pos.y == 10
|
|
|
|
def test_position_addition(self) -> None:
|
|
"""Test adding a direction to a position."""
|
|
pos = Position(5, 10)
|
|
new_pos = pos + (1, -1)
|
|
assert new_pos.x == 6
|
|
assert new_pos.y == 9
|
|
|
|
def test_position_to_tuple(self) -> None:
|
|
"""Test converting position to tuple."""
|
|
pos = Position(3, 7)
|
|
assert pos.to_tuple() == (3, 7)
|
|
|
|
def test_position_from_tuple(self) -> None:
|
|
"""Test creating position from tuple."""
|
|
pos = Position.from_tuple((3, 7))
|
|
assert pos.x == 3
|
|
assert pos.y == 7
|
|
|
|
|
|
class TestSnake:
|
|
"""Test suite for Snake class."""
|
|
|
|
def test_snake_creation(self) -> None:
|
|
"""Test creating a snake."""
|
|
snake = Snake(player_id="test123")
|
|
assert snake.player_id == "test123"
|
|
assert snake.alive is True
|
|
assert snake.score == 0
|
|
assert snake.direction == (1, 0)
|
|
|
|
def test_get_head(self) -> None:
|
|
"""Test getting snake head position."""
|
|
snake = Snake(
|
|
player_id="test",
|
|
body=[Position(5, 5), Position(4, 5), Position(3, 5)]
|
|
)
|
|
head = snake.get_head()
|
|
assert head.x == 5
|
|
assert head.y == 5
|
|
|
|
def test_snake_serialization(self) -> None:
|
|
"""Test snake to_dict and from_dict."""
|
|
snake = Snake(
|
|
player_id="test123",
|
|
body=[Position(5, 5), Position(4, 5)],
|
|
direction=(0, 1),
|
|
alive=False,
|
|
score=100
|
|
)
|
|
|
|
# Serialize
|
|
data = snake.to_dict()
|
|
assert data["player_id"] == "test123"
|
|
assert data["body"] == [(5, 5), (4, 5)]
|
|
assert data["direction"] == (0, 1)
|
|
assert data["alive"] is False
|
|
assert data["score"] == 100
|
|
|
|
# Deserialize
|
|
snake2 = Snake.from_dict(data)
|
|
assert snake2.player_id == snake.player_id
|
|
assert len(snake2.body) == len(snake.body)
|
|
assert snake2.body[0].x == snake.body[0].x
|
|
assert snake2.direction == snake.direction
|
|
assert snake2.alive == snake.alive
|
|
assert snake2.score == snake.score
|
|
|
|
|
|
class TestFood:
|
|
"""Test suite for Food class."""
|
|
|
|
def test_food_creation(self) -> None:
|
|
"""Test creating food."""
|
|
food = Food(position=Position(10, 15))
|
|
assert food.position.x == 10
|
|
assert food.position.y == 15
|
|
|
|
def test_food_serialization(self) -> None:
|
|
"""Test food to_dict and from_dict."""
|
|
food = Food(position=Position(10, 15))
|
|
|
|
# Serialize
|
|
data = food.to_dict()
|
|
assert data["position"] == (10, 15)
|
|
|
|
# Deserialize
|
|
food2 = Food.from_dict(data)
|
|
assert food2.position.x == food.position.x
|
|
assert food2.position.y == food.position.y
|
|
|
|
|
|
class TestGameState:
|
|
"""Test suite for GameState class."""
|
|
|
|
def test_game_state_creation(self) -> None:
|
|
"""Test creating game state."""
|
|
state = GameState()
|
|
assert len(state.snakes) == 0
|
|
assert len(state.food) == 0
|
|
assert state.game_running is False
|
|
|
|
def test_game_state_serialization(self) -> None:
|
|
"""Test game state to_dict and from_dict."""
|
|
state = GameState()
|
|
state.snakes.append(Snake(player_id="p1", body=[Position(5, 5)]))
|
|
state.food.append(Food(position=Position(10, 10)))
|
|
state.game_running = True
|
|
|
|
# Serialize
|
|
data = state.to_dict()
|
|
assert len(data["snakes"]) == 1
|
|
assert len(data["food"]) == 1
|
|
assert data["game_running"] is True
|
|
|
|
# Deserialize
|
|
state2 = GameState.from_dict(data)
|
|
assert len(state2.snakes) == 1
|
|
assert len(state2.food) == 1
|
|
assert state2.game_running is True
|
|
assert state2.snakes[0].player_id == "p1"
|