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>
126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
"""Server beacon for multicast discovery."""
|
|
|
|
import asyncio
|
|
import socket
|
|
from typing import Callable
|
|
|
|
from ..shared.discovery import (
|
|
ServerInfo,
|
|
DiscoveryMessage,
|
|
create_multicast_socket,
|
|
get_local_ip,
|
|
)
|
|
from ..shared.constants import MULTICAST_GROUP, MULTICAST_PORT, BEACON_INTERVAL
|
|
|
|
|
|
class ServerBeacon:
|
|
"""Broadcasts server presence and responds to discovery requests."""
|
|
|
|
def __init__(
|
|
self,
|
|
server_name: str,
|
|
server_port: int,
|
|
get_player_count: Callable[[], int],
|
|
):
|
|
"""Initialize the server beacon.
|
|
|
|
Args:
|
|
server_name: Name of the server
|
|
server_port: TCP port the game server is listening on
|
|
get_player_count: Callable that returns current player count
|
|
"""
|
|
self.server_name = server_name
|
|
self.server_port = server_port
|
|
self.get_player_count = get_player_count
|
|
self.running = False
|
|
self.sock: socket.socket | None = None
|
|
self.local_ip = get_local_ip()
|
|
|
|
def create_server_info(self) -> ServerInfo:
|
|
"""Create ServerInfo with current server state.
|
|
|
|
Returns:
|
|
Current server information
|
|
"""
|
|
return ServerInfo(
|
|
host=self.local_ip,
|
|
port=self.server_port,
|
|
server_name=self.server_name,
|
|
players_count=self.get_player_count(),
|
|
)
|
|
|
|
async def start(self) -> None:
|
|
"""Start the beacon service."""
|
|
self.running = True
|
|
self.sock = create_multicast_socket(bind=True)
|
|
self.sock.setblocking(False)
|
|
|
|
print(f"Server beacon started on {MULTICAST_GROUP}:{MULTICAST_PORT}")
|
|
print(f"Server IP: {self.local_ip}, Port: {self.server_port}")
|
|
|
|
# Run both listener and broadcaster concurrently
|
|
await asyncio.gather(
|
|
self.listen_for_discovery(),
|
|
self.broadcast_presence(),
|
|
)
|
|
|
|
async def listen_for_discovery(self) -> None:
|
|
"""Listen for DISCOVER messages and respond."""
|
|
loop = asyncio.get_event_loop()
|
|
|
|
while self.running:
|
|
try:
|
|
# Receive data (non-blocking with asyncio)
|
|
data, addr = await loop.sock_recvfrom(self.sock, 1024)
|
|
|
|
# Parse message
|
|
msg_type, msg_data = DiscoveryMessage.parse(data)
|
|
|
|
if msg_type == DiscoveryMessage.DISCOVER:
|
|
# Respond to discovery request
|
|
await self.send_announce(addr)
|
|
|
|
except Exception as e:
|
|
# Ignore errors and continue listening
|
|
await asyncio.sleep(0.1)
|
|
|
|
async def send_announce(self, addr: tuple) -> None:
|
|
"""Send SERVER_ANNOUNCE message to a specific address.
|
|
|
|
Args:
|
|
addr: (host, port) tuple to send to
|
|
"""
|
|
try:
|
|
server_info = self.create_server_info()
|
|
message = DiscoveryMessage.create_announce(server_info)
|
|
|
|
# Send directly to the requester
|
|
self.sock.sendto(message, addr)
|
|
print(f"Sent announcement to {addr}")
|
|
|
|
except Exception as e:
|
|
print(f"Error sending announcement: {e}")
|
|
|
|
async def broadcast_presence(self) -> None:
|
|
"""Periodically broadcast server presence to multicast group."""
|
|
while self.running:
|
|
try:
|
|
server_info = self.create_server_info()
|
|
message = DiscoveryMessage.create_announce(server_info)
|
|
|
|
# Broadcast to multicast group
|
|
self.sock.sendto(message, (MULTICAST_GROUP, MULTICAST_PORT))
|
|
|
|
except Exception as e:
|
|
print(f"Error broadcasting presence: {e}")
|
|
|
|
# Wait before next broadcast
|
|
await asyncio.sleep(BEACON_INTERVAL)
|
|
|
|
async def stop(self) -> None:
|
|
"""Stop the beacon service."""
|
|
self.running = False
|
|
if self.sock:
|
|
self.sock.close()
|
|
print("Server beacon stopped")
|