VaultBreakers
A daily shared vault heist that lives inside a Reddit post where everyone breaks into the same vault, and the comment section is the multiplayer.
Inspiration
Two things collided. First, the r/Wordle insight: a game gets dramatically more social when everyone faces the exact same challenge on the same day the fun isn't just playing, it's comparing notes afterward. Second, tabletop push-your-luck games like Diamant/Incan Gold, where the entire design is one delicious question: bank what you have, or go one room deeper?
Reddit's Devvit platform felt like the perfect home, because Reddit already has the missing multiplayer layer built in: the comment thread. I didn't need lobbies, matchmaking, or real-time sync. I needed a 60–90 second run that ends with something worth posting.
What it does
Every day at midnight UTC, the app publishes a new vault post. Each player gets one run per day:
- Draft 1 of 3 ability cards (Lucky Boots, Treasure Hunter, Second Sight).
- Enter a eight-room vault with the same layout for every player that day and mixing it with timed traps (3-second decisions), riddle-style choices, and chests.
- At chests, choose: open it for more gold, bank your haul at a checkpoint, or retreat safely.
- Escape with everything, retreat with what you banked, or die and lose half your unbanked gold.
- Your result posts as a native Reddit comment (opt-in by default, moderator-configurable) that comment is the leaderboard entry, the brag, and the bait for the next player.
How I built it
Stack: TypeScript end-to-end · Devvit Web · Phaser 4 for the animated stage · Hono on Devvit's serverless runtime · Redis for persistence · Vite. Shared request/response types in src/shared keep client and server honest.
The daily vault is pure determinism. The date string is hashed with FNV-1a into a mulberry32 PRNG, which Fisher–Yates-shuffles the room library. Same seed, same vault, for everyone, with zero stored content. One design constraint made this fun: traps must never appear back-to-back. For $k = 3$ traps in $n = 6$ ordered rooms, the number of non-adjacent placements is
$$\binom{n-k+1}{k} = \binom{4}{3} = 4$$
so the generator picks one of exactly those four legal layouts, then deals shuffled trap and calm rooms into it.
The economy is tuned around one inequality. Suppose you carry $u$ unbanked gold into a room you'll survive with probability $p$, where surviving pays $r$ and failing on your last heart leaves you $\lfloor u/2 \rfloor$. Pushing on beats banking when
$$p(u + r) + (1-p)\frac{u}{2} > u \iff p > \frac{u}{u + 2r}$$
Room rewards escalate (35 → 120 gold) specifically so this threshold hovers near a coin flip late in the vault the "one more room?" decision should always hurt a little.
The server is the only authority. The client never learns which choice is safe; it only sends intents (choose, open, bank, retreat, timeout) and receives a view model with the answer stripped out unless the Second Sight ability explicitly reveals it.
Challenges I ran into
- Trap timers vs. untrusted clocks. A 3-second timer enforced by the server breaks the moment a phone's clock is skewed. Fix: every response carries
serverNow, and the client reconstructs a local deadline as $d_{local} = t_{client} + \max(0,\, d_{server} - t_{serverNow})$, while the server independently rejects late answers with a small grace window. Clock skew became irrelevant. - Idempotent daily posts on serverless. The cron trigger, the install trigger, and a moderator menu action can all try to create "today's post" concurrently. A Redis
SET NXlock with a TTL, plus careful cleanup on failure, guarantees exactly one post per day no matter who fires first. - Live data migration mid-project. Splitting a single
lootfield into banked/unbanked/lost piles would have corrupted existing runs, so every read passes through amigrateRunshim that upgrades old records on the fly. - Juice with zero art assets. Every texture sparks, coins, dust motes is generated at runtime with Phaser's
Graphics.generateTexture. Torch flames, gold rain, camera shake, and the depleting trap ring are all procedural, and every one of them is skipped underprefers-reduced-motion. - Phaser inside a Reddit webview. The embedded viewport resizes unpredictably, so the scene runs in
RESIZEscale mode with a debounced restart, rebuilding the stage from a serialized state machine so nothing visual is lost.
What I learned
- Determinism is content compression. One hashed date string is the day's level no level storage, no sync protocol, and the shared-challenge social hook comes free.
- Server-authoritative design matters even for tiny games. The moment results become public comments, someone will open DevTools.
- Design for the comment, not just the run. Framing the result as a story ("Fell in Room 4, lost 85 unbanked gold") shaped the whole loot system banked vs. at-risk gold exists because it makes better sentences.
- Platform fluency pays off: Devvit's scheduler, settings, menu actions, Redis, and
runAs: USERcomment scopes each replaced a subsystem I'd otherwise have built by hand.
What's next
The GDD already goes further than this slice: daily leaderboards, streaks, Snoovatar-based characters so your actual Reddit avatar runs the vault, a larger trap/ability library, and an earned second-attempt economy.`
Built With
- css3
- devvit
- eslint
- hono
- html5
- javascript
- node.js
- phaser.js
- redis
- serverless
- typescript
- vite
Log in or sign up for Devpost to join the conversation.