- TypeScript 91.1%
- CSS 5.5%
- HTML 3.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Both defaults collided with other services on the machine. The server now listens on 7788 and Vite on 7789, and the proxy target follows PORT so overriding the server port keeps the client pointed at it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
| packages | ||
| .gitignore | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| PROMPT.md | ||
| README.md | ||
π’ agar.io clone
Server-authoritative agar.io clone: Node + TypeScript game server, HTML5 canvas client, accounts with uploadable skins. Implements the design in PROMPT.md on a pnpm/TypeScript stack instead of Rust.
Run it
pnpm install
pnpm run dev # game server on :7788, client on http://localhost:7789
pnpm run dev runs both packages in parallel. The client talks to the server through
Vite's proxy, so /api/* and /ws work from the Vite origin with no CORS setup.
The ports avoid the usual 3000/5173 crowd; if 7789 is taken anyway, Vite prints the port
it picked. PORT overrides the server port and the proxy target together.
Production:
pnpm run build # bundles the client into packages/web/dist
pnpm run start # single Node process serving the bundle + API + WebSocket on :7788
Layout
packages/
βββ shared/ # wire protocol + constants both sides must agree on
βββ server/
β βββ src/config.ts # every tunable game constant
β βββ src/db/ # node:sqlite schema, accounts, skins
β βββ src/game/ # engine (tick loop), world, player, food, physics
β βββ src/server/ # HTTP routes, static files, WebSocket routing
βββ web/
βββ src/ # canvas renderer, snapshot interpolation, menus
How it works
The server owns all state and runs a fixed 60 TPS loop; the client only sends input and draws what it is told, so there is nothing worth cheating with client-side. Snapshots go out at 30 Hz, scoped to each player's viewport, and the client renders ~100 ms in the past, interpolating between the two snapshots that bracket the render time β late packets show up as slightly stale motion instead of a stutter.
Collision lookups go through a uniform spatial grid rebuilt each tick (game/physics.ts),
so eating stays O(1) per cell rather than O(nΒ²) over 500 pellets.
Mechanics
| Rule | Value |
|---|---|
| World | 4000Γ4000, 60 TPS |
| Starting mass | 10, radius 4Β·βmass |
| Eating | β₯1.25Γ the other's mass and >50% overlap |
| Food | 500 pellets, mass 1 |
| Split | Space, max 16 cells, merge after 30 s |
| Eject | W, costs 18 mass, releases 14 |
| Viruses | 20 on the map; eating one shatters you, feeding one makes it shoot a new virus |
| Decay | cells above 100 mass shrink slowly |
Accounts and skins
| Method | Path | |
|---|---|---|
POST |
/api/register |
username + password β session cookie |
POST |
/api/login |
β session cookie |
POST |
/api/logout |
|
GET |
/api/me |
current account, for the menu UI |
POST |
/api/skin |
raw image body, β€256 KB, PNG/JPEG/WebP, authed |
GET |
/api/skin/:id |
that user's skin |
GET |
/ws |
WebSocket upgrade |
Guests get a plain coloured cell, logged-in users without an upload get their initial, and
uploaded skins are drawn clipped to the circle. Passwords are scrypt-hashed with a per-user
salt; sessions are random 32-byte tokens in an HttpOnly cookie.
The database is created on first run at packages/server/data/game.db (override with DB_PATH).
Deviations from PROMPT.md
The plan specified a Rust/Axum stack. Ported to pnpm + TypeScript as requested, which forced a few equivalent substitutions:
node:sqliteinstead ofrusqliteβ same file-based SQLite, no native build step.scrypt(node:crypto) instead ofargon2β comparable password KDF without a native dependency. The stored hash records its parameters, so it can be migrated later.wsinstead oftokio-tungstenite, plainnode:httpinstead of Axum.- Skin upload is a raw body POST, not multipart β the client is the only uploader and the MIME type is read from the request header.
- Snapshots at 30 Hz, not every tick; the simulation still runs at 60 TPS. Halves bandwidth with no visible difference once interpolation is in play.
GET /api/meadded so the menu can show login state on load.