Changes in project plans: added client-side opponen movement prediction.

This commit is contained in:
Vladyslav Doloman
2025-10-07 15:27:44 +03:00
parent 28ddf4f455
commit b50b5b1ca2

View File

@@ -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 snakes 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 (~12801500 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 1520 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 (031), 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 UTF8.
- 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): senders 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 snakes 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 ticks 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: UTF8, 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 1030 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.