From 06083da5924b740d05e9e31b0dfb8f0f0f8d4d86 Mon Sep 17 00:00:00 2001 From: Vladyslav Doloman Date: Tue, 7 Oct 2025 15:27:44 +0300 Subject: [PATCH] Plan: input_broadcast + opponent prediction for late updates - Server relays player inputs immediately as input_broadcast to others - Clients mirror per-opponent buffers to predict movement when state updates are late or dropped; reconcile on next authoritative update - Add ordering/dedup rules and reconciliation guidance - Update testing and milestones to include prediction paths Rationale: reduce perceived latency and jitter under packet loss. --- PROJECT_PLAN.md | 131 +++++++++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 58 deletions(-) diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index fa398a5..cdde929 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -1,25 +1,25 @@ -# Multiplayer Snake (WebTransport) — Project Plan +# Multiplayer Snake (WebTransport) - Project Plan ## Overview - Goal: Build a real-time, networked multiplayer Snake game with a Python 3 authoritative server and a web client using WebTransport datagrams for low-latency updates. -- Gameplay: Continuous, persistent world (no rounds). Players can join/leave anytime. Display each snake’s current length; the longest snake is the momentary “leader”. -- Field: Default 60×40 grid; protocol supports 3×3 up to 255×255. -- Networking: Unreliable, unordered datagrams with sequence numbers and wraparound handling. Compression and packet partitioning to fit MTU (~1280–1500 bytes). +- Gameplay: Continuous, persistent world (no rounds). Players can join/leave anytime. Display each snake's current length; the longest snake is the momentary leader. +- Field: Default 60x40 grid; protocol supports 3x3 up to 255x255. +- Networking: Unreliable, unordered datagrams with sequence numbers and wraparound handling. Compression and packet partitioning to fit MTU (~1280-1500 bytes). ## Core Mechanics -- Collision handling: When the snake head would hit an obstacle (wall, self, other snake), the head stays in place (blocked) and the tail shrinks by 1 per tick until the player turns to a free direction. +- Collision handling: If the head would hit an obstacle (wall, self, other snake), the head stays in place (blocked) and the tail shrinks by 1 per tick until the player turns to a free direction. - Self-collision: Temporary; tail shrink can clear the blocking segment. - Other-snake collision: Clears when the other snake moves or shrinks away. - Minimum length: A snake cannot be shorter than its head; minimum length is 1. - Turning rules: - - Length > 1: 180° turns are invalid and ignored at consumption time. - - Length = 1 (only head left): 180° turns are valid. + - Length > 1: 180-degree turns are invalid and ignored at consumption time. + - Length = 1 (only head left): 180-degree turns are valid. - Apples and scoring: - Replace score display with current snake length. - - Eating apple grows snake by 1 immediately. + - Eating an apple grows snake by 1 immediately. - When 0 players remain connected, pre-populate field with exactly 3 apples. - Colors: Assign a color when a client connects and keep it stable for that session. -- Spectator join: On initial connection, show current gameplay and overlay “press space to join”. +- Spectator join: On initial connection, show current gameplay and overlay "press space to join". ## Input Buffer Rules - Maintain a small buffer of up to 3 upcoming direction changes. @@ -28,117 +28,131 @@ - Overflow policy: If buffer is full, replace the last element with the new input. - At each tick, before moving: - Consume at most one input from the buffer. - - If length > 1 and it is a 180° turn, ignore it and immediately try the next buffered input; if none valid, keep current direction. + - If length > 1 and it is a 180-degree turn, ignore it and immediately try the next buffered input; if none valid, keep current direction. ## Server Architecture (Python 3) - Runtime: Python 3 with asyncio. -- Transport: WebTransport (HTTP/3 datagrams). Candidate library: `aioquic` for server-side H3 and datagrams. +- Transport: WebTransport (HTTP/3 datagrams). Candidate library: aioquic for server-side H3 and datagrams. - Model: - - Authoritative simulation on the server with a fixed tick rate (target 15–20 TPS; start with 15 for network headroom). - - Each tick: process inputs, update snakes, resolve collisions (blocked/shrink), move apples, generate state deltas. + - Authoritative simulation on the server with a fixed tick rate (target 15-20 TPS; start with 15 for network headroom). + - Each tick: process inputs, update snakes, resolve collisions (blocked/shrink), manage apples, generate state deltas. - Entities: - Field: width, height, random seed, apple set. - - Snake: id, name (≤16 chars), color id (0–31), deque of cells (head-first), current direction, input buffer, blocked flag, last-move tick, last-known client seq. + - Snake: id, name (<=16 chars), color id (0-31), deque of cells (head-first), current direction, input buffer, blocked flag, last-move tick, last-known client seq. - Player session: transport bindings, last-received input seq per player, anti-spam counters. - Spawn: - New snake spawns at a random free cell with a random legal starting direction, length 3 by default (configurable). - On apple eat: grow by 1; spawn a replacement apple at a free cell. -- Disconnect: Immediately remove snake and its score/length; apples persist. If all disconnect, ensure field contains 3 apples. +- Disconnect: Immediately remove snake and its length; apples persist. If all disconnect, ensure field contains 3 apples. - Rate limiting: Cap input datagrams per second and buffer growth per client. ## Client Architecture (Web) -- Tech: Browser WebTransport (H3 datagrams), Canvas/WebGL rendering, vanilla or minimal framework for UI overlay. +- Tech: Browser WebTransport (H3 datagrams), Canvas/WebGL rendering, minimal UI framework. - Modes: - - Spectator: Render world, show overlay “press space to join”. - - Player: Capture keyboard, build input buffer client-side, send inputs with sequence numbers; predict local movement for smoothness but reconcile to server. + - Spectator: Render world, show overlay "press space to join". + - Player: Capture keyboard, build input buffer client-side, send inputs with sequence numbers; predict local movement for smoothness and reconcile to server. - Rendering: - Gridless pixel or cell-based canvas. - Colors: 32-color predefined palette; deterministic mapping from player id to color id. - HUD: Current length; leaderboard (top N by length). +- Opponent prediction: When server state updates are late, step other snakes using server input broadcasts from their last known state; maintain mirrored per-opponent buffers and reconcile on the next authoritative update. ## Networking & Protocol Constraints and goals: - Use datagrams (unreliable, unordered). Include a packet sequence number for late-packet discard with wraparound handling. -- Keep payloads ≤1280 bytes to avoid fragmentation; if larger, partition logically. -- Support up to 32 concurrent players; names limited to 16 bytes UTF‑8. +- Keep payloads <=1280 bytes to avoid fragmentation; if larger, partition logically. +- Support up to 32 concurrent players; names limited to 16 bytes UTF-8. Header (all packets): -- `ver` (u8): protocol version. -- `type` (u8): packet type (join, join_ack, input, state_delta, state_full, part, ping, pong, error). -- `flags` (u8): bit flags (compression, is_last_part, etc.). -- `seq` (u16): sender’s monotonically incrementing sequence number (wraps at 65535). Newer-than logic uses half-range window. -- Optional `tick` (u16): simulation tick the packet refers to (on server updates). +- ver (u8): protocol version. +- type (u8): packet type (join, join_ack, input, input_broadcast, state_delta, state_full, part, ping, pong, error). +- flags (u8): bit flags (compression, is_last_part, etc.). +- seq (u16): sender's monotonically incrementing sequence number (wraps at 65535). Newer-than logic uses half-range window. +- Optional tick (u16): simulation tick the packet refers to (on server updates). -Handshake (join → join_ack): -- Client sends: desired name (≤16), optional preferred color id. +Handshake (join -> join_ack): +- Client sends: desired name (<=16), optional preferred color id. - Server replies: assigned player id (u8), color id (u8), field size (width u8, height u8), tick rate (u8), random seed (u32), palette, and initial full snapshot. -Inputs (client → server): -- Packet `input` includes: last-acknowledged server seq (u16), local `input_seq` (u16), one or more direction events with timestamps or relative tick offsets. Client pre-filters per buffer rules. +Inputs (client -> server): +- Packet input includes: last-acknowledged server seq (u16), local input_seq (u16), one or more direction events with timestamps or relative tick offsets. Client pre-filters per buffer rules. -State updates (server → client): +Input broadcast (server -> clients): +- Upon receiving a player's input, the server immediately relays those input events to all other clients as input_broadcast packets. +- Contents: player_id (u8), the player's input_seq (u16), direction events, and apply_at_tick (u16) assigned by the server (typically current_tick+1) so clients can align predictions. +- Purpose: Enable client-side opponent prediction during late or lost state updates. Broadcasts are small; apply rate limits if needed. + +State updates (server -> client): - Types: - - `state_full`: rare (on join or periodic recovery); includes all snakes and apples. - - `state_delta`: frequent; contains only changed snakes (head move, tail shrink/grow) and apple changes since last acked tick. + - state_full: rare (on join or periodic recovery); includes all snakes and apples. + - state_delta: frequent; contains only changed snakes (head move, tail shrink/grow) and apple changes since last acked tick. - Per-snake encoding (compressed): - - `id` (u8), `len` (u16), head `(x,y)` (u8,u8), direction (2 bits), and a compact body representation: + - id (u8), len (u16), head (x,y) (u8,u8), direction (2 bits), and a compact body representation: - Option A: Run-length of axis-aligned segments (dir, run u16) starting at head. - Option B: Delta-coded cells with RLE for straight runs. Compression: -- Primary: domain-specific packing (bits + RLE for segments and apples), then optional DEFLATE flag (`flags.compressed=1`) if still large. +- Primary: domain-specific packing (bits + RLE for segments and apples), then optional DEFLATE flag (flags.compressed=1) if still large. - Client decompresses if set; otherwise reads packed structs directly. Packet partitioning (if >1280 bytes): -- Split `state_full`/`state_delta` by snakes into multiple independent sub-updates. -- Each part has: `update_id` (u16), `part_index` (u8), `parts_total` (u8). Clients apply parts as they arrive; missing parts simply leave some snakes stale until the next update. -- Extremely long single-snake updates: split that snake’s body into balanced segments by RLE chunks. +- Split state_full/state_delta by snakes into multiple independent sub-updates. +- Each part has: update_id (u16), part_index (u8), parts_total (u8). Clients apply parts as they arrive; missing parts simply leave some snakes stale until the next update. +- Extremely long single-snake updates: split that snake's body into balanced segments by RLE chunks. Sequence wrap & ordering: -- Define `is_newer(a, b)` using signed 16-bit difference: `((a - b) & 0xFFFF) < 0x8000`. +- Define is_newer(a, b) using signed 16-bit difference: ((a - b) & 0xFFFF) < 0x8000. - Clients ignore updates older-than last applied per-sender. +- For input_broadcast, deduplicate per (player_id, input_seq) and apply in order per player; if apply_at_tick is in the past, apply immediately up to current tick. ## Simulation Details - Tick order per tick: - 1) Incorporate at most one valid buffered input per snake (with 180° rule). - 2) Compute intended head step; if blocked, set `blocked=true` and keep head stationary. - 3) If `blocked=true`: shrink tail by 1 (to min length 1). If the blocking cell becomes free (due to own shrink or others moving), the next tick’s step in current direction proceeds. + 1) Incorporate at most one valid buffered input per snake (with 180-degree rule). + 2) Compute intended head step; if blocked, set blocked=true and keep head stationary. + 3) If blocked=true: shrink tail by 1 (to min length 1). If the blocking cell becomes free (due to own shrink or others moving), the next tick's step in current direction proceeds. 4) If moving into an apple: grow by 1 (do not shrink tail that tick) and respawn an apple. 5) Emit per-snake delta (move, grow, or shrink) and global apple changes. - Blocking detection: walls (0..width-1, 0..height-1), any occupied snake cell (including heads and tails that are not about to vacate this tick). -- 180° turn allowance when length==1 only. +- 180-degree turn allowance when length == 1 only. + +### Client-side Opponent Prediction Heuristics +- Use last authoritative state per opponent as baseline; apply input_broadcast events at apply_at_tick boundaries. +- Apply the same input buffer rules and 180-degree constraints as the server. +- Approximate blocking: if predicted head step hits an occupied cell in the last known occupancy, mark blocked and simulate tail shrink until an alternate direction arrives; reconciliation will correct drift. ## Data Model & Structures -- Field cells: Represent coordinates as `(x:u8, y:u8)`. -- Snakes: Deque of cells or segment list; store length as `deque.size()` and maintain head index. +- Field cells: Represent coordinates as (x:u8, y:u8). +- Snakes: Deque of cells or segment list; store length as deque.size() and maintain head index. - Apples: Hash set of cells for O(1) lookup; spawn on empty cells only. -- Occupancy: Sparse set/map from cell→(snake_id, index) for O(1) collision checks. +- Occupancy: Sparse set/map from cell -> (snake_id, index) for O(1) collision checks. ## Limits & Config - Players: max 32 (ids 0..31); deny joins beyond capacity. -- Names: UTF‑8, truncate to 16 bytes; filter control chars. -- Field: default 60×40; min 3×3; max 255×255; provided by server in join_ack. -- Tick rate: default 15 TPS; configurable 10–30 TPS. +- Names: UTF-8, truncate to 16 bytes; filter control chars. +- Field: default 60x40; min 3x3; max 255x255; provided by server in join_ack. +- Tick rate: default 15 TPS; configurable 10-30 TPS. ## Error Handling & Resilience -- Invalid packets: drop and optionally send `error` with code. +- Invalid packets: drop and optionally send error with code. - Flooding: server-side rate limits and strike-based disconnects. -- Out-of-order: discard using `is_newer` check. -- Desync recovery: server sends `state_full` periodically or when client reports large gaps. +- Out-of-order: discard using is_newer check. +- Desync recovery: server sends state_full periodically or when client reports large gaps. ## Testing Strategy -- Unit tests: input buffer rules, 180° logic, blocking/shrink, collision resolution, sequence wrap comparison. +- Unit tests: input buffer rules, 180-degree logic, blocking/shrink, collision resolution, sequence wrap comparison. - Property tests: snake movement invariants (no duplicates in body except expected at head on block), apple placement safety. - Soak tests: bot clients sending random but rate-limited inputs; measure packet size and latency. +- Prediction tests: delayed/missing state updates with input_broadcast streams; assert bounded drift and successful reconciliation. ## Milestones -1) Protocol draft + wire structs (this plan). +1) Protocol draft and wire structs (this plan). 2) Server skeleton: tick loop, entities, occupancy, apples, basic WebTransport datagrams. 3) Input handling: buffer logic and per-tick consumption; collision/blocking/shrink rules. -4) State encoding: deltas, compression, and partitioning; client renderer (spectator). -5) Player flow: join overlay, name/color, spawn, HUD and leaderboard. -6) Performance pass: packet size tuning, RLE, optional DEFLATE, rate limits. -7) Polishing: error messages, reconnect, telemetry/logging, Docker/dev scripts. +4) Input relay and opponent prediction: server input_broadcast and client prediction + reconciliation. +5) State encoding: deltas, compression, and partitioning; client renderer (spectator). +6) Player flow: join overlay, name/color, spawn, HUD and leaderboard. +7) Performance pass: packet size tuning, RLE, optional DEFLATE, rate limits. +8) Polishing: error messages, reconnect, telemetry/logging, Docker/dev scripts. ## Open Questions - Exact tick rate target and interpolation strategy on client. @@ -147,10 +161,11 @@ Sequence wrap & ordering: - Whether to allow wrap-around at field edges (currently: walls are blocking). - Compression choice tradeoffs: custom bitpacking only vs optional DEFLATE. - Minimum viable browser targets (WebTransport support varies). +- Should input_broadcast include server-assigned apply_at_tick or relative tick offsets only? ## Next Steps - Validate the protocol choices and mechanics with stakeholders. - Decide on tick rate and initial lengths. -- Confirm library choices (`aioquic` server; client-side `WebTransport` API usage). +- Confirm library choices (aioquic server; client-side WebTransport API usage). - Begin Milestone 2: implement server skeleton and simulation core.