Class_generator/src/core/GameLoader.js
StillHammer c7f48405a7 Add diagnostic homework planning documentation
- Create comprehensive DIAGNOSTIC_HOMEWORK_PLAN.md
- Document current situation with SBS2/SBS8 mixed class
- Define diagnostic homework structure (1 text + 3 comp + 3 prod questions)
- Outline exam prep strategy phases (diagnostic → drills → mock exams)
- Specify data collection goals and success metrics
- Plan AI-powered analysis and personalized prep modules

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 19:59:59 +08:00

324 lines
11 KiB
JavaScript

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<string>} 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;