A browser-playable, extraction shooter. Built to understand competitive-shooter netcode from the engineer's side (peek mechanics)
64Hz authoritative simulation · client-side prediction & reconciliation · lag-compensated hit registration · delta-compressed snapshots · a measured peeker's-advantage window you can tune live in the browser.
rl/ trains reinforcement-learning agents to play Exfil raids on the
actual deterministic simulation the game ships. The Python movement port is
validated bit-exact against the cross-language conformance corpus (2000
ticks, every FNV-1a world hash matches the Rust sim), so the agents learn on the
real maze and physics. A PPO agent trained with a self-play curriculum
(random -> scripted -> a mixed opponent pool) learns line-of-sight hitscan
combat and extraction-hold play, reaching a 100% win rate vs random (by
extraction) and vs a scripted aggressor (by elimination). See
rl/README.md.
The trained policy is the enemy AI. It is exported to a small JSON
(server/internal/scav/policy.json) and run natively in the Go server (three
matmuls + argmax, no Python at runtime) to drive the in-game scavs. A
server-side vision + memory layer sits on top of the learned
navigate-and-shoot behavior: a scav acquires you only on line of sight (no
wallhack), tracks your last-known position for ~5s after you break it, then
gives up and returns to patrolling the room it spawned in. That
corner-anchored roaming keeps the central spawn clear, and a few seconds of
spawn protection mean you never load straight into a crossfire.
I used to play Tarkov, and like everyone who plays it I spent a lot of time complaining about "peeker's advantage." I have this distinct memory of sitting in a market corner thinking I was safe to heal and getting bum-rushed, only to have 0 time to shoot back. I wanted to actually understand the tradeoffs competitive shooters make by building the netcode myself. Exfil is a top-down extraction shooter that's built to make tradeoffs visible.
You spawn into a neon maze with a four-weapon loadout. Fight or dodge the AI scavs, then reach a green extraction zone and hold it for three seconds to get out alive. The whole thing runs in the browser against a real authoritative game server; the right sidebar is a live readout of the netcode doing its job.
| Input | Action |
|---|---|
| WASD | move |
| Mouse | aim |
| Left click | shoot (hold for the auto rifle) |
| 1 – 4 | switch weapon |
| E / F | grab loot |
You carry all four from the start (press 1–4); each is a distinct, server-authoritative weapon:
| # | Weapon | Damage | Range | Fire |
|---|---|---|---|---|
| 1 | Pistol | 25 | medium | semi-auto, punchy |
| 2 | Auto Rifle | 14 | medium-long | full-auto (hold to fire) |
| 3 | Sniper | 90 | long | slow, near one-shot |
| 4 | Shotgun | 12 × 7 pellets | short | spread — devastating up close |
Shots are validated on the server and blocked by walls. This type of situation is where the netcode matters.
This is a distributed-systems / real-time-netcode project dressed up as a game. The interesting parts:
- Authoritative server — Go, deterministic simulation at a fixed 64Hz tick, per-client input queues. The server is the single source of truth.
- Cross-language determinism — the simulation is implemented twice (Rust and Go) against a written contract (
protocol/SIMULATION.md), and CI replays a shared trace to prove both produce identical per-tick world hashes. Fixed-point math, seeded PCG32 RNG, no floats in the sim. - Client-side prediction + reconciliation — the Rust client (compiled to WebAssembly) applies your input immediately, then replays unacknowledged inputs from the server's corrected state.
- Lag compensation — the server keeps a ~1s ring buffer of past world states and rewinds to the shooter's view tick to validate hits (favor-the-shooter), with line-of-sight checks against cover.
- Server-side AI — the scavs are driven by the self-play PPO policy (exported, run natively in Go), with a vision + memory layer that acquires you only on line of sight, tracks your last-known position briefly, then returns to patrolling. They path around walls (BFS) and hold fire without line of sight.
- Bandwidth engineering — delta-compressed snapshots (only what changed) + interest management (you only receive entities you can see/hear).
- The headline: the peeker's-advantage window is measured, not hand-waved — and you can move it.
The in-game HUD shows live ping, server tick, prediction error, packets/sec, and the peeker's-advantage window. Two sliders let you inject simulated latency and tune the interpolation buffer in real time — and watch the peeker's-advantage number move as you trade smoothness against fairness.
Reach a green zone and a server-authoritative hold timer counts you down to extraction:
The repo includes a headless verification harness (bot clients with simulated latency) that measures, rather than asserts:
- 64Hz tick sustained with zero tick drops over a match.
- Sub-1KB delta snapshots (P95 ~93 bytes at 6 players with interest management).
- Lag-compensated hit registration validated across 50/100/150 ms simulated pings.
- Peeker's-advantage window swept across interpolation depth (the gold interview material), persisted to Postgres telemetry.
| Layer | Tech |
|---|---|
| Server simulation | Go — fixed 64Hz authoritative loop, lag-comp ring buffer, anti-cheat (input/shot validation, rate limiting) |
| Deterministic sim | Rust crate (shared) + Go mirror, Q16.16 fixed-point, conformance-tested |
| Client | Rust → WebAssembly (prediction, reconciliation, interpolation) |
| Rendering | Three.js neon-noir 3D (bloom, tracers, damage numbers, kill feed) |
| Audio | procedural Web Audio synthesis — per-weapon SFX, scav alerts, hit/extract cues, ambient pad (no asset files) |
| Frontend shell | TypeScript + React + Vite |
| Protocol | custom binary WebSocket protocol, delta-compressed |
| Persistence / infra | Postgres (stash, telemetry), Redis matchmaker, Docker, Prometheus + Grafana |
| CI | GitHub Actions — builds WASM, runs Go (-race) + Rust + web tests, cross-language conformance |
# 1. Game server (standalone — no DB needed for the core loop)
cd server && go run ./cmd/exfil-server # listens on :8080
# 2. Web client (in another terminal)
cd web && npm install && npm run dev # serves http://localhost:5173Open http://localhost:5173, click Queue for raid, and play. (Turn your sound on.)
make build # build the sim crate + Go server
make test # Go (-race) + Rust + cross-language conformanceThe verification harness — cd server && go run ./cmd/harness — prints the measured snapshot sizes, hit-registration accuracy across ping ranges, and the peeker's-advantage sweep.
Exfil is under active development. Next up:
- Real loot — functional items you pick up and use.
- Loot persistence — a Tarkov-style stash that survives across raids (extract to keep it, die and lose it), backed by Postgres.
- Multiple maps — more arenas with distinct layouts.
- Deploy-ready: the server packages as a distroless Docker image for Fly.io; the web client is a static build for Cloudflare Pages. Dockerfiles,
fly.toml, and Cloudflare config are checked in and build-verified — seedeploy/README.md. - Design docs: the deterministic-simulation contract is documented in
protocol/SIMULATION.md.



