A real-time multiplayer strategy game where players compete by picking numbers strategically. The player furthest from 80% of the average loses points each round — last player standing wins!
balance-scale-showdown/
├── backend/ # Node.js + Express + Socket.io
│ ├── models/
│ │ ├── Room.js # MongoDB Room schema
│ │ └── GameHistory.js # Game history schema
│ ├── routes/
│ │ ├── rooms.js # REST: room lookup
│ │ └── games.js # REST: history/leaderboard
│ ├── socket/
│ │ └── socketHandlers.js # All Socket.io events
│ ├── utils/
│ │ └── roomManager.js # In-memory room state (singleton)
│ ├── server.js # Entry point
│ ├── package.json
│ └── .env.example
│
├── frontend/ # React + Vite + TailwindCSS
│ ├── src/
│ │ ├── context/
│ │ │ ├── SocketContext.jsx # Socket.io client provider
│ │ │ └── GameContext.jsx # Global game state (useReducer)
│ │ ├── hooks/
│ │ │ └── useSocketEvents.js # Maps socket events → state
│ │ ├── pages/
│ │ │ ├── HomePage.jsx # Landing + create/join
│ │ │ ├── LobbyPage.jsx # Waiting room
│ │ │ ├── GamePage.jsx # Main gameplay
│ │ │ ├── ResultsPage.jsx # Round reveal animation
│ │ │ └── GameOverPage.jsx # Winner + stats
│ │ ├── components/
│ │ │ ├── game/
│ │ │ │ ├── TimerRing.jsx # SVG countdown timer
│ │ │ │ └── ScoreBoard.jsx # Live player scores
│ │ │ ├── lobby/
│ │ │ │ └── PlayerCard.jsx # Lobby player row
│ │ │ └── ui/
│ │ │ └── ChatPanel.jsx # Real-time chat
│ │ ├── App.jsx # Screen router
│ │ ├── main.jsx
│ │ └── index.css # Tailwind + custom styles
│ ├── index.html
│ ├── vite.config.js
│ ├── tailwind.config.js
│ └── package.json
│
├── render.yaml # One-click Render deployment
├── package.json # Monorepo convenience scripts
└── README.md
| Rule | Value |
|---|---|
| Starting points | 10 per player |
| Points lost (loser/round) | 2 |
| Round timer | 20 seconds |
| Number range | 0 – 100 |
| Target formula | 80% × average of all picks |
| Elimination | 0 points → out |
| Win condition | Last player alive |
Example: Players pick 30, 50, 70.
Average = 50. Target = 40.
Player who picked 70 (distance 30) is furthest → loses 2 points.
| Event | Payload | Description |
|---|---|---|
create_room |
{ username, playerId? } |
Create a new room |
join_room |
{ roomCode, username, playerId? } |
Join existing room |
player_ready |
{ roomCode, playerId } |
Toggle ready status |
start_game |
{ roomCode, playerId } |
Host starts game |
submit_number |
{ roomCode, playerId, number } |
Submit round pick |
chat_message |
{ roomCode, playerId, message } |
Send chat |
| Event | Payload | Description |
|---|---|---|
room_created |
{ roomCode, playerId, room } |
Room created confirmation |
room_joined |
{ roomCode, playerId, room, rejoined, asSpectator } |
Join confirmation |
player_joined |
{ room, username, rejoined } |
Broadcast to room |
player_ready_update |
{ room, playerId, isReady } |
Ready state changed |
game_started |
{ room, round } |
Game begins |
round_timer |
{ timeLeft, total, round } |
Countdown tick |
number_submitted |
{ success, number } |
Submission confirmed |
submission_update |
{ submittedCount, totalCount, playerId } |
Submission progress |
reveal_results |
{ result, room } |
Round results |
update_scores |
{ players } |
Score sync |
eliminate_player |
{ playerId, username } |
Player eliminated |
next_round |
{ round, room } |
New round starts |
game_over |
{ winner, room, roundHistory } |
Game finished |
chat_message |
{ id, playerId, username, message, timestamp } |
Chat message |
player_disconnected |
{ room, playerId, username } |
Player went offline |
error |
{ message } |
Error notification |
- Node.js 18+
- MongoDB (local or Atlas free tier)
git clone <your-repo-url>
cd balance-scale-showdown
# Install all dependencies
npm run install:all
# or manually:
cd backend && npm install
cd ../frontend && npm install# Backend
cp backend/.env.example backend/.env
# Edit: set MONGODB_URI (or leave default for local MongoDB)
# Frontend
cp frontend/.env.example frontend/.env
# Edit: VITE_BACKEND_URL=http://localhost:3001# Start both together (requires concurrently in root)
npm install # installs concurrently
npm run dev
# Or separately in two terminals:
cd backend && npm run dev # → http://localhost:3001
cd frontend && npm run dev # → http://localhost:5173Go to http://localhost:5173 in two or more browser tabs/windows to test multiplayer.
No MongoDB? The server runs in in-memory mode automatically — game state works but won't persist across restarts.
- Push your code to GitHub
- Go to render.com → New → Blueprint
- Connect your repo — Render auto-detects
render.yaml - Set environment variables in the Render dashboard:
- Backend:
MONGODB_URI,CLIENT_URL(your frontend URL) - Frontend:
VITE_BACKEND_URL(your backend URL)
- Backend:
- Deploy!
# Install Railway CLI
npm install -g @railway/cli
railway login
# Deploy backend
cd backend
railway init
railway up
# Set env vars
railway variables set MONGODB_URI="mongodb+srv://..."
railway variables set CLIENT_URL="https://your-frontend.vercel.app"Then deploy frontend to Vercel:
cd frontend
npx vercel
# Set VITE_BACKEND_URL to your Railway backend URL# Install Node.js + PM2
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
npm install -g pm2
# Clone & setup
git clone <repo> && cd balance-scale-showdown
cd backend && npm install
cp .env.example .env && nano .env # fill in your values
# Start backend with PM2
pm2 start server.js --name bss-backend
pm2 save && pm2 startup
# Build frontend
cd ../frontend && npm install
VITE_BACKEND_URL=https://your-backend.com npm run build
# Serve frontend with Nginx (or use Vercel/Netlify)
sudo cp -r dist /var/www/bss
# Configure Nginx to serve /var/www/bss + proxy /api and /socket.io to :3001- Deploy backend to Railway/Render/VPS
- In
frontend/.env: setVITE_BACKEND_URL=<your backend URL> - Run
npm run buildin/frontend - Deploy
dist/to Vercel, Netlify, or Cloudflare Pages
Edit backend/utils/roomManager.js → settings object to tune gameplay:
settings: {
roundTimeLimit: 20, // seconds per round
startingPoints: 10, // points each player begins with
pointsDeductedPerRound: 2, // penalty for the round loser
targetMultiplier: 0.8, // target = average × this
}- In-memory state:
RoomManagerholds all live game state in aMap. Fast, zero latency. MongoDB syncs for persistence but is optional. - Rejoin support: Player IDs are saved in
localStorage. On reconnect, the same ID re-attaches the socket to the existing room slot. - Spectator mode: Players who join a room mid-game are added as spectators automatically.
- Timer authority: The server owns the round timer. Clients display it but can't manipulate it.
- Auto-cleanup: Rooms with TTL index in MongoDB auto-delete after 24h. In-memory rooms are cleaned up when all players disconnect.
| Layer | Tech |
|---|---|
| Frontend | React 18, Vite, TailwindCSS, Framer Motion |
| Backend | Node.js, Express, Socket.io |
| Database | MongoDB + Mongoose (optional) |
| Real-time | Socket.io (WebSockets with fallback) |
| Deployment | Render / Railway / Vercel / VPS |