A production-grade, real-time multiplayer Tic-Tac-Toe platform with unbeatable AI, live WebSocket gameplay, and persistent game history.

A full-stack Tic-Tac-Toe application demonstrating advanced frontend engineering and distributed backend architecture. The backend combines a Go WebSocket server for real-time play and a Node.js REST API for player management and persistence, using Redis and MongoDB. The frontend is a modern React SPA with Minimax AI (with teaching mode), multiplayer via WebSockets, IndexedDB game history, and a mobile-first, accessible UI.
Ensuring atomic move validation and preventing race conditions in multiplayer
Used Redis Lua scripts for atomic move validation and pub/sub for event broadcasting
Minimax AI with Alpha-Beta Pruning
function minimax(board, depth, alpha, beta, maximizing) {
if (isTerminal(board) || depth === 0) return evaluate(board);
if (maximizing) {
let maxEval = -Infinity;
for (const move of getMoves(board)) {
const eval = minimax(applyMove(board, move), depth - 1, alpha, beta, false);
maxEval = Math.max(maxEval, eval);
alpha = Math.max(alpha, eval);
if (beta <= alpha) break;
}
return maxEval;
} else {
let minEval = Infinity;
for (const move of getMoves(board)) {
const eval = minimax(applyMove(board, move), depth - 1, alpha, beta, true);
minEval = Math.min(minEval, eval);
beta = Math.min(beta, eval);
if (beta <= alpha) break;
}
return minEval;
}
}Scaling WebSocket connections for thousands of concurrent players
Stateless Go WebSocket server with Redis connection pooling for horizontal scaling
Atomic Move Validation with Redis Lua
// Lua script for atomic move validation
EVAL "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 then redis.call('HSET', KEYS[1], ARGV[1], ARGV[2]); return 1 else return 0 end" 1 game:1234 moveX 'X'
Providing a non-blocking, explainable AI experience in the browser
Web Worker offloading for AI calculations and teaching mode explanations
Web Worker AI Offloading
// Offload AI to a Web Worker
const worker = new Worker('minimax.worker.js');
worker.postMessage({ board, difficulty });
worker.onmessage = (e) => setBestMove(e.data.move);