โ† All projects

Case study

How I built Ragdoll Fling

A 30-second browser physics game, shipped the way real teams ship software: small pull requests, automated checks, a staging environment and an approval gate before production.

Ragdoll Fling mid-round: the ragdoll is being flung across the arena, with the damage counter at 2,438 and a x3.00 combo.
A real moment from the live game: mid-fling, with a x3 combo building.
Built
13โ€“14 November 2026
Changes
21 pull requests (20 merged, 1 blocked on purpose)
Stack
TypeScript, Vite, Matter.js, HTML Canvas, Cloudflare Workers and D1, GitHub Actions
Live at
fling.jeffreydoughty.com

The idea

A ragdoll sits in a walled arena. You grab it with the mouse (or a finger), fling it into the walls, and every impact deals damage based on how fast it hit. Hits in quick succession build a combo multiplier up to x5. After 30 seconds you get your total, and coins to spend on upgrades.

I chose Matter.js for the physics (rigid bodies and joints) and plain HTML Canvas for drawing, instead of a full game engine like Phaser. That kept the moving parts small and meant I wrote and understood the game loop myself. Every shape is drawn in code; there are no image files.

How a change reaches players

Nothing goes straight to the live game. The main branch is protected, so every change, even a one-line fix, travels this path:

  1. Pull request The change is proposed on its own branch, small enough to review in one sitting.
  2. Automated checks GitHub Actions runs lint, type-check, tests and a build. If any fail, merging is blocked.
  3. Preview link The build is uploaded as a separate preview version and the link is posted on the pull request, so the change can be played before it's merged.
  4. Staging Merging deploys automatically to a staging copy of the game, with its own database.
  5. Approval The pipeline pauses. Production only continues after I approve it.
  6. Production The exact same build that was tested on staging goes live. Database migrations run first.

All hosting configuration (Workers, custom domains, databases, rate limits) lives in the repository as code, not in dashboard settings, so it's reviewed like any other change.

Problems I solved

A ragdoll lying still was scoring points

Playtesting showed damage numbers ticking up while nobody was touching the game: a resting limb twitched against the floor, and each tiny contact counted as a hit. Fix: an impact only counts if the body was moving faster than a minimum speed.

Holding the ragdoll against a wall beat skilful throws

Grabbing the ragdoll and pumping it against a wall scored more than actually throwing it, which made the game boring. Fix: hits while you're still holding on score only 25% and don't build the combo. Because that changed what a score means, I moved saved best scores to a new storage key so old and new scores weren't mixed.

Proving the safety net works

A pipeline is only useful if it actually stops bad changes. So I opened a pull request with a deliberately broken test. The required check failed, GitHub blocked the merge, and I closed it unmerged. That's the one pull request of the 21 that never shipped.

A leaderboard that's hard to cheat and stores nothing personal

Scores are saved through a small API on Cloudflare Workers into a D1 (SQL) database. The server rejects malformed data and impossible scores (over 300,000, about five times the best real round), and rate-limits each visitor to 5 submissions a minute. Players enter 3-letter initials only, so there's no free text to moderate. The database stores initials, score and date, and IP addresses are used in memory for rate limiting but never saved. The board only accepts rounds played with upgrades switched off, so it measures skill, not how long you've grinded.

When the approve button didn't work

GitHub's "approve deployment" button sometimes didn't register, leaving releases stuck. Instead of clicking harder, I added a small script, npm run approve, that finds the waiting deployment and approves it through the GitHub API.

What I'd tell another team

  • Small pull requests are easier to review, easier to undo and easier to explain.
  • Test the safety net itself: a check nobody has seen fail might not be checking anything.
  • Ship the exact build you tested. Staging and production deploy the same files.
  • Playtesting finds the real bugs. Two of my biggest fixes were about scoring that felt wrong, not code that crashed.

โ† Back to all projects