workshop

← all creations

NYC Conductor

games · created 2026-08-22

First-person subway cab sim — work the master controller down a signalled line, berth on the board, key the doors, make the call, keep the card.

simulationphysicscanvas

You are in the cab of a ten-car local, uptown from South Ferry, and you have the whole job: drive the train, stop it on the conductor’s board, key the doors, make the announcement, and hold the timetable — without running a red. Six stops, one canvas, no engine.

The view out of the window is a pseudo-3D tunnel drawn entirely with 2D canvas calls. Everything in the world lives on a single axis: the line is one array of feet, and stations, signals, restrictions and the train ahead of you are all just positions on it. Perspective is the only place a second and third dimension appear, and only at draw time.

What it actually simulates

Reuse

Five framework-free ES modules in src/. The sim core has no DOM in it at all — test/run.test.mjs drives an entire run headless in Node.

line.js — the route as data. Stations with a berth mark and a platform side, posted limits, timers, and curves in 1/ft. Editing this array is how you build a different line; everything else reads it:

export const DEFAULT_LINE = {
  name: 'K Local · Uptown', length: 14400, maxSpeed: 40,
  stations: [{ name: 'Canal St', mark: 3400, side: 'right', note: 'Transfer: J N Q R W' }, /* … */],
  limits: [{ from: -200, to: 1150, mph: 15, label: 'CURVE' }],
  timers: [{ pos: 4600, mph: 20, label: 'GT' }],
  curves: [{ from: 150, to: 1150, k: 0.00075 }],
};

train.jscreateTrain() / updateTrain(train, dt), plus moveHandle, toggleDoors, applyEmergency and stoppingDistance(). Pure state math in feet and seconds; the door interlock and the emergency latch live here, not in the game.

signals.jscreateSignalSystem(line) returns refresh(player, others) to recompute every aspect from block occupancy, and crossed(from, to) for the signals a train’s front passed during a step, each carrying the aspect it was showing at that moment. That pairing is the whole trip-arm mechanic.

run.js — the orchestrator: ticks the train, drives the leader, runs the station state machine, runs the seeded scenario engine (door obstruction, dispatch hold, dwell and lateness nags), scores it. update(dt, commands) takes an array of plain strings ('handle-up', 'doors', 'announce', …), and guidance() returns everything a cab display wants in one object — including the ranked alerts list — so the HUD never reaches into the sim’s internals. createRun({ seed }) makes any run replayable.

view.jscreateView(canvas, { line }).draw(state). The projection is three lines: a point x feet right of the centreline, y feet up, d feet ahead lands at focal/d from the vanishing point. Curves are integrated ahead of the camera into a lateral offset per distance, which is why the tunnel bends without the world ever leaving one dimension.

audio.js — synthesised cab sound: a traction whine that tracks speed, rolling noise under it, and the door chime, horn and brake-pipe hiss. No assets.

Gotchas