Skip to content

Derezo/escape-ai

Repository files navigation

Escape AI

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-O-Matic mapping

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
Technical #116 — quicksave 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.

Architecture

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 (PhaserRendererBabylonRenderer), 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.

Credits & lineage

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.

Quick start (development)

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.ps1

Both 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.sh on Linux is exercised constantly and its dependency preflight is covered by unit tests (cd scripts && npm run test:shell). The macOS branch of run-dev.sh and the entire Windows run-dev.ps1 are 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, netstat parsing, cmd /c quoting), 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 dev

Open 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 compiled dist/ at boot.

Point the client at another server with VITE_SERVER_URL:

VITE_SERVER_URL=https://your-vps.example.com npm run dev

Production build & deploy

The 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.

Configuration — scripts/deploy.env

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

One-time provisioning (run once, from your dev machine)

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.sh

If DNS for APP_DOMAIN doesn't resolve to the VPS yet, run with SKIP_CERTBOT=1 (SKIP_CERTBOT=1 ./scripts/provision-escape.sh) to provision everything but TLS, then rerun once DNS is live to issue the cert.

Deploy (from your dev machine, repeatably)

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.sh

Android build

Reviewers 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.apk

Release 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.

Assets

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 sprites

sharp 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 sample

Generation 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: 1010 means a header/IP block, not a bad key.

scripts/gen-placeholder-sprites.js still emits simple static SVGs as a fallback reference.

Development workflow

Read CLAUDE.md before contributing. Two standing rules:

  • Every commit updates CHANGELOG.md and any docs whose behavior changed. A commit-msg hook reminds you — install it with bash scripts/hooks/install.sh.
  • After completing a plan, run /plan-validation-and-review before 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 gates

Layout

client/         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)

License

zlib. See LICENSE. Third-party npm dependencies retain their own licenses, listed in THIRD_PARTY_NOTICES.md.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors