import { StartGameCommand } from './StartGameCommand'; import { GameAggregate, GameDeck, GameCard, DeckType, GameState } from '../../../Domain/Game/GameAggregate'; import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; import { DIContainer } from '../../Services/DIContainer'; import { RedisService } from '../../Services/RedisService'; import { logOther, logError } from '../../Services/Logger'; import { randomBytes } from 'crypto'; import { GenerateBoardCommand } from './GenerateBoardCommand'; export interface ActiveGameData { gameId: string; gameCode: string; hostId?: string; maxPlayers: number; currentPlayers: GamePlayerData[]; state: GameState; createdAt: Date; startedAt?: Date; currentTurn?: string; websocketRoom: string; } export interface GamePlayerData { playerId: string; playerName?: string; joinedAt: Date; isOnline: boolean; position?: number; } export class StartGameCommandHandler { private gameRepository: IGameRepository; private deckRepository: IDeckRepository; private redisService: RedisService; constructor() { this.gameRepository = DIContainer.getInstance().gameRepository; this.deckRepository = DIContainer.getInstance().deckRepository; this.redisService = RedisService.getInstance(); } async handle(command: StartGameCommand): Promise { const startTime = performance.now(); try { logOther('Starting game creation', `deckCount: ${command.deckids.length}, maxPlayers: ${command.maxplayers}, loginType: ${command.logintype}`); // Generate unique game code const gamecode = this.generateGameCode(); // Fetch all decks by IDs const decks = await this.fetchDecks(command.deckids); // Validate we have 3 deck types this.validateDeckTypes(decks); // Group decks by type and shuffle cards within each type const gamedecks = await this.createShuffledGameDecks(decks); // Create the game aggregate const gameData: Partial = { gamecode, maxplayers: command.maxplayers, logintype: command.logintype, createdby: command.userid!, orgid: command.orgid || null, gamedecks, players: [], winner: null, state: GameState.WAITING, startdate: null, enddate: null }; // Save the game to database const savedGame = await this.gameRepository.create(gameData); // Create Redis object for real-time game management await this.createGameInRedis(savedGame, command.userid); // Trigger async board generation (don't block game creation) this.triggerAsyncBoardGeneration(savedGame.id).catch((error: Error) => { logError('Async board generation failed', error); }); const endTime = performance.now(); logOther('Game created successfully', `gameId: ${savedGame.id}, gameCode: ${savedGame.gamecode}, executionTime: ${Math.round(endTime - startTime)}ms`); return savedGame; } catch (error) { const endTime = performance.now(); logError('Failed to create game', error instanceof Error ? error : new Error(String(error))); logOther('Game creation failed', `executionTime: ${Math.round(endTime - startTime)}ms`); throw new Error('Failed to start game: ' + (error instanceof Error ? error.message : String(error))); } } private generateGameCode(): string { // Generate a 6-character alphanumeric game code const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let result = ''; const randomBytesArray = randomBytes(6); for (let i = 0; i < 6; i++) { result += chars[randomBytesArray[i] % chars.length]; } return result; } private async fetchDecks(deckIds: string[]): Promise { const decks: DeckAggregate[] = []; for (const deckId of deckIds) { const deck = await this.deckRepository.findById(deckId); if (!deck) { throw new Error(`Deck with ID ${deckId} not found`); } decks.push(deck); } return decks; } private validateDeckTypes(decks: DeckAggregate[]): void { const deckTypes = new Set(decks.map(deck => deck.type)); // Check if we have all 3 required deck types (LUCK=0, JOKER=1, QUESTION=2) const requiredTypes = [0, 1, 2]; // Based on Type enum in DeckAggregate const missingTypes = requiredTypes.filter(type => !deckTypes.has(type)); if (missingTypes.length > 0) { throw new Error(`Missing required deck types: ${missingTypes.join(', ')}. Game requires LUCK, JOKER, and QUESTION deck types.`); } logOther('Deck types validation passed', `foundTypes: [${Array.from(deckTypes).join(', ')}]`); } private async createShuffledGameDecks(decks: DeckAggregate[]): Promise { // Group decks by type const decksByType = new Map(); decks.forEach(deck => { if (!decksByType.has(deck.type)) { decksByType.set(deck.type, []); } decksByType.get(deck.type)!.push(deck); }); const gamedecks: GameDeck[] = []; // Process each deck type for (const [deckType, typeDecks] of decksByType) { // Collect all cards from decks of this type const allCards: GameCard[] = []; typeDecks.forEach(deck => { deck.cards.forEach(card => { const gameCard: GameCard = { cardid: this.generateCardId(), question: card.text, answer: card.answer || undefined, consequence: card.consequence || null, played: false, playerid: undefined }; allCards.push(gameCard); }); }); // Shuffle all cards of this type const shuffledCards = this.shuffleArray(allCards); // Create game deck for this type const gameDeck: GameDeck = { deckid: typeDecks[0].id, // Use first deck ID as representative decktype: this.mapDeckTypeToGameDeckType(deckType), cards: shuffledCards }; gamedecks.push(gameDeck); logOther('Created shuffled game deck', `type: ${deckType}, cardCount: ${shuffledCards.length}, sourceDecks: ${typeDecks.length}`); } return gamedecks; } private mapDeckTypeToGameDeckType(deckType: number): DeckType { // Map DeckAggregate.Type to GameAggregate.DeckType switch (deckType) { case 0: return DeckType.LUCK; // LUCK = 0 case 1: return DeckType.JOCKER; // JOKER = 1 case 2: return DeckType.QUEST; // QUESTION = 2 default: throw new Error(`Unknown deck type: ${deckType}`); } } private shuffleArray(array: T[]): T[] { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } private generateCardId(): string { return randomBytes(8).toString('hex'); } private async createGameInRedis(game: GameAggregate, hostId?: string): Promise { try { const redisKey = `game:${game.id}`; const gameData: ActiveGameData = { gameId: game.id, gameCode: game.gamecode, hostId: hostId, maxPlayers: game.maxplayers, currentPlayers: [], state: game.state, createdAt: game.createdate, websocketRoom: `game_${game.gamecode}` }; // Store game data in Redis with TTL (24 hours) await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); // Create game room for WebSocket connections await this.redisService.set(`game_room:${game.gamecode}`, game.id); logOther('Game created in Redis', { gameId: game.id, gameCode: game.gamecode, hostId: hostId, websocketRoom: gameData.websocketRoom, redisKey }); } catch (error) { logError('Failed to create game in Redis', error instanceof Error ? error : new Error(String(error))); // Don't throw error here - Redis failure shouldn't prevent game creation logOther('Game created successfully despite Redis error', { gameId: game.id, gameCode: game.gamecode }); } } private async triggerAsyncBoardGeneration(gameId: string): Promise { try { // Calculate default field counts based on game configuration // For now, use reasonable defaults - this should be configurable by host in the future const maxSpecialFieldsPercentage = parseInt(process.env.MAX_SPECIAL_FIELDS_PERCENTAGE || '67'); const maxSpecialFields = Math.floor((100 * maxSpecialFieldsPercentage) / 100); // Default distribution: 60% positive, 25% negative, 15% luck const positiveFieldCount = Math.floor(maxSpecialFields * 0.6); const negativeFieldCount = Math.floor(maxSpecialFields * 0.25); const luckFieldCount = Math.floor(maxSpecialFields * 0.15); const command: GenerateBoardCommand = { gameId, positiveFieldCount, negativeFieldCount, luckFieldCount }; logOther(`Triggering async board generation for game ${gameId}`, { positiveFieldCount, negativeFieldCount, luckFieldCount, totalSpecialFields: positiveFieldCount + negativeFieldCount + luckFieldCount }); // Execute board generation in background await DIContainer.getInstance().generateBoardCommandHandler.execute(command); } catch (error) { logError(`Async board generation failed for game ${gameId}`, error as Error); // Don't propagate error - board generation failure shouldn't affect game creation } } }