A production-grade real-time multiplayer Tic-Tac-Toe game with AI opponent, WebSocket-based gameplay, and atomic move validation

A full-stack multiplayer Tic-Tac-Toe application demonstrating real-time communication patterns and distributed systems design. The backend features a Go WebSocket server for sub-millisecond game events, Node.js REST API for authentication and persistence, with Redis handling atomic move validation via Lua scripting. The frontend is a responsive React SPA with Zustand state management and optimistic UI updates. The system ensures game integrity through Redis-based atomic operations, preventing race conditions in concurrent move scenarios.
Race Condition in Concurrent Moves - Two players submitting moves simultaneously could cause invalid game states
Implemented atomic move validation using Redis Lua scripts that validate, update board state, check win conditions, and publish events in a single atomic operation
Atomic Move Validation with Redis Lua
-- Atomic move validation in Redis
local gameKey = KEYS[1]
local movesKey = KEYS[2]
local playerId = ARGV[1]
local cell = tonumber(ARGV[3])
-- Get current game state
local state = redis.call('HGETALL', gameKey)
-- Validate it's player's turn
if state.turn ~= playerId then
return {'err', 'NOT_YOUR_TURN'}
end
-- Validate cell is empty
local board = cjson.decode(state.board)
if board[cell + 1] ~= '' then
return {'err', 'CELL_OCCUPIED'}
end
-- Apply move and check winner
board[cell + 1] = state.playerSymbols[playerId]
local winner, winningLine = checkWinner(board)
-- Update state atomically
redis.call('HSET', gameKey,
'board', cjson.encode(board),
'turn', getNextPlayer(state),
'status', winner and 'finished' or 'playing'
)
-- Set TTL on finished games (1 hour cleanup)
if winner then
redis.call('EXPIRE', gameKey, 3600)
redis.call('EXPIRE', movesKey, 3600)
end
-- Publish move event to all subscribers
redis.call('PUBLISH', 'game:'..gameId..':events', eventPayload)
return {'ok', 'true', 'state', newState}Cross-platform bcrypt compilation - Native bcrypt module failing in Docker containers built on Windows
Replaced native bcrypt with bcryptjs (pure JavaScript) to eliminate platform-specific compilation issues in containerized environments
WebSocket Connection Manager
// Manager handles WebSocket connections and game events
type Manager struct {
clients map[*Client]bool
games map[string]map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan GameEvent
redis *redis.Client
nodeAPIURL string
}
// Run starts the manager's event loop
func (m *Manager) Run() {
for {
select {
case client := <-m.register:
m.clients[client] = true
log.Printf("[MANAGER] Client connected: %s", client.id)
case client := <-m.unregister:
if _, ok := m.clients[client]; ok {
delete(m.clients, client)
close(client.send)
m.removeFromGame(client)
}
case event := <-m.broadcast:
// Broadcast to all clients in the game room
if clients, ok := m.games[event.GameID]; ok {
for client := range clients {
select {
case client.send <- event.Data:
default:
close(client.send)
delete(clients, client)
}
}
}
}
}
}
// persistFinishedGame sends game data to Node API for MongoDB storage
func (m *Manager) persistFinishedGame(event map[string]interface{}) {
jsonData, _ := json.Marshal(event)
url := m.nodeAPIURL + "/api/persist-finished"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode == 200 {
log.Printf("[MANAGER] Game persisted to MongoDB")
}
}WebSocket origin validation - Handling dynamic CORS origins for development and production environments
Created flexible origin checking with wildcard support and environment-based configuration for seamless dev-to-prod transitions
WebSocket Origin Validation with Wildcard Support
// NewWebSocketHandler configures origin checking for dev/prod environments
func NewWebSocketHandler(manager *ws.Manager, allowedOrigins []string) *WebSocketHandler {
originsMap := make(map[string]bool)
allowAll := false
// Build whitelist, detect wildcard mode
for _, origin := range allowedOrigins {
if origin == "*" {
allowAll = true // Development mode: allow all origins
}
originsMap[origin] = true
}
// Configure WebSocket upgrader with dynamic origin checking
upgrader.CheckOrigin = func(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true // Allow same-origin requests
}
// Wildcard mode for local development
if allowAll {
return true
}
// Production: strict whitelist checking
allowed := originsMap[origin]
if !allowed {
log.Printf("[WEBSOCKET] Rejected unauthorized origin: %s", origin)
}
return allowed
}
return &WebSocketHandler{manager: manager, allowedOrigins: originsMap}
}