Projects

$ ls ~/games

Games

Four original games for the browser, each designed, written and shipped by me, from the first greybox to the deploy pipeline. Three are playable right now and the fourth has its first level up: no download, no install, just click and play.

They're also where I push the habits I bring to client work: game rules in pure, tested TypeScript kept apart from the rendering, content in validated data files instead of code, and CI that ships every green push.

Tethered Darkness — The first boss, the Mire Widow, with the party's turn gauges filling on the right.
The first boss, the Mire Widow, with the party's turn gauges filling on the right.

Playable now: start to finish, final art in progress

Tethered Darkness

Gothic horror JRPG

Every grave in Hollowmere has a lantern, and the lanterns keep the dead below. Tonight they are going out.

A disgraced knight, an occultist and a church cantor are left on a fen jetty by a ferryman who won't wait. Up in Broodhollow the churchyard lanterns are dying, the dead are climbing out, and somewhere beneath the water a drowned bell screams each time another light fails. They follow the sound down.

It's the 16-bit Final Fantasy you grew up with, with the dial turned from adventure to dread: fog, candlelight, drowned churches, and things that move just past the edge of your lantern. The horror comes from what you can't see, and from what the light costs you.

Play Tethered Darkness
  • Light is a resource The floors before every boss are dark. Spend scarce lantern oil to light them, or fight in a shrinking circle of light against monsters 20% stronger.
  • Real-time turn gauges Heroes and monsters fill their gauges in real time, so hesitating in a menu is a decision too.
  • Learn from the dead The cantor Absorbs enemies to learn their skills and fill in the bestiary, the occultist steals, and the knight takes the hit for whoever needs it.
  • No random battles Every enemy is on the map. Slip past the fights you don't want, or pick them for what they carry.
  • Six regions down From the fens through frozen wastes, jagged peaks, a sunless chasm and a foundry of lava, then back through a portal to a Hollowmere gone wrong, and down to whatever has been ringing.
  • Five endings At the bottom, a choice: silence the bell, or relight every lantern and choose who stays below to tend them. Each of the first five regions hides an optional dungeon with a charm, and finding all five changes what that choice costs.
  • Plays anywhere Keyboard, gamepad or a touch D-pad on your phone, with cloud saves across three slots.

Under the hood

TypeScript, Phaser 4 and Vite, with every enemy, skill and item in JSON checked by Zod, and branching dialogue written in Ink. The battle engine is pure TypeScript with a seeded random generator and no Phaser imports, so a headless simulator can play any encounter thousands of times, and a route tool plays each region in story order, carrying levels, gear and items from fight to fight, to balance every boss against the level a player really arrives at. Accounts and cloud saves run on a small Symfony 8.1 API with MySQL, and every green push to main deploys itself through GitHub Actions.

The Village of Suffering — Concept art, made locally with SDXL: the army, the demons, the plague doctor, the Red Cardinal, a necromancer and the graveyard.
Concept art, made locally with SDXL: the army, the demons, the plague doctor, the Red Cardinal, a necromancer and the graveyard.

Playable now: the whole loop, final art in progress

The Village of Suffering

Grim isometric real-time strategy

Villagers dig up the graveyard. The dead rise as skeletons. The skeletons go below to fight demons for the organs that make the next army stronger.

Your village knows exactly what becomes of its dead, and works for you anyway. By night the gravediggers sink the graveyard one level deeper. By day you stitch what they find into skeletons, arm them at the forge, and march them down the one grave that breaks open into a fresh dungeon at dawn.

Every demon down there drops something worth stitching in: a heart for strength and speed, a brain for resistances and a better grip on a weapon. The deeper you dig, the stronger the army you can build to dig deeper still, until something on the twenty-fifth level wakes up and notices you.

Play The Village of Suffering
  • Build your army from the dead Six bones make a skeleton; up to twelve make a monster with extra skulls, arms and torsos. Organs, flesh and armour layer on top, with no fixed recipes.
  • A new dungeon every dawn Each morning one grave breaks open into a freshly generated dungeon that closes at the next dawn. An army still below is stranded and has to dig its own way out.
  • Bring your own light Armies below see only by their light keeper's torch or lantern. Lose it and your skeletons fight half-blind.
  • Trouble from above The church next door is watching. Dig up too many of its dead and suspicion becomes a mob, while the Red Cardinal steals corpses from your ranks.
  • Every villager counts One small pool of villagers is split across eight jobs each dawn. The dead never come back, but they rise into your army with a perk from the trade they died in.
  • Loot worth going deeper for Four tiers of every weapon and armour piece at the forge, gems with rolled affixes from the deepest lairs and sockets to set them in, and a trading post that sells on credit until dawn.
  • Teaches itself, plays anywhere A guide takes new players through their first three days a prompt at a time, then first-time hints take over. Play with a mouse, or on a phone held sideways with tap, drag, pinch and box-select, on Gentle, Grim or Merciless.

Under the hood

TypeScript and Phaser 4 over a simulation in plain TypeScript that runs on a fixed tick with a seeded random generator, so any fight can be replayed exactly. Isometric is only how it's drawn: the rules work on a simple grid with A* pathfinding. Bones, organs, demons, gear and difficulty all live in JSON, and a scripted player plays whole seasons on every difficulty inside the test suite to check the balance. The art pipeline renders characters from 3D models in Blender as separate layers (bones, flesh, armour, weapons) in eight directions and stacks them at runtime, the way Diablo II did, while the village itself is painted in ComfyUI over Blender blockouts. Saves use the same Symfony API as Tethered Darkness.

Assimilate — Three moments on the live build: aiming at a match, two Tentacles assimilating into a Siamese, and the jar overflowing.
Three moments on the live build: aiming at a match, two Tentacles assimilating into a Siamese, and the jar overflowing.

