🌍 Inspiration

Reddit is built on collective action — upvotes, downvotes, communities deciding together what matters. We asked: what if that same energy could shape something alive? Not just content rankings, but a living world that visibly responds to how people vote?

TerraVote was born from that idea. A terrarium ecosystem — temperature, moisture, biodiversity, pollution — that the entire subreddit steers together, one vote per day, with consequences that carry forward into tomorrow.


🎮 What it does

TerraVote is a persistent community ecosystem game embedded directly in Reddit as a Devvit WebView post.

Every day, the community is presented with 3 ecological events — things like Butterfly Migration, Acid Rain, or Wildfire — each with measurable effects on four ecosystem stats. Players cast one vote per day. At midnight UTC, the winning event is applied, the world visibly changes, and the next day begins.

Key features:

  • 🌿 Living Phaser 4 terrarium — a fully animated canvas ecosystem: fireflies with Lissajous float paths, sine-wave water surface, procedural grass animation, parallax cloud layers, lightning strikes, shooting stars, pollen drift, leaf fall, embers, snowflakes, and 12 wandering creatures with glow halos — all driven by the current ecosystem stats in real time
  • 📊 Four stats, real consequences — Temperature, Moisture, Biodiversity, and Pollution shift with each day's winning vote. A healthy ecosystem looks lush and alive. A damaged one turns grey, hazy, and barren.
  • 🚨 Crisis Mode — when stats hit danger thresholds, normal vote options are replaced with 5 emergency interventions. The canvas flashes red, a shockwave rings out, lightning strikes, and ash begins to fall. The community must act together to pull the ecosystem back from the edge.
  • 📖 Ecosystem Chronicle — a scrollable history panel recording every past day: the winning event, vote count, ecosystem health score, and whether it was a crisis day.
  • 🔥 Streaks & Ranks — daily voting builds a streak. Players progress from Wanderer → Seedling → Sprout → Guardian → Elder → Terraformer, with a progress bar toward the next rank.
  • Tipping Point — when a vote flips the leading option, an 80-particle shockwave + expanding ring + camera shake fires across the canvas.
  • 📣 Share to Comments — players can post their vote as a Reddit comment, showing their rank and streak to the community.
  • 🖱️ Tap Interactions — the canvas is fully interactive: tap the pond for ripple rings and a splash, tap a tree to shake loose a leaf burst, tap creatures to make them jump, tap fireflies to scatter them, tap the sky to trigger a shooting star.
  • 💡 Contextual Tooltips — hovering any stat chip, the rank badge, or the chronicle/leaderboard buttons reveals a plain-English explanation of what it means and why it matters. Tooltips are edge-anchored so they never clip off screen.
  • 🎓 First-Time Tutorial — new players see a 3-step guided overlay (Welcome → Stats explained → Streaks & ranks) with dot navigation. It fires exactly once per user, persisted in Redis, and can be reopened any time via the ❓ button.

🔨 How we built it

TerraVote runs entirely within Reddit using the Devvit Web platform. The architecture is a clean client/server split:

Frontend (Phaser 4 + DOM overlay)

  • All animation runs in a Phaser 4.1.0 Scene — procedural textures generated at startup, no external assets
  • UI text (stats, vote options, chronicle panel) lives in a DOM overlay with pointer-events: none on spacer regions so taps reach the canvas beneath; interactive children opt back in with pointer-events: auto
  • Pure-CSS [data-tooltip]::after system with per-element edge anchoring via :first-child / :last-child overrides — no JavaScript needed
  • Responsive via clamp() font sizes and 100dvh layout

Backend (Hono + Devvit server)

  • A Hono HTTP router handles /api/init, /api/vote, /api/share, /api/chronicle, and /api/mark-ftue
  • Ecosystem state is stored in Redis (@devvit/web/server) under namespaced keys
  • FTUE seen-state is stored per user under tv2:ftue:<username> and returned on every /api/init call — localStorage is not used because Devvit WebView iframes are sandboxed and don't persist storage across sessions
  • Day rollover is detected on getInitData() — winner effects are committed, chronicle is appended, and a bot comment is posted to the thread
  • A deterministic mulberry32 PRNG seeded by the day number selects the daily options, ensuring every player sees the same 3 choices

🧗 Challenges

  • all: unset + box-sizing — Resetting button styles wiped box-sizing: border-box, causing vote option content to overflow on mobile. Required restoring it explicitly on every reset element.
  • Phaser 4 breaking changes — Phaser 4 removed resolution: dpr from GameConfig, requires override update(), and changed several particle emitter APIs from Phaser 3.
  • Crisp text on HiDPI — Phaser canvas text pixelates on retina screens. Solution: move all text to DOM overlay, keep Phaser purely for visuals.
  • Reddit postId prefixcontext.postId already includes the t3_ prefix; manually prepending it caused InvalidIdsError on comment submission.
  • Concurrent day rollover — Multiple simultaneous requests on rollover could double-post bot comments. Fixed with a Redis flag per day acting as a distributed lock.
  • pointer-events: none and hover events — The DOM overlay uses pointer-events: none globally to let canvas taps through. This silently blocked all CSS :hover states on stat chips, breaking tooltips. Fixed by opting the stats bar back in with pointer-events: auto and leaving only the spacer transparent.
  • FTUE persistence across sessionslocalStorage doesn't survive Devvit WebView iframe reloads, so the tutorial re-appeared every session. Moved the seen-flag to Redis with a dedicated /api/mark-ftue endpoint called immediately on first display.

🌱 What we learned

Building for Reddit's Devvit platform means embracing constraints creatively. No persistent WebSockets, no external CDNs, no server-side rendering — just Redis, Hono routes, and a Phaser canvas talking to DOM. Those constraints pushed us to build something genuinely self-contained and fast.

The most satisfying discovery: a Phaser update() loop drawing sine-wave water, waving grass blades, and floating fireflies — all computed from the live ecosystem state — makes a vote feel consequential in a way no static UI ever could.

Built With

Share this project:

Updates