Train to Train
Published May 30, 2026
A subway trivia game I built with my wife. She came up with the concept and did the UI; I wrote the data layer and the game loop. We finished a v1 and shelved it, but I think it deserves a write-up here.
The concept
You ride these lines every day. How many stops do you know? Pick a route bullet, look at five stops with one missing, and pick the right name from four choices.
The wireframe
We sketched the screen: a route bullet (the MTA’s colored circle for the line), a strip of five consecutive stops along that line with the middle one masked as ?, and four answer tiles for the player to pick from. This means the game is multiple-choice with map context, which keeps the implementation simple.
The data
NY State Open Data hosts MTA Subway Stations. A single GET returns JSON:
curl 'https://data.ny.gov/resource/39hk-dx4f.json?$limit=2000' | jq '.[0]'
{
"stop_name": "Astor Pl",
"gtfs_stop_id": "128",
"daytime_routes": "6",
"borough": "M",
"gtfs_latitude": "40.730054",
"gtfs_longitude": "-73.991070",
"north_direction_label": "Uptown & The Bronx",
"south_direction_label": "Downtown & Brooklyn"
}
The dataset is a list of stations. The game needs ordered stops per route. So we wrote buildRoutes to reshape one into the other:
function buildRoutes(stations) {
const routes = {};
for (const s of stations) {
if (!s.daytime_routes) continue;
for (const r of s.daytime_routes.trim().split(/\s+/)) {
(routes[r] ||= []).push(s);
}
}
for (const r in routes) {
const stops = routes[r];
const lats = stops.map(s => +s.gtfs_latitude);
const lngs = stops.map(s => +s.gtfs_longitude);
const latRange = Math.max(...lats) - Math.min(...lats);
const lngRange = Math.max(...lngs) - Math.min(...lngs);
stops.sort(latRange >= lngRange
? (a, b) => +b.gtfs_latitude - +a.gtfs_latitude
: (a, b) => +a.gtfs_longitude - +b.gtfs_longitude);
const seen = new Set();
routes[r] = stops.filter(s => !seen.has(s.stop_name) && seen.add(s.stop_name));
}
return routes;
}
The mockups
From sketch to mockup we iterated on a few details: the route bullet picked up the real MTA color, the answer tiles got tightened and rounded, and the prompt became “What is the Missing Stop?”.
Playable Demo
Here’s a playable demo that fetches stations once and caches the cleaned route map in localStorage.