Watch
1
0
Fork
You've already forked opus5-agario
0
Opus 5 version of agario clone thing
  • TypeScript 91.1%
  • CSS 5.5%
  • HTML 3.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Stella Reine 429a6c1a8d fix: move dev ports off 3000/5173
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>
2026-07-24 13:59:18 -05:00
packages fix: move dev ports off 3000/5173 2026-07-24 13:59:18 -05:00
.gitignore feat: agar.io clone on pnpm + TypeScript 2026-07-24 13:57:21 -05:00
package.json feat: agar.io clone on pnpm + TypeScript 2026-07-24 13:57:21 -05:00
pnpm-lock.yaml feat: agar.io clone on pnpm + TypeScript 2026-07-24 13:57:21 -05:00
pnpm-workspace.yaml feat: agar.io clone on pnpm + TypeScript 2026-07-24 13:57:21 -05:00
PROMPT.md feat: agar.io clone on pnpm + TypeScript 2026-07-24 13:57:21 -05:00
README.md fix: move dev ports off 3000/5173 2026-07-24 13:59:18 -05:00

🟒 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:sqlite instead of rusqlite β€” same file-based SQLite, no native build step.
  • scrypt (node:crypto) instead of argon2 β€” comparable password KDF without a native dependency. The stored hash records its parameters, so it can be migrated later.
  • ws instead of tokio-tungstenite, plain node:http instead 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/me added so the menu can show login state on load.