Dev Patel
Jul 2026 → PresentSource

Rune Duel

A real-time 3D wizard duel played by drawing runes with a finger or a mouse — in any browser, with the server re-drawing every stroke so a client cannot lie about it.

A practice duel in the arena — health bars, the five-spell hand, and the rune you draw to cast.
Type
Artifact
Where
Personal
When
Jul 2026 → Present
Duration
2 mo
Status
Live on web
Source
GitHub
The problem

The problem

Gesture spellcasting is a VR idea. I wanted the feel of it — draw a shape, a spell fires — on the surfaces people actually carry: a phone screen and a trackpad. No headset, no install, no download between wanting to play and playing.

Taking it online turns a rendering problem into a trust problem. The moment two strangers duel over the internet, the number that decides how much damage a spell does is being computed on a machine I do not control.

What I built

What I built

The game is React Three Fiber over Three.js: a first-person arena, an opponent across from you, and a $1 gesture recognizer that scores a single-stroke rune against a template. Power is accuracy weighted against speed, so a clean slow rune and a fast sloppy one are both punished — that weighting is the whole game, and it is why drawing has to feel like skill rather than a menu with extra steps.

The recognizer and the combat resolution live in a shared workspace that both sides import. Offline practice runs them in the browser; an online duel runs them again on a Node and Socket.io server that re-recognizes the raw stroke points itself and ignores whatever the client claimed the draw was worth. Same code on both sides of the socket, so the two can never disagree about what a drawing meant.

Accounts, ranked Elo and the leaderboard sit in Supabase — Postgres with row-level security and a function that settles both players' ratings in one transaction — while guests keep an unranked record on device and never see a sign-in wall. The same web build is wrapped with Capacitor for iOS, so the phone app and the browser are one codebase drawing on one pool of opponents.

Screens

Screens

Before the duel: five spells chosen from thirty, quick match or a room code, and a graphics tier detected per device.
Before the duel: five spells chosen from thirty, quick match or a room code, and a graphics tier detected per device.
What I considered

What I considered

Rebuild the game natively in Unity for iOS

Rejected. Better frame pacing and store-native tooling, but it forks the product into two codebases and splits the player pool — fatal for a 1v1 game nobody has heard of yet.

Stay WebXR-first and require a headset

Rejected. It is the obvious reading of a spellcasting game, and it shrinks the audience to people who own a headset and are willing to put it on before they know whether the game is any good.

Flat-first web build, wrapped with Capacitor for the App Store

Chosen. One build, one match server, one matchmaking pool; the phone gets an installable app and everyone else gets the same game at a URL. The cost is the WebGL ceiling on graphics, which is the cheapest thing in the list to give up.

Results

Results

30spells, five to a deck
Serverre-scores every stroke
1codebase, web + iOS
  • Playable in a browser today: practice against three AI tiers offline, or duel someone through quick match or a six-letter room code.
  • Online 1v1 verified end to end over the internet, with the server scoring strokes rather than trusting the client's claim.
  • Ranked accounts, Elo and a leaderboard on Supabase, with guests kept playable and unranked on device.
  • Packaged for iOS with Capacitor from the same build and submitted to the App Store.
Architecture

Architecture

Browser / iOSReact Three Fiber· Capacitor shellStroke capturepoints + timingShared module$1 recognizer· combat rulesMatch serverNode · Socket.iore-recognizes server-sideSupabaseaccounts · Elo· leaderboard
The recognizer is the same module on both sides of the socket. The server is the copy that counts.
In hindsight

In hindsight

Rooms live in one server process's memory. That kept the matchmaking code small and honest, and it makes the match server a single point of failure — when its hosting lapsed, online play went dark while practice and pass-and-play carried on regardless. Moving rooms behind Redis is the difference between a game that survives a restart and one that does not.

Stack

Stack

JavaScriptReactThree.jsReact Three FiberNode.jsSocket.ioSupabasePostgreSQLCapacitorViteFly.ioVercel