WIP: Add input broadcasting and client-side prediction features
Changes include: - Client: INPUT_BROADCAST packet handling and opponent prediction rendering - Client: Protocol parsing for INPUT_BROADCAST packets - Server: Input broadcasting to all clients except sender 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { PacketType, Direction, packHeader, parseHeader, buildJoin, buildInput, parseJoinAck, parseStateFullBody } from './protocol.js';
|
||||
import { PacketType, Direction, packHeader, parseHeader, buildJoin, buildInput, parseJoinAck, parseStateFullBody, parseStateDeltaBody } from './protocol.js';
|
||||
|
||||
const ui = {
|
||||
url: document.getElementById('url'),
|
||||
@@ -109,7 +109,79 @@ async function readLoop() {
|
||||
break;
|
||||
}
|
||||
case PacketType.STATE_DELTA: {
|
||||
// Minimal: ignore detailed deltas for now; rely on periodic full (good enough for a demo)
|
||||
const delta = parseStateDeltaBody(dv, off);
|
||||
// Apply changes to local snake state
|
||||
for (const ch of delta.changes) {
|
||||
let snake = snakes.get(ch.snakeId);
|
||||
if (!snake) {
|
||||
// New snake appeared; skip for now (will get it in next full update)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update direction (always present)
|
||||
// Note: direction is the current direction, not necessarily the new head direction
|
||||
|
||||
if (ch.headMoved) {
|
||||
// Snake moved: prepend new head position
|
||||
// Compute direction from old head to new head
|
||||
const oldHx = snake.hx;
|
||||
const oldHy = snake.hy;
|
||||
const newHx = ch.newHeadX;
|
||||
const newHy = ch.newHeadY;
|
||||
|
||||
// Determine direction of movement
|
||||
let moveDir = ch.direction;
|
||||
if (newHx === oldHx && newHy === oldHy - 1) moveDir = Direction.UP;
|
||||
else if (newHx === oldHx + 1 && newHy === oldHy) moveDir = Direction.RIGHT;
|
||||
else if (newHx === oldHx && newHy === oldHy + 1) moveDir = Direction.DOWN;
|
||||
else if (newHx === oldHx - 1 && newHy === oldHy) moveDir = Direction.LEFT;
|
||||
|
||||
// Update head position
|
||||
snake.hx = newHx;
|
||||
snake.hy = newHy;
|
||||
|
||||
// If tail was removed (normal move), keep dirs same length by prepending new dir
|
||||
// If grew (ate apple), prepend without removing tail
|
||||
if (ch.grew) {
|
||||
// Growing: add direction to front, don't remove from back
|
||||
snake.dirs = [moveDir, ...snake.dirs];
|
||||
snake.len += 1;
|
||||
} else if (ch.tailRemoved) {
|
||||
// Normal move: head advances, tail shrinks
|
||||
// Keep dirs array same size (represents len-1 directions)
|
||||
// Actually, we need to be careful here about the representation
|
||||
// dirs[i] tells us how to get from cell i to cell i+1
|
||||
// When head moves and tail shrinks, we add new dir at front, remove old from back
|
||||
if (snake.dirs.length > 0) {
|
||||
snake.dirs.pop(); // remove tail direction
|
||||
}
|
||||
snake.dirs = [moveDir, ...snake.dirs];
|
||||
} else {
|
||||
// Head moved but tail didn't remove (shouldn't happen in normal gameplay)
|
||||
snake.dirs = [moveDir, ...snake.dirs];
|
||||
snake.len += 1;
|
||||
}
|
||||
} else if (ch.blocked) {
|
||||
// Snake is blocked: head stayed in place, tail shrunk
|
||||
if (ch.tailRemoved && snake.dirs.length > 0) {
|
||||
snake.dirs.pop(); // remove last direction (tail shrinks)
|
||||
snake.len = Math.max(1, snake.len - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply apple changes
|
||||
const appleSet = new Set(apples.map(a => `${a[0]},${a[1]}`));
|
||||
for (const [x, y] of delta.applesAdded) {
|
||||
appleSet.add(`${x},${y}`);
|
||||
}
|
||||
for (const [x, y] of delta.applesRemoved) {
|
||||
appleSet.delete(`${x},${y}`);
|
||||
}
|
||||
apples = Array.from(appleSet).map(s => {
|
||||
const [x, y] = s.split(',').map(Number);
|
||||
return [x, y];
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
|
||||
Reference in New Issue
Block a user