Files
vngeoguessr/docs/game-flow.md
T
tiennm99 76e72c0b00 docs: record the URL rules, the 404 surfaces and the e2e harness
game-flow gains the re-casing-only rule and its percent-encoding exception
(/game/%74phcm plays, because the router decodes the segment before the page
sees it; reading the raw form would mean giving up static rendering on all 85
pages), plus per-region titles.

project-structure gains InlineScript and the e2e entries that had drifted:
routing.spec.js is most of the suite now, and global-setup.js is otherwise
deletion bait.

Two plan corrections: a success criterion was ticked against DN-HOANGSA, which
is not a region at all -- that URL 404s and never demonstrated the coverage
panel it claimed to. TPHCM-CUCHI is the fixture actually tested. And the build
gate named `npm run build`, which exits 0 without building when a dev server
holds .next; build:check is the gate that cannot silently pass.

Reports carry the full evidence trail, including two claims corrected after
measurement contradicted them.
2026-09-06 19:51:26 +07:00

7.5 KiB
Raw Blame History

Gameplay Flow

Complete Game Flow

1. Username Setup

  • Check localStorage for existing username
  • When no name is stored, UsernameModal opens on landing to suggest picking one; it also opens at a Play click if a name is somehow still missing, and navigation resumes after the name is settled
  • Skip, dismissing the prompt (Esc / overlay click), or a deep link straight into /game generates a random Player-xxxxxx name and stores it like a typed one
  • The header chip ("Playing as X" / "Set name") reopens the modal to change the name at any time
  • Store username for leaderboard tracking

2. Region Selection

  • Choose anywhere on the region tree:
    • Vietnam — draws from every covered province
    • A province — Ha Noi, Ho Chi Minh, Da Nang, Lam Dong, Long An, Dong Nai, Binh Duong, Thanh Hoa, Quang Nam
    • A district or town — 75 of them, expanded from their province
  • Regions with no usable panoramas are listed but disabled, with the reason shown. See the Coverage note in project-overview.md

The game URL

The region is a lowercase path segment: /game/tphcm, /game/vn, /game/hn-badinh. Codes are uppercase inside the app (they key the region tree); regionSlug / regionFromSlug in src/lib/regions.js own the conversion, and are the only place the URL casing convention lives.

  • An unknown region is a 404. A real region with no imagery is not — it renders and shows the coverage message, which is a better answer than a 404
  • An uppercase URL plays rather than redirecting. There is deliberately no canonical-casing redirect: a redirect on a prerendered route gets cached as that route's response, and nothing in the app generates an uppercase link
  • Re-casing is the only difference regionFromSlug accepts. A spelling that merely uppercases into a code (hn-badınh, dotless i) is a 404: it is a different URL, so it would be its own cache entry and its own analytics row. Percent-encoding is the exception it cannot reach — the router decodes the segment first, so /game/%74phcm plays; seeing the raw form would mean giving up static rendering on all 85 pages
  • Each region page carries its own title and description (Ba Dinh — VNGeoGuessr), so the tab, the history entry and a bookmark name the region
  • /game with no region plays the whole country
  • ?region= and ?location= still work; they redirect to /game/{slug} and are kept for links and bookmarks already in the wild
  • API routes deliberately keep their query params. /api/new-game?region=, /api/leaderboard?region= and the debug routes are unchanged and should stay that way — they are fetch calls, not navigations, so there is nothing for a path segment to buy there

3. Session Creation

  • Server generates unique UUID v4 session ID
  • Server stores the exact target location and the district the panorama sits in in Redis with 30-minute expiry
  • Client receives session ID only — never the coordinates, never the district

4. Location Display Process

  • Identify the browser: an anonymous vng_pid cookie, minted server-side on the first round and refreshed on every one after (src/lib/player-id.js). It exists only so the next step can happen; it is never joined to a username or a score
  • Exclude what this player just saw: their last 50 panorama ids (src/lib/pano-history.js) are passed to the draw as an exclusion set. If they would empty a small region's pool the filter is dropped and a repeat allowed — a repeat is always preferable to "no coverage here"
  • Pick from the index: the server draws a random panorama id from the prebuilt index in Postgres for the selected region (pickRandomPano in src/lib/pano-index.js). A country draw picks a province uniformly first, so the round is not dominated by whichever province has the most panoramas
  • Resolve the image: one Mapillary lookup by id, ~230ms. Up to three candidates are tried in case an image was deleted upstream
  • Credit the district that won: the resolving district comes from the attempt that succeeded, not the first candidate — each retry may sit in a different district
  • Record it as seen: the chosen panorama joins the player's history here, at round creation rather than at guess time, so a round they skip still counts
  • Image Display: thumb_2048_url, falling back to thumb_original_url
  • Security: client never receives the coordinates or the district

5. Guessing Phase

  • Image Interaction: View street-level thumbnail image
  • Map Interaction: Place guess marker on interactive Leaflet map with OpenStreetMap
  • Map Search: optional search for a district, street, or place to pan the map before clicking; results never reveal or place the answer
  • Submission: Click submit button to finalize guess

6. Server-Side Processing

  • Input: Client submits guess coordinates + session ID only
  • Retrieval: Server retrieves exact location from Redis session storage
  • Validation: Server validates coordinate ranges and session existence
  • Calculation: Server calculates distance using Turf.js distance function
  • Scoring: Server-side score calculation with distance-based points
  • Cleanup: the session is claimed with an atomic DEL before any score is written, so a replay or a concurrent submit scores exactly once

7. Scoring System

Distance-based points (0-5 scale), on one ladder for every region — the picked region's size does not move a threshold:

  • 0-50m: 5 points
  • 50m-100m: 4 points
  • 100m-200m: 3 points
  • 200m-500m: 2 points
  • 500m-1km: 1 point
  • beyond: 0 points

See SCORE_BANDS in src/lib/game.js. Leaderboards use the same ladder (submitRoundScore in src/lib/leaderboard.js), so the headline score and the points added at every level are the same number.

8. Results Display

  • Show calculated distance between guess and actual location
  • Display earned points for current round (0-5 scale)
  • Reveal the region path the panorama was in, e.g. Vietnam › Ho Chi Minh › District 7 — this is the first time the client learns the district
  • Show the accumulated total and rank at each of the three levels
  • Show distance leaderboard rankings for the current game at each level
  • Reveal exact target coordinates and compare guess vs actual on the map

9. Leaderboard Management

  • Rollup fan-out: each game updates the score board and the distance board at every level above the panorama — normally district, province and Vietnam, or province and Vietnam when the panorama fell outside every district outline. The levels are written concurrently
  • Score Leaderboards: Accumulated scoring system with single entry per user
  • Distance Leaderboards: Best distance records with multiple entries per user allowed
  • Redis Sorted Sets: Persistent storage using ZADD/ZRANGE operations
  • Top 200 Limit: Automatic trimming per leaderboard to maintain top performers only
  • Score Accumulation: New scores added to existing totals in score leaderboards
  • Distance Records: Each game creates new timestamped distance record entry
  • Real-time Ranking: Dynamic rank calculation using ZREVRANK/ZRANK for all leaderboard types
  • Persistent Storage: No expiration on leaderboard data

10. Continue or Exit

  • Option to start next round with new session and location in the same region
  • Option to return to the region picker and choose somewhere else
  • Option to view full leaderboard with pagination
  • Redis session cleanup ensures fresh start for each round