Major networking overhaul to reduce latency and bandwidth: UDP Protocol Implementation: - Created UDP server handler with sequence number tracking (uint32 with wrapping support) - Implemented 1000-packet window for reordering tolerance - Packet structure: [seq_num(4) + msg_type(1) + update_id(2) + payload] - Handles 4+ billion packets without sequence number issues - Auto-fallback to TCP on >20% packet loss Binary Codec with Schema Versioning: - Extensible field-based format with version negotiation - Position encoding: 11-bit packed (6-bit x + 5-bit y for 40x30 grid) - Delta encoding for snake bodies: 2 bits per segment direction - Variable-length integers for compact numbers - String encoding: up to 16 chars with 4-bit length prefix - Player ID hashing: CRC32 for compact representation - zlib compression for payload reduction Partial Update System: - Splits large game states into independent packets <1280 bytes (IPv6 MTU) - Each packet is self-contained (packet loss affects only subset of snakes) - Smart snake segmenting for very long snakes (>100 segments) - Player name caching: sent once per player, then omitted - Metadata (food, game_running) separated from snake data 32-Player Support: - Extended COLOR_SNAKES array to 32 distinct colors - Server enforces MAX_PLAYERS=32 limit - Player names limited to MAX_PLAYER_NAME_LENGTH=16 - Name validation and sanitization - Color assignment with rotation through 32 colors Desktop Client Components: - UDP client with automatic TCP fallback - Partial state reassembly and tracking - Sequence validation and duplicate detection - Statistics tracking for fallback decisions Web Client Components: - 32-color palette matching Python colors - JavaScript binary codec (mirrors Python implementation) - Partial state tracker for reassembly - WebRTC DataChannel transport skeleton (for future use) - Graceful fallback to WebSocket Server Integration: - UDP server on port 8890 (configurable via --udp-port) - Integrated with existing TCP (8888) and WebSocket (8889) servers - Proper cleanup on shutdown - Command-line argument: --udp-port (0 to disable, default 8890) Performance Improvements: - ~75% bandwidth reduction (binary + compression vs JSON) - All packets guaranteed <1280 bytes (safe for all networks) - UDP eliminates TCP head-of-line blocking for lower latency - Independent partial updates gracefully handle packet loss - Delta encoding dramatically reduces snake body size Comprehensive Testing: - 46 tests total, all passing (100% success rate) - 15 UDP protocol tests (sequence wrapping, packet parsing, compression) - 20 binary codec tests (encoding, delta compression, strings, varint) - 11 partial update tests (splitting, reassembly, packet loss resilience) Files Added: - src/shared/binary_codec.py: Extensible binary serialization - src/shared/udp_protocol.py: UDP packet handling with sequence numbers - src/server/udp_handler.py: Async UDP server - src/server/partial_update.py: State splitting logic - src/client/udp_client.py: Desktop UDP client with TCP fallback - src/client/partial_state_tracker.py: Client-side reassembly - web/binary_codec.js: JavaScript binary codec - web/partial_state_tracker.js: JavaScript reassembly - web/webrtc_transport.js: WebRTC transport (ready for future use) - tests/test_udp_protocol.py: UDP protocol tests - tests/test_binary_codec.py: Binary codec tests - tests/test_partial_updates.py: Partial update tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
108 lines
3.1 KiB
Python
108 lines
3.1 KiB
Python
"""Run the Snake game server."""
|
|
|
|
import asyncio
|
|
import argparse
|
|
from pathlib import Path
|
|
from src.server.game_server import GameServer
|
|
from src.server.http_server import HTTPServer
|
|
from src.shared.constants import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_WS_PORT, DEFAULT_UDP_PORT, DEFAULT_HTTP_PORT
|
|
|
|
|
|
async def main() -> None:
|
|
"""Run the server with command line arguments."""
|
|
parser = argparse.ArgumentParser(description="Run the Snake game server")
|
|
parser.add_argument(
|
|
"--host",
|
|
default=DEFAULT_HOST,
|
|
help=f"Host address to bind to (default: {DEFAULT_HOST})",
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=DEFAULT_PORT,
|
|
help=f"TCP port number (default: {DEFAULT_PORT})",
|
|
)
|
|
parser.add_argument(
|
|
"--ws-port",
|
|
type=int,
|
|
default=DEFAULT_WS_PORT,
|
|
help=f"WebSocket port (default: {DEFAULT_WS_PORT}, 0 to disable)",
|
|
)
|
|
parser.add_argument(
|
|
"--udp-port",
|
|
type=int,
|
|
default=DEFAULT_UDP_PORT,
|
|
help=f"UDP port (default: {DEFAULT_UDP_PORT}, 0 to disable)",
|
|
)
|
|
parser.add_argument(
|
|
"--http-port",
|
|
type=int,
|
|
default=DEFAULT_HTTP_PORT,
|
|
help=f"HTTP server port (default: {DEFAULT_HTTP_PORT}, 0 to disable)",
|
|
)
|
|
parser.add_argument(
|
|
"--web-dir",
|
|
default="web",
|
|
help="Directory containing web client files (default: web)",
|
|
)
|
|
parser.add_argument(
|
|
"--name",
|
|
default="Snake Server",
|
|
help="Server name for discovery (default: Snake Server)",
|
|
)
|
|
parser.add_argument(
|
|
"--no-discovery",
|
|
action="store_true",
|
|
help="Disable multicast discovery beacon",
|
|
)
|
|
parser.add_argument(
|
|
"--no-websocket",
|
|
action="store_true",
|
|
help="Disable WebSocket server",
|
|
)
|
|
parser.add_argument(
|
|
"--no-http",
|
|
action="store_true",
|
|
help="Disable HTTP server (for production with external web server)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Determine WebSocket port
|
|
ws_port = None if args.no_websocket or args.ws_port == 0 else args.ws_port
|
|
|
|
# Determine UDP port (False means disabled, None means use default)
|
|
udp_port = False if args.udp_port == 0 else args.udp_port
|
|
|
|
# Create game server
|
|
server = GameServer(
|
|
host=args.host,
|
|
port=args.port,
|
|
server_name=args.name,
|
|
enable_discovery=not args.no_discovery,
|
|
ws_port=ws_port,
|
|
udp_port=udp_port,
|
|
)
|
|
|
|
# Start HTTP server if enabled
|
|
http_server = None
|
|
if not args.no_http and args.http_port > 0:
|
|
web_dir = Path(args.web_dir)
|
|
if web_dir.exists():
|
|
# Use same host as game server for HTTP
|
|
http_server = HTTPServer(web_dir, args.http_port, args.host)
|
|
await http_server.start()
|
|
else:
|
|
print(f"Warning: Web directory '{web_dir}' not found. HTTP server disabled.")
|
|
|
|
# Start game server
|
|
try:
|
|
await server.start()
|
|
finally:
|
|
if http_server:
|
|
await http_server.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|