import { StartGamePlayCommand } from './StartGamePlayCommand'; import { GameAggregate, GameState, BoardData, GameField } from '../../../Domain/Game/GameAggregate'; import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; import { DIContainer } from '../../Services/DIContainer'; import { RedisService } from '../../Services/RedisService'; import { WebSocketService } from '../../Services/WebSocketService'; import { logOther, logError } from '../../Services/Logger'; export interface GamePlayerPosition { playerId: string; playerName?: string; position: number; // Board position (starts at 0) turnOrder: number; // Random number to determine turn sequence isOnline: boolean; joinedAt: Date; } export interface ActiveGamePlayData { gameId: string; gameCode: string; hostId?: string; maxPlayers: number; players: GamePlayerPosition[]; state: GameState; createdAt: Date; startedAt: Date; currentTurn: number; // Index of current player in turn order turnSequence: string[]; // Ordered array of player IDs based on turnOrder websocketRoom: string; gamePhase: 'starting' | 'playing' | 'paused' | 'finished'; boardData: BoardData; // Generated board with fields } export interface GameStartResult { game: GameAggregate; boardData: BoardData; } export class StartGamePlayCommandHandler { private gameRepository: IGameRepository; private redisService: RedisService; constructor() { this.gameRepository = DIContainer.getInstance().gameRepository; this.redisService = RedisService.getInstance(); } async handle(command: StartGamePlayCommand): Promise { const startTime = performance.now(); try { logOther('Starting game play', `gameId: ${command.gameId}, userId: ${command.userId || 'system'}`); // Find the game const game = await this.gameRepository.findById(command.gameId); if (!game) { throw new Error(`Game with ID ${command.gameId} not found`); } // Validate game can be started this.validateGameCanStart(game, command.userId); // Wait for board generation to complete (max 20 seconds) const boardData = await this.waitForBoardGeneration(game.id); // Update game state in database const updatedGame = await this.gameRepository.update(game.id, { state: GameState.ACTIVE, startdate: new Date() }); if (!updatedGame) { throw new Error('Failed to update game state'); } // Initialize game play in Redis with board data await this.initializeGamePlayInRedis(updatedGame, boardData); // Notify all players via WebSocket await this.notifyGameStart(updatedGame); const endTime = performance.now(); logOther('Game play started successfully', { gameId: updatedGame.id, gameCode: updatedGame.gamecode, playerCount: updatedGame.players.length, executionTime: Math.round(endTime - startTime) }); return { game: updatedGame, boardData: boardData }; } catch (error) { const endTime = performance.now(); logError('Failed to start game play', error instanceof Error ? error : new Error(String(error))); logOther('Game start failed', { gameId: command.gameId, userId: command.userId, executionTime: Math.round(endTime - startTime) }); throw error; } } private validateGameCanStart(game: GameAggregate, userId?: string): void { // Check if game is in waiting state if (game.state !== GameState.WAITING) { throw new Error('Game is not in waiting state and cannot be started'); } // Check if there are enough players (at least 2) if (game.players.length < 2) { throw new Error('Game needs at least 2 players to start'); } // For private and organization games, check if user is game master if (game.createdby && userId && game.createdby !== userId) { throw new Error('Only the game master can start this game'); } logOther('Game start validation passed', { gameId: game.id, gameCode: game.gamecode, playerCount: game.players.length, gameState: game.state, isGameMaster: !game.createdby || (userId && game.createdby === userId) }); } private async initializeGamePlayInRedis(game: GameAggregate, boardData: BoardData): Promise { try { const redisKey = `gameplay:${game.id}`; // Generate random turn orders for all players const playersWithPositions = this.initializePlayerPositions(game.players); // Sort by turn order to create turn sequence const turnSequence = [...playersWithPositions] .sort((a, b) => a.turnOrder - b.turnOrder) .map(p => p.playerId); const gamePlayData: ActiveGamePlayData = { gameId: game.id, gameCode: game.gamecode, hostId: game.createdby || undefined, maxPlayers: game.maxplayers, players: playersWithPositions, state: GameState.ACTIVE, createdAt: game.createdate, startedAt: new Date(), currentTurn: 0, // Start with first player in sequence turnSequence, websocketRoom: `game_${game.gamecode}`, gamePhase: 'starting', boardData }; // Store game play data in Redis with TTL (24 hours) await this.redisService.setWithExpiry(redisKey, JSON.stringify(gamePlayData), 24 * 60 * 60); // Create turn sequence mapping for quick lookups await this.redisService.setWithExpiry( `game_turns:${game.id}`, JSON.stringify(turnSequence), 24 * 60 * 60 ); logOther('Game play initialized in Redis', { gameId: game.id, gameCode: game.gamecode, playerCount: playersWithPositions.length, turnSequence, currentPlayer: turnSequence[0], redisKey }); } catch (error) { logError('Failed to initialize game play in Redis', error instanceof Error ? error : new Error(String(error))); throw new Error('Failed to initialize game session'); } } private initializePlayerPositions(playerIds: string[]): GamePlayerPosition[] { const players: GamePlayerPosition[] = []; // Generate random turn orders (1 to playerCount) const turnOrders = this.generateRandomTurnOrders(playerIds.length); playerIds.forEach((playerId, index) => { players.push({ playerId, position: 0, // All players start at position 0 turnOrder: turnOrders[index], isOnline: true, // Assume online when game starts joinedAt: new Date() }); }); logOther('Player positions initialized', { playerCount: players.length, turnOrders: turnOrders, playersData: players.map(p => ({ playerId: p.playerId, position: p.position, turnOrder: p.turnOrder })) }); return players; } private generateRandomTurnOrders(playerCount: number): number[] { // Create array [1, 2, 3, ..., playerCount] const orders = Array.from({ length: playerCount }, (_, i) => i + 1); // Fisher-Yates shuffle for (let i = orders.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [orders[i], orders[j]] = [orders[j], orders[i]]; } return orders; } private async notifyGameStart(game: GameAggregate): Promise { try { // Note: WebSocket notifications will be handled when WebSocket service is available // For now, just log the game start logOther('Game start notifications prepared', { gameId: game.id, gameCode: game.gamecode, playerCount: game.players.length, websocketRoom: `game_${game.gamecode}` }); // TODO: Implement WebSocket notifications when service is properly integrated // wsService.notifyGameStart(game.gamecode, game.players); // wsService.broadcastGameStateUpdate(game.gamecode, gameStateData); } catch (error) { logError('Failed to prepare game start notifications', error instanceof Error ? error : new Error(String(error))); // Don't throw error here - notification failure shouldn't prevent game start } } async getGamePlayFromRedis(gameId: string): Promise { try { const redisKey = `gameplay:${gameId}`; const data = await this.redisService.get(redisKey); return data ? JSON.parse(data) as ActiveGamePlayData : null; } catch (error) { logError('Failed to get game play from Redis', error instanceof Error ? error : new Error(String(error))); return null; } } async updatePlayerPosition(gameId: string, playerId: string, newPosition: number): Promise { try { const gameData = await this.getGamePlayFromRedis(gameId); if (!gameData) { throw new Error('Game session not found'); } // Update player position const player = gameData.players.find(p => p.playerId === playerId); if (player) { player.position = newPosition; // Save back to Redis const redisKey = `gameplay:${gameId}`; await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); logOther('Player position updated', { gameId, playerId, newPosition }); } } catch (error) { logError('Failed to update player position', error instanceof Error ? error : new Error(String(error))); throw error; } } async getNextPlayer(gameId: string): Promise { try { const gameData = await this.getGamePlayFromRedis(gameId); if (!gameData) { return null; } const nextTurnIndex = (gameData.currentTurn + 1) % gameData.turnSequence.length; return gameData.turnSequence[nextTurnIndex]; } catch (error) { logError('Failed to get next player', error instanceof Error ? error : new Error(String(error))); return null; } } async advanceTurn(gameId: string): Promise { try { const gameData = await this.getGamePlayFromRedis(gameId); if (!gameData) { return null; } // Advance to next player gameData.currentTurn = (gameData.currentTurn + 1) % gameData.turnSequence.length; const currentPlayer = gameData.turnSequence[gameData.currentTurn]; // Save back to Redis const redisKey = `gameplay:${gameId}`; await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); logOther('Turn advanced', { gameId, currentTurn: gameData.currentTurn, currentPlayer }); return currentPlayer; } catch (error) { logError('Failed to advance turn', error instanceof Error ? error : new Error(String(error))); return null; } } private async waitForBoardGeneration(gameId: string): Promise { const maxWaitTime = parseInt(process.env.MAX_GENERATION_TIME_SECONDS || '20') * 1000; const pollInterval = 500; // Check every 500ms const startTime = Date.now(); logOther(`Waiting for board generation for game ${gameId}`, { maxWaitTime: maxWaitTime / 1000, pollInterval, redisKey: `game_board_${gameId}` }); while (Date.now() - startTime < maxWaitTime) { try { const redisKey = `game_board_${gameId}`; const boardDataStr = await this.redisService.get(redisKey); logOther(`Board generation check for game ${gameId}`, { attempt: Math.floor((Date.now() - startTime) / pollInterval) + 1, hasData: !!boardDataStr, dataLength: boardDataStr ? boardDataStr.length : 0, waitTime: Date.now() - startTime }); if (boardDataStr) { const boardData: BoardData = JSON.parse(boardDataStr); logOther(`Board data found for game ${gameId}`, { generationComplete: boardData.generationComplete, hasError: !!boardData.error, fieldsCount: boardData.fields ? boardData.fields.length : 0 }); if (boardData.generationComplete) { if (boardData.error) { logError(`Board generation failed for game ${gameId}`, new Error(boardData.error)); throw new Error(`Board generation failed: ${boardData.error}`); } logOther(`Board generation completed for game ${gameId}`, { fieldCount: boardData.fields.length, waitTime: Date.now() - startTime }); return boardData; } } else { // No board data found yet - check if we need to trigger generation logOther(`No board data found yet for game ${gameId}, checking if generation was triggered...`, { waitTime: Date.now() - startTime, redisKey }); // If we've waited for 2 seconds and still no data, try to trigger generation manually if (Date.now() - startTime > 2000) { await this.ensureBoardGenerationTriggered(gameId); } } // Wait before next poll await new Promise(resolve => setTimeout(resolve, pollInterval)); } catch (error) { logError(`Error checking board generation status for game ${gameId}`, error as Error); throw new Error(`Failed to retrieve board data: ${error instanceof Error ? error.message : String(error)}`); } } // Timeout reached logError(`Board generation timeout for game ${gameId}`, new Error(`Generation took longer than ${maxWaitTime / 1000} seconds`)); throw new Error(`Board generation timeout. Game ${gameId} is not ready to start. Please try again later.`); } private async ensureBoardGenerationTriggered(gameId: string): Promise { try { logOther(`Ensuring board generation is triggered for game ${gameId}`); // Check if generation was already triggered by looking for any board data const redisKey = `game_board_${gameId}`; const existingData = await this.redisService.get(redisKey); if (!existingData) { // No data at all - trigger generation manually logOther(`No board generation found for game ${gameId}, triggering manually`); // Use DIContainer to trigger board generation const generateBoardCommand = { gameId, positiveFieldCount: Math.floor(67 * 0.6), // Default: 60% positive negativeFieldCount: Math.floor(67 * 0.25), // Default: 25% negative luckFieldCount: Math.floor(67 * 0.15) // Default: 15% luck }; await DIContainer.getInstance().generateBoardCommandHandler.execute(generateBoardCommand); logOther(`Board generation manually triggered for game ${gameId}`); } } catch (error) { logError(`Failed to ensure board generation for game ${gameId}`, error as Error); // Don't throw here - let the main wait loop handle the timeout } } }