import { JoinGameCommand } from './JoinGameCommand'; import { GameAggregate, GameState, LoginType } from '../../../Domain/Game/GameAggregate'; import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; import { DIContainer } from '../../Services/DIContainer'; import { RedisService } from '../../Services/RedisService'; import { logOther, logError } from '../../Services/Logger'; import { v4 as uuidv4 } from 'uuid'; export interface GamePlayerData { playerId: string; playerName?: string; joinedAt: Date; isOnline: boolean; position?: number; // For game board position (to be used later) } export interface ActiveGameData { gameId: string; gameCode: string; hostId?: string; maxPlayers: number; currentPlayers: GamePlayerData[]; state: GameState; createdAt: Date; startedAt?: Date; currentTurn?: string; // Player ID whose turn it is websocketRoom: string; // WebSocket room name for real-time updates } export class JoinGameCommandHandler { private gameRepository: IGameRepository; private redisService: RedisService; constructor() { this.gameRepository = DIContainer.getInstance().gameRepository; this.redisService = RedisService.getInstance(); } async handle(command: JoinGameCommand): Promise { const startTime = performance.now(); try { logOther('Joining game', `gameCode: ${command.gameCode}, playerId: ${command.playerId || 'anonymous'}, loginType: ${command.loginType}`); // Find the game by game code const game = await this.gameRepository.findByGameCode(command.gameCode); if (!game) { throw new Error(`Game with code ${command.gameCode} not found`); } // Generate player ID for public games or use provided one const actualPlayerId = command.playerId || uuidv4(); // Validate game joinability (authentication/org checks done in router) this.validateGameJoinability(game, actualPlayerId, command); // Add player to database const updatedGame = await this.gameRepository.addPlayerToGame(game.id, actualPlayerId); if (!updatedGame) { throw new Error('Failed to add player to game'); } // Update Redis with the new player await this.updateGameInRedis(updatedGame, { ...command, playerId: actualPlayerId }); const endTime = performance.now(); logOther('Player joined game successfully', { gameId: game.id, gameCode: game.gamecode, playerId: actualPlayerId, playerCount: updatedGame.players.length, maxPlayers: updatedGame.maxplayers, loginType: game.logintype, executionTime: Math.round(endTime - startTime) }); return updatedGame; } catch (error) { const endTime = performance.now(); logError('Failed to join game', error instanceof Error ? error : new Error(String(error))); logOther('Game join failed', { gameCode: command.gameCode, playerId: command.playerId || 'anonymous', loginType: command.loginType, executionTime: Math.round(endTime - startTime) }); throw error; } } private validateGameJoinability(game: GameAggregate, playerId: string, command: JoinGameCommand): void { // Check if game is in waiting state if (game.state !== GameState.WAITING) { throw new Error('Game is not accepting new players'); } // Check if player is already in the game if (game.players.includes(playerId)) { throw new Error('Player is already in this game'); } // Check if game is full if (game.players.length >= game.maxplayers) { throw new Error('Game is full'); } // Note: Login type validation is now handled in the router before reaching this handler // This ensures proper authentication and organization membership checks are done first logOther('Game join validation passed', { gameId: game.id, gameCode: game.gamecode, currentPlayers: game.players.length, maxPlayers: game.maxplayers, gameState: game.state, loginType: game.logintype, playerId: playerId, isAuthenticated: !!command.playerId }); } private async updateGameInRedis(game: GameAggregate, command: JoinGameCommand & { playerId: string }): Promise { try { const redisKey = `game:${game.id}`; // Get existing game data from Redis or create new let gameData: ActiveGameData; const existingData = await this.redisService.get(redisKey); if (existingData) { gameData = JSON.parse(existingData) as ActiveGameData; } else { // Create new game data structure gameData = { gameId: game.id, gameCode: game.gamecode, maxPlayers: game.maxplayers, currentPlayers: [], state: game.state, createdAt: game.createdate, websocketRoom: `game_${game.gamecode}` }; } // Add the new player const newPlayer: GamePlayerData = { playerId: command.playerId, playerName: command.playerName, joinedAt: new Date(), isOnline: true }; <<<<<<< HEAD // Check if player name is already in use by a different player const existingPlayerWithName = gameData.currentPlayers.find( p => p.playerName === command.playerName && p.playerId !== command.playerId ); if (existingPlayerWithName) { throw new Error(`Player name "${command.playerName}" is already in use in this game`); } ======= >>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 // Update players list (remove if exists, then add) gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId); gameData.currentPlayers.push(newPlayer); // Update game state and player count gameData.state = game.state; // Store updated data in Redis with TTL (24 hours) await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); <<<<<<< HEAD ======= // Add player to active players set await this.redisService.setAdd(`active_players:${game.id}`, command.playerId); >>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 logOther('Game data updated in Redis', { gameId: game.id, gameCode: game.gamecode, redisKey, playerCount: gameData.currentPlayers.length, websocketRoom: gameData.websocketRoom, playerId: command.playerId }); } catch (error) { logError('Failed to update game in Redis', error instanceof Error ? error : new Error(String(error))); // Don't throw error here - Redis failure shouldn't prevent game join logOther('Game join completed despite Redis error', { gameId: game.id, playerId: command.playerId }); } } async getGameFromRedis(gameId: string): Promise { try { const redisKey = `game:${gameId}`; const data = await this.redisService.get(redisKey); return data ? JSON.parse(data) as ActiveGameData : null; } catch (error) { logError('Failed to get game from Redis', error instanceof Error ? error : new Error(String(error))); return null; } } async removePlayerFromRedis(gameId: string, playerId: string): Promise { try { const redisKey = `game:${gameId}`; const existingData = await this.redisService.get(redisKey); if (existingData) { const gameData = JSON.parse(existingData) as ActiveGameData; gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId); await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); <<<<<<< HEAD ======= await this.redisService.setRemove(`active_players:${gameId}`, playerId); >>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 } } catch (error) { logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error))); } } }