A browser team tank battle in the spirit of Battle City β destructible terrain, power-ups, and a flag you have to defend.
Blue against red, as many tanks a side as the map puts spawn points. Bring friends over a 6-character room code, hand a second player the arrow keys on your own machine, or take on a team of bots β every slot nobody claims is filled by one. Runs in any modern browser, desktop or phone, with no server, no install and no account.
Status: playable prototype. A full local match works today: menu β room β a live match with terrain, bonuses, flags and bots, plus a level editor and a headless test suite (
npm test). Multiplayer (PeerJS) is implemented but not yet verified across two real devices over the internet; the touch controls are verified in an emulated phone browser, not yet on real hardware. SPEC.md indexes the authoritative design; Β§13 Plan lists what's left.
| π§© Teams sized by the map | The roster is the map's spawn count, not a fixed number β a 1v1 test arena and a 16-a-side brawl are the same engine, and every free slot is filled by a bot |
| βοΈ Team battle with an objective | Destroy the enemy flag to take the round, or grind the enemy out of respawns before the clock runs out |
| π Played as a series | A match is best-of: rounds run back to back on rebuilt terrain, and the first team to the room's win target (1β10, default 5) takes it. No draws β a round the clock ran out on goes to whoever has lives left, or got deepest into the enemy base |
| π€ Bots on every free slot | Easy / Medium / Hard / Extreme, set per bot or for the whole roster β genuinely different tactics, not just better aim |
| π§± Destructible terrain | Brick crumbles cell by cell, steel resists until you're upgraded, forest conceals, water stops tanks but not bullets, ice slides, sand slows |
| β¨ Eight power-ups | Four are yours, four swing the whole team |
| π Peer-to-peer multiplayer | Join by code or invite link over WebRTC β no server to run, no public IP, no accounts |
| π A room code you can keep | Name your room (SERGEY, DVOR) instead of dictating six characters, and the same invite link works every evening |
| πΉοΈ Two players, one keyboard | WASD and the arrow keys, same team or opposite ones |
| πΊοΈ Six maps + a level editor | Paint your own arena, validate it, play it β and it travels to your guests over the wire |
| π± Phone-ready | Split-screen touch controls and a real fullscreen mode, the whole arena on screen |
Bonuses drop on the battlefield now and then β drive over one to take it. While a bonus is on you, its icon spins around your tank in that bonus's colour, so everyone can see what you picked up.
HELMET STAR SPEED MINE are yours alone; SHOVEL CLOCK GRENADE RESPAWN swing your
whole team. GRENADE is lobbed at the biggest knot of enemies and takes out whoever is still inside
its ring when it lands β at most half their team, with a blast that grows with the map.
- Create a room β pick your nickname, the map and the difficulty its bots start at. You get a code and an invite link. Leave the code field empty for a random one, or type your own (3β12 letters or digits): it's remembered, so the link you send your friends keeps working.
- Set up the match β rounds to win, round time limit, respawns, bonuses per minute and friendly fire are all in the room itself, so you can retune them while people arrive; everyone sees the change.
- Take a slot β click any slot on either team; the preview marks the spawn you would get. Friends join with the code; bots hold everything nobody claimed.
- Fight β the host's browser simulates everything and streams snapshots to the guests at 20 Hz.
- Take the round β blow up the enemy flag, or outlast them on respawns. The score goes up over the frozen battlefield, and three seconds later the next round starts on the same map with the walls rebuilt and the clock back to full.
- Win the match β first team to the room's win target. The result screen states the series
score (
BLUE WINS 5:2), the full match scoreboard, and a way straight back to the room.
| Action | Player 1 | Player 2 (same machine) |
|---|---|---|
| Move | W A S D |
Arrow keys |
| Fire | 1 |
N |
Drop mine (needs the MINE bonus) |
2 |
M |
| Scoreboard | hold Tab |
|
| Menu / pause | Esc |
Hold two direction keys at once to drive diagonally. In the menus, Enter triggers the screen's
primary button. Esc opens the in-match menu β playing on your own that really pauses, with the
simulation stopping dead and picking up where it left off; with other players connected the match
can't be frozen, so it's just a menu over a running game.
On a phone or tablet β the battlefield is split in half and each half is a control
- Movement half (left by default) β put a thumb down anywhere in it and drag. A stick appears under your thumb, wherever that is, and the tank drives that way in any of 8 directions. Lift to stop. Drag far and the stick follows you, so you never run out of travel or have to re-grab.
- Fire half (right by default) β tap anywhere to fire, hold to keep firing. The tank shoots where it faces, so there's nothing to aim and no button to find.
MINEsits in the outer top corner, the only fixed button β out of the way of the firing thumb.- Left-handed? Settings β Movement stick side swaps the two halves and the
MINEbutton with them. - The top bar carries the three buttons a phone needs and a keyboard doesn't: scoreboard, fullscreen, and the pause menu.
Landscape only β portrait shows a rotate prompt.
Fullscreen is how it's meant to be played on a phone: browser chrome eats a fifth of a landscape
screen and its show/hide animation resizes the arena mid-fight. Starting a match on a touch device
goes fullscreen and asks for a landscape orientation lock automatically (Settings β Fullscreen on
match start, on by default). It's also a button in the match top bar and on the main menu β worth
knowing, since a phone has no Esc to get back out with.
Six maps ship with the game β classic, crossroads, swamp, thicket, fortress and
iceworks β chosen by the host in the lobby and previewed live, with your spawn point highlighted,
before the match starts. They don't all point the same way: some face the two bases top and bottom,
thicket and iceworks put them left and right, and fortress buries them in opposite corners.
A map is a plain block of text and carries its own size and roster, anything from 2Γ2 up to
128Γ128, so the team size comes from the map rather than the other way round. Build your own in the
Level Editor: paint terrain with a Paint-like palette, drag to fill rectangles, place flags,
spawns and bonus points, resize the grid around any anchor, undo a gesture at a time, and hit
Test play to drop straight into a match on it. A map is validated before it can be played, so an
unplayable one never reaches a match. Built-in maps are read-only but can be copied. Custom maps live
in localStorage, export and import as text, and travel to your guests over the wire when you host
on one. Sign in with Google in Settings (optional) and your maps follow you to your other
devices.
One player creates a room and gets a code like K7QM2X (or an invite link ?room=K7QM2X). Everyone
else joins with it, picks a slot on either team β the preview shows the map, your spawn point, and
your tank before you commit β and hits ready. Any slot you don't fill is played by a bot, so the
teams are always full whether there are two of you or ten. Clicking another slot moves you there;
clicking your own hands it to a bot and makes you a spectator, and clicking any bot slot puts you
back in.
The room creator hosts the match: their browser runs the authoritative simulation and all the bots. If someone drops, a bot takes their tank over for the rest of the match.
Your lobby setup sticks, too: Create Room reopens on the map you played last, and each map keeps its own time limit, respawn pool, bot difficulty and friendly-fire toggle.
npm install
npm run dev # http://localhost:5173/tanks/
npm run build # type-check + production build into ./dist
npm run preview # serve the build locally
npm run deploy # publish ./dist to the gh-pages branch
npm run maps:check # validate the built-in maps in src/world/maps/
npm test # headless suite β movement, map format, editor, botsvite.config.ts sets base: "/tanks/", the repo subpath GitHub Pages serves from β which is also
why npm run dev opens at http://localhost:5173/tanks/ rather than the bare root. Pushing to
main deploys automatically via .github/workflows/deploy.yml;
npm run deploy publishes from your machine instead. The build is a plain static site β ./dist/
can be hosted anywhere that serves it from /tanks/.
Optional cloud maps. Google sign-in and map sync need a Supabase project; without one the game
simply has no cloud section. To enable it: run supabase/schema.sql in the
project's SQL editor, enable the Google provider under Authentication, add
http://localhost:5173/tanks/ and your Pages URL to the allowed redirect URLs, then put
VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in .env.local (see .env.example) and, for the
automatic deploy, in the repo's Actions secrets. The anon key is public by design β row-level
security is what protects the data; never use the service_role key.
TypeScript Β· React (menus, HUD) Β· PixiJS (WebGL rendering) Β· PeerJS (WebRTC DataChannel) Β· Vite Β· static hosting on GitHub Pages. No backend, and no shipped art β every texture, from the tanks to the power-up icons, is generated procedurally at boot.
Project structure
src/
main.tsx # entry: debug shortcut, ?room= deep link, or the main menu
ui/ # React: screens (menu, create, join, room, match, result, settings,
# editor), the HUD, and the route value that says which one is up
game/ # constants, config, settings, touch controls, the live menu backdrop
constants.ts # every tuning number, grouped: map, tank, shooting, bonuses, rounds, bots, net, editor
config.ts # ids and labels: teams, bot difficulties, the DEBUG switch
world/ # the simulation β no rendering, no DOM, no network
sim.ts # authoritative tick: movement, bullets, bonuses, flags
grid.ts bullet.ts bonus.ts flag.ts rules.ts tank.ts
maps/ # map sources as text, parser/validator, editor model, custom-map library
ai/ # bots: per-tank controller, pathfinder, team role planner
net/ # RoomHost / RoomClient behind one RoomController interface
peer.ts # PeerJS plumbing; protocol.ts β wire messages
render/ # PixiJS: arena, procedurally generated atlas, previews, bonus art
util/ # input, audio, fullscreen, math, storage
tests/ # headless node:test suites
scripts/ # map validation and the test runner
docs/screenshots/ # the images in this README
Docs
- SPEC.md β the authoritative design, an index over the section files in specs/.
- specs/level-editor.md β the editor's own detail sheet.
- AGENTS.md β working notes: current status, how to launch and verify the game.
Milestones M0 β M10 are laid out in Β§13 Plan.
- M0βM6 β scaffold, terrain, tanks, match rules, bots, bonuses, screens + local co-op
- M7 β multiplayer (PeerJS star topology, room code, host-authoritative sim) β implemented, not yet played across two real devices
- M8 β mobile touch controls, fullscreen and the orientation gate β verified in an emulated phone browser, not yet on real hardware
- Level editor β paint, validate, save, share; custom maps over the network
- M9 β
fortressandiceworksmaps, real audio mixing pass, effects/kill-feed polish, balance - M10 β first playtest round
Deliberately not planned: host migration, dedicated servers, accounts and ranking, matchmaking, replays. See Β§15 Out of scope.
MIT.
Inspired by Battle City (Namco, 1985).





