Quake-style team deathmatch in your browser, in one 10 MB Go binary. BLUE versus GREEN, forever. The server, the bots, and the whole Tron-flavored Three.js client are baked into a single executable, so there is nothing to build and nothing for players to install.
Play it live: tinyarena.online — the bots are already fighting; they don't wait for you.
go build -o tiny-arena-server . && ./tiny-arena-server
# open http://localhost:3377Or without Go, straight from the registry:
docker run -p 3377:3377 ghcr.io/tpoxa/tinyarenaColleagues on the same network: give them http://<your-lan-ip>:3377. That's the whole deployment story.
Env: PORT (default 3377), BOTS (default 3), MAPS (rotation order, default neon-yard,circuit,vertigo,nexus), MAP (pin a single map, no rotation), MATCH_SECONDS (default 480), DEV=1 serves public/ and shared/ from disk so client edits apply on refresh.
Four maps ship in the binary, and the server rotates to the next one after every match — clients rebuild the world in place, mid-session, without a page reload. neon-yard is the classic yard with side platforms. circuit is a ring around a void pit: a mega-health island reached by two narrow bridges, four corner platforms fed by diagonal jump pads, and a teleporter out when the island gets too warm. vertigo goes up instead of out: four corner platforms around a high central tower, pad chains to climb, and the quad at the very top for whoever dares to hold it. nexus is a solid-floor arena with four pad-fed corner sniper nests and the quad sitting exposed in a ring of pillar cover — grab it and everyone comes for you.
Every match is team deathmatch — there is no other mode. Pick a side at the join screen (it shows the live headcount of each team) and your color follows you everywhere: the ring under your feet, your name tag, the killfeed, the scoreboard. First team to 25 points wins the round; if the eight-minute clock runs out first, the leading team takes it, and a dead-even score is an honest draw. Friendly fire is on at half damage, and a teamkill costs you a frag and your team a point — aim your rockets with love.
Bots keep the teams honest: they always join the smaller side, they never shoot their own, and the roster rotates itself — every few minutes one wanders off and a new body joins.
Private rooms. The front page has a create a private room button that drops you at tinyarena.online/r/<code> with your own isolated arena and a link to send — grab a colleague and settle it. A fresh room comes seeded with a couple of bots so you're never alone, and it tears itself down once the last human leaves. The public arena at /play is the shared free-for-all. Under the hood the server is a room hub: the public game is the empty-code room, and each /r/<code> is its own game loop with its own map rotation and match clock.
Pick a body — rubber duck, pizza slice, Christmas tree, retro PC, and eleven more low-poly desk heroes. Bots pick their own.
- WASD + mouse, SPACE jump (hold it for auto-hop)
- Four weapons — machinegun, rockets, railgun, shotgun. Switch with 1/2/3/4, the wheel, or Q/E/right-click/C. The shotgun near-one-shots point-blank and does nothing at range; the railgun is a precise beam; rockets throw people off the map.
- TAB scoreboard. B adds a bot to your team, Shift+B to the enemy; N / Shift+N remove one from your / the enemy team (max 8)
- Rocket-jump: fire at your feet mid-jump. Knockback is real, and the void keeps score.
- QUAD DAMAGE spawns on a high platform every 60 s: 3× damage for 20 s, lost on death, and everyone gets told you have it
- Fast frags stack DOUBLE / TRIPLE / MULTI / MONSTER KILL; staying alive stacks KILLING SPREE (5), RAMPAGE (8), GODLIKE (12)
- You feel every hit: a red pulse when you take damage, an arc pointing at whoever shot you, and a heartbeat glow when you're low. Each pickup has its own sound, so you know what you grabbed without looking.
- Death matters: the camera pulls out behind you while your body bursts into cubes that ride the killing blow
- Bots have a human-like reaction delay and a random skill per bot — some are crazy-sharp, some are lame — so peeking and strafing beat them. They ride the jump pads, strafe-weave in firefights, snipe with the railgun, and run for health when hurt.
- Bots are named after their bodies: when QUACKERS rockets you off the map, that was the rubber duck. Watch out for FROSTY, SLICE, BOO, DUSTER, and the rest of the cast
Aim at someone to see their name. Yours is on the HUD.
The Go server owns the truth. It runs the simulation at 30 Hz and sends snapshots at 20 Hz, all as plain JSON WebSocket messages you can read with wscat. Movement is client-predicted (Quake-style ground friction, air control, bunny-hop), but every shot is validated and resolved server-side: hitscan and rockets ray-march against player spheres and the map's AABBs, so a laggy client can't invent hits. Remote players render about 120 ms in the past and interpolate between snapshots.
main.go— WebSocket plumbing; one goroutine owns all game state, connections talk to it via channelsgame.go— teams, combat, pickups, streaks, quad, the match clock, map rotation, snapshotsbots.go— bots walk a line-of-sight waypoint graph (layer-aware, with walkability checks so nobody strolls into a void pit), ride jump pads up to the platforms, and switch to pure server-side ballistics when a rocket sends them flyingarena.go— map loading + AABB raycastsshared/maps/*.json— each map is one JSON file (geometry, weapons, balance, nav graph) read by both Go and JS; the server serves the active one to the browserpublic/— the client: Three.js scene, prediction, HUD, procedural WebAudio sound (zero asset files), and the model factory (public/js/models.js)
The player bodies are built from Three.js primitives at runtime; there are no model files in the repo. Sounds work the same way: every effect is synthesized from oscillators and filtered noise. The only vendored dependency is Three.js.
PORT=3388 BOTS=0 ./tiny-arena-server & # protocol smoke test needs a bot-free server
node server/smoke.jsserver/smoke.js drives two WebSocket clients through join → shoot → frag → respawn against a real server.
Every push to main builds a distroless image (with the smoke test as a gate), tags it main-<sha>-<timestamp>, and pushes it to ghcr. FluxCD on a GKE cluster watches the registry, bumps the tag in its GitOps repo, and rolls the deployment — single replica with a Recreate strategy, because the game world lives in one process's memory. cert-manager handles TLS, and nginx-ingress proxies the websockets. Nobody deploys anything by hand.
This game was designed, written, debugged, and screenshot-tested by Claude Code across a handful of sessions, steered by one human with opinions about how rocket jumps should feel. The bugs were also written by Claude Code, to be fair. Then found and fixed by it, using headless-browser screenshots and WebSocket protocol tests it wrote for itself.
MIT — see LICENSE.










0 comments
log in to comment.