MIDTOWN DASH

A New York sidewalk runner built in Phaser 3 — sprint Midtown, dodge the tourists.

▶ Play the game

The brief, and how each goal was met

Three priorities were set. Here is the scoreboard before the details.

1 · Look like Midtown met A canyon of setback skyscrapers with lit window grids & storefront awnings, concrete sidewalk with expansion joints, avenue crosswalks, steaming subway grates, yellow cabs, hot-dog carts, hydrants, newsstands and a checkerboard manhole — all drawn procedurally, no image files.
2 · Data-driven environment met Every "place" lives in js/core/environments.js as a self-contained data object — the engine never names a city, so a new one is pure data. Ships with Midtown, plus a gameplay-entered subway stage. (An alternate-city switch and a Brooklyn Bridge stage were built and then removed to keep the game focused on Midtown — the data-driven architecture that made them trivial still stands.)
3 · Fun & addicting met Endless run, rising speed, one-more-go scoring built on a near-miss combo system (squeeze past tourists for escalating points), power-ups, hearts, screen shake / flash / particles, a persistent best score, and instant restart.

See it

01Concept & genre

Endless vertical runnerBest fit for "run as fast as you can & dodge." No finish line means difficulty can ramp forever and the score chase drives replay. Distance (in meters) is the spine of the game.
You run "into" the screenA Temple-Run-style vanishing point on the horizon: the avenue rushes toward the camera while the player sits in the lower third. This sells forward speed and gives you time to read oncoming tourists growing out of the distance.
Crowd weaving, not jumpingThe fantasy is a New Yorker slicing through clueless tourists — so the core verb is strafing left/right to thread the crowd, rewarded by the near-miss system, rather than platformer-style jumping.

02Perspective & art direction

Pseudo-3D (no 3D engine)The OutRun / Temple Run trick: keep flat 2D sprites, but route every object's world position (lateral x∈[-1,1], depth z) through a project() with 1/z perspective. Things spawn tiny at the horizon and scale up as they rush at you. The road is drawn as depth-sorted trapezoid segments with alternating shade — the classic moving "rumble" that screams speed.
On the sidewalk, not the streetMidtown frames you on the right-hand sidewalk: the avenue (with its yellow centre line and cabs) recedes to your left and is off-limits — you clamp at the curb — while a wall of buildings rises on your right. It reads as walking a real Manhattan block rather than running down the middle of the road.
Big Manhattan blocksBuildings are drawn as chunky blocks spanning ~1.7 units of depth with randomised heights and tower off the top of the screen, plus a set-back row across the avenue. Earlier the walls were thin per-segment slices that flickered past too fast and felt strange; fewer, larger blocks read as solid skyscrapers and calm the motion.
Tourists face you, player faces awayOncoming tourists show faces, cameras, hats & selfie sticks; the player is seen from behind — red hoodie, headphones, yellow backpack. Instantly tells you who is who and which way everyone is going.
100% procedural artEvery sprite is drawn with code into a texture at boot (the TextureFactory in js/textures/); the road, walls, sky gradient and skyline are drawn live each frame. Zero image assets to load, manage, or license.
Deterministic city, authored skylineBuilding colours and windows are seeded by a tiny hash of position, so the canyon is endless yet stable at zero memory cost — while the horizon is a fixed, hand-drawn NYC silhouette (One WTC, the Empire State, Chrysler, a 432-Park slim, blinking beacons) that never scrolls or re-rolls.
MoodA CSS vignette frames the canvas; a dusk gradient sky with warm lit windows so Midtown glows.

02·5Real Midtown proportions to scale

The street, sidewalk and crowd are sized off real measurements, not vibes.

