Plan: add input_broadcast + opponent prediction; 2-bit body encoding; QUIC TLV framing; fragmentation-safe partitioning

This commit is contained in:
Vladyslav Doloman
2025-10-07 18:19:05 +03:00
parent b50b5b1ca2
commit cdec79fbaa

View File

@@ -87,18 +87,35 @@ State updates (server -> client):
- 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:
- Option A: Run-length of axis-aligned segments (dir, run u16) starting at head.
- Option B: Delta-coded cells with RLE for straight runs.
- Header fields: `id` (u8), `len` (u16), `head` `(x,y)` (u8,u8).
- Body TLV framing:
- Type (T): QUIC varint. Values: 0=body_2bit, 1=body_rle, 0x10=body_2bit_chunk, 0x11=body_rle_chunk.
- Length (L): QUIC varint; byte size of the Value.
- Value (V): payload depends on Type (see below). TLV enables easy skipping and robust validation.
- 2-bit body (T=0): direction stream from head toward tail using 2 bits per step: up=00, right=01, down=10, left=11. Total bits = (len-1)*2. Pack LSB-first within bytes; pad the last byte with zeros. Expected body bytes = ceil(((len-1)*2)/8).
- Decoder: read exactly `len-1` directions; verify body size matches expectation and padding bits are zero.
- RLE body (T=1): sequence of runs describing straight segments from head toward tail.
- Each run: `dir` (u8: 0=up,1=right,2=down,3=left), `count` (QUIC varint, number of steps, >=1).
- Decoder: accumulate counts until total equals `len-1`; reject if over/under.
- Chunked variants (T=0x10, 0x11): used only when a single snake must be split.
- Prefix V with `start_index` (u16, first direction offset from head) and `dirs_in_chunk` (u16); then the encoding for that range (2-bit or RLE respectively).
- Client buffers chunks by `(update_id, snake_id)` and assembles when the full [0..len-2] range is present; then applies.
Compression:
- 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.
Packet partitioning (if >1280 bytes after compression):
- Apply to state_full (join and periodic recovery) and large state_delta updates.
- Goal: avoid IP fragmentation. Ensure each compressed datagram payload is <1280 bytes.
- Strategy (whole-snake first):
- Sort snakes by estimated compressed size (head + body TLV) descending.
- Greedily pack one or more complete snakes per packet while keeping the compressed payload <1280 bytes.
- If a snake does not fit with others, send it alone if it fits <1280 bytes.
- If a single snake still exceeds 1280 bytes by itself, split that snake into multiple similar-sized chunks using the chunked TLV types.
- Framing:
- Each part carries `update_id` (u16), `part_index` (u8), `parts_total` (u8), and a sequence of per-snake TLVs (body_2bit/body_rle) and/or chunk TLVs (body_2bit_chunk/body_rle_chunk).
- Clients apply complete per-snake TLVs immediately. Chunk TLVs are buffered and assembled by `(update_id, snake_id)` using `start_index` and `dirs_in_chunk` before applying.
Sequence wrap & ordering:
- Define is_newer(a, b) using signed 16-bit difference: ((a - b) & 0xFFFF) < 0x8000.
@@ -168,4 +185,3 @@ Sequence wrap & ordering:
- Decide on tick rate and initial lengths.
- Confirm library choices (aioquic server; client-side WebTransport API usage).
- Begin Milestone 2: implement server skeleton and simulation core.