Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻译准备中
A browser-based programming game. Write JavaScript "bots" and set them loose in a shared arena to find and destroy the competition. Brainstorm a strategy, program it, save, and watch your team immediately adapt and battle in real time. Onwards to fame and glory!
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
A browser-based programming game. Write JavaScript "bots" — the AI for a team of tanks — and set them loose in a shared arena to find and destroy the competition. Brainstorm a strategy, program it, save, and watch your team immediately adapt and battle in real time. Onwards to fame and glory!查看完整 README ↗A browser-based programming game. Write JavaScript "bots" — the AI for a team of tanks — and set them loose in a shared arena to find and destroy the competition. Brainstorm a strategy, program it, save, and watch your team immediately adapt and battle in real time. Onwards to fame and glory!
The arena is a square space where teams of tanks fight. Each app you write is the artificial intelligence shared by one team (five tanks). Every tank has a radar to detect enemies, a turret with a reloading cannon, and a drivetrain that accelerates and turns over time. Your goal: eliminate the other teams before they eliminate you.
You program against a bot object (plus arena and clock) and react to events — START, TICK, SCANNED, HIT, COLLIDED, and more. Because movement, firing, and scanning all take time, the time-based actions return Promises so you can wait for them. Edit your code and save, and every tank on your team picks up the new logic instantly.
bot.setName('My First Bot');
// Get moving when the match starts.
bot.on(Event.START, () => {
bot.setSpeed(10);
bot.radar.setOrientation(0);
bot.turret.setOrientation(0);
});
// Every clock tick: scan, and fire at any enemy we find.
clock.on(Event.TICK, async () => {
const targets = await bot.radar.onReady().then(bot.radar.scan);
if (targets.length > 0 && !targets[0].friendly) {
return bot.turret.onReady().then(bot.turret.fire);
}
});
// Bounced off a wall or another tank? Turn and keep going.
bot.on(Event.COLLIDED, () => bot.turn(40).then(() => bot.setSpeed(10)));
The full bot API is documented in ui/public/docs/ (also served in-app at /dev), and there are worked examples in ui/public/samples/.
RobocodeJs is a two-package monorepo plus a tiny root dev proxy:
index.js — a root reverse proxy (port 5000) that routes /api and /health to the server and everything else to the UI. This is the single port you open in development.server/ (@robocodejs/server) — an Express + TypeScript API and the game simulation engine (port 8080). See server/README.md.ui/ — a Vite + React + TypeScript front end (port 3000) that renders the arena as SVG and hosts the bot code editor. See ui/README.md.A few things worth knowing about how it fits together:
isolated-vm V8 isolate (one per app, shared by that team's five tanks). The bot-facing API (bot, arena, clock, console, timers, Event) is bridged into the isolate in server/src/util/compiler.ts. Date is deliberately removed so bots stay deterministic — they read time via clock.getTime().server/src/util/simulation.ts) advances the world one tick at a time — running bot event handlers, firing tick-driven timers, moving tanks, resolving collisions and bullet hits, and applying damage. The tick rate is adjustable (and can run unbounded for headless play), and with a fixed random seed a match replays identically.POST /api/mcp) lets an AI client (Claude, or any MCP client) write, run, and watch bots with the same tools a person uses — setup guide at /mcp.ui/src/util/simulate.ts) for smooth motion.The same words name both a database row and a live object. The persisted entities (left) map onto the in-memory runtime (right) when an arena starts running:
graph TD
subgraph persist["Persisted (PostgreSQL)"]
User["User"]
App["App — bot source"]
Arena["Arena"]
Member["ArenaMember — join table"]
end
subgraph runtime["In-memory (while an arena runs)"]
Env["Environment — one per running arena"]
Proc["Process — one per app in the arena"]
Iso[["ivm.Isolate — 8 MB sandbox"]]
Tank["Tank ×5 — share the isolate"]
Turret["Turret"]
Radar["Radar"]
Bullet["Bullet"]
end
User -->|owns| Arena
User -->|writes| App
Arena -->|has| Member
Member -->|references| App
Arena -.->|"resume() via EnvironmentService"| Env
App -.->|"compiled by compiler.ts"| Proc
Env -->|hosts| Proc
Proc -->|owns| Iso
Proc -->|"controls 5"| Tank
Tank --> Turret
Turret --> Radar
Tank -->|fires| Bullet
In words: a User owns one or more Arenas and writes Apps (bot programs); an ArenaMember row records that an app has joined an arena. When an arena is running, EnvironmentService holds one Environment for it in memory; each member app becomes a Process that owns an 8 MB isolated-vm Isolate and controls a team of 5 Tanks (which share that isolate). Each Tank has a Turret with a Radar, and fires Bullets.
isolated-vm build; a .devcontainer with Node 24 is included, and CI/deploy pin Node 24).RDS_* env vars) and Google OAuth are only needed for a production-like setup.From the repo root, install all three packages once, then start everything with a single command:
npm run install:all # installs root + server + ui dependencies
npm start # runs the proxy, server, and UI together (Ctrl-C stops all)
Then open http://localhost:5000. With no configuration, you land on a running arena with starter bots — no database, no Google account, nothing to set up.
npm start uses concurrently to run the three processes with prefixed, color-coded output. Prefer separate terminals? Run them yourself:
node index.js # root proxy on :5000 (open this one)
(cd server && npm run dev) # API + engine on :8080
(cd ui && npm run dev) # Vite dev server on :3000
This works because the server picks one of two modes at startup based on a single signal — whether RDS_HOSTNAME is set:
| Local-dev mode (default) | Production-like mode | |
|---|---|---|
| Trigger | RDS_HOSTNAME unset and NODE_ENV ≠ production/test | RDS_HOSTNAME set (or NODE_ENV=production) |
| Database | In-memory pg-mem — created on boot, resets on every restart | Real PostgreSQL via the RDS_* variables |
| Auth | Bypassed — every request is a fixed "Local Dev" user; no sign-in, the Google button never appears | Real Google OAuth (GOOGLE_CLIENT_ID); sign in with the in-app button |
| First load | "Local Dev" user is auto-created with starter bots and a live arena | A user/arena is created on first sign-in |
The toggle is computed in server/src/util/devMode.ts (isLocalDev). Local-dev mode is for your machine only: it can never activate when NODE_ENV=production (re-checked at the auth-bypass site), and it's disabled under NODE_ENV=test so the test suite exercises the real code paths. A real deployment always sets both NODE_ENV=production and RDS_HOSTNAME, so it gets production-like mode.
To run the production-like stack locally (e.g. to test persistence or the OAuth flow), set the database variables before npm run dev — their presence alone flips the server out of local-dev mode:
export RDS_HOSTNAME=localhost RDS_PORT=5432 \
RDS_DB_NAME=robocode RDS_USERNAME=postgres RDS_PASSWORD=postgres
export GOOGLE_CLIENT_ID=<your-oauth-client-id> # must match the id the UI signs in with
Tables are created lazily on first use, so an empty database is fine. See server/README.md for the full variable list and the auth details (the UI posts the Google credential to POST /api/session, which sets an HttpOnly cookie the server verifies).
npm run build # from the root: builds the UI then the server
# …or each package directly:
(cd ui && npm run build) # type-checks, then builds into server/dist/public
(cd server && npm run build) # tsc -> server/dist
The UI build outputs directly into server/dist/public, which the server serves as static files in production.
Other root scripts: npm test and npm run lint run the respective task across both packages; npm run install:all installs all three.
npm run release (from the root) prepares a release in one step:
npm run install:all # once, if dependencies aren't installed
npm run release # builds everything, bumps the version, refreshes the shrinkwrap
It runs the full build (npm run build — UI into server/dist/public, server into server/dist/src) as a type-check gate, then bumps the patch version and refreshes npm-shrinkwrap.json. Commit the bump (server/package.json + server/npm-shrinkwrap.json) and push it, then push a vX.Y.Z tag — the tag is what deploys (the CodePipeline v* tag trigger rebuilds from source and deploys to Elastic Beanstalk). Merging to main on its own does not deploy.
Release does not build a zip: the pipeline rebuilds from source, so nothing consumes a local artifact. If you ever need to deploy by hand (e.g. the pipeline is down), npm run bundle (build + zip, no version bump) writes server/robocodejs-<version>.zip for a manual EB-console upload. The zip contains exactly what Elastic Beanstalk needs — package.json, the lockfile, the compiled server (dist/src), the built UI (dist/public), and .ebextensions — and excludes the TypeScript sources (src/), tests, coverage, node_modules (reinstalled on deploy), and any prior zips. It's git-ignored.
Formatting is governed by a single root .prettierrc.json for the whole repo. A Husky pre-commit hook runs lint-staged, which applies prettier --write to staged files (JS/TS/CSS/Markdown/JSON/YAML/HTML) so commits stay consistently formatted automatically. The hook is installed by the prepare script when you run npm install at the root (part of npm run install:all); no manual setup is needed. To format the whole repo on demand, run npx prettier --write . from the root.
CONTRIBUTING.md — how to set up, run the checks, and open a pull request. Contributions are welcome!server/README.md — API endpoints, the sandbox/compiler, the simulation engine, the MCP server, security, data model, environment variables.ui/README.md — app structure, arena rendering, the SSE event reducer, client-side interpolation, the code editor, theming.CLAUDE.md — orientation for working in the codebase (commands, conventions, gotchas); includes the security posture and accepted risks (a full OWASP Top 10 audit was completed and all medium-and-above findings remediated).ui/public/docs/ — the in-app bot author documentation (also served at /dev); MCP setup guide at ui/public/docs/mcp.md.The app deploys via AWS CodeBuild (buildspec.yaml) to Elastic Beanstalk (config in server/.ebextensions):
ui is built into server/dist/public and server is compiled to server/dist.8080.The production runtime needs the same configuration as local dev — the RDS_* Postgres variables and GOOGLE_CLIENT_ID — plus NODE_ENV=production, which sets the Secure flag on the session cookie. The native isolated-vm module is compiled on deploy, so the instance needs gcc/gcc-c++ (installed via server/.ebextensions/options.config). Deploys are tag-triggered — push a vX.Y.Z tag and CodePipeline rebuilds from source and deploys; see Cutting a release. For a manual break-glass zip, run npm run bundle from the root.
Released under the MIT License — © 2022–2026 Lee Adcock.