Files
vngeoguessr/docs/features.md
T
tiennm99 c9a35dca9a docs: describe the region tree, and finish the coverage page migration
Reconcile every doc with the shipped code. All six described a flat
five-city model, and features.md, tech-stack.md, game-flow.md and
project-structure.md still documented the dart-throw over /images?bbox=
that the prebuilt panorama indexes replaced.

project-overview.md gains a Coverage note that classifies absent coverage
into its three causes -- not yet added, no street imagery, missing from
the boundary -- because a note that only says "partial" teaches
maintainers to ignore real gaps. Cu Chi is named as the one instance of
the third, which is the only one that is a defect.

A context hook had been denying access to src/app/debug/coverage/page.js,
so this also lands the two items earlier phases recorded as
undeliverable: the api/debug/city-coverage -> region-coverage rename, and
the page's migration from a flat city list to RegionSelect. With its last
caller gone, game.js drops CITIES, cities, cityNames, cityCenters and
cityBboxes; getCityIndex, indexedCities and fetchCityPanorama take names
that match what they now take.

Selecting a district with no boundary returned a 400 and left the
previous region's panorama count and outline on screen beside the error,
so Ho Chi Minh's 184,938 read as Cu Chi's. The error path now clears
everything derived from the previous region, and a null boundary removes
the outline rather than skipping the redraw.

Two plan-time claims did not survive contact with the code and the docs
follow the code: VN scores like any other node rather than being a
zero-scoring exploration mode, and the fan-out credits two levels when a
panorama falls outside every district outline.
2026-08-30 19:46:13 +07:00

3.7 KiB

Game Features

Location Coverage

  • Three-level region tree: Vietnam → five provinces → 61 districts and towns, generated into src/data/regions/ and traversed through src/lib/regions.js
  • Play at any level: the whole country, one province, or one district
  • Pre-2025-merger boundaries: Da Lat sits under Lam Dong, Duc Hoa under Long An
  • Partial by design: see the Coverage note in project-overview.md for the three distinct reasons a region can be unavailable

Street View System

  • Prebuilt panorama indexes: each province ships a list of Mapillary panorama ids, their coordinates, and the district each falls in, built offline by scripts/build-pano-index.mjs and scripts/assign-pano-districts.mjs
  • Mapillary vector tiles: the index is built from the z14 image layer, not from /images?bbox= search, which returns HTTP 500 in exactly the dense districts the game wants to play. See the header of src/lib/mapillary.js
  • Runtime cost is one lookup: fetchPanoramaById resolves a chosen id in ~230ms; a couple of alternates are tried in case an image was deleted upstream
  • Country draws pick a province first, uniformly, so Vietnam rounds are not 97% Ha Noi and Ho Chi Minh by panorama count
  • Panoramas only: non-panoramic images are filtered out when the index is built, so every indexed point is a panorama. The flag itself is not stored
  • Thumbnail display: thumb_2048_url, falling back to thumb_original_url

Interactive Maps

  • Leaflet Integration: OpenStreetMap-based interactive mapping
  • Click-to-Place: Intuitive guess marker placement

Anti-Cheat Security

  • Redis Session Management: target coordinates stored server-side in Redis
  • Server-resolved region: the district a panorama sits in is decided at session creation and never sent to the client; a regionCode in the guess request body is ignored
  • UUID Session IDs: unique session identifiers using uuid v4
  • 30-minute Expiry: automatic Redis session cleanup
  • Single-use sessions: the session is claimed with an atomic DEL before any score is written, so a replayed or concurrent submit scores exactly once
  • Server-side Calculations: all distance and scoring computed server-side using Turf.js

Scoring System

Distance-based points (0-5 scale):

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

Leaderboards

  • Rollup fan-out: one guess credits the district its panorama sat in, then that district's province, then Vietnam — three score boards and three distance boards. A panorama that fell outside every district outline credits two levels instead of three; no province currently has such a panorama
  • Score Leaderboards: accumulated points, one entry per user per board
  • Distance Leaderboards: best-distance records, multiple entries per user
  • Existing scores preserved: Vietnam keeps the pre-existing leaderboard:vietnam / distance:vietnam keys rather than starting a new leaderboard:city:vn. HN, DN and TPHCM kept their codes, so their boards carried over untouched. Only two codes moved — Da Lat's board was backfilled into Lam Dong's and Duc Hoa's into Long An's by scripts/migrate-leaderboards.mjs, which copies rather than moves. No score was reset
  • Redis Sorted Sets: persistent leaderboard data using ZADD/ZRANGE
  • Top 200 Entries: automatic trimming per leaderboard
  • Real-time Ranking: rank calculated with ZREVRANK/ZRANK
  • Persistent Storage: no expiration on leaderboard data

The leaderboard:city: / distance:city: key prefix is kept deliberately — renaming it would orphan every score already recorded under it.