Skip to content
WorkExperienceAboutContact
Request my CVOpen to work

StillKoi pond

Layer some sounds while you read

Sounds from StillKoi →

Name More!

  1. +30
  2. ?
  3. +92
  4. ?
  5. +40

Name a sea creature. The rarer, the better.

Play the mini board →

To do

  • Build my own OS
  • Ship my own products
  • Find my next team
  • More mint tea

Name More!

Name More!

In closed testing on iOS and AndroidSole engineer, from game design to servers

Website ↗
OverviewThe game and what I builtPlayA small board you can play hereProductScreens, scoring, economyEngineeringServer, matching, realtimeDecisionsTrade-offs, tests, operationsNext projectAI Watermarkless →

Architecture

The server decides everything

Phones never decide whether an answer is right, what it is worth or who claimed it first. A phone sends what the player typed; the server matches it, orders it, scores it and broadcasts the result to the room.

architecture / overview

React Native · Expo

Client

  • Colyseus SDK
  • i18n + RTL
  • Firebase auth
  • Haptics + audio
→

Fastify · Colyseus

Game server

  • Layered matcher
  • Live rooms + reconnect
  • Ghost opponents
  • Scoring + economy
→

Postgres · Drizzle

Data

  • Runs + claims
  • Observed rarity
  • Leagues + wallet
  • Automated backups

nginx · Cloudflare

Edge

  • TLS + /api/v1
  • Web build
  • Image CDN
  • playnamemore.com

Whole stack on one VPS in Docker Compose · about 850 MB resident on a 2 GB box · GitHub Actions for CI and deploys

Answer matching

Fair is the hard part

If a player types “refrigerator” and the answer is “fridge”, or types “narut” for Naruto, the game has to accept it. If it accepts a wrong answer, the game feels rigged. Matching sits on the critical path of every claim, so it runs on the server with a 200 ms end-to-end budget.

The matcher is a chain of layers behind one interface. Normalized exact matches and generated synonyms resolve through hash lookups. Only when both miss does a typo-tolerant fallback run, against the board’s answers already held in memory.

The first fallback was a semantic embedding model. I measured it on a fandom board and found it accepted unrelated short names as matches, with no threshold that separated real typos from noise. I replaced it with a guarded edit-distance layer: a minimum input length, an edit budget that scales with length, a first-character anchor and rejection of ties. It is stricter, needs no model in memory and is faster.

server/src/matching/fuzzy.ts (excerpt)
// Short inputs get one edit; longer ones, where a typo is
// a smaller share of the word, get two or three.
function editBudget(length) {
  if (length <= 7) return 1
  if (length <= 11) return 2
  return 3
}

const consider = (candidate, answerId) => {
  // A real near-miss keeps its first letter
  if (candidate[0] !== normalized[0]) return
  const distance = editDistance(normalized, candidate, bestDistance)
  if (distance > budget) return
  if (distance < bestDistance) { bestDistance = distance; bestAnswerId = answerId }
  else if (distance === bestDistance && answerId !== bestAnswerId) ambiguous = true
}
  • ↳

    Sub-microsecond common path

    Exact and synonym matches resolve in 0.3 to 0.7 µs through O(1) hash maps.

  • ↳

    Fallback that scales

    The worst-case fuzzy scan has a p95 of 25 µs at 1,000 answers and 210 µs at 10,000, roughly a thousand times under budget.

  • ↳

    Locale-aware

    Normalization rules are registered per locale, because diacritics carry meaning in some languages and not in others.

Realtime

Built for phones that drop signal

Live modes run in Colyseus rooms. One server process holds each room’s truth: who is seated, which answers are taken, in what order and every player’s score. Claims go through a single authoritative path, so two players can never both believe they were first.

Phones lose signal in lifts and on trains, so a dropped player gets a 20-second grace period to reconnect with their seat, score and claims intact. Room state records each finder’s order and value so that a reconnect and the final results are exact.

  • ↳

    Friend rooms

    Up to six players join with a six-character code or a native Universal or App Link. No deep-link vendor is needed, so it works even on a friend’s phone.

  • ↳

    Quick match

    Strict 1v1 that is never empty. When no human is free, a ghost replays a real player’s recorded run, paced to the match length.

  • ↳

    Daily World

    One shared board for everyone for 24 hours, ranked in weekly cohort leagues of about 30 players instead of one global list.