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.

Train to Train home screen on an iPhone, dark MTA-style UI with the Train 2 Train logo and difficulty buttons.

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.

Early grayscale wireframe of the Train to Train home screen.
Early home-screen sketch.

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?”.

Final mockup of the Train to Train home screen on iPhone.
Final home screen.
Final mockup of a Train to Train question screen for the L line.
Final question screen.

Playable Demo

Here’s a playable demo that fetches stations once and caches the cleaned route map in localStorage.

Loading subway stations…