How growordie.io works

growordie.io is a massively-multiplayer snake game running on vanilla Node.js — one shared arena, every collision lethal, ~1,000 players per CPU core. The interesting parts aren't the rules, they're the engine. Below are the five mechanisms that make it hold together, each as an animated diagram. No slides, no walls of text — watch the thing move.

Everything here is server-authoritative: the client sends a heading, the server simulates at a fixed 30 Hz and streams back tiny binary snapshots. For the why-and-numbers of the whole stack, see 1,000 players per CPU core. This page is the how, in motion.

01 · The incremental spatial hash

+ head world grid · one bucket = one cell head enters → insert O(1) tail leaves → remove O(1)

O(1) per snake per tick· no world rebuild

Collision needs every head tested against every body segment, every tick. We bucket segments into a grid — but we never rebuild it. A snake only grows one segment at its head and drops one at its tail, so each tick is one insert and one remove: constant time, no matter if the snake is 2 m or 100 m.

A 100-meter titan costs the grid exactly as much upkeep as a rookie. The structure scales with player count, not with total mass in the arena.

02 · Area-of-interest (AOI)

arena · everyone the server simulates your camera +add −del

you only receive what's near your camera· known-set delta

The server simulates every snake, but sending all of them to everyone is O(N²) bandwidth. Instead each client gets snapshots only for snakes intersecting its viewport — a cheap query on the same grid the collision system already maintains.

The client keeps a known set: a snake crossing into view is a +add, one leaving is a −del. Your bandwidth tracks the density of the action around you, not the server's population.

03 · Netcode timing

SERVER · sim 30 Hz 1 second · gold = snapshot tick (15 Hz) CLIENT · interpolates & renders 60 fps 27 B 34 B 22 B 29 B 31 B 3 B 3 B 3 B 22 ms tick ms snapshots 15 Hz tick > 22 ms → degrade to 10 Hz

30 Hz sim · 15 Hz binary snapshots· 3 B up / ~20–40 B per snake down

Two separate rates run in parallel — the whole idea is that they're separate:

Simulation rate (30 Hz) — how often the server recomputes the world: moves every snake a step, lays body points, checks collisions. This is the game's truth.
Observation rate (15 Hz) — how often the server sends that state to you: the snapshots your screen draws. Between two snapshots the client interpolates, so it looks smooth even though it only hears from the server 15×/s.

In the diagram: the tick marks are the 30 Hz sim, the taller gold ones are the 15 Hz snapshots (every 2nd tick). The little boxes are packets — tiny 3 byte inputs going up, ~20–40 byte snapshots going down. The red bar on the right is how long the current tick took.

When that bar blows past 22 ms (the server is struggling), it has to give something up — and the two options aren't equal. Slow the simulation and each step spans more time → snakes jump → collision tunneling and unfair deaths, for everyone. Slow the observation (15→10 Hz) and the physics stays perfect; your view is just ~50 ms staler, which the interpolation hides. So it always degrades the observation rate, never the simulation. Players forgive a slightly older frame — they never even notice. They don't forgive a collision that ate their run.

04 · Swept segment-vs-segment collision

BEFORE · point-vs-point body sampled every 10 u, small radii gap passes through — bug AFTER · swept segment × segment body is continuous; head path is a segment segSegDist2 blocked — KILL

killing the tunnel bug· lib/geom.js · segSegDist2

The body used to be tested as a row of points sampled every 10 u. A thin, fast head could travel far enough in one 33 ms tick to thread between two of them and come out the other side alive — a classic tunneling bug that ate real runs.

The fix treats the body as continuous segments and the head's motion this tick as its own swept segment, then measures the closest distance between the two (Ericson, Real-Time Collision Detection §5.1.9). If it's under the kill radius, it's a hit — no gap to slip through, and the lateral kill distance stays exactly the same.

05 · Neon without shadowBlur

recipe · 4 strokes, same path w 22 · a .08 — halo w 14 · a .12 w 8 · a .20 w 3 · a 1 — core stacking translucent strokes… glow — 0.1 ms/frame, zero GC shadowBlur ✕ slow strokes ✓ cheap

the no-shadowBlur glow· 0.1 ms/frame · zero GC

Canvas 2D's shadowBlur is the classic perf killer — a per-pixel gaussian that tanks the frame budget. growordie never calls it. The neon halo is faked by stroking the same path several times: a wide, very-low-alpha pass for the outer glow, down to a thin bright core.

Overlapping translucent strokes sum toward white in the middle and fade at the edges — exactly the falloff a blur would give, for a handful of cheap stroke calls and no allocation on the hot path.

Every diagram on this page is inline SVG animated with SMIL and CSS keyframes — no JavaScript, no canvas, no external assets. It's the same instinct as the engine itself: do the cheap thing that looks expensive.

— the growordie team