Playable now: final art and sound to come

Assimilate

Physics merge puzzler

Two of a kind that touch assimilate into the next thing up. It's fifteen steps from a heart to Nanna.

Drop things into the jar. Two of a kind that touch become one of the next thing up: hearts into leaves, kiwis into cats, cats into things with too many eyes, all the way up to Nanna. The pile only grows, and the run is over when it stays above the line for two seconds.

It's the merge game you know, with rot in it. Anything left a minute lights a fuse and burns out into a dead black block, and a black block left five minutes explodes and throws the jar about. You can't nurse one old stack for ever: the longer a piece sits, the more of a problem it becomes.

Play Assimilate
  • Fifteen tiers A heart, cannabis leaves, a kiwi, four breeds of cat and some grotesques, up through the Assimilator to Nanna. Merge two Nannas and both vanish for a bonus.
  • Fuses and black blocks Pieces age from the moment they land. Merge one before its fuse runs out or it dies as a black block, which only merges with another of its kind to come back to life.
  • Explosions A black block left five minutes goes off, scoring nothing and throwing everything near it outward, sometimes over the line.
  • Chains that pop Each merge within 0.6 seconds of the last builds a combo worth up to three times the points, and bigger merges pop harder, shoving the pile into a new shape.
  • One thumb A portrait game made for phones: aim and drop with a thumb, a mouse or the keys.

Under the hood

TypeScript and Phaser 3 for drawing, input and scaling, over a rules engine in plain TypeScript on Matter.js, used directly rather than through Phaser's plugin, so Vitest plays whole runs headless and any run replays from its seed. Merges are found by measuring after each physics step instead of waiting for collision events, because Matter's circles are polygons that can rest a pixel apart while looking touching. Every tuning value lives in one file, with a bot playing seeded runs at three paces to tune it. The placeholder art is fifteen subjects drawn in code as SVG and packed into a WebP atlas, the sound and music are synthesised with Web Audio, and every green push deploys itself.

Life Detected — The grunt, remade: a flayed miner sculpted on a MakeHuman body, lunging out of the dark.
The grunt, remade: a flayed miner sculpted on a MakeHuman body, lunging out of the dark.

In development: the first level is playable

Life Detected

Sci-fi horror first-person shooter

A motion tracker you listen to, and corridors darker than they should be.

You hear it before you see it. The tracker sweeps once a second, and every ping shows movement, never walls: something is in here with you, and you can't tell where the room ends. Creatures give themselves away by sound first, so the audio isn't decoration. It's the level design.

It's built the way the best mid-90s shooters were, where every level has a job to do, not just an exit to find: get the keycards, destroy the nests, bring the survivors out, with a briefing going in and a tally on the way out. The look aims well past the era: sculpted creatures, scanned materials and a torch that throws real shadows, somewhere between Doom 3 and the 2016 Doom.

Play the first level
  • The motion tracker A sweep and a ping every second, with blips fading between sweeps, so you're always acting on a picture that's already out of date. Hold it up and the gun goes away.
  • Objectives, not just exits Two to four objectives per level, with progress you can check mid-mission. The first wants two keycards found and sixteen sacs destroyed before the exit lets you leave.
  • Things that hatch Sacs rooted into the floor open as you come near and hatch a parasite that leaps for you. Grunts, flayed miners, hunt you through the corridors and open doors.
  • Your torch is the main light A shadow-casting flashlight, fog and glow over corridors that are dark on purpose.
  • Instant to play Runs in the browser on WebGPU where it's available and WebGL 2 everywhere else, with keyboard and mouse or a gamepad.

Under the hood

Three.js with its WebGPU renderer and automatic WebGL 2 fallback, over a pure TypeScript engine stepping at a fixed 60 Hz with a seeded random generator, so a Vitest bot plays the first level from briefing to tally with no browser at all. Hand-built grid levels in JSON checked by Zod, and grid collision and A* for the creatures. The grunt is sculpted by script on a MakeHuman body in Blender, baked onto a game mesh with normal maps and rigged; the walls and floors are scanned CC0 materials packed as KTX2. The first level goes in front of playtesters before the campaign gets built.

$ ls ~/apps

Web apps

Full-stack tools built to solve a real problem, running live on their own subdomains.

Miniature Collection Tracker

Miniature Collection Tracker — Army List screenshot Miniature Collection Tracker — Army Detail screenshot Miniature Collection Tracker — Unit Detail screenshot

Problem: Tracking a growing tabletop miniature collection — armies, units, painting progress, and game history — in spreadsheets doesn't scale and leaves no visual record.

What was built: A full-stack tracker with user accounts, army/unit management, game logging, and photo galleries for painted miniatures.

Tech used: Symfony (API/backend), React + Vite (frontend), MySQL, deployed via GitHub Actions CI/CD.

View project

Choose-Your-Own-Adventure Builder

Choose-Your-Own-Adventure Builder — Homepage screenshot Choose-Your-Own-Adventure Builder — Gamebooks screenshot Choose-Your-Own-Adventure Builder — Play Story screenshot Choose-Your-Own-Adventure Builder — Play Combat screenshot

Problem: Writing a branching "choose your own adventure" gamebook by hand means tracking dozens of paragraphs, battles, and item/character state from memory — error-prone past a certain size.

What was built: A server-rendered authoring tool for building and playing branching adventures — paragraphs, battles, heroes, equipment, and magic — with full CRUD for authors and a playable runtime for readers.

Tech used: Symfony, Twig, MySQL/MariaDB. Actively maintained — recently upgraded across three major Symfony versions (6→8) and to PHP 8.4.

View project