A peer-to-peer party game in the spirit of Wavelength. One player (the Psychic) sees a hidden target on a spectrum between two odd extremes โ "Hot โ Cold", "Culturally significant โ insignificant" โ gives a short clue, and the team drags a live shared dial to guess it. Closer = more points. Psychic rotates every round. Big celebratory finale. Then back to the lobby.
No server. No database. No accounts. No setup. Just open the page and play.
๐ฎ Play: https://vviseguy.github.io/frequency/
- One person taps Host a new game โ gets a 4-letter room code + QR + share link.
- Everyone else opens the link (or enters the code) on their phone.
- The host taps Start โ no settings to fiddle with.
- Everyone writes a clue at the same time for their own hidden target on a weird spectrum.
- The game then cycles through each player's clue: everyone else drags the shared dial and taps Lock it in; the target is revealed and the clue-giver scores by how close the group got.
- Game length auto-scales with the room โ smaller groups give more clues each (3 per person โค4 players, 2 per person โค8, 1 in big groups). No options to set.
- A scoreboard breather between sets, then an animated recap builds to the champion and everyone bounces back to the lobby.
- Hosting auto-falls-back: if the host closes their tab/loses signal, the most senior player (earliest to join) seamlessly becomes the new host and the game continues. Keep the host on a device that won't sleep for the smoothest ride.
- Reactions (๐๐๐ฅ) pop a sound and rain that emoji across everyone's screen โ a small budget per turn so it never gets noisy.
- Top-left menu (the โฐ): light/dark mode, sound on/off, how-to-play, and leave-game. The drifting background calms to a freeze during focused play.
Prompts are organised into versioned topic packs under
public/prompts/ โ currently 22 packs, ~880 spectrum pairs.
The host can pick which topics are in play from the lobby (๐ฒ Topics).
prompts/index.jsonโ lists which pack ids are active.prompts/<id>.jsonโ one topic pack:{ name, emoji, version, prompts }.
To add prompts: open a pack file on GitHub, append a line, bump its version,
commit to main. No IDs to manage โ they're derived automatically.
{ "left": "One extreme", "right": "The other extreme" }To add a whole topic: drop a new prompts/<id>.json (same shape, with a name
and an emoji) and add its id to prompts/index.json. It shows up as a card in the
topic picker automatically. The site auto-redeploys in ~1 minute.
npm install
npm run dev # http://localhost:5173/frequency/ (also exposed on your LAN for phone testing)
npm run build # typecheck + production build into dist/
npm run preview # serve the production build locallyTo test multiplayer locally, open the dev URL in several browser tabs/devices โ create a room in one, join with the code in the others.
npm test # Vitest unit suite (scoring, reducer/state machine, rounds, room codes, migration)
npm run test:e2e # Playwright: real multi-peer WebRTC e2e against a local PeerJS brokerThe e2e suite spins up its own local PeerJS server (no public broker, fully
deterministic) and drives three isolated browser contexts through: a complete
2-round game (host โ join โ clue โ guess โ reveal โ recap โ lobby) and a
host-migration scenario (kill the host mid-lobby; assert the most-senior player
takes over and can keep running the game). CI runs both via
.github/workflows/ci.yml, separate from the Pages
deploy so a flaky network never blocks shipping.
P2P uses PeerJS's free public broker for signaling only;
gameplay traffic is direct WebRTC between devices. Two situational env vars (create
a .env file) help on hostile networks:
| Var | Purpose |
|---|---|
VITE_TURN_URL / VITE_TURN_USER / VITE_TURN_CRED |
A TURN relay for strict/cellular NATs where direct WebRTC fails. |
VITE_PEER_HOST / VITE_PEER_PORT / VITE_PEER_PATH |
Point signaling at your own self-hosted PeerServer instead of the public broker. |
Neither is needed for normal play (same Wi-Fi / typical home networks).
Push to main โ GitHub Actions builds and publishes to GitHub Pages
(.github/workflows/deploy.yml). Vite base is /frequency/; routing keys off the
?room= query so deep links survive a hard refresh.
Built with React + TypeScript + Vite + Tailwind + Framer Motion + PeerJS.