import { createServer } from 'http'; import { Server } from 'socket.io'; import { GameState, Player, GameMove } from '@/lib/types'; import { dealTiles, findStartingPlayer, executeMove, canPlayerMove, isGameBlocked, determineBlockedWinner } from '@/lib/gameLogic'; const httpServer = createServer(); const io = new Server(httpServer, { cors: { origin: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000', methods: ['GET', 'POST'], }, }); // Store active game rooms const gameRooms = new Map(); const playerRooms = new Map(); // playerId -> roomId // Generate unique room ID function generateRoomId(): string { return Math.random().toString(36).substring(2, 8).toUpperCase(); } // Create a new game state function createGameState(roomId: string): GameState { return { id: roomId, players: [], currentPlayerIndex: 0, board: [], boneyard: [], boardEnds: [], winner: null, isGameOver: false, turnsPassed: 0, gameMode: 'waiting', }; } // Initialize game when all players are ready function startGame(roomId: string): GameState { const gameState = gameRooms.get(roomId); if (!gameState) throw new Error('Game not found'); const { playerTiles, boneyard } = dealTiles(gameState.players.length); const updatedPlayers = gameState.players.map((player, index) => ({ ...player, tiles: playerTiles[index], score: 0, })); const startingPlayerIndex = findStartingPlayer(updatedPlayers); const newGameState: GameState = { ...gameState, players: updatedPlayers, currentPlayerIndex: startingPlayerIndex, boneyard, gameMode: 'playing', }; gameRooms.set(roomId, newGameState); return newGameState; } io.on('connection', (socket) => { console.log('Client connected:', socket.id); // Create room socket.on('create-room', () => { const roomId = generateRoomId(); const gameState = createGameState(roomId); gameRooms.set(roomId, gameState); socket.join(roomId); socket.emit('room-created', roomId); console.log('Room created:', roomId); }); // Join room socket.on('join-room', (roomId: string, playerName: string) => { const gameState = gameRooms.get(roomId); if (!gameState) { socket.emit('error', 'Room not found'); return; } if (gameState.players.length >= 4) { socket.emit('error', 'Room is full'); return; } if (gameState.gameMode !== 'waiting') { socket.emit('error', 'Game already started'); return; } const player: Player = { id: socket.id, name: playerName, tiles: [], score: 0, isAI: false, isReady: false, }; gameState.players.push(player); playerRooms.set(socket.id, roomId); socket.join(roomId); socket.emit('room-joined', gameState, socket.id); socket.to(roomId).emit('player-joined', player); console.log(`Player ${playerName} joined room ${roomId}`); }); // Player ready socket.on('player-ready', (roomId: string) => { const gameState = gameRooms.get(roomId); if (!gameState) return; const player = gameState.players.find(p => p.id === socket.id); if (!player) return; player.isReady = true; gameRooms.set(roomId, gameState); // Start game if all players are ready and at least 2 players const allReady = gameState.players.length >= 2 && gameState.players.every(p => p.isReady); if (allReady) { const startedGame = startGame(roomId); io.to(roomId).emit('game-started', startedGame); console.log('Game started in room:', roomId); } else { io.to(roomId).emit('game-state-updated', gameState); } }); // Make move socket.on('make-move', (roomId: string, move: GameMove) => { const gameState = gameRooms.get(roomId); if (!gameState) return; const currentPlayer = gameState.players[gameState.currentPlayerIndex]; if (currentPlayer.id !== socket.id) { socket.emit('invalid-move', 'Not your turn'); return; } try { const newGameState = executeMove(gameState, move); // Check if game is blocked if (isGameBlocked(newGameState)) { const winnerId = determineBlockedWinner(newGameState); newGameState.winner = winnerId; newGameState.isGameOver = true; newGameState.gameMode = 'finished'; } gameRooms.set(roomId, newGameState); io.to(roomId).emit('game-state-updated', newGameState); } catch { socket.emit('invalid-move', 'Invalid move'); } }); // Draw tile socket.on('draw-tile', (roomId: string) => { const gameState = gameRooms.get(roomId); if (!gameState) return; const currentPlayer = gameState.players[gameState.currentPlayerIndex]; if (currentPlayer.id !== socket.id) { socket.emit('invalid-move', 'Not your turn'); return; } if (gameState.boneyard.length === 0) { socket.emit('invalid-move', 'No tiles left to draw'); return; } const drawnTile = gameState.boneyard.pop()!; currentPlayer.tiles.push(drawnTile); // Check if player can move now if (!canPlayerMove(currentPlayer, gameState.boardEnds)) { gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length; } gameRooms.set(roomId, gameState); io.to(roomId).emit('game-state-updated', gameState); }); // Leave room socket.on('leave-room', (roomId: string) => { handlePlayerLeave(socket.id, roomId); }); // Disconnect socket.on('disconnect', () => { const roomId = playerRooms.get(socket.id); if (roomId) { handlePlayerLeave(socket.id, roomId); } console.log('Client disconnected:', socket.id); }); }); function handlePlayerLeave(playerId: string, roomId: string) { const gameState = gameRooms.get(roomId); if (!gameState) return; gameState.players = gameState.players.filter(p => p.id !== playerId); playerRooms.delete(playerId); if (gameState.players.length === 0) { gameRooms.delete(roomId); console.log('Room deleted:', roomId); } else { gameRooms.set(roomId, gameState); io.to(roomId).emit('player-left', playerId); io.to(roomId).emit('game-state-updated', gameState); } } const PORT = process.env.SOCKET_PORT || 3001; httpServer.listen(PORT, () => { console.log(`Socket.IO server running on port ${PORT}`); }); export { io };