FangDash
Multiplayer

How It Works

Technical details about FangDash's multiplayer system.

TL;DR - Real-time multiplayer powered by PartyKit WebSockets - A shared seed ensures all players face identical obstacles — no server-side simulation needed - Ghost players update live via WebSocket position broadcasts

FangDash uses WebSockets for real-time multiplayer communication, powered by PartyKit.

Architecture

ComponentRole
PartyKit ServerManages race rooms, player connections, and game state
WebSocket ConnectionsEach player maintains a persistent connection to the race room
Seeded PRNGA shared seed ensures all players face identical obstacles

Race Flow

Lobby — Players join a room and mark themselves as ready
Countdown — Once all players are ready, a 3-second countdown begins

Racing — Players run through the same obstacle course. Position updates are broadcast to all players in real-time

Results — When all players finish (or hit an obstacle), final scores are compared and placements are saved

Ghost Players

Other players appear as semi-transparent "ghost" wolves on your screen. Their positions update in real-time via WebSocket messages. You can see them running, jumping, and hitting obstacles.

Deterministic Obstacles (Seeded Play)

FangDash uses a seeded PRNG so every player in a race faces the exact same obstacle course — without the server needing to simulate the game world.

How it works

In a typical multiplayer game, a central server runs the game simulation and streams the world state to every client. FangDash takes a different approach: deterministic generation from a shared seed. Instead of syncing obstacle positions over the network, each client generates them independently — and because they all start from the same seed, they all produce the identical sequence.

Step by step

  1. Seed creation — When a race starts, the PartyKit server generates a random seed with crypto.randomUUID() (e.g. "a3f1b2c4-...")
  2. Broadcast — The server sends a race_start message containing the seed to every connected player
  3. Local generation — Each client creates a SeededRandom instance from that seed. The seed string is hashed (djb2) into a 32-bit integer, which initialises the PRNG state
  4. Obstacle spawning — As the game runs, the ObstacleSpawner calls rng.next(), rng.between(), and rng.pick() to decide obstacle type, position, and spacing. Because every client calls these methods in the same order with the same initial state, every client sees the same obstacles at the same positions
  5. Race reset — When a new race begins, the server generates a fresh crypto.randomUUID() seed, so every race has a unique obstacle layout

The algorithm: mulberry32

The PRNG is a mulberry32 generator — a fast, 32-bit algorithm that produces well-distributed numbers. It lives in packages/shared/src/seeded-random.ts and exposes:

MethodReturnsUsed for
next()Float in [0, 1)Raw random value
between(min, max)Float in [min, max)Obstacle spacing, positions
intBetween(min, max)Integer in [min, max]Selecting obstacle variants
pick(array)Random elementChoosing obstacle types

Solo mode

Solo mode uses the same seeded PRNG system. The difference is where the seed comes from:

ModeSeed sourceWhen generated
MultiplayerPartyKit server via crypto.randomUUID()Once per race, broadcast to all players
SoloClient-side via crypto.randomUUID()Once per run (new seed on each restart)

This means both modes run through the same SeededRandom code path — the only difference is that multiplayer seeds are shared, while solo seeds are local.

Why this matters

  • Fair races — Every player faces identical obstacles, so skill is the only differentiator
  • Consistent code path — Both solo and multiplayer use SeededRandom, reducing the chance of mode-specific bugs
  • No server simulation — The server only manages connections and broadcasts events, not the game world. This keeps latency low and server costs minimal
  • Reproducible — A race can be fully reconstructed from its seed alone, which is useful for replays or verifying scores
  • Bandwidth-efficient — Only a single UUID needs to be sent to synchronise the entire obstacle course across all players

On this page