The figuresManhattan avenues are 100 ft wide building-to-building; a 100-ft avenue splits into roughly a 70-ft roadway + two ~15-ft sidewalks. Sixth Avenue in Midtown is one-way with ~6 travel lanes. A person is ~0.5 m across the shoulders.
How it's appliedThe world is measured in metres and converted to game units by a single MPU (metres-per-unit), so the roadway comes out ~4.7× the width of the sidewalk. Six lanes are painted across the avenue, and every prop is scaled from its real width via realScale() — a cab reads as a cab beside a person, not a toy.
Traffic on the avenueThe six lanes carry mostly ordinary cars in random colours, plus yellow cabs (checker + roof medallion light) and the occasional city bus. Each is built in perspective from its real metres (car/cab 1.8×4.6×1.5 m, bus 2.6×11.5×3.1 m) as a proper vehicle silhouette — a low hull (hood/trunk) with a raised, windowed cabin and visible tires for cars, a tall windowed body for buses. The rear face stays an upright rectangle (wheels on the road, no roll) while the roof and camera-side recede toward the vanishing point, so the vehicle points up its own lane in every lane. The pointing is guarded by tests (forward axis must aim at the vanishing point in all six lanes). Manholes and grates, being genuinely flat, stay projected onto the road plane.
Framing the shotThe camera is shifted sideways (camX) so you walk the right-hand sidewalk with the big avenue opening to your left and the wall on your right. Because the shift fades with 1/z, all lines still converge on a single vanishing point — the player doesn't drift off-centre at the horizon.
Intersecting cross streetsEvery ~264 ft (one N–S block) the avenue meets a cross street. Standard cross streets are 60 ft wide (majors like 42nd are 100 ft). The crossing paves the corner, lays crosswalks across the sidewalk and across the avenue, and opens a gap in the building wall where the side street cuts through. Each corner gets a NYC mast-arm traffic signal (red/yellow/green, cycling) and a green street-name sign that counts up the avenue — W 11 ST, W 12 ST, W 13 ST… ~20% of crossings spawn as the wider 100-ft majors.
Subway entrancesListed cross streets (in config.js → subway.streets, e.g. W 14 St) get a pair of entrances — one on each corner of your sidewalk: a hole with steps descending below the sidewalk, green pipe railings on three sides (the mouth open), the green globe lamp, and a corner signpost with a black line-marker sign (the available trains, shown as the game's own line markers) and a small screen on top — just like the real thing. Each takes up half the sidewalk (the curb side) and blocks you (run in and you're stopped), while tourists steadily climb out of it. Add more by listing the street numbers.
SourcesNYC avenue/street widths (1892 World Almanac) · NYC DOT — Sixth Ave lanes

03Data-driven environment architecture the extensibility goal

This is the decision the brief cared about most, so it got a clean seam.

One file = one data contractEach environment is a plain-data object: a layout, a sky gradient, a skyline type, road colours, a walls descriptor, a props table, and a landmark bandKey/bandEvery. The perspective engine reads that data and renders it — it never names a city.
Tourists are shared, stages are notObstacles, dodging, scoring, power-ups and HUD are all engine-level and reused for free. Reskinning the stage can't break gameplay — exactly the requirement.
Adding a levelPush one more object onto ENVIRONMENTS (e.g. Times Square, Central Park) — touch no other file; the engine renders whatever data it's handed. The game currently ships only Midtown, plus the gameplay-entered subway stage.
City switch + Brooklyn Bridge removedA second city — the Brooklyn Bridge (plank promenade, suspension cables, river, the gothic stone tower) — and an in-run SWAP/[E] hot-swap were built on this seam, then cut to keep the game focused on Midtown. The data-driven seam that made them a few hours' work still stands for the next one.

04The tourists (obstacle design)

Variety is what keeps dodging interesting. Six archetypes, each with a behavior that changes how you read it.

Collisions at the planeA hit resolves the instant a tourist crosses your depth plane — just compare lateral gap to a per-archetype width (selfie stick is wider). Simple, fair, and frame-rate-proof: fast speeds can't tunnel through anyone.
Flavorful failureEach archetype writes its own game-over line ("Took a selfie stick to the ribs.") so deaths feel earned and funny.
Rules IDsThe archetype behaviours above are the classic rules, still what a seed-only link replays. The default NYC1 rules make ordinary walkers keep right and walk straight, and add short, telegraphed New York encounters instead. A rules ID never changes once published, so a shared link always replays the same street.
Depth sortingEvery actor's draw order tracks its z, so nearer tourists correctly overlap farther ones and you weave through the crowd.

05Controls

KeyboardStrafe left/right with arrows or A/D, with acceleration + friction so it feels weighty, plus a slight lean into the turn. Single-axis dodging keeps the perspective game readable and twitchy — pure Temple Run.
Touch / mousePress-and-drag steers the runner toward your finger/cursor's lane — one-handed mobile play with no on-screen buttons cluttering the street. Drag below the runner (pull back on the reins) or hold right-click to brake.
Global keys[SPACE]/tap = start & restart (and the turnstile hop mid-run), [P]/[ESC] = pause, [M] = mute, [C] = reduced motion. The on-screen ♪ and ✦ buttons mirror the last two. Cities are not swapped by hand — the avenue changes environment on its own as you run.

06The "addicting" loop

Near-miss comboThe heart of the fun: shave past a tourist without hitting them and you bank points that escalate with your combo (×2, ×3…), with a "Excuse me!" pop. Risk is rewarded, so good players push closer.
Rising tensionSpeed climbs with distance (capped) and tourists spawn faster, so every run tightens toward an inevitable, replayable end.
JuiceScreen shake + red flash on hits, gold sparks on near-misses & pickups, steam from grates, floating score text, a pumping start button. Feedback for everything.
Three lives, not oneHearts + brief invulnerability after a hit keep a great run from dying to one unlucky bump — but a combo reset still stings.
Best scoreSaved to localStorage and shown on the menu & game-over. The number to beat.
Zero-friction restartGame over → Space/tap → running again in under a second.

