A real-time multiplayer number guessing game with voice chat, built with React, Node.js, Socket.io, WebRTC, and Tailwind CSS.
Players join a room with a code, one player secretly picks a number, and everyone else races to guess it. The server responds to every guess with ▲ UP, ▼ DOWN, or ✓ CORRECT.
| Feature | Details |
|---|---|
| 🎮 Real-time multiplayer | 2–6 players per room via Socket.io |
| 🔢 Configurable range | 1–10,000, set by host when creating room |
| ⏱ Turn timer | Off / 30s / 1m / 2m / 3m / custom — set by host |
| 🎤 Voice chat | Peer-to-peer WebRTC audio, no extra server needed |
| 🔇 Mic + Speaker mute | Each player controls their own mic and speaker independently |
| 💬 Text chat | Real-time chat with emoji reactions in every room |
| 🏆 Scoreboard | Live win counts, round-robin picker rotation |
| 📱 Mobile-first | Bottom tab bar on phones, 3-column layout on desktop |
| 🔔 Exit confirmation | Popup before leaving or refreshing mid-game |
| 🔄 Auto-reconnect | Rejoins room automatically if connection drops |
| 🔒 Anti-cheat | Secret number never sent to clients — server only |
updown-game/
├── package.json ← root convenience scripts
├── render.yaml ← Render one-click deploy config
├── README.md
│
├── server/
│ ├── server.js ← Express + Socket.io + WebRTC signaling
│ ├── rooms.js ← In-memory room store & player management
│ ├── gameLogic.js ← Secret number, guess evaluation, rotation
│ └── package.json
│
└── client/
├── index.html
├── vite.config.js
├── tailwind.config.js
├── vercel.json ← Vercel deploy config
└── src/
├── main.jsx ← React entry point
├── index.css ← Tailwind + global styles
├── socket.js ← Socket.io singleton
├── sounds.js ← Procedural sound effects (Web Audio API)
├── useVoiceChat.js ← WebRTC voice chat hook
├── App.jsx ← Root component, view routing, global listeners
├── Home.jsx ← Create / Join room + voice toggle
├── Lobby.jsx ← Waiting room with player list
└── GameRoom.jsx ← Main game: picking, guessing, chat, voice
Home page → Create / Join room → Lobby → Game starts
↓
Round 1: Picker chooses secret number
↓
Guessers submit numbers → Server responds: UP / DOWN / CORRECT
↓
First correct guess wins the round → Scores updated
↓
Host clicks "Next Round" → Picker rotates to next player
↓
Repeat
| State | Who can act |
|---|---|
lobby |
Host starts the game |
picking |
Current picker sets secret number |
guessing |
All non-pickers submit guesses |
roundEnd |
Host advances to next round |
Simple round-robin: nextPickerIndex = (currentIndex + 1) % players.length
The secretNumber field is never sent to clients during active play. The publicRoom() function in rooms.js strips it before every broadcast. It is only revealed in the roundWon event after the round ends.
Voice chat is peer-to-peer — the server only relays connection setup signals, never the audio itself.
Player A joins voice
↓
Server tells Player B: "A is in voice"
↓
Player B creates RTCPeerConnection, sends offer to A via server
↓
Player A answers → ICE candidates exchanged via server
↓
Direct audio connection established between A and B
↓
Audio flows peer-to-peer (server not involved)
| Control | What it does |
|---|---|
| Join Voice | Requests mic permission, connects to all voice peers |
| Mic button | Mutes / unmutes your microphone |
| Speaker button | Mutes / unmutes all incoming audio (deafen) |
| End | Leaves the voice call |
Both mic and speaker can be toggled independently. You can hear others while muted, or talk while deafened. Mute states apply instantly without any reconnection.
- Uses Google's public STUN servers (free, no config needed)
- Works on ~85–90% of consumer internet connections
- For users behind very strict corporate NAT, a TURN server would improve reliability
- Enabled via toggle on the Home page — off by default
| Event | Payload | Description |
|---|---|---|
createRoom |
{ playerName, range, timerSeconds } |
Create a new room |
joinRoom |
{ roomCode, playerName } |
Join existing room |
startGame |
{ roomCode } |
Host starts the game |
pickNumber |
{ roomCode, number } |
Picker locks in secret |
guessNumber |
{ roomCode, guess } |
Guesser submits a number |
nextRound |
{ roomCode } |
Host advances to next round |
sendChat |
{ roomCode, message } |
Send a chat message |
voice-joined |
{ roomCode } |
Announce joining voice chat |
voice-left |
{ roomCode } |
Announce leaving voice chat |
signal-offer |
{ toId, offer } |
WebRTC offer relay |
signal-answer |
{ toId, answer } |
WebRTC answer relay |
signal-ice |
{ toId, candidate } |
ICE candidate relay |
| Event | Payload | Description |
|---|---|---|
roomCreated |
{ room } |
You created a room |
roomJoined |
{ room } |
You joined a room |
playerJoined |
{ room } |
Someone else joined |
playerLeft |
{ room } |
Someone disconnected |
gameStarted |
{ room } |
Game is starting |
waitingForPick |
{ room } |
Picker's turn to set secret |
numberPicked |
{ room } |
Secret locked; guessing begins |
hint |
{ guess, hint, playerId, playerName, room } |
UP / DOWN / CORRECT |
roundWon |
{ winnerId, winnerName, secretNumber, room } |
Someone guessed correctly |
timeUp |
{ room } |
Turn timer expired |
nextRound |
{ room } |
New round started |
chatMessage |
{ id, playerId, playerName, message, timestamp } |
Chat message |
voice-joined |
{ fromId, playerName } |
A peer joined voice |
voice-left |
{ fromId } |
A peer left voice |
signal-offer |
{ fromId, offer } |
Relayed WebRTC offer |
signal-answer |
{ fromId, answer } |
Relayed WebRTC answer |
signal-ice |
{ fromId, candidate } |
Relayed ICE candidate |
error |
{ message } |
Something went wrong |
- Node.js ≥ 18 — download from nodejs.org (click the LTS button)
- npm ≥ 9 (comes with Node.js)
# Backend
cd server
npm install
# Frontend (open a second terminal)
cd client
npm installcd server
npm run dev
# Server starts at http://localhost:4000
# Health check: http://localhost:4000/healthcd client
npm run dev
# Opens at http://localhost:5173Open two or more browser tabs at http://localhost:5173.
- Tab 1: Enter a name → Create Room → note the 6-letter code
- Tab 2: Enter a different name → Join Room → type the code
- Back in Tab 1: Click Start Game
The first player is the picker — enter a secret number and lock it in. Other tabs guess. The game runs in real time.
- Push code to GitHub
- Go to render.com → New → Web Service
- Connect your GitHub repository
- Set these values:
| Setting | Value |
|---|---|
| Root Directory | server |
| Build Command | npm install |
| Start Command | npm start |
| Instance Type | Free |
- Add environment variable:
| Key | Value |
|---|---|
FRONTEND_URL |
* (update after Vercel deploy) |
- Deploy — copy your Render URL (e.g.
https://updown-game.onrender.com)
- Go to vercel.com → New Project → Import your repo
- Set Root Directory to
client - Add environment variable:
| Key | Value |
|---|---|
VITE_SERVER_URL |
your Render URL from above |
- Deploy — copy your Vercel URL (e.g.
https://updown-game.vercel.app)
Go back to Render → your service → Environment → update:
| Key | Value |
|---|---|
FRONTEND_URL |
your Vercel URL |
Then: Render → Manual Deploy → Deploy latest commit.
| Where | Variable | Value |
|---|---|---|
| Server (Render) | PORT |
Set automatically by Render |
| Server (Render) | FRONTEND_URL |
https://your-app.vercel.app |
| Client (Vercel) | VITE_SERVER_URL |
https://your-app.onrender.com |
You can point your own domain to the game without moving away from Vercel + Render.
- Go to your domain registrar (Hostinger, GoDaddy, etc.) → DNS settings
- Add a CNAME record:
| Type | Name | Value |
|---|---|---|
| CNAME | @ or www |
cname.vercel-dns.com |
- In Vercel → your project → Settings → Domains → add your domain
- Vercel provides free SSL automatically
| Subdomain | Points to | Purpose |
|---|---|---|
yourdomain.com |
WordPress / Hostinger | Your blog |
game.yourdomain.com |
Vercel (CNAME) | This game |
api.yourdomain.com |
Render (optional) | Backend API |
"Reconnecting to server" banner appears
- Make sure
VITE_SERVER_URLin Vercel matches your exact Render URL - No trailing slash, must use
https://
"Room not found" error
- This is handled automatically via
roomCodeRef— the room code never goes stale even after reconnects
Render free tier is slow to start
- Free tier services sleep after 15 minutes of inactivity
- First connection after sleeping takes ~30 seconds to wake up
- Fix: use UptimeRobot to ping
/healthevery 10 minutes for free, or upgrade to Render's $7/month paid plan
Voice chat not working
- Browser must be on HTTPS (works on Vercel/Render, not on plain
http://localhost) - For local testing, use
http://localhost:5173— browsers allow mic on localhost - User must click "Allow" when browser asks for microphone permission
- If behind a corporate VPN or strict firewall, STUN may fail — a TURN server would help
Microphone permission denied
- Chrome/Firefox: click the lock icon in the address bar → allow microphone
- iOS Safari: Settings → Safari → Microphone → allow
Deployment fails on Vercel with dependency error
- Make sure Root Directory is set to
clientin Vercel project settings - Vite version in
client/package.jsonmust be^5.0.0or higher
In client/src/Home.jsx, change the default selected timer:
const [timerChoice, setTimerChoice] = useState(60); // change 60 to any preset valueconst [rangeMin, setRangeMin] = useState(1);
const [rangeMax, setRangeMax] = useState(1000); // change to any value up to 10000In client/src/Home.jsx, change the default:
const [voiceEnabled, setVoiceEnabled] = useState(false); // already off by defaultOr remove the voice toggle UI entirely and never pass voiceEnabled={true}.
In client/src/useVoiceChat.js:
const ICE_SERVERS = [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'your-username',
credential: 'your-password',
},
];Free TURN servers: Open Relay or self-host coturn.
Replace the in-memory rooms.js with a Mongoose-backed version. The room schema matches the object shape documented at the top of rooms.js. Use MongoDB Atlas free tier.
| Layer | Technology |
|---|---|
| Frontend framework | React 18 |
| Styling | Tailwind CSS v3 |
| Build tool | Vite |
| Real-time communication | Socket.io v4 |
| Voice chat | WebRTC (native browser API) |
| Backend runtime | Node.js + Express |
| Data storage | In-memory (no database) |
| Frontend hosting | Vercel (free) |
| Backend hosting | Render (free) |
MIT — use freely, modify, deploy, have fun.