A modern, real-time 2-player gaming platform with full multiplayer support, server-authoritative timers, rejoin grace periods, and rich game modes. Built with React, Node.js, Socket.IO, and MongoDB.
| Game | Description | AI Depth | Bot Support |
|---|---|---|---|
| Chess | Full FEN-based state, move validation, pawn promotion | Minimax depth 2 | ✅ |
| Connect Four | 7×6 board, win detection, piece scoring | Minimax depth 5 w/ alpha-beta pruning | ✅ |
| Battleship | (In progress) Ship placement + grid targeting | Strategic hunt patterns | ✅ |
- vs Bot — Play against AI with configurable difficulty, 10-minute per-player timer
- Local 2 Player — Pass-and-play with random avatar pairs, 10-minute per-player timer
- Multiplayer Online — Real-time 2-player via Socket.IO rooms with:
- 30-second reconnect grace period (pause & wait)
- 10-minute per-player chess clock (server authoritative)
- Move validation on server (cheating-proof)
- Auto-forfeit on inactivity timeout or disconnect expiry
- Rematch voting system
- Firebase (Google OAuth + Email/Password)
- Guest mode with localStorage persistence
- Profile sync across device refreshes
- Avatar selection (8 preset styles)
- Unified disconnect/leave grace — 30-second window to rejoin before forfeit
- Server-authoritative clocks — 10 minutes per player, auto-timeout loss
- Move pause during absence — Game blocked while opponent reconnects
- Stale socket protection — Ignores disconnect events from old sockets after reconnect
- Dark-themed UI with Tailwind CSS
- Fully responsive (mobile, tablet, desktop)
- Real-time countdown in-game for opponent absence
- Clock display with low-time (< 60s) styling
- Rematch button with voting
- MongoDB user profiles (wins/losses/draws)
- Match history with game details
- Per-game statistics dashboard
- Result persistence during multiplayer auto-forfeit
| Layer | Technology |
|---|---|
| Framework | React 19 |
| Build Tool | Vite 7 |
| Styling | Tailwind CSS v4 |
| Routing | react-router-dom v7 |
| Real-time | Socket.IO client |
| Auth | Firebase + localStorage |
| Chess Engine | chess.js + react-chessboard |
| UI Icons | Lucide React |
| Animations | Framer Motion |
| Layer | Technology |
|---|---|
| Runtime | Node.js 22 |
| Web Framework | Express.js |
| Real-time | Socket.IO 4 |
| Database | MongoDB 7 |
| Auth | Firebase (client-side token validation) |
| Game Engines | Shared JS modules (Connect Four, Chess) |
src/
├── api/
│ ├── users.js # User profile fetch & avatar update
│ └── matches.js # Match history & save
├── components/
│ ├── games/
│ │ ├── ChessGame.jsx # Chess UI + local/bot/multiplayer modes
│ │ ├── ConnectFourGame.jsx # Connect Four UI + all modes
│ │ └── BattleshipGame.jsx # Battleship (in progress)
│ ├── Button.jsx # Reusable button component
│ ├── GameCard.jsx # Game card with preview
│ ├── InputField.jsx # Reusable input field
│ ├── Navbar.jsx # Public navbar
│ ├── PlayerCard.jsx # Player status card
│ ├── ProtectedRoute.jsx # Auth guard wrapper
│ └── Sidebar.jsx # Dashboard navigation
├── contexts/
│ ├── AuthContext.jsx # Firebase + guest auth
│ └── RoomContext.jsx # Socket.IO room state & events
├── game-engines/
│ ├── chess/
│ │ ├── engine.js # Move validation, status
│ │ └── index.js # Re-exports
│ ├── connectFour/
│ │ ├── engine.js # Minimax AI + validation
│ │ └── index.js # Re-exports
│ └── battleship/
│ └── engine.js # (In progress)
├── hooks/
│ └── useGameStats.js # Match stats fetching & caching
├── layouts/
│ ├── DashboardLayout.jsx # Sidebar + outlet layout
│ ├── GameLayout.jsx # Full-screen game layout
│ ├── PublicLayout.jsx # Public pages layout
│ └── index.js # Layout re-exports
├── pages/
│ ├── CreateRoomPage.jsx # Room creation UI
│ ├── DashboardPage.jsx # Game library & selection
│ ├── GameScreen.jsx # Main game renderer (multiplayer/local/bot)
│ ├── HomePage.jsx # Dashboard home with stats
│ ├── JoinRoomPage.jsx # Room join by code
│ ├── LandingPage.jsx # Public landing page
│ ├── LoginPage.jsx # Firebase login form
│ ├── ProfilePage.jsx # User profile & avatar picker
│ ├── RoomLobbyPage.jsx # Pre-game room lobby
│ ├── SignupPage.jsx # Firebase signup form
│ └── index.js # Page re-exports
├── router/
│ └── index.jsx # React Router configuration
├── socket/
│ └── client.js # Socket.IO initialization
├── utils/
│ ├── avatarMap.js # Preset avatar URLs
│ ├── guestIdentity.js # Guest badge formatting
│ ├── matchAvatarUtils.js # Avatar assignment per mode
│ └── matchClock.js # **Shared clock utility** (local + server)
├── firebase.js # Firebase initialization
├── index.css # Tailwind + theme tokens
└── main.jsx # React entry point
server/
├── config.js # Environment variables
├── index.js # Server bootstrap + Socket.IO setup
├── app.js # Express app creation
├── db/
│ └── connect.js # MongoDB connection
├── models/
│ ├── User.js # User schema (uid, stats, avatar)
│ └── Match.js # Match schema (gameId, result, players)
├── routes/
│ ├── matchRoutes.js # GET /matches, POST /matches
│ └── userRoutes.js # GET /users/profile, PATCH /users/avatar
├── services/
│ ├── roomService.js # **Core room logic** (state, moves, timers, grace)
│ └── persistenceService.js # MongoDB CRUD for users & matches
├── sockets/
│ ├── index.js # Socket handler registration
│ └── registerRoomHandlers.js # room:* events (join, leave, move, rematch)
├── store/
│ └── roomStore.js # In-memory room store (Map-based)
├── game/
│ ├── connectFourState.js # Game state initializer (re-export)
│ └── chessState.js # Game state initializer (re-export)
└── utils/
└── roomCode.js # Unique room code generator
graph TD
A[index.html] --> B[src/main.jsx]
B --> C[src/App.jsx]
C --> D[AuthProvider]
C --> E[RoomProvider]
C --> F[RouterProvider]
D --> D1["src/firebase.js<br/>Google OAuth + Email"]
D --> D2[src/contexts/AuthContext.jsx]
D2 --> D3["src/api/users.js<br/>Fetch Profile"]
D3 --> G["Express /api/users<br/>GET /profile<br/>PATCH /avatar"]
E --> E1["src/socket/client.js<br/>Socket.IO Init"]
E --> E2[src/contexts/RoomContext.jsx]
E2 --> H["Socket.IO Server<br/>localhost:4000"]
H --> H1["server/src/sockets/<br/>registerRoomHandlers.js"]
H1 --> H2["server/src/services/<br/>roomService.js"]
H2 --> H3["Shared Game Engines<br/>Connect Four / Chess"]
H2 --> H4["Shared Clock Utils<br/>matchClock.js"]
H2 --> DB[(MongoDB)]
G --> G1["server/src/services/<br/>persistenceService.js"]
G1 --> DB
G1 --> M1["User Model<br/>Profile + Stats"]
G1 --> M2["Match Model<br/>History + Results"]
F --> P["src/pages/*<br/>GameScreen<br/>RoomLobby<br/>etc."]
Player 1 clicks "Create Room"
↓
RoomContext emits socket: room:create { playerId, displayName, avatarId, gameId }
↓
RoomService creates room (roomCode, seat 1, status: waiting)
↓
Server broadcasts: room:state
↓
Player redirects to: /room/:roomCode
Player 2 enters room code
↓
RoomContext emits socket: room:join { roomCode, playerId, displayName, avatarId }
↓
RoomService verifies room exists, adds Player 2 (seat 2)
↓
Server broadcasts: room:state to all players in room
↓
Both players see lobby with 2/2 slots filled
Player 1 (host) clicks "Start Game"
↓
RoomContext emits socket: room:start { roomCode, playerId }
↓
RoomService:
- Sets room status = ACTIVE
- Initializes gameState (Connect Four / Chess FEN)
- Creates match clock (10 min per player)
- Starts clock timeout timer
↓
Server broadcasts: room:state with clock
↓
GameScreen receives room + clock, renders game UI
Player 1 makes move (column / chess move)
↓
GameScreen calls: RoomContext.makeMove(move, roomCode)
↓
RoomContext emits socket: make_move { roomCode, playerId, move/column }
↓
RoomService:
- Checks move validity
- Updates gameState
- Switches clock turn
- Checks for timeout (auto-lose)
- Persists result if game ends
↓
Server broadcasts: room:state
↓
Both players see updated board + clock
Player leaves or network drops
↓
RoomService marks player: connected = false, disconnectGraceUntil = now + 30s
↓
Server pauses clock, schedules absence timer
↓
Server broadcasts: room:state with absence { playerId, remainingMs }
↓
Opponent sees: "Opponent left/disconnected. Waiting for reconnection... 30s"
↓
Moves blocked for both players
Player 1 refreshes browser (new socket)
↓
GameScreen calls: RoomContext.joinRoom(roomCode)
↓
RoomContext emits socket: room:join { roomCode, playerId, ... }
↓
RoomService:
- Finds existing player by playerId
- Updates socket ID
- Clears absence (disconnectGraceUntil = null)
- Resumes clock
↓
Server broadcasts: room:state (absence cleared)
↓
Opponent sees countdown disappear; game resumes
30 seconds elapsed without reconnect
↓
RoomService auto-finishes room:
- Sets status = FINISHED
- Creates game result: opponent wins (forfeit)
- Pauses clock
↓
Server broadcasts: room:state
↓
Both players see game over: "Opponent forfeited due to inactivity"
↓
Persists to MongoDB (match history)
Active player's clock reaches 0:00
↓
Server clock timeout timer fires
↓
RoomService:
- Detects timeout via checkMatchClockTimeout()
- Auto-finishes room: opposite player wins
- Pauses clock
↓
Server broadcasts: room:state
↓
Both players see: "Opponent lost on time"
Both players click "Rematch" button
↓
RoomContext emits socket: rematch:request { roomCode, playerId }
↓
RoomService toggles rematchVotes[playerId] = true
↓
If both players voted:
- Resets gameState (new FEN / new board)
- Resets clock (10 min each)
- Status = ACTIVE
↓
Server broadcasts: room:state
↓
Game screen auto-resets; new match starts
Key Methods:
createRoom()— Initialize room, host gets seat 1joinRoom()— Add/rejoin player, auto-rejoin within gracestartGame()— Initialize clock + game statemakeMove()— Validate + apply move, switch turn, check timeoutleaveRoom()— Mark player absent + start grace timermarkDisconnected()— Handle socket disconnect (same as leave)requestRematch()— Vote, auto-reset if both agree
Automatic Actions:
- Clock Timeout: If active player clock ≤ 0, auto-finish (forfeit)
- Absence Timeout: If player grace expires, auto-finish (forfeit)
- Stale Socket Ignore: Discard disconnect from old socket after rejoin
- Resume on Rejoin: Auto-resume clock when reconnected within grace
| Event | Direction | Payload |
|---|---|---|
room:create |
C→S | { playerId, displayName, avatarId, gameId } |
room:join |
C→S | { roomCode, playerId, displayName, avatarId } |
room:start |
C→S | { roomCode, playerId } |
room:get-state |
C→S | { roomCode } |
room:leave |
C→S | { roomCode, playerId } |
make_move |
C→S | { roomCode, playerId, column/move } |
rematch:request |
C→S | { roomCode, playerId } |
room:state |
S→C | Full room snapshot (broadcast) |
{
uid: String (unique),
displayName: String,
avatarId: String (enum: avatar1–8),
stats: {
wins: Number,
losses: Number,
draws: Number
},
createdAt: Date
}{
gameId: String (connect-four, chess, battleship),
mode: String (multiplayer),
players: [{ uid, displayName }],
userOutcomes: [{ uid, result: win|loss|draw }],
winner: String (uid or 'draw'),
status: String ('finished'),
outcome: String,
detail: String (optional),
createdAt: Date,
finishedAt: Date
}- Node.js 18+
- npm 9+
- MongoDB (local or Atlas connection string)
- Firebase credentials (optional for auth)
Frontend (.env.local):
VITE_SOCKET_URL=http://localhost:4000
VITE_API_URL=http://localhost:4000
VITE_FIREBASE_API_KEY=xxx
VITE_FIREBASE_AUTH_DOMAIN=xxx
VITE_FIREBASE_PROJECT_ID=xxx
VITE_FIREBASE_APP_ID=xxx
VITE_FIREBASE_MESSAGING_SENDER_ID=xxx
VITE_FIREBASE_STORAGE_BUCKET=xxx
Backend (server/.env):
PORT=4000
DATABASE_URL=mongodb://localhost:27017/gamearena
CLIENT_ORIGIN=http://localhost:5173
NODE_ENV=development
Terminal 1 — Backend:
cd server
npm install
npm run dev
# Starts Socket.IO + Express on http://localhost:4000Terminal 2 — Frontend:
npm install
npm run dev
# Starts Vite on http://localhost:5173Frontend:
npm run build
# Outputs: dist/Backend:
cd server
npm start
# Runs from compiled src/ with production NODE_ENV- Client receives raw clock baseline from server
- Client resolves elapsed time locally (updates UI)
- Server independently checks for timeout
- No client-side clock tampering possible
- Same code path for disconnect & intentional leave
- 30-second grace window to rejoin
- Auto-forfeit on grace expiry (persisted to DB)
- Moves blocked during absence (ROOM_PAUSED error)
- After reconnect, old socket's disconnect event ignored
- Checks:
if (player.socketId && player.socketId !== incomingSocketId) return - Prevents false absence from delayed events
- Move validation on server (prevents cheating)
- Game engines (chess.js, Connect Four minimax) run in Node.js
- Client receives immutable game state snapshots
- Client only renders (no local state mutation in multiplayer)
- On game finish or forfeit, result persists immediately
- Match record created before game screen can be dismissed
- Prevents loss of history on page reload
- Stats updated atomically in MongoDB
- Elo rating system — Skill-based rankings
- Matchmaking queue — Auto-pair similar skill players
- Spectator mode — Watch ongoing matches
- Voice chat — In-game audio communication
- More games — Checkers, Reversi, Mancala, Tic Tac Toe
- Sound effects — Move & win audio
- Replay system — Review past games
- Tournaments — Multi-round competitive events
- Mobile app — React Native version
- AI improvements — Deeper minimax, opening books
This project is for personal and educational use.
Puranjay — Full-stack development with real-time multiplayer focus.
- chess.js + react-chessboard for chess engine & UI
- Socket.IO for real-time communication
- Firebase for authentication
- MongoDB for persistence
- React, Vite, Tailwind CSS for front-end stack