// Minimal protocol helpers in JS (match server/protocol.py) export const PacketType = { JOIN: 0, JOIN_ACK: 1, JOIN_DENY: 2, INPUT: 3, INPUT_BROADCAST: 4, STATE_DELTA: 5, STATE_FULL: 6, PART: 7, CONFIG_UPDATE: 8, PING: 9, PONG: 10, ERROR: 11, }; export const Direction = { UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3 }; export function quicVarintEncode(n) { if (n <= 0x3f) return new Uint8Array([n & 0x3f]); if (n <= 0x3fff) { const v = 0x4000 | n; return new Uint8Array([(v >> 8) & 0xff, v & 0xff]); } if (n <= 0x3fffffff) { const v = 0x80000000 | n; return new Uint8Array([(v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff]); } throw new Error("varint too large for demo"); } export function quicVarintDecode(view, offset) { const first = view.getUint8(offset); const prefix = first >> 6; if (prefix === 0) return [first & 0x3f, offset + 1]; if (prefix === 1) return [view.getUint16(offset) & 0x3fff, offset + 2]; if (prefix === 2) return [view.getUint32(offset) & 0x3fffffff, offset + 4]; throw new Error("8-byte varint not supported in demo"); } export function packHeader(ver, type, flags, seq, tick /* or null */) { const len = tick == null ? 5 : 7; const buf = new ArrayBuffer(len); const dv = new DataView(buf); dv.setUint8(0, ver & 0xff); dv.setUint8(1, type & 0xff); dv.setUint8(2, flags & 0xff); dv.setUint16(3, seq & 0xffff); if (tick != null) dv.setUint16(5, tick & 0xffff); return new Uint8Array(buf); } export function parseHeader(dv, offset = 0, expectTick = false) { if (dv.byteLength - offset < 5) throw new Error("small header"); const ver = dv.getUint8(offset); const type = dv.getUint8(offset + 1); const flags = dv.getUint8(offset + 2); const seq = dv.getUint16(offset + 3); let tick = null; let off = offset + 5; if (expectTick) { tick = dv.getUint16(off); off += 2; } return [ver, type, flags, seq, tick, off]; } export function buildJoin(ver, seq, name, preferredColor = null) { const enc = new TextEncoder(); const nb = enc.encode(name); const nameLen = quicVarintEncode(nb.length); const header = packHeader(ver, PacketType.JOIN, 0, seq, null); const extra = preferredColor == null ? new Uint8Array([]) : new Uint8Array([preferredColor & 0xff]); const out = new Uint8Array(header.length + nameLen.length + nb.length + extra.length); out.set(header, 0); out.set(nameLen, header.length); out.set(nb, header.length + nameLen.length); out.set(extra, header.length + nameLen.length + nb.length); return out; } export function buildInput(ver, seq, ackSeq, inputSeq, baseTick, events /* [{rel, dir}] */) { const header = packHeader(ver, PacketType.INPUT, 0, seq, null); const evParts = []; evParts.push(...[ackSeq >> 8, ackSeq & 0xff, inputSeq >> 8, inputSeq & 0xff, baseTick >> 8, baseTick & 0xff]); const evCount = quicVarintEncode(events.length); const chunks = [header, new Uint8Array(evParts), evCount]; for (const e of events) { const off = quicVarintEncode(e.rel); chunks.push(off, new Uint8Array([e.dir & 0x03])); } const size = chunks.reduce((s, a) => s + a.length, 0); const out = new Uint8Array(size); let p = 0; for (const c of chunks) { out.set(c, p); p += c.length; } return out; } export function parseJoinAck(dv, off) { const playerId = dv.getUint8(off); off += 1; const colorId = dv.getUint8(off); off += 1; const width = dv.getUint8(off); off += 1; const height = dv.getUint8(off); off += 1; const tickRate = dv.getUint8(off); off += 1; const wrapEdges = dv.getUint8(off) !== 0; off += 1; const applesPerSnake = dv.getUint8(off); off += 1; const applesCap = dv.getUint8(off); off += 1; const compressionMode = dv.getUint8(off); off += 1; return { playerId, colorId, width, height, tickRate, wrapEdges, applesPerSnake, applesCap, compressionMode, off }; } export function parseStateFullBody(dv, off) { const snakes = []; // snakes count let [n, p] = quicVarintDecode(dv, off); off = p; for (let i = 0; i < n; i++) { const id = dv.getUint8(off); off += 1; const len = dv.getUint16(off); off += 2; const hx = dv.getUint8(off); off += 1; const hy = dv.getUint8(off); off += 1; // TLV let [t, p1] = quicVarintDecode(dv, off); off = p1; let [L, p2] = quicVarintDecode(dv, off); off = p2; const tStart = off; const tEnd = off + L; let dirs = []; if (t === 0 /* 2-bit */) { const needed = Math.max(0, (len - 1)); let bits = 0, acc = 0, di = 0; for (let iB = off; iB < tEnd && di < needed; iB++) { acc |= dv.getUint8(iB) << bits; bits += 8; while (bits >= 2 && di < needed) { dirs.push(acc & 0x03); acc >>= 2; bits -= 2; di++; } } } else if (t === 1 /* RLE */) { let countAccum = 0; while (off < tEnd && countAccum < (len - 1)) { const dir = dv.getUint8(off); off += 1; let c; [c, off] = quicVarintDecode(dv, off); for (let k = 0; k < c; k++) dirs.push(dir & 0x03); countAccum += c; } } else { // chunk types not expected in full body for initial join; skip off = tEnd; } off = tEnd; snakes.push({ id, len, hx, hy, dirs }); } // apples let m; [m, off] = quicVarintDecode(dv, off); const apples = []; for (let i = 0; i < m; i++) { apples.push([dv.getUint8(off++), dv.getUint8(off++)]); } return { snakes, apples, off }; }