Skip to content

NeilP211/exfil

Repository files navigation

Exfil

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.

Exfil gameplay — fighting through the neon maze with the live netcode HUD: simulated latency cranked up, the peeker's-advantage window climbing, weapon switching, and an extraction zone.


Self-play RL on the real sim NEW

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.

Inspo

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.

The game

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.

Overview — the neon maze of rooms and corridors, AI scavs, the loadout bar, and the live netcode sidebar (ping, tick rate, prediction error, peeker's-advantage) with the kill feed up top.

Controls

Input Action
WASD move
Mouse aim
Left click shoot (hold for the auto rifle)
1 – 4 switch weapon
E / F grab loot

Loadout

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.

What it demonstrates

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.

Feel the netcode yourself

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.

Netcode HUD in action — with 80 ms of simulated latency injected, ping reads ~99 ms, prediction error goes non-zero as the client reconciles, and the peeker's-advantage window has climbed to ~174 ms.

Reach a green zone and a server-authoritative hold timer counts you down to extraction:

Standing on a green extraction zone with the shotgun equipped, a scav closing in, while the netcode HUD keeps ticking.

Measured results

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.

Tech stack

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

Run it locally

# 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:5173

Open 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 conformance

The 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.

Notes

  • 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 — see deploy/README.md.
  • Design docs: the deterministic-simulation contract is documented in protocol/SIMULATION.md.

About

Browser-playable authoritative-server extraction shooter exploring competitive-shooter netcode: 64Hz tick, client prediction, lag compensation, and a measured peeker's-advantage window. Go + Rust/WASM + TypeScript.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages