PromptGolf ⛳

Inspiration

We've all played Jackbox at a party and felt that electric moment when the room erupts, not because a game mechanic fired correctly, but because someone said something funny.

We wanted to capture that energy but swapb the punchline for a creative challenge: can you describe an image in words well enough to fool an AI into recreating it?

The "golf" framing came naturally. In golf, fewer strokes is better. Here, a tighter, more precise prompt is better, you're not writing an essay, you're landing a ball on a green. The constraint is the game.

What We Built

PromptGolf is a multiplayer party game where players race to recreate a target image using text prompts fed to an AI image generator. Every round:

  1. A target image appears — generated from a secret prompt using FLUX schnell
  2. Players have a configurable timer (30–120s) to write prompts and generate candidate images
  3. Each player picks their best attempt
  4. Everyone votes for the image (not their own) they think comes closest to the target
  5. Votes are points. Cumulative highest score across rounds wins.

The scoring is purely social, no algorithm judges your image. Voters are the jury, which keeps things funny and human.

Tiebreaks cascade: cumulative score → character count → token count → submission timestamp:

Tech Stack

Built in 24 hours by a team of 3 on:

  • Next.js 15 App Router + TypeScript + Turbopack
  • fal.ai FLUX schnell — 4-step image generation, ~1s warm
  • Upstash Redis — all game state, 1h TTL, no SQL
  • Pusher Channels — presence + real-time events (no polling)
  • ElevenLabs Scribe v2 — push-to-talk voice prompting
  • Framer Motion + Howler.js — animations and sound effects
  • Vercel — deploy target, @vercel/og for share cards

How We Built It

We split into three parallel tracks from the start: backend (rooms, Redis, APIs), UI (theme, lobby, game screens) and content (target image curation).

The single biggest architectural decision was keeping all state in Redis, no database, no ORM, just JSON blobs with 1-hour TTLs. For a party game that lives and dies in a single session, that's the right call.

Real-time sync uses two layers: REST APIs mutate Redis (source of truth), and Pusher broadcasts the result to every connected client. Clients never poll. Pusher presence channels handle connectivity detection automatically, when a tab closes, pusher:member_removed fires and we start a 30-second grace timer before DNF'ing the player.

The server is authoritative on timing. Every phase transition stamps phaseEndsAt server-side. Clients render Math.max(0, phaseEndsAt - Date.now()) and call advance when it hits zero, the server rejects early calls with a 409 and the real deadline, so clients can't shave time.

Challenges

Scoring philosophy. We originally planned algorithmic scoring using CLIP embeddings to measure image similarity how close is your generated image to the target, numerically? We built it, calibrated it, and dropped it the same day.

The threshold was impossible to tune fairly across wildly different image styles, and worse, it removed the social element that makes party games fun. Pure player voting was simpler, fairer, and more entertaining.

Timer drift across devices. With players on phones, laptops, and tablets all rendering the same countdown, clock skew becomes visible. Clients computing phaseEndsAt - Date.now() can drift by hundreds of milliseconds.

We solved it by stamping transitions server-side and having the server reject premature advance calls, good enough for a party game where ±1 second doesn't matter.

Mobile push-to-talk. getUserMedia only works on HTTPS, MediaRecorder MIME support varies wildly by platform (iOS only supports audio/mp4, not audio/webm), and iOS Safari requires mic permission to be triggered inside a direct user gesture.

We handled it with a MIME type probe at record-start and a lobby permission button that pre-warms the browser permission dialog before the game starts.

Cold starts. FLUX schnell on fal.ai has a ~3–4 second cold start on the first request. We fire a dummy generation request when the lobby mounts to pre-warm the worker, so by the time the host hits Start the model is already hot.

What We Learned

  • Social mechanics beat algorithmic scoring for party games every time.
  • Redis-as-primary-store is underrated for ephemeral, session-scoped applications.
  • Pusher presence channels eliminate an entire class of "who's online?" bookkeeping.
  • Ship the spectator view early, it transforms a demo from "people staring at their phones" into a shared moment on the big screen.

Built With

Share this project:

Updates