07Power-ups

Spawn weights adapt — e.g. pizza is likelier when you're hurt — so pickups feel responsive rather than random.

08Audio

Synthesized, no filesA tiny WebAudio synth makes every sound on the fly: footsteps, near-miss whoosh, pickup chime, hit thud, a four-note game-over sting, and ambient taxi honks (Midtown) / seagulls (the bridge). Keeps the game a self-contained folder and lets ambience react to the environment.
Muteable, gesture-gatedAudio context starts on first interaction (browser policy) and [M] toggles it.

09Tech & delivery

Phaser 3.80Mature, batteries-included 2D engine: scenes, input, particles, tweens, WebGL. The obvious pick for "a Phaser game."
No build stepPlain ES classes loaded via <script>. Double-click index.html (or serve the folder) and it runs. Nothing to compile or install.
Offline-firstPhaser is vendored at vendor/phaser.min.js; if it's ever missing the page falls back to the CDN. The game works with no internet.
Single fixed canvas, scaled to fitA 480×800 portrait stage scaled with Scale.FIT + center — identical layout on phone and desktop, no responsive-layout bugs.
Pseudo-3D projectionA single project(x, z) using 1/z perspective drives the whole look — no WebGL meshes or 3D engine. Cheap, deterministic, and it kept all the existing 2D art and game systems when the view was switched from flat top-down to a vanishing point.
Manual movement, not arcade physicsHand-rolled positions & plane-crossing collisions: deterministic, debuggable, and lighter than a physics engine for a game this shape.
Verified headlesslyBuilt and smoke-tested in headless Chromium (Playwright): boot, texture generation, a live run, a mid-run swap, and game-over were all confirmed error-free, and the screenshots above were captured from the real build.

10Trade-offs I knowingly made

11Where everything lives

index.htmlPage shell, vignette, and the script loader (with CDN fallback) — its file list is the dependency order: core → textures → scene → entities/world → render → launch.
js/core/Tuning, constants & data. config.js — all gameplay tuning in one place (speed, lives, strafe feel, scoring & combos, collision/near-miss windows, power-ups, traffic cadence, crowd density, which streets get subway entrances, a dev invincible mode). geometry.js — the projection constants + the documented draw-depth map. environments.js — the swappable cities. Plus util, data, and the sfx synth.
js/scene/The Phaser scene. game.js — the menu/play/over state machine + main loop (and it exposes the window.DASH test contract). scene-geometry.js — the 1/z projection + the layering keys. scene-ui.js — HUD, hearts, menus. boot.js / launch.js bootstrap it.
js/entities/The movers. The runner (player.js), the crowd + subway rats (pedestrians.js), street props (props.js) and their z-sorted road draw (road-pool.js), pickups, and traffic.js — the signal clock + right-of-way (mast-arm heads, pedestrian signals, crosstown cars and ambulances all derive from CONFIG.signals).
js/world/Intersections (signs, signals, ambulance spawning), the underground subway stage (fare gate, cop chase, train + express, surfacing), and street furniture.
js/render/ · js/textures/The perspective drawing. Road, buildings, storefronts, the subway station, furniture and vehicles — split across render-*.js; and the boot-time sprite factory in textures/.
vendor/phaser.min.jsBundled engine for offline play.
tests/A 94-check suite — proportions, gameplay rules, z-order, subway, storefronts, input/HUD, timing, plus 13 appearance snapshots — all driving the real game headlessly via Playwright over a static dev server.
decisions.htmlThis document.
rules.htmlThe rules of the street — the maintained catalogue of every gameplay / right-of-way / rendering rule the city obeys. Updated whenever a rule changes.

12Keeping the proportions honest tests

The realistic street : sidewalk : person sizing is now guarded by a test suite, so a refactor can't quietly drift it.

A published contractjs/scene/game.js exposes window.DASH — the real-world metres, the derived world units, and the prop widths. The tests assert against the running game (loaded headlessly via Playwright), not a copy, so they test the truth.
What's lockedThe roadway stays ~4.7× the sidewalk; metres→units go through one MPU; the curb/building geometry stays consistent; lanes (3.5 m) and a person (~0.5 m) stay realistic; cross streets stay ~60 ft on a ~264 ft block; every prop scales from its real width; all lines converge on one vanishing point; and — an integration test — you still can't walk off the sidewalk onto the avenue.
Run itnpm install && npx playwright install chromium && npm test → the full 94-check suite (the proportions are the core of it). If a change breaks a proportion, the failing test names exactly which one.