Files
claudePySnake/src/shared/discovery.py
Vladyslav Doloman 0703561446 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>
2025-10-04 13:50:16 +03:00

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"