Selected work
I build and ship complete products — interface, systems, backend and release.
Software & systems engineer
Thirty-three projects built, fifteen of them shipped: applications on three storefronts, real-time multiplayer running on free infrastructure, and a dozen browser projects that cost nothing to operate. The work below is chosen for the engineering behind it rather than the screenshots — generated content that has to stay deterministic across machines, simulation that has to stay solvable, and multiplayer that had to fit inside a free tier.
- 0
- projects built
- 0
- shipped
- 0
- storefronts
- $0
- $/mo to run the web projects
Sizzle reels
Three titles, cut to the beat.
Captured from the shipping builds and cut against each track's own tempo map, so the on-screen events land on the beat. Drag the handsets; tap the front one for sound.
GeoPinQuest
Spin. Pin. Win.Reel 1 of 3 · 1:03
01 Shipped · Steam, App Store, Google Play
GeoPinQuest
Half a million places, inside a mobile download.
A 3D-globe geography game in Godot 4: spin the Earth and tap a country, pin a city under a bullseye, match a flag, or call higher-or-lower on a population. Five game types crossed with timed/untimed and fixed-round/endless gives twenty mode variants off one rule engine rather than twenty screens.
The constraint that shaped it was data. 251 countries come from a real GeoJSON boundary set, and roughly half a million localities from per-country NDJSON — far more than belongs in a phone's memory. The gazetteer ships gzip-compressed and is decompressed per country on demand, so the download stays reasonable and only the continent you are playing is ever resident.
- 251
- countries
- ~500k
- localities
- 20
- mode variants
- Godot 4
- GDScript
- GeoJSON
- Steamworks
- iOS
- Android
02 Mobile · releasing on Google Play and the App Store
PopBlocks3D
A match puzzle with nothing holding the board up.
If the level starts as a cube, the cube exists only because several hundred coloured pieces are arranged into one. There is no frame behind them and no backing plate. Every piece cleared is gone from the solid for good, so the shape sags, opens into tunnels, loses corners, and when the last piece goes there is nothing left behind it.
That removes the assumption most match puzzles are built on: a fixed grid to clear against. Adjacency becomes a 3D neighbour query over an irregular solid that changes every move and that the player is also rotating — and the camera still has to keep a legible read on it.
- 120+
- generated levels
- 12
- regions
- 0
- pieces left at the end
- Godot 4
- GDScript
- Procedural generation
- Headless test harness
- Android
03 Mobile · releasing on Google Play and the App Store
PickupStix3D
A physics pile where losing is emergent, not scripted.
A heap of coloured sticks dropped on a round table — an actual rigid-body pile, not a picture of one. Orbit it with a drag, work out which stick is genuinely free, and pull it into a matching bucket, or park it in a four-slot rack and gamble that the right bucket comes round. Pull the wrong one and the rest start rolling, sometimes off the table entirely.
Nothing about the failure state is authored. You lose when a stick has nowhere left to go, which is a consequence of how the pile happened to settle and what you have already taken out of it. That is the appeal, and it is also the problem: a level that physics settles differently every time cannot be hand-verified.
- 120
- levels
- 4
- rack slots
- 1 in 5
- is a bonus round
- Godot 4.6
- GDScript
- Rigid-body physics
- Headless test harness
- iOS
- Android
04 Browser · generative multiplayer
GENOBA
The roster is the dictionary, and it costs nothing to run.
A three-lane battler where you summon by typing. Any word becomes a unit with its own stats and its own generated pixel art, and power runs from a worm at 1 through a dragon at 750 up to things that answer prayers. Around 47,000 WordNet-derived nouns are understood; anything unseen goes to an AI oracle that works out what it ought to be.
The hard requirement was zero running cost: no match server, no sign-in, AI only on a free tier with hard daily caps. That ruled out a shared server catalogue, so the catalogue is deterministic instead — text compiles into an immutable unit definition, and identical text produces an identical unit and identical art on every machine. Two players get the same quokka warlord without anything being synchronised.

