# Neon Pinball A browser pinball game served by a tiny, zero-dependency Node.js server. The table is modelled at real-world scale (millimetres), the physics runs at a fixed 1000 Hz with interpolated 60/120 Hz rendering, and every sound is synthesised with the Web Audio API. Play as long as you can keep the ball alive — the score keeps building until you lose your third ball. ## Run it Requires Node.js 20 or newer. There is nothing to install. ```bash npm start ``` Then open . Use `PORT=8080 npm start` to pick another port. > Opening `public/index.html` directly from disk will not work: browsers block ES module > scripts on `file://` URLs, so the game has to be served over HTTP. ## Controls | Key | Action | | --- | --- | | `A` or `←` | Left flipper | | `D` or `→` | Right flipper | | `Space` | Start a game; hold to pull the plunger back, release to launch | | `P` / `Esc` | Pause | | `M` | Sound on/off | ## Rules - **3 balls per game.** When the third ball drains, it's game over. Your best score is saved in the browser. - **Plunger:** a short hold (below the white tick on the power meter) is a soft plunge that drops the ball into the top lanes; a long hold sends it all the way round the orbit. - **Skill shot (25,000):** one top lane blinks at launch. Soft-plunge the ball into the lanes and steer the blinking lane under it with the flippers. - **Top lanes:** light all three to raise the end-of-ball bonus multiplier (2X–5X). The flipper buttons shift the lit lanes left and right. - **Drop targets (left):** knock all three down for 10,000, 20 seconds of Super Bumpers (1,000 per hit), and to relight the kickback. - **Stand-up targets (right):** light all three for 15,000. - **Kickback:** lit at the start of the game; when a ball goes down the left outlane it's fired back into play. - **Ramps:** a hard flipper shot up either outer lane climbs a ramp and loops back over the pop bumpers to feed the same flipper again — too weak a shot and it rolls back out of the entrance mouth. - **Lock & multiball:** shoot the center scoop (threaded between the two upper pop bumpers) to lock a ball — it doesn't cost you a turn, a fresh ball is served immediately. Lock two, and the next scoop shot starts 3-ball multiball with a jackpot lit on both ramps. - **Jackpots:** during multiball, clearing a lit ramp scores its jackpot (climbing in value each time) and unlights it; collecting both lights the Super Jackpot at the scoop. - **Bonus:** targets and lanes build a bonus that is multiplied and paid when each ball drains. ## Tests ```bash npm test ``` The physics and rules have no DOM dependencies, so the test suite (Node's built-in `node:test`) drives the real game headlessly. It covers the flipper collision shape against a brute-force reference, cradling and flipping, plunger power, drains and game over, the scoring features, ramp riding (a track sampled against a brute-force distance check, a weak shot rolling back vs. a hard shot cresting it), locking, multiball, jackpots and the super jackpot, and a three-minute random-play run — now driving up to three simultaneous balls — that checks no ball ever tunnels through a wall or escapes the table. ## Project layout ``` server.js static file server (node:http, correct MIME types, no path traversal) public/index.html page and backbox HUD public/style.css public/js/physics.js ball, walls, posts, tapered flippers, collision response public/js/table.js table geometry at real-world scale public/js/game.js rules, scoring, fixed-timestep simulation public/js/render.js canvas renderer (static layer cached, dynamic parts per frame) public/js/audio.js synthesised sound effects public/js/main.js game loop, keyboard input, HUD test/ node:test suites ``` ## How it works, and where the numbers come from - **Scale.** The playfield is a standard 20.25" × 42" (514 × 1067 mm) with a 1-1/16" (27 mm) ball. The flippers are 3" bats (3.25" with rubber) whose pivots are 7" apart, resting 31° below horizontal and swinging to 20° above (Visual Pinball's defaults of 121° and 70°, measured clockwise from 12 o'clock — about the 52° swing of a real flipper). - **Gravity.** A real table is tilted 6.5°, giving 9.81 × sin 6.5° ≈ 1.11 m/s² along the playfield. The game uses 1.5 m/s² for a snappier, arcade feel (tuned by play-testing). - **Collisions** follow the "Ten Minute Physics" pinball approach: push the ball out along the contact normal, then correct its normal velocity. On top of that there is speed-dependent restitution (flipper elasticity 0.8, from Visual Pinball's defaults) and moving-surface contacts for the flippers. The flipper is an exact tapered capsule (Inigo Quilez's uneven-capsule distance function), so what you see is exactly what the ball hits. Multiball uses the same tutorial's equal-mass ball-vs-ball collision for balls bumping into each other. - **Ramps and the wireform return.** A ramp is a Catmull-Rom spline through hand-placed 3D control points (x, y, and height above the playfield); the ball rides it like a bead on a wire, gaining or losing speed to gravity along the climb (scaled down from the real 9.81 m/s² for an arcade feel) plus rolling friction. Too weak a shot loses all its speed partway up and rolls back out of the entrance, exactly as on a real ramp. The entrance itself sits well past the flipper, not right at its tip: an early version put the mouth exactly where a hard flip's tip ends up, which turned out to catch nearly *every* flip regardless of aim — there's no aiming to a mouth that every shot already passes through. Simulating a cradled-ball flip across a wide sweep of hold times and searching the resulting trajectories for a spot only a specific, contiguous band of hold times actually reaches (not a guess) found a real one: holding the flip for roughly a beat past the instinctive snap sends the ball on a different, later-diverging arc that the mouth sits on. Each ramp climbs from there over the pop bumpers and loops back down to the natural post-flip point above the same flipper — no aim needed for the return, only the entrance demands it. - **Lock, multiball and jackpots** follow the standard pattern on games like Medieval Madness and Attack from Mars: a captive-ball scoop below the pop bumpers builds a lock without costing a turn, then kicks off real 3-ball multiball; ramp shots score an escalating jackpot while it's lit, and clearing both lights a Super Jackpot back at the scoop. The scoop's plain "bonus, kicked back out" case ejects the ball with a randomised sideways component and a short cooldown — an earlier straight-down, dead-centre eject could fall onto a pop bumper and bounce straight back up into the scoop over and over, a perfectly symmetric loop that trapped a ball indefinitely. - **The stuck-ball rescue** doesn't watch instantaneous speed — a ball can be stuck while moving fast the whole time, cycling energetically around a loop through several colliders (bumper → bumper → bumper → repeat) that never actually goes anywhere. It watches the bounding box the ball has visited over a rolling multi-second window instead: a real loop can't escape a modest box no matter how many laps it runs, so once that window elapses without the box growing, it forces a hard rescue kick. This is what caught a real game-breaking bug (found through extended simulation, not by inspection): a ball could settle into a stable circuit around the pop bumpers and rack up score indefinitely without ever draining. - **The 3D tilt** is a real CSS 3D transform (`perspective` + `rotateX`) on the rendered table, not a change to the game itself: physics and input stay in the flat, straight-down coordinate system the ball actually moves in, and the browser tilts that finished picture in 3D space for display, anchored at the bottom (flipper) edge so the far end recedes correctly. - **Ramp and wireform rendering** is the classic 2D "cylinder" trick also used for the lane guides (`metalRail`): several strokes of shrinking width and lightening colour nested on the same centreline read as a round chrome tube, with a coloured LED-style accent stripe down one side that brightens when that ramp's jackpot is lit. - **Layout.** The overall arrangement — a decorative structure at the top of the arch, two big tube ramps crossing over a pop-bumper triangle, bold arrow-shaped target banks flanking the bumpers, a captive lock/spinner feature just below them, and lightning-bolt slingshots by the flippers — is patterned after a user-supplied reference photo of a real cabinet, with none of its branding, colours, or characters carried over. - **Game loop.** A fixed-timestep accumulator with render interpolation ("Fix Your Timestep!"), with the frame time clamped to 0.25 s to avoid a spiral of death after a stall. ### Sources - Playfield size and slope: [Dimensions.com – Pinball Machines](https://www.dimensions.com/element/pinball-machines), [VPForums – Playfield sizes](https://www.vpforums.org/index.php?showtopic=2762) - Ball size: [Marco Specialties – 1-1/16" ball](https://www.marcospecialties.com/pinball-parts/PB116), [Pinball Life – standard pinball](https://www.pinballlife.com/1-116-pinball-standard-size.html) - Flipper spacing and length: [Pinside – distance between flippers](https://pinside.com/pinball/forum/topic/what-is-the-regular-distance-between-flippers) - Flipper swing arc: [VPForums – flipper angles](https://www.vpforums.org/index.php?showtopic=39652) - Visual Pinball flipper defaults and angle convention: [vpinball `flipper.cpp`](https://github.com/freezy/vpinball/blob/master/flipper.cpp), [VP10 physics notes](https://github.com/c-f-h/vpinball/wiki/VP10-Physics) - Collision approach (including ball-vs-ball): [Ten Minute Physics – pinball](https://github.com/matthias-research/pages/blob/master/tenMinutePhysics/04-pinball.html) - Tapered capsule SDF: [Inigo Quilez – 2D distance functions](https://iquilezles.org/articles/distfunctions2d/) - Playfield layout, flow, orbits and ramps feeding a flipper: [Mission Pinball Framework – Layout considerations](https://docs.missionpinball.org/en/latest/physical_building/layout_considerations.html) - Ramps, wireforms, scoops/VUKs and locks: [Pinball Makers wiki – Design](https://pinballmakers.com/wiki/index.php?title=Design), [Wikipedia – Pinball (playfield components, ball save, tilt)](https://en.wikipedia.org/wiki/Pinball) - Real lock/multiball/jackpot examples referenced for the design: [Wikipedia – Twilight Zone (pinball)](https://en.wikipedia.org/wiki/Twilight_Zone_(pinball)) - Game loop: [Gaffer on Games – Fix Your Timestep!](https://gafferongames.com/post/fix_your_timestep/) - Browser APIs (MDN): [KeyboardEvent.code values](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values), [devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio), [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame), [JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), [Web Audio autoplay](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices), [User activation](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation) - CSS 3D transforms (MDN): [perspective()](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/perspective), [rotateX()](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotateX)