John Aleman
All projects

Spot The Difference

A real-time multiplayer game. Find the one smiling flower before the timer runs out, and before everyone else.

Live
Spot The Difference round reveal: a field of sad-faced flowers with the one smiling flower found, a Fast Hands bonus and the live scoreboard
The problem
Real-time games feel unfair the moment two players disagree about who clicked first, or a refresh loses a score.
Who it’s for
A group in the same room, joining from their own phones with a short code, with no accounts.
What I built
A multiplayer round-based game whose server owns the clock, the scoring and the room, with a weekly leaderboard.
Key decision
One Cloudflare Durable Object per room is the single source of truth. Clients send clicks, not scores.

The problem

Back to contents

Every round shows a field of nearly identical flowers, and exactly one is smiling. The first correct answer scores double. That rule is only fun if everyone believes the result, which makes fairness, not graphics, the hard part.

It also runs on Cloudflare Durable Objects, which are taken out of memory whenever a room goes quiet, even in the middle of a game, and rebuilt when the next message arrives. Every seat, score and countdown has to live in durable storage rather than in memory, so nothing is lost when the room wakes up.

Product experience

What the product does for the people using it, and why it works that way.

Back to contents
  1. The timer alone advances the game

    There is no “next question” button anywhere, so nobody waits on the host and nobody can skip ahead. The shared timer moves every round forward.

  2. A countdown that is biased toward the player

    The server sends a duration, never a deadline, because browser clocks are routinely minutes off. The client subtracts half its measured round trip, so the bar reaches zero slightly early, and a click at the last visible instant still lands inside a 250 ms grace window.

  3. Results revealed to everyone at once

    A player who locks in learns only that the answer was accepted. If the fastest player were told immediately, the room would read it off their face.

  4. Joining without signing up

    Players type a name and an email at the join screen. A magic link would mean checking an inbox while four people wait in a lobby. The email is the identity a weekly total accumulates against. It is unverified, and that is documented as a limitation.

  5. Honest about accessibility limits

    Flowers are real buttons with 44 px targets, focus rings and keyboard support, and petals stop spinning under reduced motion. They are labelled by position, never by face, because naming the smile would give away the answer. The game is visual, and the documentation says so rather than claiming otherwise.

Engineering decisions

Each one as the decision, the reasoning, the trade-off accepted and the outcome.

Back to contents

Durable Objects and native WebSockets, not Socket.IO

Decision
Each room code maps to one Durable Object that holds state, the clock and the sockets. Browsers connect directly with native WebSockets, and a typed discriminated union defines the wire protocol.
Why
Workers have no Node HTTP server, so engine.io cannot run there. A single object per room serializes every message, which makes it the natural owner of who was first.
Trade-off
Two deployments with one narrow seam: a recordGameResults mutation authenticated by a shared secret that lives in both systems.
Outcome
The server validates every click against its own layout. A player can read the answer from the DOM, but nobody can post a score they did not earn.

Game modes as pure, synchronous reducers

Decision
A mode is a function (state, event, ctx) => { state, effects }. The room assigns the new state before it awaits anything.
Why
In a Durable Object every await is a point where another message can run. An async check-then-write would let two players both claim the Fast Hands bonus.
Trade-off
Effects such as broadcasting and storage have to be returned and interpreted, rather than performed inline.
Outcome
A test fires five simultaneous correct answers and asserts exactly one winner. Adding a mode needs no transport changes.

Alarms are only a nudge

Decision
The deadline lives in storage. Three independent mechanisms drive the game: a per-question idempotency token, a constructor watchdog that re-arms a missing alarm, and reconciliation on every inbound message.
Why
setTimeout blocks hibernation and is lost on eviction. Alarms survive both, but they are delivered at least once and can fail to fire when past-dated.
Trade-off
More moving parts than a single timer, and hibernation behaves differently locally than when deployed, so checks have to be repeated against a deployed preview.
Outcome
Any one of the three mechanisms can fail and the game still finishes. Refreshing mid-question restores the seat, the score and the countdown.

Results reporting that survives eviction

Decision
The results payload is persisted before the first attempt. Retries run on the alarm chain, the game id is an idempotency key and the resolver upserts.
Why
It is the only step whose failure loses a completed game, and it runs just as the room becomes idle and can be evicted.
Trade-off
After the retry schedule is exhausted, the payload stays in storage and the failure is logged loudly. It is not dropped, but it still needs attention.
Outcome
Re-sending the same results duplicates nothing. The weekly leaderboard is a query over games, so there is no second copy of the truth to drift.

Outcomes and lessons

Back to contents

Shipped

  • Streak mode, hosting and joining by code, Fast Hands bonuses, reconnect and resume, and a weekly leaderboard that resets at local midnight on Monday without a scheduled job.

Validated

  • Worker tests for the reducer, including the simultaneous Fast Hands race. Playwright covers the landing, host, join and leaderboard routes. The checks for hibernation and refreshing mid-question are documented as manual checks to repeat against a deployed preview.

Known limits and next steps

  • Only one mode exists, so the mode interface’s generality is still a claim rather than a demonstrated fact.
  • Email is unverified, so the leaderboard can be spoofed, and room creation is not yet rate-limited per IP.
  • Questions never end early, even once every player has answered.
  • Cloudflare Durable Objects
  • WebSockets
  • Zustand