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 runner | Best 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 screen | A 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 jumping | The 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 street | Midtown 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 blocks | Buildings 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 away | Oncoming 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 art | Every 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 skyline | Building 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. |
| Mood | A 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 figures | Manhattan 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 applied | The 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 avenue | The 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 shot | The 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 streets | Every ~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 entrances | Listed 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. |
| Sources | NYC 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 contract | Each 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 not | Obstacles, 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 level | Push 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 removed | A 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.
- Strollers — walk straight at you. The baseline rhythm.
- Map-readers — drift side to side, so their lane is a moving target.
- Photographers — periodically stop dead and flash a camera; great for sucker-punching greedy players.
- Selfie-stick crowd — extra-wide hitbox; respect the personal space.
- Sun-hat tourists — wander unpredictably.
- Groups — occasionally spawn as a tight cluster of three, a wall to thread.
| Collisions at the plane | A 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 failure | Each archetype writes its own game-over line ("Took a selfie stick to the ribs.") so deaths feel earned and funny. |
| Rules IDs | The 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 sorting | Every actor's draw order tracks its z, so nearer tourists correctly overlap farther ones and you weave through the crowd. |
05Controls
| Keyboard | Strafe 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 / mouse | Press-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 combo | The 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 tension | Speed climbs with distance (capped) and tourists spawn faster, so every run tightens toward an inevitable, replayable end. |
| Juice | Screen 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 one | Hearts + brief invulnerability after a hit keep a great run from dying to one unlucky bump — but a combo reset still stings. |
| Best score | Saved to localStorage and shown on the menu & game-over. The number to beat. |
| Zero-friction restart | Game over → Space/tap → running again in under a second. |
07Power-ups
- ☕ Coffee — speed boost and a 2× score multiplier for a few seconds. High risk/high reward.
- 🟨 Fare card — a fare you collect and hold (max one; it shows in the HUD), spent to swipe in at the next subway entrance, where the turnstiles open for you instead of blocking. A spare you can't hold converts to +250 points. removed An earlier design made it a brief invincibility — plough through tourists under a blue tint — which never shipped.
- 🍕 Pizza slice — refills a heart (up to five), or +250 points if you're already full.
Spawn weights adapt — e.g. pizza is likelier when you're hurt — so pickups feel responsive rather than random.
08Audio
| Synthesized, no files | A 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-gated | Audio context starts on first interaction (browser policy) and [M] toggles it. |
09Tech & delivery
| Phaser 3.80 | Mature, batteries-included 2D engine: scenes, input, particles, tweens, WebGL. The obvious pick for "a Phaser game." |
| No build step | Plain ES classes loaded via <script>. Double-click index.html (or serve the folder) and it runs. Nothing to compile or install. |
| Offline-first | Phaser 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 fit | A 480×800 portrait stage scaled with Scale.FIT + center — identical layout on phone and desktop, no responsive-layout bugs. |
| Pseudo-3D projection | A 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 physics | Hand-rolled positions & plane-crossing collisions: deterministic, debuggable, and lighter than a physics engine for a game this shape. |
| Verified headlessly | Built 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
- Procedural art over hand-drawn sprites — instantly themeable and dependency-free, at the cost of pixel-art polish. The right call for a swap-the-world brief.
- Two environments, not ten — I invested in the seam (the swap architecture) over sheer content, so the third, fourth and fifth cities are a few minutes of work each.
- Decorative props don't collide — cabs, carts and benches are scenery; only tourists are hazards. Keeps the difficulty fair and legible.
- Synth audio over recorded SFX — charming and zero-asset, not AAA. Matches the art direction.
- Score & best are local — no backend/leaderboard server; the best score lives in the browser. Keeps it a pure double-click game.
11Where everything lives
index.html | Page 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.js | Bundled 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.html | This document. |
rules.html | The 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 contract | js/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 locked | The 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 it | npm 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. |