Code documentation

A file-by-file walkthrough of the Collector codebase, written so somebody who has never seen the project can read the code in a sensible order and understand why each piece exists — not just what it does.

The top-level ../README.md is the product/assessment write-up: setup, design decisions, stress results. These docs are the code tour.

Read in this order

#DocumentCovers
101-architecture.mdThe big picture, the layers, the one rule that shapes everything
202-flows.mdEnd-to-end walkthroughs: join, move, collect, game over, reconnect
303-config-and-startup.mdconfig.js, server.js, app.js — how the process boots
404-game-engine.mdsrc/game/ — all the rules, zero dependencies
505-realtime.mdsrc/realtime/ — Socket.IO wiring and the 20 Hz loop
606-http-api.mdsrc/routes/, src/controllers/, src/middleware/
707-persistence.mdPrisma schema, src/db/, src/services/
808-frontend.mdpublic/ — the browser client
909-tests-and-scripts.mdtests/, scripts/

In a hurry? Read 01-architecture.md, then the "collect" section of 02-flows.md. Those two cover the parts of this project that are actually interesting.

The 30-second version

One Node process runs Express (HTTP) and Socket.IO (websockets) on the same port.

  • Live game state lives in memory, in plain JavaScript objects. Movement and scoring never touch the database.
  • Postgres stores only what outlives a match: finished games, per-player results, and an all-time leaderboard aggregate.
  • One 20 Hz loop broadcasts a snapshot per lobby. Player input mutates memory but never triggers a broadcast of its own.
  • Socket.IO rooms keep lobbies isolated from each other.

Two conventions used everywhere

No classes. Game state is a plain object; every rule is a plain exported function whose first argument is that object:

const game = createGame({ code: 'AB12CD' });
addPlayer(game, { username: 'alice' });
tryCollect(game, 'p0', 'c3');

There is no this anywhere in src/. State stays JSON-serialisable and every rule is callable from a test with a literal object.

Results, not exceptions. Game functions return { ok: true, ... } or { ok: false, reason: 'SOME_CODE' }. Throwing is reserved for genuinely exceptional situations. Callers branch on ok; the reason codes are what the socket layer forwards to clients.

Built with LogoFlowershow