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:
Vladyslav Doloman
2025-10-04 13:50:16 +03:00
commit 0703561446
28 changed files with 2523 additions and 0 deletions

148
tests/test_game_logic.py Normal file
View 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"