Full-featured WebSocket Connect Four platform with real-time synchronization
Connect Four Online is a full-featured, real-time multiplayer Connect Four platform built with WebSocket technology. This project demonstrates advanced WebSocket implementation patterns, real-time game state synchronization, complete Connect Four rules validation including win detection for horizontal, vertical, and diagonal patterns, and a polished user experience for competitive online matches.
The platform supports multiple concurrent games, player matchmaking, move validation with gravity simulation, game history tracking, spectator mode, and real-time chat. Built with scalability and performance in mind, it can handle thousands of simultaneous players with minimal latency.
Instant move synchronization across all connected clients with sub-20ms latency. Watch your opponent's moves appear in real-time with smooth animations.
Advanced algorithm detects wins in all directions (horizontal, vertical, and diagonal) with visual highlighting of winning sequences.
Practice against an intelligent AI using minimax algorithm with alpha-beta pruning for challenging single-player matches.
Create or join game rooms with unique codes. Support for private matches and public matchmaking with skill-based pairing.
Integrated chat system for players to communicate during matches. Support for emojis and quick reactions.
Comprehensive stats tracking including wins, losses, streaks, and ELO rating system for competitive rankings.
Watch ongoing games as a spectator with real-time updates. Perfect for tournaments and learning from skilled players.
Automatic reconnection handling with game state restoration. Never lose progress due to temporary network issues.
Multiple board themes and piece designs. Personalize your gaming experience with custom color schemes.
The client is built with vanilla JavaScript and HTML5 Canvas for smooth animations. The WebSocket client maintains a persistent connection to the server, handling all real-time events including moves, chat messages, and game state updates. The UI uses CSS Grid for responsive layouts and CSS animations for piece drops and win celebrations.
// WebSocket connection setup
const socket = io('wss://connectfour-server.com', {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5
});
// Game state management
class ConnectFourGame {
constructor() {
this.board = Array(6).fill().map(() => Array(7).fill(null));
this.currentPlayer = 'red';
this.gameOver = false;
}
dropPiece(column) {
// Find lowest available row
for (let row = 5; row >= 0; row--) {
if (!this.board[row][column]) {
this.board[row][column] = this.currentPlayer;
return { row, column };
}
}
return null;
}
checkWin(row, col) {
const player = this.board[row][col];
return this.checkDirection(row, col, 1, 0, player) || // Horizontal
this.checkDirection(row, col, 0, 1, player) || // Vertical
this.checkDirection(row, col, 1, 1, player) || // Diagonal \
this.checkDirection(row, col, 1, -1, player); // Diagonal /
}
}
The server is built with Node.js and Socket.io for WebSocket management. Redis is used for session storage and game state caching, ensuring fast access and horizontal scalability. PostgreSQL stores persistent data including user profiles, match history, and statistics.
// Server-side game room management
const io = require('socket.io')(server, {
cors: { origin: '*' }
});
const gameRooms = new Map();
io.on('connection', (socket) => {
console.log(`Player connected: ${socket.id}`);
socket.on('joinRoom', async ({ roomId, playerName }) => {
socket.join(roomId);
let room = gameRooms.get(roomId) || createNewRoom(roomId);
room.addPlayer(socket.id, playerName);
if (room.isReady()) {
io.to(roomId).emit('gameStart', {
player1: room.player1,
player2: room.player2
});
}
});
socket.on('makeMove', ({ roomId, column }) => {
const room = gameRooms.get(roomId);
const result = room.makeMove(socket.id, column);
if (result.valid) {
io.to(roomId).emit('moveMade', {
row: result.row,
column: result.column,
player: result.player,
winner: result.winner
});
}
});
});
Game state is maintained both client-side and server-side. The server is the source of truth, validating all moves before broadcasting to clients. Client-side prediction provides instant feedback while waiting for server confirmation. Conflicts are resolved by reverting to the server state.
All moves are validated server-side to prevent cheating. Rate limiting prevents spam and abuse. JWT tokens authenticate users and secure game rooms. Input sanitization prevents injection attacks.
The WebSocket connection is established on page load and maintained throughout the gaming session. Automatic reconnection logic handles network interruptions, restoring the game state seamlessly.
class WebSocketManager {
constructor(url) {
this.url = url;
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.connect();
}
connect() {
this.socket = io(this.url, {
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: this.maxReconnectAttempts
});
this.socket.on('connect', () => {
console.log('Connected to game server');
this.reconnectAttempts = 0;
this.onConnect();
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
this.onDisconnect(reason);
});
this.socket.on('reconnect', (attemptNumber) => {
console.log(`Reconnected after ${attemptNumber} attempts`);
this.restoreGameState();
});
}
emit(event, data) {
if (this.socket && this.socket.connected) {
this.socket.emit(event, data);
}
}
on(event, callback) {
if (this.socket) {
this.socket.on(event, callback);
}
}
}
All game events are handled through structured event emitters. Events include player moves, chat messages, game state updates, and player connections/disconnections.
// Client-side event handlers
wsManager.on('moveMade', (data) => {
const { row, column, player, winner } = data;
// Animate piece drop
animatePieceDrop(row, column, player);
// Update local board state
game.board[row][column] = player;
// Check if game ended
if (winner) {
handleGameEnd(winner);
} else {
// Switch turns
game.currentPlayer = player === 'red' ? 'yellow' : 'red';
updateGameStatus();
}
});
wsManager.on('playerJoined', (data) => {
updatePlayerList(data.players);
showNotification(`${data.playerName} joined the game`);
});
wsManager.on('playerLeft', (data) => {
updatePlayerList(data.players);
showNotification(`${data.playerName} left the game`);
});
wsManager.on('chatMessage', (data) => {
addChatMessage(data.sender, data.message, data.timestamp);
});
wsManager.on('error', (error) => {
console.error('WebSocket error:', error);
showError(error.message);
});
Players can create or join game rooms using unique room codes. The server manages room lifecycle, player assignment, and broadcasts game events only to players in the same room.
// Room creation and joining
function createRoom(playerName) {
const roomId = generateRoomId();
wsManager.emit('createRoom', { roomId, playerName });
wsManager.on('roomCreated', (data) => {
currentRoomId = data.roomId;
showWaitingScreen(roomId);
});
}
function joinRoom(roomId, playerName) {
wsManager.emit('joinRoom', { roomId, playerName });
wsManager.on('roomJoined', (data) => {
currentRoomId = roomId;
if (data.gameReady) {
startGame(data.players);
}
});
wsManager.on('roomFull', () => {
showError('Room is full');
});
wsManager.on('roomNotFound', () => {
showError('Room not found');
});
}
function generateRoomId() {
return Math.random().toString(36).substring(2, 8).toUpperCase();
}
The game board is represented as a 2D array (6 rows × 7 columns). Each cell can contain null (empty), 'red', or 'yellow'. The board uses gravity simulation where pieces always fall to the lowest available position.
class ConnectFourEngine {
constructor() {
this.rows = 6;
this.cols = 7;
this.board = this.createEmptyBoard();
this.currentPlayer = 'red';
this.winner = null;
}
createEmptyBoard() {
return Array(this.rows).fill().map(() => Array(this.cols).fill(null));
}
isValidMove(column) {
return column >= 0 && column < this.cols && this.board[0][column] === null;
}
makeMove(column) {
if (!this.isValidMove(column)) {
return { valid: false, error: 'Invalid move' };
}
// Find lowest available row
let row = -1;
for (let r = this.rows - 1; r >= 0; r--) {
if (this.board[r][column] === null) {
row = r;
break;
}
}
// Place piece
this.board[row][column] = this.currentPlayer;
// Check for win
const winner = this.checkWin(row, column);
if (winner) {
this.winner = winner;
return { valid: true, row, column, winner, winningCells: this.winningCells };
}
// Check for draw
if (this.isBoardFull()) {
return { valid: true, row, column, draw: true };
}
// Switch player
this.currentPlayer = this.currentPlayer === 'red' ? 'yellow' : 'red';
return { valid: true, row, column };
}
isBoardFull() {
return this.board[0].every(cell => cell !== null);
}
}
The win detection algorithm checks for four consecutive pieces in horizontal, vertical, and both diagonal directions. It's optimized to only check from the last placed piece, making it O(1) complexity.
checkWin(row, col) {
const player = this.board[row][col];
// Check all four directions
const directions = [
{ dr: 0, dc: 1 }, // Horizontal
{ dr: 1, dc: 0 }, // Vertical
{ dr: 1, dc: 1 }, // Diagonal \
{ dr: 1, dc: -1 } // Diagonal /
];
for (const { dr, dc } of directions) {
const cells = this.checkDirection(row, col, dr, dc, player);
if (cells.length >= 4) {
this.winningCells = cells;
return player;
}
}
return null;
}
checkDirection(row, col, dr, dc, player) {
const cells = [{ row, col }];
// Check forward direction
let r = row + dr;
let c = col + dc;
while (r >= 0 && r < this.rows && c >= 0 && c < this.cols &&
this.board[r][c] === player) {
cells.push({ row: r, col: c });
r += dr;
c += dc;
}
// Check backward direction
r = row - dr;
c = col - dc;
while (r >= 0 && r < this.rows && c >= 0 && c < this.cols &&
this.board[r][c] === player) {
cells.push({ row: r, col: c });
r -= dr;
c -= dc;
}
return cells;
}
The AI opponent uses the Minimax algorithm with alpha-beta pruning for efficient decision making. It evaluates board positions up to a certain depth and chooses the move with the best outcome.
class ConnectFourAI {
constructor(depth = 4) {
this.depth = depth;
}
getBestMove(board, player) {
let bestScore = -Infinity;
let bestColumn = null;
for (let col = 0; col < 7; col++) {
if (this.isValidMove(board, col)) {
const newBoard = this.simulateMove(board, col, player);
const score = this.minimax(newBoard, this.depth - 1, -Infinity, Infinity, false, player);
if (score > bestScore) {
bestScore = score;
bestColumn = col;
}
}
}
return bestColumn;
}
minimax(board, depth, alpha, beta, isMaximizing, player) {
// Check terminal states
const winner = this.checkWin(board);
if (winner === player) return 1000;
if (winner && winner !== player) return -1000;
if (this.isBoardFull(board) || depth === 0) return this.evaluateBoard(board, player);
const opponent = player === 'red' ? 'yellow' : 'red';
if (isMaximizing) {
let maxScore = -Infinity;
for (let col = 0; col < 7; col++) {
if (this.isValidMove(board, col)) {
const newBoard = this.simulateMove(board, col, player);
const score = this.minimax(newBoard, depth - 1, alpha, beta, false, player);
maxScore = Math.max(maxScore, score);
alpha = Math.max(alpha, score);
if (beta <= alpha) break;
}
}
return maxScore;
} else {
let minScore = Infinity;
for (let col = 0; col < 7; col++) {
if (this.isValidMove(board, col)) {
const newBoard = this.simulateMove(board, col, opponent);
const score = this.minimax(newBoard, depth - 1, alpha, beta, true, player);
minScore = Math.min(minScore, score);
beta = Math.min(beta, score);
if (beta <= alpha) break;
}
}
return minScore;
}
}
evaluateBoard(board, player) {
let score = 0;
// Evaluate horizontal, vertical, and diagonal patterns
// Award points for potential winning sequences
// ... evaluation logic
return score;
}
}
Click on a column to drop your piece!
You play as Red. Computer plays as Yellow. Get four in a row to win!
The application is deployed on a containerized infrastructure using Docker and Kubernetes. This allows for easy scaling and zero-downtime deployments.
# Dockerfile FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["node", "server.js"]
Redis Pub/Sub enables horizontal scaling across multiple server instances. Game state is shared through Redis, allowing any server to handle any game room.
// Redis adapter for Socket.io
const redis = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');
const pubClient = redis.createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
console.log('Redis adapter configured');
});
// Store game state in Redis
async function saveGameState(roomId, gameState) {
await pubClient.setEx(
`game:${roomId}`,
3600, // 1 hour TTL
JSON.stringify(gameState)
);
}
async function loadGameState(roomId) {
const data = await pubClient.get(`game:${roomId}`);
return data ? JSON.parse(data) : null;
}
Real-time monitoring tracks server health, active games, player count, and error rates. Analytics provide insights into player behavior and game patterns.
NGINX handles WebSocket connections with sticky sessions, distributing load across multiple server instances.
Kubernetes HPA automatically scales pods based on CPU and memory usage, handling traffic spikes seamlessly.
PostgreSQL with connection pooling and read replicas ensures fast data access even under heavy load.
Static assets served through CloudFlare CDN for global low-latency access to game assets and UI.
Sentry integration captures and reports errors in real-time for quick debugging and resolution.