Play it live: jammers.dilger.dev — open on a big screen, players join by scanning the QR with their phones. No installs.
Your phone is the controller. Your TV is the arena. Race, ram, and wreck your friends — straight from the browser.
Open on a big screen → players scan the QR → race or wreck. That's it.
Play Live • Quick Start • Features • How It Works • Development • Roadmap
Joystick Jammers is a Jackbox/Kahoot-style couch party game. The action plays out on a shared big screen (TV, projector, or laptop), and everyone joins with the device already in their pocket — their phone becomes the controller. No app store, no extra hardware, no installs.
Two ways to play:
- 🏁 Race — weaponised laps around procedurally generated tracks. Grab pickups, take the racing line, leave your friends in the dust.
- 💥 Demolition Derby — last car standing. The arena shrinks, weapons escalate, and chaos compounds. Best of 3 rounds.
Perfect for party nights, living-room gaming, and questionable driving decisions.
| 📱 Phone as controller | Touch joystick + buttons. Just scan and play. |
| 📺 Big-screen arena | 3D action with bloom, particles, and a chase cam. |
| 🔗 One-tap join | QR code or a 4-letter room code. |
| 💣 8 weapons & pickups | Missile, Mine, Boost, Oil Slick, Sniper, Shield, EMP, Flamethrower. |
| 🌍 Procedural arenas | New tracks and terrain every game. |
| 👥 Built for a crowd | Plenty of players, one screen, total mayhem. |
- Python 3.11+ with pip
- Node.js 20+ with npm
- A modern browser with WebGL
git clone https://github.com/cdilga/multiplayer-racer.git
cd multiplayer-racer
pip install -r requirements.txt # Python deps
npm install # Node deps
npm run build # Build the frontend into dist/python server/app.pyOpen http://localhost:8000 — you'll land on the start screen.
- Host Now → the big-screen host (
/host) - Join Game → the phone controller (
/player)
💡 Dev tip: append
?dev=1to the landing URL (http://localhost:8000/?dev=1) to skip straight to the host every time — handy for rapid iteration. Use?dev=0to turn the bypass back off.
- Host opens the game on a big screen and clicks Host Now.
- Players scan the QR code (or type the 4-letter room code) on their phones.
- Everyone picks a name and lands in the lobby.
- Host chooses Race or Derby and starts the game.
- Race — configurable laps, weapon pickups, lap timing, live positions.
- Demolition Derby — last-car-standing elimination, best-of-3 rounds, a shrinking arena, and weapons that escalate as the match heats up.
Eight pickups across rarity tiers — Missile, Mine, Boost, Oil Slick, Sniper, Shield, EMP, Flamethrower — with rarer drops appearing later in a match. Grab one, hold it, fire at the perfect moment.
- Rapier 3D vehicle physics (WASM) with collision damage and destruction.
- Three.js rendering with bloom, fog, particles, trails, and a dynamic chase camera.
- Procedural tracks + terrain so no two arenas feel the same.
- Touch joystick for steering, thumb-friendly accelerate/brake, and a fire button in combat.
- Full-screen support and a responsive layout tuned for phones.
- One-tap Reset My Car escape hatch when you get stuck or flipped.
- Multiple music tracks for different phases, synthesised engine sound, and SFX with ducking so effects cut through.
- A Report a Bug button in both the host and player menus captures a screenshot plus a game-state snapshot (room code, mode, players, FPS) and opens a pre-filled email — so reports can be matched to server logs.
| Key | Action |
|---|---|
D |
Toggle debug info |
F2 |
Physics tuning panel |
F3 |
Stats overlay (FPS, players, state) |
F4 |
Physics debug visualisation |
flowchart TB
subgraph HOST["🖥️ HOST — big screen (/host)"]
direction LR
L["📋 Lobby<br/>QR + Players"]
R["🏎️ Race / 💥 Derby<br/>Physics + Rendering"]
E["🏆 Results"]
L --> R --> E
end
subgraph SERVER["⚡ Flask + Socket.IO"]
RM["Rooms"]
PS["Player Sync"]
CR["Control Routing"]
QR["QR Generation"]
end
subgraph PLAYERS["📱 Phone controllers (/player)"]
P1["Player 1"]
P2["Player 2"]
PN["Player N"]
end
HOST <-->|"WebSocket"| SERVER
SERVER <-->|"WebSocket"| PLAYERS
The host renders the 3D world and runs the physics; phones stream control input over WebSockets. A lightweight landing page at / is the shareable front door and routes players to the right screen.
| Layer | Technology |
|---|---|
| Frontend | Three.js, vanilla JS (ES modules), CSS |
| Physics | Rapier 3D (WASM) |
| Backend | Flask + Flask-SocketIO |
| Real-time | Socket.IO |
| Build | Vite (landing / host / player entry points) |
| Testing | Vitest (unit/integration) + Playwright (E2E) |
| Deploy | Docker + Cloudflare Tunnel (jammers.dilger.dev) |
multiplayer-racer/
├── server/app.py # Flask + Socket.IO (rooms, QR, routes: / /host /player)
├── frontend/
│ ├── landing/ # Marketing landing page (Vite entry)
│ ├── host/ # Big-screen host (Vite entry)
│ └── player/ # Phone controller (Vite entry)
├── src/host/main.js # Host bootstrap (loads GameHost)
├── static/
│ ├── js/
│ │ ├── GameHost.js # Host orchestrator
│ │ ├── engine/ # Engine, GameLoop, EventBus, StateMachine
│ │ ├── systems/ # Render, Physics, Network, Race, Derby, Weapons, Audio…
│ │ ├── entities/ # Vehicle, Track
│ │ ├── resources/ # TrackFactory, terrain, procedural generation
│ │ ├── ui/ # LobbyUI, RaceUI, GameMenuUI, BugReportUI…
│ │ ├── input/ # InputManager, TouchController
│ │ └── player.js # Phone controller logic
│ ├── css/ # host.css, player.css, landing.css
│ ├── audio/ # Music & SFX
│ └── og-image.png # Social share image
├── tests/ # Vitest (unit/integration) + Playwright (e2e)
└── docs/images/ # README media
⚠️ The Flask server serves fromdist/when it exists. After changing any JS/CSS, runnpm run buildbefore testing in the browser. See CLAUDE.md.
npm test # unit + integration (Vitest)
npm run test:e2e # core 4-player flow (Playwright)
npm run test:e2e:all # full E2E suite
npm run test:headed # E2E with a visible browserPlayable today:
- Landing page + one-tap QR/room-code join
- Vite-bundled ESM architecture (no CDNs)
- Race mode (laps, pickups, timing)
- Demolition Derby (elimination, shrinking arena, weapon escalation)
- 8 weapons & pickups
- Rapier physics, collision damage & destruction
- Procedural tracks + terrain
- Audio (music, engine synth, SFX)
- In-game bug reporter
- Live deploy at jammers.dilger.dev
Next up:
| Phase | Ideas |
|---|---|
| June 2026 Swarm | Polishing Pass: Wheelie mechanics, boost payoff, player identity/markers, controller reconnect stability, and map/collision fixes. |
| Near term | More tracks & arena hazards, car customisation, spectator polish |
| Later | Public lobbies, persistent stats/leaderboards, more modes |
Contributions welcome — this project follows Test-Driven Development:
- Fork and branch (
git checkout -b feature/amazing-thing) - Write a failing test first
- Implement until it passes
npm run buildand run the suite- Open a PR
See CLAUDE.md for detailed guidelines.
Licensed under the GNU General Public License v3.0 — see LICENSE.
- Three.js — 3D graphics
- Rapier — physics
- Flask & Socket.IO — backend & real-time
- Playwright & Vitest — testing
Made for couches, parties, and questionable driving decisions.