05 Browser · real-time multiplayer
Balloon Pop
Real-time multiplayer with no server bill.
One balloon, a capacity nobody is told, and the growing suspicion that you should have stopped a second ago. Players hold to inflate, rates sum while several hold at once, and whoever is still holding when it bursts loses.
It existed to answer a specific question: how little infrastructure does real-time multiplayer actually need? The answer was a Durable Object per room holding the WebSockets and the authoritative clock, plus a static SPA on GitHub Pages — two independent deploys whose only coupling is one build-time URL, both inside free tiers.
- 2–10
- players per room
- ~0.3s
- to test the rules
- $0
- to operate
- TypeScript
- Cloudflare Workers
- Durable Objects
- WebSockets
- React
- Vitest
06 Browser · simulation
Paper Plane
Fold it yourself, and the fold is what flies.
You get a flat sheet, three tools, and a tower in an empty hangar. Draw creases freehand, pick a face up by an edge and drag it over, then throw what you built and watch it go.
The paper is a tree of flat faces joined by hinges: creasing partitions a face, folding rotates one face about one hinge, and nothing is ever mirrored, so a face is exactly where it looks like it is. Either side of a crease can be the side that moves, because the tree is re-rooted into the half that should stay still — which is what makes drawing on a flap standing up at 70° work at all.
- 3
- folding tools
- 0
- scripted flight paths
- ∞
- foldable designs
- TypeScript
- Three.js
- Computational geometry
- Rigid-body aerodynamics
- Vite
07 Browser · action roguelite
Boardgame Roguelite
Five heroes that play like five different games.
Tabletop archetypes rebuilt as action heroes climbing a tower: three waves and a boss per floor, a reward after each, and a run that lasts the better part of an hour. The chess champion fires down ranks and files and cares where everything stands; the tycoon weaponises his bank balance; the general arrives alone and leaves with an army; the tinkerer builds machines that set off other machines.
The design rule was that a hero had to change how the game is played, not which numbers are larger. Every boss chest rewrites an ability rather than scaling it, so by the top floor the rules have stopped applying to you in a way specific to the character you picked.
- 5
- heroes
- 35–60
- minutes per run
- 3+1
- waves and a boss per floor
- TypeScript
- Three.js
- Headless simulation
- Automated balance probe
- Playwright
08 Browser · open world
Bugs to Bucket
Catching them is the easy part.
A fenced 240-metre plot on a sunny afternoon: a pond you can swim, a cliff you can stand on, a cave at its foot, a corn field and a forest. Twenty-eight species live where they ought to — skaters on the water, cicadas up the trunks, something glowing in the dark.
The inversion is the design. Swinging the net is trivial, so the net is not the game: it leaks fast, the bucket leaks slowly, and the more a bug is worth the harder it fights to get out. Value is tied directly to escape pressure, which turns the walk back to the shed into the actual risk and makes a full bucket a liability rather than a score.
- 28
- species
- 240m
- square of world
- 10×
- for a gold find
- TypeScript
- Three.js
- Behaviour-driven spawning
- Vite
09 Browser · precision platformer
Hop to the Top
1,577 metres, and you can see every one of them.
Jump King as a frog. Hold to gather a leap, release to commit to it, and accept that the frog is a physics object from the moment it leaves — as is the ledge you misjudged. Nine zones stack from pond water and oversized lily pads through an ancient tree, a tower nobody maintained, machinery, ice, and cloud that is only ever a strongly worded suggestion.
The whole climb is one connected world rather than a stack of separate levels, and that is a rendering decision made for a design reason: from the clouds you can still see the pond. Falling is only frightening if you can see exactly how much you are about to lose, so the game spends its frame budget keeping the drop visible.
- 1,577m
- pond to moon
- 9
- zones
- 1
- mistake needed
- JavaScript
- Three.js
- Physics-driven movement
- Playwright
- Vite
10 Browser · 3D platformer
Frog Platformer
Every jump in every level, re-flown on every build.
Five levels of low-poly climbing with a portal that stays shut until every bug on the level is inside you. Hold to charge a leap; hold right-click to aim the tongue and release to fire it. The tongue eats bugs and sticks to scenery, so it is a weapon and a grapple depending on what you point it at.
Meadow Steps teaches the charge on honest footing, the green gives way to snow, Whiteout turns the ground to ice, and the last level removes the ground entirely.
- 5
- levels
- 100%
- of jumps re-flown in CI
- 2
- projects' assets reused
- TypeScript
- Three.js
- Automated traversal tests
- Shared rigs
- Vite
11 Browser · endless runner
Sewer Run
A study of predatory design, built honestly.
You are a small log of poo with big dreams, riding the current toward the water treatment plant. Three gates arrive at a time and exactly one is a good idea: multiply your size, bolt on lumps, or take the one that halves you. Jump the weirs, duck the drums, and do not let the heat dry you out, because a poo that stops moving has lost.
The brief was explicitly to take the visual grammar of those unskippable mobile ad games — the three-lane choice gates, the multiplier maths, the escalating crowd — and build the version that is actually a game, responsive on desktop rather than a funnel toward a store page.
- 3
- lanes of choice
- 2
- ways to grow, two to shrink
- 1
- very determined log
- TypeScript
- Three.js
- Vite
12 Godot · physics stacking
Circus Drop
A walrus does not sit on a swan the way you hoped.
Forty-eight low-poly animal figurines across six sets, balanced on a raised carnival deck with a U-shaped moat bin running around it. Set them down carefully, merge matching pairs, and try not to put the whole tower on the floor.
You work it from a console of levers and buttons, with a camera you can walk the entire way around the stage — which matters, because whether a piece is actually seated is a question you can only answer by looking at it from somewhere else. Eleven skins sit on top of that, from the classic to the one that is on fire.
13 Browser · order management
The Sandman
Overcooked, except the orders are people.
A town falls asleep beneath you and every sleeper needs a dream mixed to fit the day they have had and the one they are dreading. Pour coloured sand to exactly the bowl's capacity, in the proportions that sleeper's colour scores call for, and send it before they wake up on their own and count as zero.
Matching is scored out of 100 against a colour model rather than a recipe lookup, so dreams are near-misses rather than right or wrong, and three deliveries above 85 in a row start a multiplier. Nights ramp by adding mechanics — plot lines on night two, antagonists on night three, and the free aura readings taken away.
14 Browser · a joke, executed properly
The British Chef
Three menu options. One outcome. That is the joke.
You are sat at a kitchen table and offered breakfast, lunch or dinner. Whichever you pick, the camera pushes in on a slice of toast, a tin of beans arrives from above, tips over, and empties itself across it. Order again and the kitchen does exactly the same thing, with a receipt that gets less apologetic each time.
One shared timeline drives all three options — same kitchen, same light, same tin. The repetition is implemented deliberately rather than achieved by copy-paste; the only things that vary are the name on the receipt and the flavour text.
15 Browser · instrument
1-Minute
Find out whether you know how long a second is.
Click or hit space once a second, sixty times. That is the entire game, and it is over in a minute.
The product is the report at the end: how far off a minute you landed, your average interval, your steadiness, your fastest and slowest gap, and the drift that crept in while you were quite certain you had it. A per-tap chart shows where you lost it, which is almost always somewhere around tap forty.
- 60
- taps
- 6
- statistics reported
- 0
- dependencies
- JavaScript
- No build step
- No dependencies

paintbucket