- 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>
324 lines
11 KiB
JavaScript
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; |