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>
143 lines
3.7 KiB
Python
143 lines
3.7 KiB
Python
"""Server discovery utilities using UDP multicast."""
|
|
|
|
import json
|
|
import socket
|
|
import struct
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from .constants import MULTICAST_GROUP, MULTICAST_PORT
|
|
|
|
|
|
@dataclass
|
|
class ServerInfo:
|
|
"""Information about a discovered server."""
|
|
host: str
|
|
port: int
|
|
server_name: str
|
|
players_count: int
|
|
last_seen: float = 0.0
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary for serialization."""
|
|
return {
|
|
"host": self.host,
|
|
"port": self.port,
|
|
"server_name": self.server_name,
|
|
"players_count": self.players_count,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict, host: str = None) -> "ServerInfo":
|
|
"""Create ServerInfo from dictionary.
|
|
|
|
Args:
|
|
data: Dictionary with server information
|
|
host: Optional override for host address
|
|
"""
|
|
return cls(
|
|
host=host or data["host"],
|
|
port=data["port"],
|
|
server_name=data["server_name"],
|
|
players_count=data["players_count"],
|
|
last_seen=time.time(),
|
|
)
|
|
|
|
|
|
class DiscoveryMessage:
|
|
"""Discovery protocol messages."""
|
|
|
|
DISCOVER = "DISCOVER"
|
|
SERVER_ANNOUNCE = "SERVER_ANNOUNCE"
|
|
|
|
@staticmethod
|
|
def create_discover() -> bytes:
|
|
"""Create a DISCOVER message.
|
|
|
|
Returns:
|
|
Encoded message bytes
|
|
"""
|
|
msg = {"type": DiscoveryMessage.DISCOVER}
|
|
return json.dumps(msg).encode("utf-8")
|
|
|
|
@staticmethod
|
|
def create_announce(server_info: ServerInfo) -> bytes:
|
|
"""Create a SERVER_ANNOUNCE message.
|
|
|
|
Args:
|
|
server_info: Server information to announce
|
|
|
|
Returns:
|
|
Encoded message bytes
|
|
"""
|
|
msg = {
|
|
"type": DiscoveryMessage.SERVER_ANNOUNCE,
|
|
"data": server_info.to_dict(),
|
|
}
|
|
return json.dumps(msg).encode("utf-8")
|
|
|
|
@staticmethod
|
|
def parse(data: bytes) -> tuple[str, Optional[dict]]:
|
|
"""Parse a discovery message.
|
|
|
|
Args:
|
|
data: Raw message bytes
|
|
|
|
Returns:
|
|
Tuple of (message_type, data_dict)
|
|
"""
|
|
try:
|
|
msg = json.loads(data.decode("utf-8"))
|
|
msg_type = msg.get("type")
|
|
msg_data = msg.get("data")
|
|
return msg_type, msg_data
|
|
except Exception:
|
|
return None, None
|
|
|
|
|
|
def create_multicast_socket(bind: bool = False) -> socket.socket:
|
|
"""Create a UDP socket configured for multicast.
|
|
|
|
Args:
|
|
bind: If True, bind to multicast group (for receiving)
|
|
|
|
Returns:
|
|
Configured socket
|
|
"""
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
|
|
# Allow multiple sockets to bind to the same port
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
if bind:
|
|
# Bind to the multicast port
|
|
sock.bind(("", MULTICAST_PORT))
|
|
|
|
# Join the multicast group
|
|
mreq = struct.pack("4sl", socket.inet_aton(MULTICAST_GROUP), socket.INADDR_ANY)
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
|
|
# Set multicast TTL (time-to-live) for sending
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
|
|
|
return sock
|
|
|
|
|
|
def get_local_ip() -> str:
|
|
"""Get the local IP address for LAN communication.
|
|
|
|
Returns:
|
|
Local IP address as string
|
|
"""
|
|
try:
|
|
# Create a UDP socket and connect to a public DNS server
|
|
# This doesn't actually send data, just determines routing
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
s.connect(("8.8.8.8", 80))
|
|
local_ip = s.getsockname()[0]
|
|
s.close()
|
|
return local_ip
|
|
except Exception:
|
|
return "127.0.0.1"
|