---
title: One Game Library, Imported by Both the Client and the Server
description: A mobile multiplayer browser game where client and server linked the same TypeScript module. One source of truth, one fewer class of bug.
date: 2026-05-07
projectPeriod: 'Autumn-Winter 2020'
thumbnail:
  src: ./_assets/decla-red.jpg
  alt: The decla.red browser game interface showing a space scene.
tags: ['games', 'web', 'systems']
role: Game and backend systems author
stack: ['TypeScript', 'Node.js', 'WebSockets', 'Firebase', 'WebGL', 'SDF-2D']
scale: Multiple game servers, each talking to 16–32 clients, browser and mobile
outcome: A multiplayer browser game that proved SDF-2D survived a real game loop
audience: technical
links:
  - label: Source
    url: https://github.com/schmelczer/decla.red
  - label: BSc thesis
    url: /media/downloads/sdf2d-andras-schmelczer.pdf
    download: true
media:
  - type: image
    src: ./_assets/decla-red.jpg
    alt: The decla.red browser game interface showing a space scene with team controls and planets.
    caption: A real game loop is a worse audience than a tech demo. That's the point.
---

My thesis was a renderer; proving it in a real multiplayer loop was the point. A real game loop is a worse audience than a tech demo. That's the point. So through autumn 2020 I built decla.red on top of [SDF-2D](/articles/sdf-2d-ray-tracing/): a conquest-style space shooter, two teams, small planets, ray-traced 2D rendering, browser and mobile. The architecture decision worth remembering came out of needing the server and the client to stop lying to each other: one TypeScript module containing the game rules, linked by both sides of the wire.

## The split that usually goes wrong

Real-time multiplayer has an awkward two-machine problem. The server has to be authoritative or the game is cheatable; the client has to feel immediate or the game is unplayable. If you write the rules twice, once on each side, they will drift. Eventually a player's screen will say one thing and the server will think another.

I wanted the server's "compute the next state" function and the client's "predict the next state locally" function to be literally the same function. So I put the rules in a shared TypeScript library, published nothing, and had both `package.json` files link to it.

The win wasn't elegance, it was the bugs that didn't happen. Client prediction stopped being an approximation of the server; it _was_ the server, run optimistically and reconciled when the authoritative update came back.

## Other choices worth a sentence

- **k-d trees for spatial queries.** Once the world held more than a few dozen objects, naive collision and proximity checks dominated the server tick. A k-d tree dropped them out of the profile.
- **Message-passing object model.** Lifted from Smalltalk's `doesNotUnderstand:` idea. Entities respond to messages they care about and ignore the rest. Easier to extend than the inheritance tree I tried first, and less brittle.
- **Firebase only for server discovery.** Not for game state, just for "which servers are currently in the pool." Tiny consistent store, didn't need to write one.

## What I'd change

- **Observability for desync.** Multiplayer systems live or die by visibility into divergence. I had logs; I needed dashboards showing the rate, the shape, and the triggering interaction for every prediction miss. Without those, debugging was guessing.
- **Don't tangle rendering and networking in the same tree.** Both were interesting, both put different kinds of pressure on the architecture, and the directories grew into each other. Separate top-level folders from day one next time.
- **Skip multi-server until the math demands it.** I wired up multi-server early because it sounded right. With 16–32 clients per server I was nowhere near needing it; the complexity wasn't free.
