How it was built
Building COTE
A 3D globe, typo-forgiving spell checking, and a leaderboard shared across every device — built iteratively with Claude Code from first prompt to public deployment.
The requirements
Five things were non-negotiable from the start:
- A 3D interactive globe updating in real time as players answer
- Countries filled in with real topographical colors when guessed
- A global leaderboard shared across all devices, no login
- Smart spell-checking — accept small typos, suggest near-misses
- Zero setup for the player: open a link, start typing
The stack, and why each piece
React + Vite
React breaks the game into components (globe, input bar, leaderboard) and re-renders automatically as state changes; Vite provides the instant dev server and production bundling.
D3.js + TopoJSON
D3 projects spherical map data onto a 2D screen and animates rotation; TopoJSON stores every country's outline in a compact format D3 reads directly.
Supabase
A cloud Postgres database with a REST API — the entire backend. It stores the leaderboard so every device reads and writes the same top-10 list.
Claude Code
Wrote, debugged, and iterated the whole codebase — including running a full 197-country simulation to verify rendering before launch.
Under the hood
Rendering the globe
The globe is D3's geoOrthographic projection mapping spherical
coordinates to SVG paths — one <path> per country. A
requestAnimationFrame loop runs continuously but only re-renders when
the projection is "dirty" (the player dragged, or a zoom animation is playing). Fill
colors come from a 197-entry TOPO_COLOR table keyed by country ID.
Typo forgiveness, done twice
Two Levenshtein edit-distance functions do different jobs. A fast version with an early exit at distance 2 decides acceptance: any input within one edit of a country name or alias counts. A separate full version computes true similarity for the "Did you mean?" feature — an unguessed country scoring ≥ 75% similarity gets suggested. They were originally one function; splitting them fixed a real bug (see below).
A leaderboard with no server
The game calls Supabase's REST API directly — no SDK, no backend code. Row Level
Security allows public SELECT, INSERT, and
DELETE, but blocks PATCH; updating a returning player's
score is therefore a delete-then-insert, done only when the new score beats their
best. Score formula: countries × 1000 − seconds, so more countries
always outranks a faster clock.
Bugs that taught something
The gray-country bug
Correctly guessed countries stayed gray. TopoJSON zero-pads IDs ("076" for Brazil) while the game data doesn't ("76"). One conversion — String(+d.id) — fixed all 197, then a simulation of every country name verified it.
"Frnce" → North Macedonia?
The suggestion engine once offered absurd matches. Cause: the fast edit-distance's early exit returned capped values that inflated similarity scores. Fix: a dedicated full-distance function for suggestions only.
The lonely leaderboard
v1 stored scores in localStorage — every device saw only itself. The fix was replacing it wholesale with Supabase, turning a private scoreboard into a global one.
Tuning the zoom
The zoom-on-guess animation shipped at 7×, felt like whiplash, dropped to 5×, still too much — 3× stuck. Feel is found by iteration, not specification.
The Claude Code process
The game was built in phases — core loop, topographic colors, spell checking, cloud leaderboard, then polish — each one specified in conversation, implemented by Claude Code, played, critiqued, and revised. Features were cut as deliberately as they were added: an email-invite button (needed a backend), a copy-link share button (wrong URL in production), and a "share with friends" section all got removed to keep the end screen focused. The result ships as a single JSX file by design — fast to build, easy to reason about, no over-engineering for a game this size.