import Module from './Module.js'; /** * GameLoader - Discovers and manages game modules * Handles dynamic loading, compatibility scoring, and game lifecycle */ class GameLoader extends Module { constructor(name, dependencies, config = {}) { super(name, ['eventBus']); // Validate dependencies if (!dependencies.eventBus) { throw new Error('GameLoader requires EventBus dependency'); } this._eventBus = dependencies.eventBus; this._config = config; // Game management this._games = new Map(); // gameId -> game info this._gameInstances = new Map(); // instanceId -> game instance this._currentContent = null; // Game discovery paths this._gamePaths = [ '../games/FlashcardLearning.js', '../games/StoryReader.js', '../games/LetterDiscovery.js', '../games/QuizGame.js', '../games/ThematicQuestions.js', '../games/AdventureReader.js', '../games/WizardSpellCaster.js', '../games/TeamWizardBattle.js', '../games/WordStorm.js', '../games/WhackAMole.js', '../games/WordDiscovery.js', '../games/GrammarDiscovery.js', '../games/FillTheBlank.js', '../games/RiverRun.js', '../games/ChineseStudy.js', '../games/WhackAMoleHard.js', '../games/MarioEducational.js', '../games/SentenceInvaders.js' // All current games with Module architecture ]; Object.seal(this); } async init() { this._validateNotDestroyed(); // Set up event listeners this._eventBus.on('content:loaded', this._handleContentUpdate.bind(this), this.name); this._eventBus.on('game:launch-request', this._handleLaunchRequest.bind(this), this.name); this._eventBus.on('game:exit-request', this._handleExitRequest.bind(this), this.name); // Discover available games await this._discoverGames(); this._setInitialized(); } async destroy() { this._validateNotDestroyed(); // Destroy all active game instances for (const [instanceId, gameInstance] of this._gameInstances) { try { await gameInstance.destroy(); } catch (error) { console.warn(`Error destroying game instance ${instanceId}:`, error); } } this._gameInstances.clear(); this._games.clear(); this._setDestroyed(); } /** * Get all discovered games with compatibility scores * @returns {Array} Array of game info objects */ getAvailableGames() { this._validateInitialized(); return Array.from(this._games.values()); } /** * Get games compatible with current content * @param {number} minScore - Minimum compatibility score (0-1) * @returns {Array} Array of compatible games sorted by score */ getCompatibleGames(minScore = 0.3) { this._validateInitialized(); return this.getAvailableGames() .filter(game => game.compatibility.score >= minScore) .sort((a, b) => b.compatibility.score - a.compatibility.score); } /** * Launch a game instance * @param {string} gameId - ID of the game to launch * @param {Object} options - Launch options * @returns {Promise} Instance ID */ async launchGame(gameId, options = {}) { this._validateInitialized(); const gameInfo = this._games.get(gameId); if (!gameInfo) { throw new Error(`Game not found: ${gameId}`); } // Check compatibility if (gameInfo.compatibility.score < 0.1) { throw new Error(`Game ${gameId} is not compatible with current content`); } try { // Create unique instance ID const instanceId = `${gameId}-${Date.now()}`; // Prepare game dependencies const gameDependencies = { eventBus: this._eventBus, content: this._currentContent, ...options.dependencies }; // Register the game instance as a module with EventBus this._eventBus.registerModule({ name: instanceId }); // Create game instance const gameInstance = new gameInfo.GameClass(instanceId, gameDependencies, { container: options.container, ...options.config }); // Initialize the game await gameInstance.init(); // Track the instance this._gameInstances.set(instanceId, gameInstance); // Emit game launched event this._eventBus.emit('game:launched', { gameId, instanceId, compatibility: gameInfo.compatibility }, this.name); return instanceId; } catch (error) { console.error(`Error launching game ${gameId}:`, error); throw error; } } /** * Exit a game instance * @param {string} instanceId - Instance ID to exit */ async exitGame(instanceId) { this._validateInitialized(); const gameInstance = this._gameInstances.get(instanceId); if (!gameInstance) { console.warn(`Game instance not found: ${instanceId}`); return; } try { await gameInstance.destroy(); this._gameInstances.delete(instanceId); // Unregister the game instance from EventBus this._eventBus.unregisterModule(instanceId); this._eventBus.emit('game:exited', { instanceId }, this.name); } catch (error) { console.error(`Error exiting game ${instanceId}:`, error); } } // Private methods async _discoverGames() { console.log('🎮 Discovering available games...'); for (const gamePath of this._gamePaths) { try { // Dynamically import the game module (resolve relative to current module) const gameModule = await import(gamePath); const GameClass = gameModule.default; if (!GameClass) { console.warn(`No default export found in ${gamePath}`); continue; } // Get game metadata const metadata = GameClass.getMetadata?.() || { name: GameClass.name, description: 'No description available', difficulty: 'unknown' }; // Calculate compatibility score const compatibility = this._calculateCompatibility(GameClass); // Store game info const gameInfo = { id: GameClass.name.toLowerCase().replace(/game$/, ''), GameClass, metadata, compatibility, path: gamePath }; this._games.set(gameInfo.id, gameInfo); console.log(`✅ Discovered game: ${metadata.name} (compatibility: ${compatibility.score.toFixed(2)})`); } catch (error) { console.warn(`Failed to load game from ${gamePath}:`, error.message); } } console.log(`🎮 Game discovery complete: ${this._games.size} games found`); } _calculateCompatibility(GameClass) { if (!this._currentContent) { return { score: 0, reason: 'No content loaded' }; } // Use game's own compatibility function if available if (typeof GameClass.getCompatibilityScore === 'function') { const result = GameClass.getCompatibilityScore(this._currentContent); // Normalize different return formats if (typeof result === 'number') { // Format 2: Direct integer (0-100) -> Convert to decimal object return { score: result / 100, reason: `Compatibility score: ${result}%`, requirements: ['content'] }; } else if (result && typeof result === 'object' && typeof result.score === 'number') { // Format 1: Object with decimal score (0-1) -> Use as-is return result; } else { // Invalid format -> Default to 0 return { score: 0, reason: 'Invalid compatibility format' }; } } // Default compatibility calculation const vocab = this._currentContent.vocabulary || {}; const vocabCount = Object.keys(vocab).length; if (vocabCount < 5) { return { score: 0, reason: 'Insufficient vocabulary (need at least 5 words)' }; } // Basic scoring based on vocabulary count const score = Math.min(vocabCount / 20, 1); // Full score at 20+ words return { score, reason: `${vocabCount} vocabulary words available`, requirements: ['vocabulary'], minWords: 5, optimalWords: 20 }; } _handleContentUpdate(event) { console.log('🔄 GameLoader: Content updated, recalculating compatibility scores'); const structure = { hasContent: !!event.data.content, contentKeys: Object.keys(event.data.content || {}), hasVocabulary: !!event.data.content?.vocabulary, vocabularyCount: event.data.content?.vocabulary ? Object.keys(event.data.content.vocabulary).length : 0, hasTexts: !!event.data.content?.texts, hasSentences: !!event.data.content?.sentences }; console.log('📦 GameLoader: event.data structure:', JSON.stringify(structure, null, 2)); this._currentContent = event.data.content; // Recalculate compatibility scores for all games for (const [gameId, gameInfo] of this._games) { const oldScore = gameInfo.compatibility?.score || 0; gameInfo.compatibility = this._calculateCompatibility(gameInfo.GameClass); const newScore = gameInfo.compatibility?.score || 0; console.log(`🎯 ${gameId}: ${oldScore.toFixed(2)} → ${newScore.toFixed(2)}`); } this._eventBus.emit('games:compatibility-updated', { gamesCount: this._games.size, compatibleCount: this.getCompatibleGames().length }, this.name); console.log('✅ Compatibility scores updated'); } _handleLaunchRequest(event) { const { gameId, options } = event.data; this.launchGame(gameId, options).catch(error => { this._eventBus.emit('game:launch-error', { gameId, error: error.message }, this.name); }); } _handleExitRequest(event) { const { instanceId } = event.data; this.exitGame(instanceId); } } export default GameLoader;