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:
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Read(//g/Coding/code2/**)",
|
||||||
|
"Bash(python -m py_compile server/server.py)",
|
||||||
|
"Bash(node --check client/client.js)",
|
||||||
|
"Bash(node --check client/protocol.js)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
241
CLAUDE.md
Normal file
241
CLAUDE.md
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a real-time multiplayer Snake game using Python 3 (server) and vanilla JavaScript (web client), communicating via WebTransport datagrams (HTTP/3) for low-latency gameplay. The game features continuous persistent gameplay, unique collision mechanics (head-blocks-and-tail-shrinks instead of death), and sophisticated UDP packet handling with compression and partitioning.
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
```bash
|
||||||
|
# Create and activate virtual environment
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Modes
|
||||||
|
|
||||||
|
The server supports multiple transport modes via `MODE` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In-memory mode (testing, no network)
|
||||||
|
MODE=mem python run.py
|
||||||
|
|
||||||
|
# WebTransport mode (HTTP/3, default for production)
|
||||||
|
MODE=wt QUIC_CERT=cert.pem QUIC_KEY=key.pem python run.py
|
||||||
|
|
||||||
|
# QUIC datagram mode only
|
||||||
|
MODE=quic QUIC_CERT=cert.pem QUIC_KEY=key.pem python run.py
|
||||||
|
|
||||||
|
# Combined mode (both WebTransport and QUIC)
|
||||||
|
MODE=net WT_PORT=4433 QUIC_PORT=4443 QUIC_CERT=cert.pem QUIC_KEY=key.pem python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
- `MODE`: Transport mode (mem|wt|quic|net)
|
||||||
|
- `QUIC_CERT` / `QUIC_KEY`: TLS certificate and key paths (required for wt/quic/net)
|
||||||
|
- `WT_CERT` / `WT_KEY`: WebTransport-specific cert/key (falls back to QUIC_* if not set)
|
||||||
|
- `WT_HOST` / `WT_PORT`: WebTransport server host/port (default: 0.0.0.0:4433)
|
||||||
|
- `QUIC_HOST` / `QUIC_PORT`: QUIC server host/port (default: 0.0.0.0:4433, or 4443 in net mode)
|
||||||
|
- `STATIC`: Enable static HTTPS server for client (default: 1 in wt mode)
|
||||||
|
- `STATIC_HOST` / `STATIC_PORT` / `STATIC_ROOT`: Static server settings (default: same as WT host, port 8443, root "client")
|
||||||
|
- `LOG_LEVEL`: Logging level (DEBUG|INFO|WARNING|ERROR)
|
||||||
|
- `RUN_SECONDS`: Optional timeout for server shutdown (testing)
|
||||||
|
|
||||||
|
### Help
|
||||||
|
```bash
|
||||||
|
python run.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Server Architecture (Python 3 + asyncio)
|
||||||
|
|
||||||
|
**Core Components:**
|
||||||
|
|
||||||
|
1. **server/server.py** (`GameServer`): Authoritative game server
|
||||||
|
- Fixed tick rate simulation (default 10 TPS, configurable 5-30)
|
||||||
|
- Manages player sessions, snakes, apples, collision detection
|
||||||
|
- Handles join/spawn logic, input processing, state broadcasting
|
||||||
|
- Automatically partitions large updates across multiple datagrams (<1200 bytes each to avoid IP fragmentation)
|
||||||
|
|
||||||
|
2. **server/model.py**: Game state entities
|
||||||
|
- `GameState`: Field grid, snakes dict, apples list, occupancy map
|
||||||
|
- `Snake`: Deque-based body representation, input buffer (capacity 3), blocked flag
|
||||||
|
- `PlayerSession`: Per-player metadata (id, name, color, peer handle)
|
||||||
|
|
||||||
|
3. **server/protocol.py**: Wire protocol implementation
|
||||||
|
- Packet types: JOIN, JOIN_ACK, JOIN_DENY, INPUT, INPUT_BROADCAST, STATE_DELTA, STATE_FULL, PART, CONFIG_UPDATE
|
||||||
|
- TLV body encoding: 2-bit direction streams, RLE compression, chunked variants for oversized snakes
|
||||||
|
- QUIC varint encoding for efficient integer serialization
|
||||||
|
- Sequence number handling with wraparound logic (16-bit)
|
||||||
|
|
||||||
|
4. **server/config.py** (`ServerConfig`): Server configuration dataclass with validation
|
||||||
|
|
||||||
|
5. **Transport layer** (pluggable):
|
||||||
|
- `server/transport.py`: Abstract `DatagramServerTransport` interface
|
||||||
|
- `server/webtransport_server.py`: WebTransport (HTTP/3) implementation using aioquic
|
||||||
|
- `server/quic_transport.py`: Raw QUIC datagram implementation
|
||||||
|
- `server/multi_transport.py`: Multiplexer for running multiple transports concurrently
|
||||||
|
- `server/static_server.py`: Optional HTTPS static file server for serving client assets
|
||||||
|
|
||||||
|
### Client Architecture (Vanilla JavaScript)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `client/index.html`: Main HTML page with canvas and UI controls
|
||||||
|
- `client/client.js`: Game client, WebTransport connection, input handling, rendering
|
||||||
|
- `client/protocol.js`: Wire protocol parsing/building (mirrors server protocol)
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- WebTransport datagram API for unreliable, unordered messaging
|
||||||
|
- Canvas-based rendering with auto-scaling grid
|
||||||
|
- Input buffering (up to 3 direction changes) with 180° turn filtering
|
||||||
|
- State update handling: STATE_FULL (initial/recovery), STATE_DELTA (per-tick), PART (fragmented)
|
||||||
|
- Packet sequence tracking to discard late/out-of-order updates
|
||||||
|
|
||||||
|
### Game Mechanics (Unique Collision System)
|
||||||
|
|
||||||
|
**No Death on Collision:**
|
||||||
|
- When a snake's head hits an obstacle (wall, self, or another snake), the head stays in place (blocked state)
|
||||||
|
- While blocked, the tail shrinks by 1 cell per tick until the player turns to a free direction
|
||||||
|
- Minimum length is 1 (head only); snake cannot disappear while player is connected
|
||||||
|
|
||||||
|
**Input Buffer Rules:**
|
||||||
|
- Client-side buffer holds up to 3 upcoming direction changes
|
||||||
|
- Opposite direction to last buffered input replaces the last entry (not appended)
|
||||||
|
- Consecutive duplicates are dropped
|
||||||
|
- Overflow policy: replace last element
|
||||||
|
- Server consumption: At each tick, consume at most one input; skip 180° turns when length > 1
|
||||||
|
|
||||||
|
**Apples & Scoring:**
|
||||||
|
- Displayed as current snake length (not separate score)
|
||||||
|
- Eating an apple grows snake by 1 immediately
|
||||||
|
- Target apple count: max(3, min(apples_cap, connected_players × apples_per_snake))
|
||||||
|
- When 0 players remain, pre-populate field with exactly 3 apples
|
||||||
|
|
||||||
|
**Spawning:**
|
||||||
|
- Try to allocate a 3-cell straight strip in a free direction
|
||||||
|
- Fallback: spawn length 1 (head only) at any free cell
|
||||||
|
- Deny join if no free cells available
|
||||||
|
|
||||||
|
### Protocol Details
|
||||||
|
|
||||||
|
**Packet Structure:**
|
||||||
|
```
|
||||||
|
Header (all packets):
|
||||||
|
ver (u8) | type (u8) | flags (u8) | seq (u16 big-endian) | [tick (u16 optional)]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sequence Numbers:**
|
||||||
|
- 16-bit wraparound with half-range window comparison: `is_newer(a,b) = ((a-b) & 0xFFFF) < 0x8000`
|
||||||
|
- Clients discard packets with older seq than last applied per sender
|
||||||
|
|
||||||
|
**State Encoding:**
|
||||||
|
- **STATE_FULL**: Complete snapshot (on join, periodic recovery); includes all snakes + apples
|
||||||
|
- Per-snake: id (u8), len (u16), head (x,y), body TLV (BODY_2BIT or BODY_RLE)
|
||||||
|
- **STATE_DELTA**: Incremental update (per tick); includes only changed snakes + apple diffs
|
||||||
|
- Per-change: snake_id, flags (head_moved|tail_removed|grew|blocked), direction, new_head coords
|
||||||
|
- **PART**: Multi-part fragmentation for large updates (>1200 bytes)
|
||||||
|
- Fields: update_id (u16), part_index, parts_total, inner_type (STATE_FULL or STATE_DELTA), chunk_payload
|
||||||
|
|
||||||
|
**Body TLV Types:**
|
||||||
|
- `BODY_2BIT (0x00)`: 2-bit direction stream (up=00, right=01, down=10, left=11), packed LSB-first
|
||||||
|
- `BODY_RLE (0x01)`: Run-length encoding (dir u8, count QUIC varint) for straight segments
|
||||||
|
- `BODY_2BIT_CHUNK (0x10)` / `BODY_RLE_CHUNK (0x11)`: Chunked variants for splitting oversized snakes across parts
|
||||||
|
- Chunk header: start_index (u16), dirs_in_chunk (u16)
|
||||||
|
|
||||||
|
**Input Broadcasting:**
|
||||||
|
- Server relays client inputs immediately to all other clients as INPUT_BROADCAST packets
|
||||||
|
- Enables client-side opponent prediction during late/lost state updates
|
||||||
|
- Fields: player_id, input_seq, base_tick, events (rel_tick_offset + direction), optional apply_at_tick
|
||||||
|
|
||||||
|
**Partitioning Strategy:**
|
||||||
|
- Soft limit: 1200 bytes per datagram (avoid fragmentation; MTU ~1280-1500)
|
||||||
|
- Whole-snake-first: pack complete snakes greedily; if a single snake exceeds budget, split using chunked TLV
|
||||||
|
- Apples included only in first part; clients merge across parts using update_id
|
||||||
|
|
||||||
|
## Key Simulation Details
|
||||||
|
|
||||||
|
**Tick Order:**
|
||||||
|
1. Consume at most one valid input per snake (apply 180° rule for length > 1)
|
||||||
|
2. Compute intended head step; if blocked (wall/occupied), set `blocked=True` and keep head stationary
|
||||||
|
3. If blocked: shrink tail by 1 (min length 1)
|
||||||
|
4. If moving into apple: grow by 1 (don't shrink tail that tick) and respawn apple
|
||||||
|
5. Emit per-snake delta and global apple changes
|
||||||
|
|
||||||
|
**Collision Detection:**
|
||||||
|
- Occupancy map tracks all snake cells as (coord) -> (snake_id, index)
|
||||||
|
- Allow moving into own tail if it will vacate (i.e., not growing)
|
||||||
|
- Walls block only when `wrap_edges=False`; wrapped coordinates computed by `_step_from()`
|
||||||
|
|
||||||
|
**180° Turn Handling:**
|
||||||
|
- Checked at consumption time using XOR: `(int(new_dir) ^ int(current_dir)) == 2`
|
||||||
|
- Allowed only when snake length == 1 (head only)
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
- **Testing**: No automated test suite currently exists. Manual testing via in-memory mode (`MODE=mem`) or live WebTransport server.
|
||||||
|
- **Logging**: Use `LOG_LEVEL=DEBUG` for verbose packet-level debugging.
|
||||||
|
- **Field Size**: Default 60×40; protocol supports 3×3 to 255×255 (negotiated in JOIN_ACK).
|
||||||
|
- **Players**: Max 32 concurrent (ids 0-31); names truncated to 16 UTF-8 bytes.
|
||||||
|
- **Colors**: 32-color palette; deterministic mapping from player_id to color_id (0-31).
|
||||||
|
- **Tick Rate Changes**: Server broadcasts CONFIG_UPDATE every 50 ticks; clients apply at next tick boundary.
|
||||||
|
- **Compression**: Optional global DEFLATE mode (handshake-only; requires server restart). Currently defaults to "none".
|
||||||
|
- **Browser Compatibility**: Requires WebTransport API support (latest Firefox/Chrome).
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
**Adding a New Packet Type:**
|
||||||
|
1. Add enum value to `PacketType` in server/protocol.py and client/protocol.js
|
||||||
|
2. Implement builder function in protocol (e.g., `build_foo()`) and parser (e.g., `parse_foo()`)
|
||||||
|
3. Handle in server's `on_datagram()` and client's `readLoop()` switch statements
|
||||||
|
4. Update header packing if tick field is required (pass tick to `pack_header()`)
|
||||||
|
|
||||||
|
**Modifying Simulation Logic:**
|
||||||
|
1. Edit `_simulate_tick()` in server/server.py for per-tick updates
|
||||||
|
2. Update `SnakeDelta` dataclass if new change types are needed
|
||||||
|
3. Adjust `build_state_delta_body()` if encoding changes
|
||||||
|
4. Mirror changes in client rendering logic (client/client.js)
|
||||||
|
|
||||||
|
**Changing Configuration:**
|
||||||
|
1. Update `ServerConfig` dataclass in server/config.py
|
||||||
|
2. Add runtime validation in `validate_runtime()`
|
||||||
|
3. If live-configurable: broadcast via CONFIG_UPDATE packet; otherwise require server restart
|
||||||
|
4. Update JOIN_ACK builder/parser if part of handshake
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
G:\Coding\code2\
|
||||||
|
├── run.py # Server entrypoint with mode switching
|
||||||
|
├── requirements.txt # Python dependencies (aioquic, cryptography)
|
||||||
|
├── PROJECT_PLAN.md # Detailed design spec (authoritative reference)
|
||||||
|
├── IDEAS.md # Original feature brainstorming
|
||||||
|
├── server/
|
||||||
|
│ ├── server.py # GameServer (tick loop, join/spawn, simulation)
|
||||||
|
│ ├── model.py # GameState, Snake, PlayerSession entities
|
||||||
|
│ ├── protocol.py # Wire protocol (packet builders/parsers, TLV encoding)
|
||||||
|
│ ├── config.py # ServerConfig dataclass
|
||||||
|
│ ├── transport.py # Abstract DatagramServerTransport interface
|
||||||
|
│ ├── webtransport_server.py # HTTP/3 WebTransport implementation
|
||||||
|
│ ├── quic_transport.py # QUIC datagram implementation
|
||||||
|
│ ├── multi_transport.py # Multi-transport multiplexer
|
||||||
|
│ ├── static_server.py # Optional HTTPS static file server
|
||||||
|
│ └── utils.py # Utility functions
|
||||||
|
└── client/
|
||||||
|
├── index.html # Main HTML page
|
||||||
|
├── client.js # Game client (WebTransport, input, rendering)
|
||||||
|
└── protocol.js # Wire protocol (mirrors server protocol.py)
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **PROJECT_PLAN.md**: Comprehensive design document covering protocol, mechanics, limits, testing strategy, and milestones.
|
||||||
|
- **aioquic**: Python QUIC and HTTP/3 library (https://github.com/aiortc/aioquic)
|
||||||
|
- **WebTransport**: W3C spec for low-latency client-server messaging over QUIC (https://w3c.github.io/webtransport/)
|
||||||
@@ -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 = {
|
const ui = {
|
||||||
url: document.getElementById('url'),
|
url: document.getElementById('url'),
|
||||||
@@ -109,7 +109,79 @@ async function readLoop() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PacketType.STATE_DELTA: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
default: break;
|
default: break;
|
||||||
|
|||||||
@@ -158,3 +158,57 @@ export function parseStateFullBody(dv, off) {
|
|||||||
return { snakes, apples, off };
|
return { snakes, apples, off };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseStateDeltaBody(dv, off) {
|
||||||
|
// Parse STATE_DELTA body format (mirrors server/protocol.py build_state_delta_body)
|
||||||
|
// update_id (u16)
|
||||||
|
const updateId = dv.getUint16(off); off += 2;
|
||||||
|
|
||||||
|
// changes count (QUIC varint)
|
||||||
|
let [changesCount, p1] = quicVarintDecode(dv, off); off = p1;
|
||||||
|
|
||||||
|
const changes = [];
|
||||||
|
for (let i = 0; i < changesCount; i++) {
|
||||||
|
const snakeId = dv.getUint8(off); off += 1;
|
||||||
|
const flags = dv.getUint8(off); off += 1;
|
||||||
|
const direction = dv.getUint8(off) & 0x03; off += 1;
|
||||||
|
|
||||||
|
const headMoved = (flags & 0x01) !== 0;
|
||||||
|
const tailRemoved = (flags & 0x02) !== 0;
|
||||||
|
const grew = (flags & 0x04) !== 0;
|
||||||
|
const blocked = (flags & 0x08) !== 0;
|
||||||
|
|
||||||
|
let newHeadX = 0, newHeadY = 0;
|
||||||
|
if (headMoved) {
|
||||||
|
newHeadX = dv.getUint8(off); off += 1;
|
||||||
|
newHeadY = dv.getUint8(off); off += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.push({
|
||||||
|
snakeId,
|
||||||
|
headMoved,
|
||||||
|
tailRemoved,
|
||||||
|
grew,
|
||||||
|
blocked,
|
||||||
|
direction,
|
||||||
|
newHeadX,
|
||||||
|
newHeadY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// apples added (count + coords)
|
||||||
|
let [applesAddedCount, p2] = quicVarintDecode(dv, off); off = p2;
|
||||||
|
const applesAdded = [];
|
||||||
|
for (let i = 0; i < applesAddedCount; i++) {
|
||||||
|
applesAdded.push([dv.getUint8(off++), dv.getUint8(off++)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apples removed (count + coords)
|
||||||
|
let [applesRemovedCount, p3] = quicVarintDecode(dv, off); off = p3;
|
||||||
|
const applesRemoved = [];
|
||||||
|
for (let i = 0; i < applesRemovedCount; i++) {
|
||||||
|
applesRemoved.push([dv.getUint8(off++), dv.getUint8(off++)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { updateId, changes, applesAdded, applesRemoved, off };
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -571,6 +571,33 @@ class GameServer:
|
|||||||
if player_id is None:
|
if player_id is None:
|
||||||
return
|
return
|
||||||
logging.debug("INPUT from player_id=%d: %d events (base_tick=%d)", player_id, len(events), base_tick)
|
logging.debug("INPUT from player_id=%d: %d events (base_tick=%d)", player_id, len(events), base_tick)
|
||||||
|
|
||||||
|
# Apply inputs to snake buffer (client-side filtering already applied)
|
||||||
|
snake = self.runtime.state.snakes.get(player_id)
|
||||||
|
if snake is not None:
|
||||||
|
for ev in events:
|
||||||
|
# Apply input buffer rules from project plan:
|
||||||
|
# - Max capacity 3
|
||||||
|
# - If opposite to last buffered, replace last
|
||||||
|
# - Drop duplicates
|
||||||
|
# - Overflow: replace last
|
||||||
|
if len(snake.input_buf) > 0:
|
||||||
|
last_dir = snake.input_buf[-1]
|
||||||
|
# Check if opposite (XOR == 2)
|
||||||
|
if (int(ev.direction) ^ int(last_dir)) == 2:
|
||||||
|
# Replace last
|
||||||
|
snake.input_buf[-1] = ev.direction
|
||||||
|
continue
|
||||||
|
# Check if duplicate
|
||||||
|
if ev.direction == last_dir:
|
||||||
|
continue
|
||||||
|
# Add to buffer
|
||||||
|
if len(snake.input_buf) < 3:
|
||||||
|
snake.input_buf.append(ev.direction)
|
||||||
|
else:
|
||||||
|
# Overflow: replace last
|
||||||
|
snake.input_buf[-1] = ev.direction
|
||||||
|
|
||||||
# Relay to others immediately for prediction
|
# Relay to others immediately for prediction
|
||||||
await self.relay_input_broadcast(
|
await self.relay_input_broadcast(
|
||||||
from_player_id=player_id,
|
from_player_id=player_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user