Code for Battle is a 2D, tile-based RTS game built with vanilla JavaScript and Canvas with WebGL (WebGPU also coming soon!).
This project started in December 2024 as an experiment and benchmark: can frontier LLMs 0-shot a complex RTS game from prompt-driven development.
Over time, that benchmark evolved into a full RTS game. The long-term vision is to support LLM-controlled AI players and provide a built-in, user-friendly programming workflow so players can automate unit behavior and strategy (TBD).
The project is fully vibe coded.
Mobile (Landscape)
|
Mobile (Portrait)
|
- Node.js 20+
- npm 10+
npm installnpm run devThe app will be available at the local URL shown by Vite in your terminal.
For invite-based WebRTC multiplayer testing, run the signalling helper in a second terminal:
npm run stunCross-device joins (iPhone, iPad, or a client on another network) need a TURN relay in addition to STUN. Same-computer browsers can connect with host candidates alone. Phones cannot open an invite whose host is localhost, and iOS Safari will not start WebRTC on a plain http:// page. Create the invite on the public HTTPS site.
Set these variables for Netlify Functions (production, deploy previews, and branch deploys) or in the environment of npm run stun. In the Netlify UI: Site configuration → Environment variables → add the key for Functions. Do not put TURN passwords in VITE_* variables for a production build; those are embedded in the client bundle and are only a fallback when GET /api/signalling/ice-servers fails.
ICE_SERVERS— JSON array of RTCIceServer objects, or{ "iceServers": [ ... ] }. Use this when a provider dashboard gives you the whole list. Public STUN is always added as well.TURN_URLS— comma-separatedturn:andturns:URLs. Include TCP andturns:on port 443 so iOS Safari and cellular networks can connect.TURN_SECRET— coturnstatic-auth-secret/use-auth-secret. The signalling API mints a 12-hour username (<expiry>:cfb) and HMAC-SHA1 credential and does not log the secret.- Or, instead of
TURN_SECRET, setTURN_USERNAMEandTURN_CREDENTIALfor a provider that issues a username and password. - Optional build-time fallback, only if the ice-servers request fails:
VITE_ICE_SERVERS(same JSON asICE_SERVERS), orVITE_TURN_URLSplusVITE_TURN_USERNAMEandVITE_TURN_CREDENTIAL.
Player counts and Quick match use the same Functions scope:
UPSTASH_REDIS_REST_URL— Upstash Redis REST URL. The presence edge function reads it withNetlify.env.get('UPSTASH_REDIS_REST_URL').UPSTASH_REDIS_REST_TOKEN— Upstash Redis REST token. The edge function reads it withNetlify.env.get('UPSTASH_REDIS_REST_TOKEN'), including when the value is marked secret. Do not put either value in aVITE_*variable.
In the Netlify UI: Site configuration → Environment variables → scope Functions (production, deploy previews, and branch deploys), same as ICE_SERVERS and TURN_*. POST /api/presence and POST /api/quick-match run as edge functions and talk to Upstash over HTTPS. Heartbeats store only an anonymous session id, a status, and a timestamp. Deploy previews include backend: redis when Upstash handled the heartbeat, or blobs when the fallback did. Production omits backend, storage, redisConfigured, and handler. If either Upstash variable is missing, the edge function forwards that request to the serverless function, which keeps approximate counts in Netlify Blobs (onlyIfNew for Quick match claims) so previews and local dev still work. npm run stun uses the same rules in memory when those variables are unset. Cross-network play still needs the TURN variables above. Same-computer Quick match can connect with public STUN.
The browser asks GET /api/signalling/ice-servers when a peer connection starts. Function logs record candidate type (host, srflx, relay, mDNS) without player names, IP addresses, usernames, or credentials. A join that only gathered mDNS host candidates and relay: 0 cannot reach another device until TURN is configured. The phone's join screen shows the ICE state (ICE checking, ICE failed) and the failure reason.
Paste credentials from the provider dashboard. This repo does not ship a live relay password.
Metered.ca (free Open Relay or metered TURN). Sign up at Metered TURN, create a credential, and copy the iceServers JSON into ICE_SERVERS. Prefer the turns: URL on port 443. Open Relay credentials from their REST API expire; when they do, paste a fresh JSON value (or use TURN_URLS / TURN_USERNAME / TURN_CREDENTIAL if the dashboard shows a longer-lived username and password). Example shape, with placeholder values:
[
{
"urls": [
"turn:global.relay.metered.ca:80",
"turn:global.relay.metered.ca:443",
"turns:global.relay.metered.ca:443?transport=tcp"
],
"username": "<from the Metered dashboard>",
"credential": "<from the Metered dashboard>"
}
]Twilio Network Traversal. Create a token with the Network Traversal Service. The response ice_servers entries expire (often within a day). Map url/urls, username, and credential into ICE_SERVERS. Do not put the Twilio auth token in the client.
Cloudflare Realtime TURN. Generate short-lived TURN credentials from the Cloudflare dashboard or API and paste the resulting turn/turns URLs plus username and credential into ICE_SERVERS or TURN_URLS + TURN_USERNAME + TURN_CREDENTIAL. Refresh them before they expire. Include a turns: URL on port 443 for iOS.
Self-hosted coturn. Set TURN_URLS and TURN_SECRET to the server's static-auth-secret. The function mints the username and HMAC itself, so you do not rotate a password by hand.
If Netlify CLI is installed globally, you can run a local Netlify environment for multiplayer/signalling endpoints:
netlify devThe marketing page is served on the same site:
/en/landing/de/landing/landing(browser language, or the last locale opened)
The in-game sidebar links to it under the legal links. See specs/090-marketing-landing-page.md.
User documentation and gameplay reference:
Technical and architecture-focused documentation:
- Architecture diagram and technical notes
- Multiplayer architecture spec
- State sync architecture notes
The previous README has been preserved at:
This project is licensed under the MIT License. See LICENSE.



0 comments
log in to comment.