diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index cdde929..63de86d 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -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. -