Play it: escape.mittonvillage.com
A co-op multiplayer animal-escape game for up to 20 players. You are animals that have escaped your enclosures in a megazoo run by humaniform positronic keeper-robots, and you must reach the perimeter gate together. The robots obey Asimov's Three Laws of Robotics — so if you make yourself look human enough, the First Law forbids them from touching you. Issue orders they must obey (Second Law), bait them away from hazards (Third Law), and watch the zoo-wide panic meter: let it overflow and the zoo slams into lockdown.
Browser-first (Phaser 3 / Vite / TypeScript) on a Socket.IO authoritative server, with an Android build via Capacitor. Built for the TINS 2026 72-hour game jam — a jam where random Rule-O-Matic rules (Genre, Graphics, Technical, Sound, Story, Bonus) are announced at the start and every entry must satisfy them; entries are judged on Art / Genre / Tech. Here is how Escape AI meets its rules:
| Rule | How we satisfy it | Where |
|---|---|---|
| Genre #157 — "It's a Zoo!" | Co-op breakout from a zoo; you play the animals. | gameplay |
| Artistic #84 — sci-fi author | Isaac Asimov: the Three Laws are the stealth mechanic. | docs/ASIMOV_REFERENCE.md |
| Technical #132 — catastrophic overflow | Global panic meter → overflow flips a lockdown world state. | shared/, server tick |
| Replaced via the Bonus rule below. | — | |
| Bonus #31 — Act of Sutskever | Invoked once; replaces #116 with an LLM-generated "double-edged element" rule, satisfied by the Second-Law order mechanic. | docs/ACT_OF_SUTSKEVER.md |
The Act-of-Sutskever transcript and its screenshot ship in
docs/as the bonus rule requires.
See ARCHITECTURE.md for the binding contract.
| Layer | Tech | Location |
|---|---|---|
| Renderer | Phaser 3 (2D default), Babylon.js (3D fallback) | client/src/render/ |
| Client | TypeScript + Vite | client/ |
| Netcode | Socket.IO authoritative server, fixed 20 Hz tick | server/ |
| Shared | TS types + net contract + deterministic step() |
shared/ |
| Android | Capacitor wraps the web build | client/capacitor.config.ts |
| Deploy | nginx + pm2 on a VPS (env-driven) | scripts/provision-escape.sh, scripts/deploy-server.sh |
The renderer sits behind a common IRenderer interface, so a "3D" genre rule is a
renderer swap (PhaserRenderer → BabylonRenderer), not a rewrite — see
shared/BABYLON_FALLBACK.md. Game logic that both
sides must agree on (movement integration, math) lives in shared/ exactly once
and is linked by client (prediction) and server (authority) alike.
The netcode is adapted from the author's Derezo/galaxy-miner
— the Express + Socket.IO bootstrap, the deps-injection socket orchestrator
(server/socket/index.js), the fixed-tick authoritative engine with delta/full
snapshot broadcast (server/game/engine.js), and the client-prediction + server-
reconciliation model. The @shared single-source-of-truth boundary and the asset/audio
generators draw on the author's Derezo/Modia and
Derezo/parasite. Released under the zlib license.
Requires Node 22+.
One command. On Linux / macOS use scripts/run-dev.sh; on Windows use
the PowerShell sibling scripts/run-dev.ps1. Both build shared/, start the
server (:3000) and the Vite client (:5173) together, and tear both down on
Ctrl-C:
# Linux / macOS
./scripts/run-dev.sh # install-if-needed, then run server + client
./scripts/run-dev.sh --clean # also wipe local dev data (fresh accounts/stats)
./scripts/run-dev.sh --force-install # reinstall deps even if up to date
./scripts/run-dev.sh --server-only # or --client-only
SERVER_PORT=3001 CLIENT_PORT=5180 ./scripts/run-dev.sh # override ports# Windows (PowerShell 5.1 or 7+)
.\scripts\run-dev.ps1 # install-if-needed, then run server + client
.\scripts\run-dev.ps1 -Clean # also wipe local dev data
.\scripts\run-dev.ps1 -ForceInstall # reinstall deps even if up to date
.\scripts\run-dev.ps1 -ServerOnly # or -ClientOnly
$env:SERVER_PORT=3001; $env:CLIENT_PORT=5180; .\scripts\run-dev.ps1 # override ports
# If scripts are blocked: powershell -ExecutionPolicy Bypass -File .\scripts\run-dev.ps1Both run a dependency preflight first: if Node (>= 22) or npm is missing they
report every problem at once with a platform-specific install hint (Homebrew on
macOS, apt/nvm on Linux, winget/nvm-windows on Windows) instead of failing deep
inside npm. They only run npm install when node_modules is missing or a
lockfile changed (no needless reinstall), and they auto-kill anything already
on the dev ports before starting — so a stale process never blocks them. Ctrl-C
takes down npm's node --watch / vite grandchildren too (no orphans).
Platform-tested status. Development happens on Linux, which is the only path the author runs day to day.
run-dev.shon Linux is exercised constantly and its dependency preflight is covered by unit tests (cd scripts && npm run test:shell). The macOS branch ofrun-dev.shand the entire Windowsrun-dev.ps1are written to be correct but have not been run on real macOS or Windows hardware by the author — the PowerShell script is parser- and PSScriptAnalyzer-clean and was reviewed against the known Windows runtime gotchas (Ctrl-C teardown, process-tree kill,netstatparsing,cmd /cquoting), but treat first-run on those platforms as unverified. If something misbehaves there, the by-hand steps below always work, and please report it.
…or run the three steps by hand
# 1. Build the shared module (client imports its source via Vite alias; building
# once also produces the dist the server can consume).
cd shared && npm install && npm run build && cd ..
# 2. Start the authoritative server (defaults to http://localhost:3000)
cd server && npm install && npm run dev # or: npm start
# leave running; in another terminal:
# 3. Start the client dev server
cd client && npm install && npm run devOpen the printed Vite URL in two or more browser tabs — each joins the default room as an escaped animal. The manual opens on first load (toggle with H/?). Reach the gate on the right edge to escape — but the gate is locked until you've finished your side-quest (shown in the HUD). The HUD also shows latency, the panic meter, your human-likeness, the lockdown state, and what you're carrying.
Controls:
| Key | Action |
|---|---|
| WASD / arrows | Walk — staying still reads as human and freezes the keeper-robots |
| Shift | Sprint — fast, but reads as prey (robots may give chase) |
| E | Interact — use a terminal, pick up a disguise prop, or collect food |
| F | Feed the nearest animal its liked food → it joins your herd and follows you |
| Q | Order a robot to stand down (Second Law) — but every order raises the panic meter |
| Space | Your species ability (a big on-screen effect everyone sees) |
| I | Inventory — collected food and which species each feeds |
| L | Leaderboard — sortable standings (score, escape time, herd) |
| / | Chat — talk to the other players in your room |
| H / ? | Toggle the in-game manual |
Herd & escape. Collecting food (E) and feeding animals (F) builds a herd that follows you to the gate — a bigger herd scores more, and you can steal followers fed by rivals. Finish your side-quest, gather your herd, and reach the gate together.
You're assigned one of 14 playable species (cycling by join order), each with a distinct, Three-Laws-tied Space ability — disguise (ape carry, chameleon cloak, tortoise shell), evasion (bird flit, rat skitter, mole burrow, kangaroo leap, cheetah dash), robot-control (elephant shove, peacock dazzle, parrot mimic, skunk stink), and panic-meta (owl hush, fox decoy). Each fires a spectacular on-screen effect every player can see.
Build
shared/first (step 1) — the server loads its compileddist/at boot.
Point the client at another server with VITE_SERVER_URL:
VITE_SERVER_URL=https://your-vps.example.com npm run devThe game deploys to a VPS as a single origin: nginx terminates TLS, serves the
static client bundle from disk, and reverse-proxies only /socket.io/ + /health
to a loopback-bound node process. Because client and server share one origin there
is no CORS surface — production CORS stays locked (origin: false). The node
process runs under a dedicated, login-disabled (nologin) system user via that
user's own pm2; the port it binds is loopback-only and never opened in the
firewall.
All host/user/domain/port values are env-driven and live in one gitignored file. Nothing about your infrastructure is hard-coded in the committed scripts — the host, the SSH login user, and the public domain have no defaults and the scripts error out if they are unset.
cp scripts/deploy.env.example scripts/deploy.env
# edit scripts/deploy.env — set DEPLOY_HOST, DEPLOY_USER, APP_DOMAIN, etc.| Variable | Required | Meaning |
|---|---|---|
DEPLOY_HOST |
yes | VPS hostname rsync/ssh connects to |
DEPLOY_USER |
yes | SSH login user (a sudoer) used to deploy |
APP_DOMAIN |
yes | public hostname the game is served at (nginx + TLS) |
APP_USER |
no (escape) |
dedicated nologin user that owns the files & runs node |
REMOTE_PATH |
no (/var/www/$APP_USER) |
deploy root on the VPS |
APP_PORT |
no (3390) |
loopback port node binds; proxied by nginx, never public |
PM2_NAME |
no ($APP_USER) |
pm2 process name (also the pm2-$APP_USER.service unit) |
SSH_KEY |
no (~/.ssh/id_ed25519) |
private key for the connection |
scripts/provision-escape.sh runs from your dev box (like the deploy): it connects
to the VPS over SSH as DEPLOY_USER (adding sudo automatically if that user isn't
root) and provisions everything securely and idempotently — the nologin app user
(no shell, no password, can't ssh in), the deploy dirs with tight ownership, a
per-user pm2 systemd unit that resurrects the process on reboot, the nginx vhost
(static client + socket/health proxy), a Let's Encrypt certificate, and the firewall
rules (allows the web edge; keeps the app port closed).
cp scripts/deploy.env.example scripts/deploy.env # then edit it (host/user/domain)
./scripts/provision-escape.shIf DNS for
APP_DOMAINdoesn't resolve to the VPS yet, run withSKIP_CERTBOT=1(SKIP_CERTBOT=1 ./scripts/provision-escape.sh) to provision everything but TLS, then rerun once DNS is live to issue the cert.
scripts/deploy-server.sh builds shared/ + the client bundle locally (baking
VITE_SERVER_URL=https://$APP_DOMAIN), rsyncs server/, shared/, and the client
bundle to the VPS, installs production deps remotely, hands ownership to the app
user, zero-downtime-reloads pm2, and health-checks.
./scripts/deploy-server.shReviewers can download the Android app at escape.mittonvillage.com/android
(or just play in the browser). Full build path in docs/ANDROID.md;
the native project (client/android/, appId com.mittonvillage.escape) is committed,
with the launcher icon/splash generated by node scripts/gen-android-icons.js. Summary:
cd client
VITE_SERVER_URL=https://escape.mittonvillage.com npm run build # bake the prod URL
npx cap sync # dist/ → android/
cd android
JAVA_HOME=/path/to/jdk17 ./gradlew assembleDebug # app-debug.apk
JAVA_HOME=/path/to/jdk17 ./gradlew assembleRelease # signed app-release.apkRelease signing reads a gitignored client/android/keystore.properties (keystore kept
outside the repo); see docs/ANDROID.md. ./scripts/deploy-server.sh
stages the built APK into the /android download page.
Animated sprite library (the zoo). Creatures render as 8-directional animated
sprites from a packed atlas, generated programmatically from vector SVG — a reusable
template system (scripts/sprites/) where each species declares geometry against a
shared body archetype (quadruped / biped / bird / serpent / robot) so all 15 creatures
stay cohesive and symmetric. The pipeline:
node scripts/gen-sprites.js # vector SVG frames → assets/sprites/frames/ (zero-dep)
node scripts/build-atlas.js # rasterise + pack → assets/sprites/atlas.{png,json} (needs sharp)
node scripts/verify-atlas.js # headless gate: every frame key present, no orphans
# or all three: cd scripts && npm run spritessharp is a scripts/-only dev dependency; the committed atlas.{png,json} means a
clean clone runs without it. If the atlas is missing, the renderer falls back to the
original geometric shapes (the game still boots with zero art). Edit a species in
scripts/sprites/species/<name>.js (or add one + a registry.js line) and rerun.
Audio. Zero-dep placeholder blips boot the game with sound on every action:
node scripts/gen-placeholder-sfx.js # → assets/sfx/*.wav (synthesized, zero-dep)For a real audio identity there's a Suno generation pipeline (sunoapi.org),
themed eerie/creepy horror — light and spooky music, punchy SFX.
asset-pipeline/manifest.json is the single source of truth for every music track and
SFX; asset-pipeline/theme.json is the editable global aesthetic. The Python scripts
(stdlib-only) read SUNOAPI_KEY from your system environment — never a repo file:
export SUNOAPI_KEY=... # your sunoapi.org key (system env, not .env)
cd scripts && npm run audio # codegen client bindings + drift gate (free)
python3 scripts/generate-sfx.py --list # status of every asset (free, no network)
python3 scripts/generate-sfx.py --key=robot_alert --dry-run # preview the request (free)
python3 scripts/generate-sfx.py --key=robot_alert # generate one (spends credits)
python3 scripts/generate-music.py --generate-all --only must # the must-have batch
python3 scripts/change-sfx-track.py --key=robot_alert --sample=2 # prefer the 2nd sampleGeneration is user-run and spends credits; --dry-run/--list/--credits are free.
Each run downloads both samples to the gitignored asset-pipeline/output/<key>/, auto-places
sample #1 at the manifest target, and writes a provenance JSON. SFX fall back to a synth WAV
until their .mp3 exists, so the game always has sound. Full guide (cost notes, prompt
best-practices, how to add an asset): docs/AUDIO_PIPELINE.md.
The API sits behind Cloudflare bot protection, so the client sends browser-like headers; a
403 error code: 1010means a header/IP block, not a bad key.
scripts/gen-placeholder-sprites.js still emits simple static SVGs as a fallback reference.
Read CLAUDE.md before contributing. Two standing rules:
- Every commit updates
CHANGELOG.mdand any docs whose behavior changed. A commit-msg hook reminds you — install it withbash scripts/hooks/install.sh. - After completing a plan, run
/plan-validation-and-reviewbefore calling the work done.
Verify everything in one command. There is no CI; scripts/verify.mjs is the
local gate that runs every check together — build shared, then the shared + server
test suites, the client typecheck, the facingFromVec determinism check, the
atlas/tileset rasterisation verifiers, and the audio drift gate. Run it before you
claim a change is green:
cd scripts && npm run verify # all gates
cd scripts && npm run verify:quick # skip the slower atlas/tileset asset gatesclient/ Vite + TS + Phaser app (entry: client/src/main.ts)
server/ Node + Socket.IO authoritative server (entry: server/index.js)
shared/ TS types, net contract, deterministic step(), renderer interface
scripts/ asset/audio generators, Suno pipeline (sunoapi/, audio/), deploy, hooks/
asset-pipeline/ Suno audio contracts: theme.json + manifest.json (output/ gitignored)
docs/ PLAYBOOK.md, AUDIO_PIPELINE.md, ANDROID.md, ASIMOV_REFERENCE.md, ACT_OF_SUTSKEVER.md, UPSTREAM_ASKS.md, archive/
assets/ generated sprites, tiles, sfx (+ music/ once generated)
zlib. See LICENSE. Third-party npm dependencies retain their own licenses, listed in THIRD_PARTY_NOTICES.md.