A flag-and-geography quiz. You're shown a country's flag and have to find that country on a world map. Single-player runs fully offline in the browser (Vite + React) — the map, the flag SVGs and the country names are all bundled at build time, so there are no runtime network requests. An optional Online mode lets you race other players live against a self-hosted dedicated server.
- A flag is shown; click the matching country on the interactive world map.
- Rounds are timed, and your accuracy and best/average times are tracked per session.
- Every round is logged to a persistent History screen.
- Indistinguishable flags are treated fairly: guessing Monaco when shown
Indonesia counts as correct — see
src/game/flagTwins.ts.
- Play — endless free practice.
- Challenge — a finite, scored run; answer in 3s for full points, decaying the longer you take.
- Online — multiplayer rooms on a shared server (see below).
- History / Settings.
Configured on the Settings screen and persisted to localStorage:
- Continents — restrict the pool to one or more regions.
- Country size — small (
< 50,000 km²), medium, or large (> 1,000,000 km²). - Confirm mode — click-to-confirm, or select then press space.
- Country labels, sound on/off, volume, answer time limit.
- Language — English or Russian.
The Online tab connects to a dedicated server that you or a friend self-host. In a room, everyone races the same server-chosen flag sequence simultaneously; the server is authoritative for the sequence, round timing, answer correctness and scoring, so results can't be forged.
Flow: open Online → enter the server URL (and a password if the server requires one) → pick a nickname and a flag avatar → create or join a room by code → the host starts the match → race, with a live scoreboard, then final standings. Match results feed an all-time leaderboard stored on the server.
The server lives in server/ and is a small Rust (axum + tokio)
binary with bundled SQLite. See server/README.md for run,
Docker and reverse-proxy (subpath wss) instructions. Quick start:
cd server
cp .env.example .env # set JWT_SECRET, optional SERVER_PASSWORD, CORS_ORIGINS
cargo run # listens on :8080Then in the game's Online tab, enter http://localhost:8080.
- React 18 + Zustand for state (separate stores for game loop, settings, history, UI, and online).
- d3-geo + topojson-client for the map projection and rendering, using
the
world-atlasTopoJSON. - flag-icons for flag SVGs; world-countries for ISO codes, areas, regions and localized names.
- Vite + TypeScript for the web app; Connect / gRPC-Web
(
@connectrpc/connect-web) is the online transport. - Rust (tonic, tonic-web, tokio, rusqlite) for the optional online server.
The wire protocol is a single protobuf contract in
proto/, compiled to Rust (prost/tonic) and TypeScript (buf), so client and server types can't drift.
proto/ The protobuf wire contract (source of truth for both sides)
scripts/ Build-time codegen (data, server data, protocol)
src/
components/ Map, flag, prompt, controls, stats, feedback
screens/ Game, Challenge, Online, History, Settings
store/ Zustand stores (game, settings, history, ui, online)
game/ Mode registry, country pool, flag twins, sound, stats
online/ Connect clients + event stream, generated protocol types, online UI
online/gen/ Generated TypeScript protocol types (buf)
data/ Generated country metadata + loader
i18n/ Localized country names + UI strings (en, ru)
map/ World TopoJSON loading
assets/ Bundled world TopoJSON
server/ Dedicated multiplayer server (Rust)
src/game/ Country table + flag twins, generated from the client data
src/grpc/ tonic services + protobuf<->domain conversion
src/protocol.rs Internal domain types for the room actor
The game loop (src/store/gameStore.ts) is renderer- and mode-agnostic. Online
play reuses the same map/board: the online store feeds round state through
gameStore, and confirm() submits to the server instead of scoring locally.
Requires Node.js. The online server additionally needs the Rust toolchain.
npm install
npm run gen-data # generate bundled country/map/locale data (one-time, or after dep bumps)
npm run dev # start the Vite dev server (http://localhost:5173)| Command | Description |
|---|---|
npm run gen-data |
Regenerate bundled data into src/data, src/i18n/locales, src/assets. |
npm run gen-server-data |
Regenerate the server's country/flag-twin tables from the client data. |
npm run gen-protocol |
Compile proto/ into TypeScript types under src/online/gen/ (buf). |
npm run dev |
Vite dev server with hot reload. |
npm run build |
Type-check and produce a production web build in dist/. |
npm run preview |
Serve the production build locally. |
npm run typecheck |
Type-check without emitting. |
npm test |
Run the unit tests (vitest). |
npm run lint |
Lint with ESLint (typescript-eslint + react-hooks). |
For the server, see server/README.md (cargo run,
cargo test, Docker).
npm run gen-data runs at build time from local npm packages and produces, all
checked into the repo so the app is self-contained:
src/assets/countries-110m.json— world map TopoJSON (fromworld-atlas).src/data/countries.json— per-country metadata (ISO codes, area, region).src/i18n/locales/{en,ru}.json— country names keyed by numeric ISO code.
Keys are normalized numeric ISO 3166-1 codes (no leading zeros) so the metadata,
locale names, and map ids always line up. To add a language, emit another locale
file in gen-data.mjs and register it in src/i18n/index.ts.
The server's copy of the country data (server/src/game/) is regenerated from
these files with npm run gen-server-data, and the online protocol types
(src/online/gen/) from the proto/ contract with npm run gen-protocol.