A real-time 1v1 competitive quiz engine for core Computer Science subjects โ built for placement prep.
Battle your peers in timed 1v1 CS quizzes covering OS, DBMS, CN, and OOPs. A server-authoritative game engine ensures fair play, an Elo-based matchmaking system guarantees balanced opponents, and an intelligent bot engine keeps wait times near zero.
๐ Live Demo: https://lunchbreak.onrender.com ๐ฆ Repository: https://github.com/sherlock-hashed/LunchBreak
- Problem Statement
- Solution
- Tech Stack
- Features
- System Architecture
- High-Level Design (HLD)
- Low-Level Design (LLD)
- Database Schema
- API Reference
- Socket.io Events
- Scoring & Rating Engine
- Bot Engine
- Security Architecture
- Project Structure
- Installation & Setup
- Deployment
- Usage
- Key Challenges & Learnings
- Future Improvements
- Author
Engineering students preparing for placement interviews lack an engaging, competitive platform to practice core Computer Science MCQs. Existing quiz apps are:
- Solo-only โ No real opponent means no competitive pressure or urgency
- Unsecured โ Answers can be inspected via browser DevTools
- Static โ No adaptive difficulty or skill-based matching
- Boring โ No gamification, rankings, or progression systems
CSClash Arena solves this by providing a real-time, multiplayer quiz experience:
| Problem | CSClash Solution |
|---|---|
| No competitive pressure | Live 1v1 matches with real-time score updates |
| Answer leaking via DevTools | Server-authoritative engine โ answers never reach the client |
| No skill-based matching | Elo rating system with progressive queue expansion |
| Long wait times | Intelligent bot fallback within 15 seconds |
| No progression system | XP, streaks, rank tiers (Explorer โ Elite), leaderboards |
| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite (SWC), Tailwind CSS, Shadcn UI, Recharts |
| Backend | Node.js, Express.js |
| Real-Time | Socket.io (WebSocket + polling fallback) |
| Database | MongoDB Atlas (Mongoose ODM) |
| Authentication | Firebase Google OAuth + JWT (custom-issued) |
| Containerization | Docker (multi-stage build) |
| Deployment | Render (unified container) |
| UI Components | Radix UI primitives, Lucide React icons, Framer Motion |
- Blitz (60s) โ Fast-paced 10-question matches
- Rapid (90s) โ Extended matches for deeper questions
- Training โ Solo practice without rating impact
- Arena (Custom Rooms) โ Private 1v1 with configurable settings (subject, duration, question type, rating impact)
- Operating Systems (OS) โ Process scheduling, memory management, deadlocks
- Database Management (DBMS) โ SQL, normalization, transactions, indexing
- Computer Networks (CN) โ OSI model, TCP/IP, routing, protocols
- Object-Oriented Programming (OOPs) โ Polymorphism, inheritance, design patterns
- Elo Rating System โ Separate ratings for Blitz, Rapid, and Arena modes
- XP & Rank Tiers โ Explorer โ Scholar โ Specialist โ Master โ Elite
- Skill Radar Chart โ Subject-wise accuracy and speed visualization
- Match History โ Detailed per-question breakdown with response times
- Score Progression Charts โ Cumulative score graphs per match
- Topic-wise Performance โ Accuracy breakdown by CS topic
- Leaderboard โ Global rankings filterable by mode (Blitz/Rapid/Overall XP)
- Adaptive Difficulty โ Bot accuracy and response speed scale with user Elo
- Realistic Behavior โ Randomized response delays with configurable variation
- Themed Names โ Kernel_King, Cache_Master, Socket_Sage, Thread_Titan, etc.
- Server-Authoritative Scoring โ Answers validated server-side only
- Payload Sanitization โ Correct answer indices stripped before client broadcast
- JWT Authentication โ Stateless session management with httpOnly cookies
- Google OAuth โ Secure SSO via Firebase with automatic account creation
- Live Score Updates โ Instant feedback on opponent's progress
- Reconnection Handling โ 5-second grace period for network drops
- Online Player Count โ Live count of connected users
- Custom Room Sharing โ 6-character room codes with link sharing
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DOCKER CONTAINER โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Node.js / Express Server โ โ
โ โ (Port 5000) โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ REST API โ โ Socket.io Server โ โ โ
โ โ โ /api/auth โ โ โ โ โ
โ โ โ /api/users โ โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โ โ โ
โ โ โ /api/matches โ โ โMatchmakerโ โ Game Engine โ โ โ โ
โ โ โ /api/questions โ โ โ Queue โ โ (In-Memory Map)โ โ โ โ
โ โ โ /api/admin โ โ โโโโโโฌโโโโโโ โโโโโโโโโฌโโโโโโโโ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ โ โ โ โ โ
โ โ โ โโโโโโดโโโโโโ โโโโโโโโโดโโโโโโโโ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ โ Rating โ โ Bot Engine โ โ โ โ
โ โ โ Static Files โ โ โ Engine โ โ (ELO-scaled) โ โ โ โ
โ โ โ (React dist/) โ โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโ โ
โ โ MongoDB Atlas โ โ
โ โ (Users, Matches โ โ
โ โ Questions) โ โ
โ โโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The application is deployed as a single unified container where Express serves both the API and the compiled React dist/ folder. This eliminates:
- CORS configuration complexity
- Multi-service orchestration overhead
- Optimal for free-tier PAAS constraints (512MB RAM on Render)
flowchart TB
subgraph Client["React Frontend"]
A[AuthContext] --> B[SocketContext]
B --> C[Arena Page]
B --> D[MatchScreen]
B --> E[CustomRoom]
B --> F[MatchResults]
B --> G[Profile]
B --> H[Leaderboard]
end
subgraph Server["Node.js Backend"]
I[Express REST API]
J[Socket.io Handler]
K[Matchmaking Service]
L[Game Engine]
M[Bot Engine]
N[Rating Engine]
O[Room Manager]
end
subgraph Database["MongoDB Atlas"]
P[(Users)]
Q[(Matches)]
R[(Questions)]
end
subgraph Auth["Firebase"]
S[Google OAuth]
end
C -->|join-queue| J
D -->|submit-answer| J
E -->|create/join room| J
J --> K
K -->|match found| L
K -->|no opponent 15s| M
L -->|match end| N
L -->|save results| Q
N -->|update ratings| P
I -->|CRUD| P
I -->|fetch questions| R
A -->|Google SSO| S
S -->|idToken| I
O -->|room state| J
sequenceDiagram
participant U as User (React)
participant S as Socket.io Server
participant MM as Matchmaking
participant GE as Game Engine
participant BE as Bot Engine
participant DB as MongoDB
U->>S: join-queue (subject, mode)
S->>MM: addToQueue(player)
MM->>MM: findMatch() every 1s
alt Human opponent found
MM->>S: match-found (both players)
else No opponent after 15s
MM->>BE: createBot(playerElo)
BE-->>MM: botProfile
MM->>S: match-found (player + bot)
end
S->>GE: createMatch(roomId, p1, p2, questions)
GE->>GE: Store in activeMatches Map
GE->>GE: startMatchTimer (1s ticks)
loop Every answer
U->>S: submit-answer (roomId, questionIdx, optionIdx)
S->>GE: submitAnswer()
GE->>GE: Validate against in-memory cache
GE-->>U: answer-result (correct, score, streak)
end
GE->>GE: Timer expires โ endMatch()
GE->>DB: Match.create() โ single batch write
GE->>DB: User.findByIdAndUpdate() โ atomic $inc/$set
GE-->>U: match-ended (final stats)
GE->>GE: activeMatches.delete(roomId)
The game engine avoids database writes during active gameplay. All match state lives in RAM:
// server/services/gameEngine.js
const activeMatches = new Map(); // roomId โ MatchState
// MatchState structure:
{
roomId: "uuid-v4",
player1: { id, username, socketId, score: 0, answers: [], streak: 0 },
player2: { id, username, socketId, score: 0, answers: [], streak: 0 },
questions: [...], // Full questions WITH correct answers
currentQuestion: 0,
timeLeft: 60, // Decremented every 1s via setInterval
status: "active",
botTimeouts: [], // Scheduled bot responses (clearable)
}Why Maps over MongoDB?
- Reading/writing to RAM = ~0ms latency
- MongoDB write = ~50-200ms network round trip
- A 60-second match with 2 players generates ~20+ score mutations
- Using Maps reduces database writes from ~20+ per match to exactly 1 (at match end)
// server/services/matchmaking.js
const queues = new Map(); // queueKey โ [QueueEntry]
const pendingMatches = new Map(); // pendingId โ PendingMatch
const botTimers = new Map(); // odlomerId โ TimeoutId
// Queue expansion logic:
// Second 0-5: ยฑ50 Elo range
// Second 5-10: ยฑ150 Elo range
// Second 10-15: ยฑ350 Elo range
// Second 15+: Bot injection (guaranteed match)flowchart LR
A["Client clicks Option B"] -->|"emit: submit-answer"| B["Socket Handler"]
B --> C["Game Engine"]
C --> D{"Compare optionIdx vs correct answer in activeMatches Map"}
D -->|"Match"| E["score += 4, streak++, emit answer-result"]
D -->|"No Match"| F["score -= 1, streak = 0, emit answer-result"]
Detailed flow:
- Client emits
submit-answerwith{ roomId, questionIdx: 3, optionIdx: 1 } - Server looks up
activeMatches.get(roomId).questions[3].correct - Compares
optionIdx === correctโ calculates score - Emits
answer-resultwith{ correct: true/false, score, streak, oppScore }
Note: The client never receives
questions[i].correct. It is stripped out insocketHandler.jsbefore thematch-foundevent is emitted.
sequenceDiagram
participant U as User Browser
participant S as Socket Server
participant GE as Game Engine
U->>U: Page refresh / network drop
S->>S: 'disconnect' event fired
S->>GE: handleDisconnect(socketId)
GE->>GE: Start 5s grace timer
alt User reconnects within 5s
U->>S: New socket connection
U->>S: rejoin-match (roomId)
S->>GE: rejoinMatch(userId, newSocketId)
GE->>GE: Cancel grace timer
GE->>GE: Update socketId in activeMatches
GE-->>U: rejoin-success (current match state)
else 5s timer expires
GE->>GE: Auto-forfeit disconnected player
GE->>GE: endMatch() โ save to MongoDB
end
erDiagram
USER {
ObjectId _id PK
String name
String username UK
String email UK
String password
String googleId
String avatar
String gender
String bio
String college
String country
Object socials
Object rating
Object stats
Number xp
Number peakRating
Date createdAt
}
USER ||--o{ MATCH : "plays in"
rating: {
blitz: { type: Number, default: 1200 },
rapid: { type: Number, default: 1200 },
arena: { type: Number, default: 1200 },
}
stats: {
matchesPlayed: Number,
wins: Number,
losses: Number,
draws: Number,
bestStreak: Number,
}erDiagram
MATCH {
ObjectId _id PK
String roomId UK
String mode
String subject
Boolean isBot
Object player1
Object player2
String winner
Number duration
Object xpAwarded
String status
Date createdAt
}
MATCH }o--|| USER : "player1.userId"
MATCH }o--o| USER : "player2.userId"
MATCH }o--o{ QUESTION : "responses.questionId"
// Each player sub-document:
player1: {
userId: ObjectId (ref: User),
username: String,
score: Number,
correct: Number,
wrong: Number,
skipped: Number,
accuracy: Number,
avgResponseTime: Number,
fastestResponse: Number,
slowestResponse: Number,
streak: Number,
ratingBefore: Number,
ratingAfter: Number,
responses: [{
questionId: ObjectId (ref: Question),
selectedOption: Number,
correct: Boolean,
responseTime: Number,
}]
}{
text: String, // "What is a deadlock?"
options: [String], // Exactly 4 options
correct: Number, // Index 0-3 (NEVER sent to client)
subject: ["OS", "DBMS", "CN", "OOPs"],
topic: String, // "Process Synchronization"
difficulty: ["Easy", "Medium", "Hard"],
type: ["MCQ", "MSQ", "Case Based Scenario"],
explanation: String, // Shown post-match for wrong answers
tags: [String],
}
// Compound index: { subject, topic, difficulty, type }| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/auth/register |
Public | Register with email/password |
POST |
/api/auth/login |
Public | Login with email/password |
POST |
/api/auth/google |
Public | Google OAuth (Firebase idToken) |
POST |
/api/auth/logout |
Private | Clear session cookie |
GET |
/api/auth/me |
Private | Get current user |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/users/leaderboard |
Public | Global leaderboard (paginated) |
GET |
/api/users/search?q= |
Private | Search users by name/username/college |
GET |
/api/users/public/:id |
Private | Public profile (view-only) |
GET |
/api/users/:id |
Private | Full user profile |
PUT |
/api/users/:id |
Private | Update own profile |
GET |
/api/users/:id/stats |
Private | Subject-wise aggregated stats |
GET |
/api/users/:id/radar |
Private | Skill radar data (per-subject accuracy/speed) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/matches/user/:userId |
Private | Match history (paginated) |
GET |
/api/matches/:roomId |
Private | Single match details (populated) |
GET |
/api/matches/:roomId/analytics |
Private | Topic-wise breakdown + time trends |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/questions |
Public | Query questions (filter by subject/topic/difficulty) |
GET |
/api/questions/random |
Public | Random question set |
GET |
/api/questions/topics |
Public | Available topics per subject |
POST |
/api/questions |
Private | Add a new question |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/admin/login |
Public | Admin authentication |
GET |
/api/admin/stats |
Admin | Dashboard stats (users, matches, avg ratings) |
GET |
/api/admin/users |
Admin | Paginated user list with search |
| Event | Payload | Description |
|---|---|---|
join-queue |
{ subject, mode } |
Enter matchmaking queue |
leave-queue |
โ | Exit matchmaking queue |
match-ready |
{ roomId, opponent, mode } |
Acknowledge match found |
submit-answer |
{ roomId, questionIdx, optionIdx } |
Submit answer to current question |
rejoin-match |
{ roomId } |
Rejoin after disconnect |
create-room |
โ | Create custom room |
join-room |
{ roomCode } |
Join existing custom room |
room-settings |
{ roomCode, settings } |
Update room settings (host only) |
start-room-match |
{ roomCode } |
Start custom room match |
leave-room |
{ roomCode } |
Leave custom room |
| Event | Payload | Description |
|---|---|---|
match-found |
{ roomId, opponent, questions, mode, isBot } |
Match created, navigate to game |
answer-result |
{ correct, score, streak, oppScore } |
Answer validation result |
timer-update |
{ timeLeft } |
1-second timer tick |
match-ended |
{ winner, stats, ratingChanges, xp } |
Match complete with full analytics |
opponent-answered |
{ oppScore, oppCorrect, oppAnswered } |
Live opponent progress |
rejoin-success |
{ matchState } |
Successful reconnection |
room-created |
{ roomCode, settings, players } |
Room created confirmation |
room-joined |
{ roomCode, settings, players } |
Joined room confirmation |
player-joined |
{ players } |
New player entered room |
settings-updated |
{ settings } |
Room settings changed |
online-count |
{ count } |
Live connected user count |
| Action | Points |
|---|---|
| Correct answer | +4 |
| Wrong answer | -1 |
| Skipped question | 0 |
| Minimum score | 0 (no negatives) |
Uses the standard Elo rating system with K-factor = 32:
Expected Score = 1 / (1 + 10^((OpponentRating - PlayerRating) / 400))
New Rating = OldRating + K ร (ActualScore - ExpectedScore)
- Win: Actual = 1.0
- Loss: Actual = 0.0
- Draw: Actual = 0.5
- Floor: Rating cannot drop below 100
XP = (score ร 2) + (accuracy ร 0.5) + (bestStreak ร 5) + (isWin ? 25 : 0)
| Tier | Elo Range | Color |
|---|---|---|
| Explorer | 0 โ 1199 | Gray |
| Scholar | 1200 โ 1499 | Green |
| Specialist | 1500 โ 1799 | Blue |
| Master | 1800 โ 1999 | Gold |
| Elite | 2000+ | Red |
When no human opponent is found within 15 seconds, the system injects an ELO-scaled bot:
| Player Elo | Bot Accuracy | Avg Response Time | Variation |
|---|---|---|---|
| โฅ 2000 (Elite) | 88% | 1.5s | ยฑ30% |
| โฅ 1600 (Specialist+) | 80% | 2.0s | ยฑ30% |
| โฅ 1200 (Scholar+) | 70% | 2.5s | ยฑ30% |
| < 1200 (Explorer) | 60% | 3.0s | ยฑ30% |
Bot Elo is generated within ยฑ100 of the human player's rating.
Bot Names: Kernel_King, Deadlock_Daemon, Cache_Master, Query_Queen, Stack_Sentinel, Mutex_Mind, Pipe_Phantom, Byte_Baron, Thread_Titan, Algo_Oracle, Logic_Lynx, Socket_Sage, Heap_Hawk, Node_Ninja
flowchart TD
A[User Login] -->|Firebase Google OAuth| B[Firebase returns idToken]
B -->|POST /api/auth/google| C[Server validates & creates JWT]
C -->|Set httpOnly cookie + return token| D[Client stores in localStorage]
D -->|Every API request| E[Axios interceptor adds Bearer token]
D -->|Socket connection| F[Socket auth middleware verifies JWT]
G[Match Start] -->|Server fetches questions from MongoDB| H[Full questions with .correct field]
H -->|Strip .correct from each question| I[Sanitized questions sent to client]
I -->|Client displays options| J[User clicks option]
J -->|submit-answer: only sends optionIdx| K[Server compares vs in-memory .correct]
K -->|Result| L[Client receives correct/wrong boolean]
- Answer Obfuscation โ The
correctfield is removed from question payloads before Socket emission - Server-Side Validation โ All scoring happens on the server; client cannot manipulate scores
- JWT Authentication โ Tokens issued by the server, verified on every protected route
- httpOnly Cookies โ Token cookies are not accessible via JavaScript (XSS-resistant)
- Password Hashing โ bcrypt with salt rounds for email/password users
- Input Validation โ Mongoose schema validators + controller-level validation
- Admin Isolation โ Separate JWT with
role: "admin"and 4-hour expiry
code-clash-arena/
โโโ server/ # Backend (Node.js + Express)
โ โโโ index.js # Entry point โ Express + Socket.io + static serving
โ โโโ config/
โ โ โโโ db.js # MongoDB connection
โ โโโ middleware/
โ โ โโโ auth.js # JWT verification middleware
โ โโโ models/
โ โ โโโ User.js # User schema (Elo, stats, socials)
โ โ โโโ Match.js # Match ledger (per-question responses)
โ โ โโโ Question.js # Question bank (subject, topic, difficulty)
โ โโโ controllers/
โ โ โโโ authController.js # Register, Login, Google OAuth, Logout
โ โ โโโ userController.js # Profile, leaderboard, subject stats, skill radar
โ โ โโโ matchController.js # Match history, analytics
โ โ โโโ questionController.js # Question CRUD, random sets
โ โ โโโ adminController.js # Admin dashboard stats
โ โโโ routes/
โ โ โโโ authRoutes.js
โ โ โโโ userRoutes.js
โ โ โโโ matchRoutes.js
โ โ โโโ questionRoutes.js
โ โ โโโ adminRoutes.js
โ โโโ socket/
โ โ โโโ socketHandler.js # Central Socket.io event hub
โ โโโ services/
โ โโโ matchmaking.js # Queue management + bot fallback
โ โโโ gameEngine.js # In-memory match state + timer loops
โ โโโ ratingEngine.js # Elo calculation + XP + rank tiers
โ โโโ botEngine.js # ELO-scaled bot behavior
โ โโโ roomManager.js # Custom room lifecycle
โ
โโโ src/ # Frontend (React + TypeScript)
โ โโโ App.tsx # Router + context providers
โ โโโ contexts/
โ โ โโโ AuthContext.jsx # Auth state + Firebase integration
โ โ โโโ SocketContext.jsx # Socket.io client lifecycle
โ โโโ pages/
โ โ โโโ Index.tsx # Landing page
โ โ โโโ Arena.tsx # Mode/subject selection + queue trigger
โ โ โโโ MatchScreen.tsx # Live game interface
โ โ โโโ MatchResults.tsx # Post-match analytics + charts
โ โ โโโ Profile.tsx # User stats, Elo history, match history
โ โ โโโ Leaderboard.tsx # Global rankings
โ โ โโโ CustomRoom.tsx # Create/join private rooms
โ โ โโโ Login.tsx
โ โ โโโ Signup.tsx
โ โโโ components/
โ โ โโโ Navbar.tsx
โ โ โโโ Footer.tsx
โ โ โโโ LoadingScreen.tsx # Matchmaking queue UI
โ โ โโโ SkillRadarChart.tsx # SVG radar visualization
โ โ โโโ ui/ # Shadcn UI primitives
โ โโโ services/
โ โ โโโ api.js # Axios instance with JWT interceptor
โ โโโ lib/
โ โโโ firebase.js # Firebase config + Google sign-in
โ โโโ utils.ts # Tailwind merge helper (cn)
โ
โโโ Dockerfile # Multi-stage build (React โ Express)
โโโ package.json
โโโ vite.config.ts
โโโ tailwind.config.ts
โโโ tsconfig.json
- Node.js โฅ 18.x
- npm โฅ 9.x
- MongoDB (local or Atlas connection string)
- Firebase Project (for Google OAuth)
git clone https://github.com/sherlock-hashed/LunchBreak.git
cd LunchBreak# Frontend dependencies
npm install
# Backend dependencies
cd server && npm install && cd ..Create a .env file in the project root:
# Backend
MONGO_URI=mongodb+srv://<user>:<pass>@cluster.mongodb.net/csclash
JWT_SECRET=your_jwt_secret_key
PORT=5000
ADMIN_EMAIL=your_admin_email
ADMIN_PASSWORD=your_admin_password
# Frontend (Vite โ must be prefixed with VITE_)
VITE_FIREBASE_API_KEY=your_firebase_api_key
VITE_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your_project_id
VITE_FIREBASE_STORAGE_BUCKET=your_project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
VITE_FIREBASE_APP_ID=your_app_id# Terminal 1 โ Backend
cd server && node index.js
# Terminal 2 โ Frontend
npm run dev- Frontend:
http://localhost:5173 - Backend API:
http://localhost:5000/api
Add questions to MongoDB via the API:
curl -X POST http://localhost:5000/api/questions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT" \
-d '{
"text": "What is a deadlock?",
"options": ["A", "B", "C", "D"],
"correct": 2,
"subject": "OS",
"topic": "Process Synchronization",
"difficulty": "Medium",
"type": "MCQ"
}'The project uses a multi-stage Docker build:
- Stage 1 (frontend-builder): Installs npm dependencies + runs
npm run buildto compile React intodist/ - Stage 2 (production): Copies only
server/+dist/into a clean Node 18 Alpine image
# Build locally
docker build -t csclash-arena .
# Run locally
docker run -p 5000:5000 --env-file .env csclash-arenaOn Render, set the following:
- Environment: Docker
- Build Args: All
VITE_FIREBASE_*variables (injected at build time) - Environment Variables:
MONGO_URI,JWT_SECRET,PORT
The Express server automatically detects NODE_ENV=production and serves the React dist/ folder as static files.
- Sign Up / Login โ Create an account or use Google OAuth
- Select Mode โ Choose Blitz (60s), Rapid (90s), or Training
- Pick Subject โ OS, DBMS, CN, OOPs, or Mixed
- Queue Up โ The matchmaking system finds an opponent or spawns a bot
- Battle โ Answer questions as fast and accurately as possible
- Review Results โ See per-question breakdown, Elo changes, and performance tips
- Track Progress โ View your profile, Elo history chart, and skill radar
- Challenge Friends โ Create a Custom Room and share the 6-character code
- Climb Ranks โ Reach Elite tier on the global leaderboard
Problem: With two players answering questions simultaneously, keeping scores, timers, and question states perfectly synchronized across clients was critical.
Solution: Made the server the single source of truth. All game state lives in an in-memory Map. The server broadcasts state updates via Socket.io events. Clients are purely display layers โ they cannot modify game state.
Problem: In a browser-based quiz, anyone can open DevTools, inspect network payloads, and find correct answers.
Solution: The server strips the correct field from all question objects before emitting them to clients. When a player submits an answer, only the optionIdx integer travels over the wire. The server compares it against its own in-memory cache. The client physically cannot know the answer until after submission.
Problem: Users refreshing their browser or experiencing brief network drops would lose their active match.
Solution: Implemented a 5-second grace period. On disconnect, the server starts a timer. If the user reconnects and emits rejoin-match within 5 seconds, their socket ID is updated in the active match map, and the match continues seamlessly. If the timer expires, the disconnected player forfeits.
Problem: With a small initial user base, matchmaking queues could leave players waiting indefinitely.
Solution: Built a progressive queue expansion system: the Elo search range widens every second. After 15 seconds, the system automatically injects a bot opponent whose accuracy and response speed are calibrated to the player's own Elo rating, guaranteeing every player gets a match.
Problem: Writing every score update and answer submission to MongoDB during a live match would create unacceptable latency and overwhelm the database.
Solution: All match state is maintained in JavaScript Map objects in server memory. The database is only written to once โ when the match ends โ in a single Match.create() call that batch-saves all analytics.
stateDiagram-v2
[*] --> Idle: User on Arena page
Idle --> Queued: join-queue emitted
Queued --> Matched: Human opponent found
Queued --> BotMatch: 15s timeout, bot injected
Matched --> Active: Both players emit match-ready
BotMatch --> Active: match-ready emitted
Active --> Ended: Timer expires OR all questions answered
Active --> Disconnected: Socket disconnect
Disconnected --> Active: Rejoin within 5s
Disconnected --> Ended: 5s grace period expires
Ended --> Saved: Match.create + User.update
Saved --> [*]
sequenceDiagram
participant H as Host
participant S as Server
participant G as Guest
H->>S: create-room
S->>S: Generate 6-char room code
S->>S: Store in customRooms Map
S-->>H: room-created (code, settings)
G->>S: join-room (roomCode)
S->>S: Validate room exists and not full
S-->>G: room-joined (settings, players)
S-->>H: player-joined (updated players list)
H->>S: room-settings (updated config)
S-->>H: settings-updated
S-->>G: settings-updated
H->>S: start-room-match
S->>S: Fetch questions from MongoDB
S->>S: Create match in activeMatches
S-->>H: match-found (questions, roomId)
S-->>G: match-found (questions, roomId)
flowchart TD
A["Bot receives question"] --> B{"Player Elo >= 2000?"}
B -->|Yes| C["accuracy = 88%, avgTime = 1.5s"]
B -->|No| D{"Player Elo >= 1600?"}
D -->|Yes| E["accuracy = 80%, avgTime = 2.0s"]
D -->|No| F{"Player Elo >= 1200?"}
F -->|Yes| G["accuracy = 70%, avgTime = 2.5s"]
F -->|No| H["accuracy = 60%, avgTime = 3.0s"]
C --> I["Apply +/-30% time variation"]
E --> I
G --> I
H --> I
I --> J{"Math.random < accuracy?"}
J -->|Yes| K["Select correct answer"]
J -->|No| L["Select random wrong answer"]
K --> M["Schedule setTimeout with computed delay"]
L --> M
M --> N["Emit submit-answer after delay"]
flowchart LR
subgraph NaiveApproach["Naive: Write Every Event"]
A1["Answer 1"] --> DB1[("MongoDB Write")]
A2["Answer 2"] --> DB2[("MongoDB Write")]
A3["Answer 3"] --> DB3[("MongoDB Write")]
A4["..."] --> DB4[("MongoDB Write")]
A5["Answer 10"] --> DB5[("MongoDB Write")]
end
subgraph CSClashApproach["CSClash: In-Memory + Batch Save"]
B1["Answer 1"] --> MAP["In-Memory Map"]
B2["Answer 2"] --> MAP
B3["Answer 3"] --> MAP
B4["..."] --> MAP
B5["Answer 10"] --> MAP
MAP -->|"Match ends"| DB6[("Single MongoDB Write")]
end
Result: 10-20 DB writes per match โ 1 DB write per match. At 100 concurrent matches, this prevents ~2000 unnecessary writes per minute.
Player A (Rating: 1400) vs Player B (Rating: 1200)
K-factor = 32
Step 1: Expected Scores
E_A = 1 / (1 + 10^((1200 - 1400) / 400))
= 1 / (1 + 10^(-0.5))
= 1 / (1 + 0.316)
= 0.76 (76% expected win rate)
E_B = 1 - 0.76 = 0.24
Step 2: Player A wins
New_A = 1400 + 32 ร (1.0 - 0.76) = 1400 + 7.7 โ 1408
New_B = 1200 + 32 ร (0.0 - 0.24) = 1200 - 7.7 โ 1192
Step 3: If the underdog (B) wins instead
New_A = 1400 + 32 ร (0.0 - 0.76) = 1400 - 24.3 โ 1376
New_B = 1200 + 32 ร (1.0 - 0.24) = 1200 + 24.3 โ 1224
โ Upsets are rewarded more. Beating a stronger player gives +24 vs +8.
flowchart TD
A["Incoming Request"] --> B{"Has Authorization header?"}
B -->|No| C{"Has token cookie?"}
C -->|No| D["401 Unauthorized"]
C -->|Yes| E["Extract token from cookie"]
B -->|Yes| F["Extract token from Bearer header"]
E --> G{"jwt.verify with JWT_SECRET"}
F --> G
G -->|Invalid/Expired| D
G -->|Valid| H["Decode userId from payload"]
H --> I["User.findById - exclude password"]
I --> J{"User exists?"}
J -->|No| D
J -->|Yes| K["Attach user to req.user"]
K --> L["next - proceed to route handler"]
Time (s) Elo Range Action
โโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
0 ยฑ50 of 1400 Queue player, start scanning
1 ยฑ70 Wider scan
2 ยฑ90 ...
5 ยฑ150 Mid-range expansion
8 ยฑ210 ...
10 ยฑ250 Aggressive expansion
12 ยฑ290 ...
15 ยฑ350+ ๐ค BOT INJECTED โ match guaranteed
Result: No player waits more than 15 seconds for a match.
- Redis Integration โ Move in-memory Maps to Redis for horizontal scaling across multiple server instances
- WebRTC Voice Chat โ Optional voice communication during matches
- Tournament Mode โ Bracket-based elimination tournaments
- Question Contribution โ Community-submitted questions with approval pipeline
- Mobile App โ React Native port with push notifications
- Performance Analytics โ AI-powered study recommendations based on weak topics
- Rate Limiting โ Express rate limiter middleware for API abuse prevention
- Load Testing โ Socket.io stress testing with Artillery/k6
- Monitoring โ Prometheus + Grafana for real-time server metrics