Class_generator/src/games/LetterDiscovery.js
StillHammer 05142bdfbc Implement comprehensive AI text report/export system
- Add AIReportSystem.js for detailed AI response capture and report generation
- Add AIReportInterface.js UI component for report access and export
- Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator
- Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator)
- Create missing content/chapters/sbs.json for book metadata
- Enhance Application.js with debug logging for module loading
- Add multi-format export capabilities (text, HTML, JSON)
- Implement automatic learning insights extraction from AI feedback
- Add session management and performance tracking for AI reports

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 21:24:13 +08:00

1205 lines
40 KiB
JavaScript

import Module from '../core/Module.js';
/**
* LetterDiscovery - Interactive letter and word discovery game
* Three phases: letter discovery, word exploration, and practice challenges
*/
class LetterDiscovery extends Module {
constructor(name, dependencies, config = {}) {
super(name, ['eventBus']);
// Validate dependencies
if (!dependencies.eventBus || !dependencies.content) {
throw new Error('LetterDiscovery requires eventBus and content dependencies');
}
this._eventBus = dependencies.eventBus;
this._content = dependencies.content;
this._config = {
container: null,
maxPracticeRounds: 8,
autoPlayTTS: true,
ttsSpeed: 0.8,
...config
};
// Game state
this._currentPhase = 'letter-discovery'; // letter-discovery, word-exploration, practice
this._currentLetterIndex = 0;
this._discoveredLetters = [];
this._currentLetter = null;
this._currentWordIndex = 0;
this._discoveredWords = [];
this._score = 0;
this._lives = 3;
// Content data
this._letters = [];
this._letterWords = {}; // Map letter -> words starting with that letter
// Practice system
this._practiceLevel = 1;
this._practiceRound = 0;
this._practiceCorrectAnswers = 0;
this._practiceErrors = 0;
this._currentPracticeItems = [];
this._currentCorrectAnswer = null;
Object.seal(this);
}
/**
* Get game metadata
* @returns {Object} Game metadata
*/
static getMetadata() {
return {
name: 'Letter Discovery',
description: 'Discover letters and explore words that start with each letter',
difficulty: 'beginner',
category: 'letters',
estimatedTime: 10, // minutes
skills: ['alphabet', 'vocabulary', 'pronunciation']
};
}
/**
* Calculate compatibility score with content
* @param {Object} content - Content to check compatibility with
* @returns {Object} Compatibility score and details
*/
static getCompatibilityScore(content) {
const letters = content?.letters || content?.rawContent?.letters;
// Try to create letters from vocabulary if direct letters not found
let lettersData = letters;
if (!lettersData && content?.vocabulary) {
lettersData = this._createLettersFromVocabulary(content.vocabulary);
}
if (!lettersData || Object.keys(lettersData).length === 0) {
return {
score: 0,
reason: 'No letter structure found',
requirements: ['letters'],
details: 'Letter Discovery requires content with predefined letters system'
};
}
const letterCount = letters ? Object.keys(letters).length : 0;
const totalWords = letters ? Object.values(letters).reduce((sum, words) => sum + (words?.length || 0), 0) : 0;
if (totalWords === 0) {
return {
score: 0.2,
reason: 'Letters found but no words',
requirements: ['letters with words'],
details: `Found ${letterCount} letters but no associated words`
};
}
// Perfect score at 26 letters, good score for 10+ letters
const score = Math.min(letterCount / 26, 1);
return {
score,
reason: `${letterCount} letters with ${totalWords} total words`,
requirements: ['letters'],
optimalLetters: 26,
details: `Can create discovery experience with ${letterCount} letters and ${totalWords} words`
};
}
async init() {
this._validateNotDestroyed();
try {
// Validate container
if (!this._config.container) {
throw new Error('Game container is required');
}
// Extract and validate content
this._extractContent();
if (this._letters.length === 0) {
throw new Error('No letter content found for discovery');
}
// Set up event listeners
this._eventBus.on('game:pause', this._handlePause.bind(this), this.name);
this._eventBus.on('game:resume', this._handleResume.bind(this), this.name);
// Initialize game interface
this._injectCSS();
this._createGameInterface();
this._setupEventListeners();
// Start with first letter
this._showLetterCard();
// Emit game ready event
this._eventBus.emit('game:ready', {
gameId: 'letter-discovery',
instanceId: this.name,
letters: this._letters.length,
totalWords: Object.values(this._letterWords).reduce((sum, words) => sum + words.length, 0)
}, this.name);
this._setInitialized();
} catch (error) {
this._showError(error.message);
throw error;
}
}
async destroy() {
this._validateNotDestroyed();
// Clean up container
if (this._config.container) {
this._config.container.innerHTML = '';
}
// Remove injected CSS
this._removeInjectedCSS();
// Emit game end event
this._eventBus.emit('game:ended', {
gameId: 'letter-discovery',
instanceId: this.name,
score: this._score,
lettersDiscovered: this._discoveredLetters.length,
wordsLearned: this._discoveredWords.length
}, this.name);
this._setDestroyed();
}
/**
* Get current game state
* @returns {Object} Current game state
*/
getGameState() {
this._validateInitialized();
return {
phase: this._currentPhase,
score: this._score,
lives: this._lives,
currentLetter: this._currentLetter,
lettersDiscovered: this._discoveredLetters.length,
totalLetters: this._letters.length,
wordsLearned: this._discoveredWords.length,
practiceAccuracy: this._config.maxPracticeRounds > 0 ?
(this._practiceCorrectAnswers / this._config.maxPracticeRounds) * 100 : 0
};
}
// Private methods
_extractContent() {
const letters = this._content.letters || this._content.rawContent?.letters;
if (letters && Object.keys(letters).length > 0) {
this._letters = Object.keys(letters).sort();
this._letterWords = letters;
} else {
this._letters = [];
this._letterWords = {};
}
}
_injectCSS() {
if (document.getElementById('letter-discovery-styles')) return;
const styleSheet = document.createElement('style');
styleSheet.id = 'letter-discovery-styles';
styleSheet.textContent = `
.letter-discovery-wrapper {
background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e0 100%);
min-height: 100vh;
padding: 20px;
position: relative;
overflow-y: auto;
box-sizing: border-box;
}
.letter-discovery-hud {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.1);
padding: 15px 20px;
border-radius: 15px;
backdrop-filter: blur(10px);
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.hud-group {
display: flex;
align-items: center;
gap: 15px;
}
.hud-item {
color: white;
font-weight: bold;
font-size: 1.1em;
}
.phase-indicator {
background: rgba(255,255,255,0.2);
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9em;
color: white;
backdrop-filter: blur(5px);
}
.letter-discovery-main {
background: rgba(255,255,255,0.1);
border-radius: 20px;
padding: 30px;
backdrop-filter: blur(10px);
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.game-content {
width: 100%;
max-width: 900px;
text-align: center;
}
/* Letter Display Styles */
.letter-card {
background: rgba(255,255,255,0.95);
border-radius: 25px;
padding: 60px 40px;
margin: 30px auto;
max-width: 400px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
transform: scale(0.8);
animation: letterAppear 0.8s ease-out forwards;
}
@keyframes letterAppear {
to { transform: scale(1); }
}
.letter-display {
font-size: 8em;
font-weight: bold;
color: #2d3748;
margin-bottom: 20px;
text-shadow: 0 4px 8px rgba(0,0,0,0.1);
font-family: 'Arial Black', Arial, sans-serif;
}
.letter-info {
font-size: 1.5em;
color: #333;
margin-bottom: 15px;
}
.letter-pronunciation {
font-size: 1.2em;
color: #666;
font-style: italic;
margin-bottom: 25px;
}
.letter-controls {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
flex-wrap: wrap;
}
/* Word Exploration Styles */
.word-exploration-header {
background: rgba(255,255,255,0.1);
padding: 20px;
border-radius: 15px;
margin-bottom: 30px;
backdrop-filter: blur(5px);
}
.exploring-letter {
font-size: 3em;
color: white;
margin-bottom: 10px;
font-weight: bold;
}
.word-progress {
color: rgba(255,255,255,0.8);
font-size: 1.1em;
}
.word-card {
background: rgba(255,255,255,0.95);
border-radius: 20px;
padding: 40px 30px;
margin: 25px auto;
max-width: 500px;
box-shadow: 0 15px 30px rgba(0,0,0,0.1);
transform: translateY(20px);
animation: wordSlideIn 0.6s ease-out forwards;
}
@keyframes wordSlideIn {
to { transform: translateY(0); }
}
.word-text {
font-size: 2.5em;
color: #2d3748;
margin-bottom: 15px;
font-weight: bold;
}
.word-translation {
font-size: 1.3em;
color: #333;
margin-bottom: 10px;
}
.word-pronunciation {
font-size: 1.1em;
color: #666;
font-style: italic;
margin-bottom: 10px;
}
.word-type {
font-size: 0.9em;
color: #2d3748;
background: rgba(66, 153, 225, 0.1);
padding: 4px 12px;
border-radius: 15px;
display: inline-block;
margin-bottom: 15px;
font-weight: 500;
}
.word-example {
font-size: 1em;
color: #555;
font-style: italic;
padding: 10px 15px;
background: rgba(0, 0, 0, 0.05);
border-left: 3px solid #4299e1;
border-radius: 0 8px 8px 0;
margin-bottom: 15px;
}
/* Practice Challenge Styles */
.practice-challenge {
text-align: center;
margin-bottom: 30px;
}
.challenge-text {
font-size: 1.8em;
color: white;
margin-bottom: 25px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.practice-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
max-width: 800px;
margin: 0 auto;
}
.practice-option {
background: rgba(255,255,255,0.9);
border: none;
border-radius: 15px;
padding: 20px;
font-size: 1.2em;
cursor: pointer;
transition: all 0.3s ease;
color: #333;
font-weight: 500;
}
.practice-option:hover:not(.correct):not(.incorrect) {
background: rgba(255,255,255,1);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
}
.practice-option.correct {
background: #4CAF50;
color: white;
animation: correctPulse 0.6s ease;
}
.practice-option.incorrect {
background: #F44336;
color: white;
animation: incorrectShake 0.6s ease;
}
@keyframes correctPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes incorrectShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.practice-stats {
display: flex;
justify-content: space-around;
margin-top: 20px;
color: white;
font-size: 1.1em;
gap: 10px;
}
.stat-item {
text-align: center;
padding: 10px;
background: rgba(255,255,255,0.1);
border-radius: 10px;
backdrop-filter: blur(5px);
flex: 1;
}
/* Control Buttons */
.discovery-btn {
background: linear-gradient(45deg, #4299e1, #3182ce);
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin: 0 5px;
}
.discovery-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.discovery-btn:active {
transform: translateY(0);
}
.audio-btn {
background: none;
border: none;
font-size: 2em;
cursor: pointer;
color: #2d3748;
transition: all 0.3s ease;
padding: 10px;
border-radius: 50%;
}
.audio-btn:hover {
transform: scale(1.2);
color: #3182ce;
background: rgba(255,255,255,0.1);
}
/* Completion Message */
.completion-message {
text-align: center;
padding: 40px;
background: rgba(255,255,255,0.1);
border-radius: 20px;
backdrop-filter: blur(10px);
color: white;
}
.completion-title {
font-size: 2.5em;
margin-bottom: 20px;
color: #00ff88;
text-shadow: 0 2px 10px rgba(0,255,136,0.3);
}
.completion-stats {
font-size: 1.3em;
margin-bottom: 30px;
line-height: 1.6;
}
.exit-btn {
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 10px 20px;
border-radius: 15px;
font-size: 1em;
cursor: pointer;
transition: all 0.3s ease;
}
.exit-btn:hover {
background: rgba(255,255,255,0.3);
}
/* Responsive Design */
@media (max-width: 768px) {
.letter-discovery-wrapper {
padding: 15px;
}
.letter-display {
font-size: 5em;
}
.word-text {
font-size: 2em;
}
.challenge-text {
font-size: 1.4em;
}
.practice-grid {
grid-template-columns: 1fr;
}
.letter-controls {
flex-direction: column;
align-items: center;
}
.discovery-btn {
margin: 5px 0;
width: 100%;
max-width: 250px;
}
.practice-stats {
flex-direction: column;
}
}
`;
document.head.appendChild(styleSheet);
}
_removeInjectedCSS() {
const styleSheet = document.getElementById('letter-discovery-styles');
if (styleSheet) {
styleSheet.remove();
}
}
_createGameInterface() {
this._config.container.innerHTML = `
<div class="letter-discovery-wrapper">
<div class="letter-discovery-hud">
<div class="hud-group">
<div class="hud-item">Score: <span id="score-display">${this._score}</span></div>
<div class="hud-item">Lives: <span id="lives-display">${this._lives}</span></div>
</div>
<div class="phase-indicator" id="phase-indicator">Letter Discovery</div>
<div class="hud-group">
<div class="hud-item">Progress: <span id="progress-display">0/${this._letters.length}</span></div>
<button class="exit-btn" id="exit-btn">← Exit</button>
</div>
</div>
<div class="letter-discovery-main">
<div class="game-content" id="game-content">
<!-- Dynamic content here -->
</div>
</div>
</div>
`;
}
_setupEventListeners() {
// Exit button
document.getElementById('exit-btn').addEventListener('click', () => {
this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name);
});
}
_updateHUD() {
const scoreDisplay = document.getElementById('score-display');
const livesDisplay = document.getElementById('lives-display');
const progressDisplay = document.getElementById('progress-display');
const phaseIndicator = document.getElementById('phase-indicator');
if (scoreDisplay) scoreDisplay.textContent = this._score;
if (livesDisplay) livesDisplay.textContent = this._lives;
if (this._currentPhase === 'letter-discovery') {
if (progressDisplay) progressDisplay.textContent = `${this._currentLetterIndex}/${this._letters.length}`;
if (phaseIndicator) phaseIndicator.textContent = 'Letter Discovery';
} else if (this._currentPhase === 'word-exploration') {
const words = this._letterWords[this._currentLetter] || [];
if (progressDisplay) progressDisplay.textContent = `${this._currentWordIndex}/${words.length}`;
if (phaseIndicator) phaseIndicator.textContent = `Exploring Letter "${this._currentLetter}"`;
} else if (this._currentPhase === 'practice') {
if (progressDisplay) progressDisplay.textContent = `Round ${this._practiceRound + 1}/${this._config.maxPracticeRounds}`;
if (phaseIndicator) phaseIndicator.textContent = `Practice - Level ${this._practiceLevel}`;
}
}
_showLetterCard() {
if (this._currentLetterIndex >= this._letters.length) {
this._showCompletion();
return;
}
const letter = this._letters[this._currentLetterIndex];
const gameContent = document.getElementById('game-content');
gameContent.innerHTML = `
<div class="letter-card">
<div class="letter-display">${letter}</div>
<div class="letter-info">Letter "${letter}"</div>
<div class="letter-pronunciation">[${this._getLetterPronunciation(letter)}]</div>
<div class="letter-controls">
<button class="discovery-btn" id="discover-letter-btn">
🔍 Discover Letter
</button>
<button class="audio-btn" id="play-letter-btn" title="Play sound">
🔊
</button>
</div>
</div>
`;
// Set up button listeners
document.getElementById('discover-letter-btn').addEventListener('click', () => {
this._discoverLetter();
});
document.getElementById('play-letter-btn').addEventListener('click', () => {
this._playLetterSound(letter);
});
this._updateHUD();
// Auto-play letter sound if enabled
if (this._config.autoPlayTTS) {
setTimeout(() => this._playLetterSound(letter), 500);
}
}
_getLetterPronunciation(letter) {
const pronunciations = {
'A': 'ay', 'B': 'bee', 'C': 'see', 'D': 'dee', 'E': 'ee',
'F': 'ef', 'G': 'gee', 'H': 'aych', 'I': 'eye', 'J': 'jay',
'K': 'kay', 'L': 'el', 'M': 'em', 'N': 'en', 'O': 'oh',
'P': 'pee', 'Q': 'cue', 'R': 'ar', 'S': 'ess', 'T': 'tee',
'U': 'you', 'V': 'vee', 'W': 'double-you', 'X': 'ex', 'Y': 'why', 'Z': 'zee'
};
return pronunciations[letter] || letter.toLowerCase();
}
_playLetterSound(letter) {
this._speakText(letter, { rate: this._config.ttsSpeed * 0.8 }); // Slower for letters
}
_discoverLetter() {
const letter = this._letters[this._currentLetterIndex];
this._discoveredLetters.push(letter);
this._score += 10;
// Emit score update event
this._eventBus.emit('game:score-update', {
gameId: 'letter-discovery',
instanceId: this.name,
score: this._score
}, this.name);
// Start word exploration for this letter
this._currentLetter = letter;
this._currentPhase = 'word-exploration';
this._currentWordIndex = 0;
this._showWordExploration();
}
_showWordExploration() {
const words = this._letterWords[this._currentLetter];
if (!words || this._currentWordIndex >= words.length) {
// Finished exploring words for this letter
this._currentPhase = 'letter-discovery';
this._currentLetterIndex++;
this._showLetterCard();
return;
}
const word = words[this._currentWordIndex];
const gameContent = document.getElementById('game-content');
gameContent.innerHTML = `
<div class="word-exploration-header">
<div class="exploring-letter">Letter "${this._currentLetter}"</div>
<div class="word-progress">Word ${this._currentWordIndex + 1} of ${words.length}</div>
</div>
<div class="word-card">
<div class="word-text">${word.word}</div>
<div class="word-translation">${word.translation}</div>
${word.pronunciation ? `<div class="word-pronunciation">[${word.pronunciation}]</div>` : ''}
${word.type ? `<div class="word-type">${word.type}</div>` : ''}
${word.example ? `<div class="word-example">"${word.example}"</div>` : ''}
<div class="letter-controls">
<button class="discovery-btn" id="next-word-btn">
➡️ Next Word
</button>
<button class="audio-btn" id="play-word-btn" title="Play sound">
🔊
</button>
</div>
</div>
`;
// Set up button listeners
document.getElementById('next-word-btn').addEventListener('click', () => {
this._nextWord();
});
document.getElementById('play-word-btn').addEventListener('click', () => {
this._playWordSound(word.word);
});
// Add word to discovered list
this._discoveredWords.push(word);
this._updateHUD();
// Auto-play word sound if enabled
if (this._config.autoPlayTTS) {
setTimeout(() => this._playWordSound(word.word), 500);
}
}
_playWordSound(word) {
this._speakText(word, { rate: this._config.ttsSpeed });
}
_nextWord() {
this._currentWordIndex++;
this._score += 5;
// Emit score update event
this._eventBus.emit('game:score-update', {
gameId: 'letter-discovery',
instanceId: this.name,
score: this._score
}, this.name);
this._showWordExploration();
}
_showCompletion() {
const gameContent = document.getElementById('game-content');
const totalWords = Object.values(this._letterWords).reduce((sum, words) => sum + words.length, 0);
gameContent.innerHTML = `
<div class="completion-message">
<div class="completion-title">🎉 All Letters Discovered!</div>
<div class="completion-stats">
Letters Discovered: ${this._discoveredLetters.length}<br>
Words Learned: ${this._discoveredWords.length}<br>
Final Score: ${this._score}
</div>
<div class="letter-controls">
<button class="discovery-btn" id="start-practice-btn">
🎮 Start Practice
</button>
<button class="discovery-btn" id="restart-btn">
🔄 Play Again
</button>
</div>
</div>
`;
// Set up button listeners
document.getElementById('start-practice-btn').addEventListener('click', () => {
this._startPractice();
});
document.getElementById('restart-btn').addEventListener('click', () => {
this._restart();
});
this._updateHUD();
}
_startPractice() {
this._currentPhase = 'practice';
this._practiceLevel = 1;
this._practiceRound = 0;
this._practiceCorrectAnswers = 0;
this._practiceErrors = 0;
// Create shuffled practice items from all discovered words
this._currentPracticeItems = this._shuffleArray([...this._discoveredWords]);
this._showPracticeChallenge();
}
_showPracticeChallenge() {
if (this._practiceRound >= this._config.maxPracticeRounds) {
this._endPractice();
return;
}
const currentItem = this._currentPracticeItems[this._practiceRound % this._currentPracticeItems.length];
const gameContent = document.getElementById('game-content');
// Generate options (correct + 3 random)
const allWords = this._discoveredWords.filter(w => w.word !== currentItem.word);
const randomOptions = this._shuffleArray([...allWords]).slice(0, 3);
const options = this._shuffleArray([currentItem, ...randomOptions]);
gameContent.innerHTML = `
<div class="practice-challenge">
<div class="challenge-text">What does "${currentItem.word}" mean?</div>
<div class="practice-grid">
${options.map((option, index) => `
<button class="practice-option" data-option-index="${index}" data-word="${option.word}">
${option.translation}
</button>
`).join('')}
</div>
<div class="practice-stats">
<div class="stat-item">Correct: ${this._practiceCorrectAnswers}</div>
<div class="stat-item">Errors: ${this._practiceErrors}</div>
<div class="stat-item">Round: ${this._practiceRound + 1}/${this._config.maxPracticeRounds}</div>
</div>
</div>
`;
// Set up option listeners
document.querySelectorAll('.practice-option').forEach(button => {
button.addEventListener('click', (e) => {
const selectedIndex = parseInt(e.target.dataset.optionIndex);
const selectedWord = e.target.dataset.word;
this._selectPracticeAnswer(selectedIndex, selectedWord);
});
});
// Store correct answer for checking
this._currentCorrectAnswer = currentItem.word;
this._updateHUD();
// Auto-play word if enabled
if (this._config.autoPlayTTS) {
setTimeout(() => this._playWordSound(currentItem.word), 500);
}
}
_selectPracticeAnswer(selectedIndex, selectedWord) {
const buttons = document.querySelectorAll('.practice-option');
const isCorrect = selectedWord === this._currentCorrectAnswer;
// Disable all buttons to prevent multiple clicks
buttons.forEach(btn => btn.disabled = true);
if (isCorrect) {
buttons[selectedIndex].classList.add('correct');
this._practiceCorrectAnswers++;
this._score += 10;
// Emit score update event
this._eventBus.emit('game:score-update', {
gameId: 'letter-discovery',
instanceId: this.name,
score: this._score
}, this.name);
} else {
buttons[selectedIndex].classList.add('incorrect');
this._practiceErrors++;
// Show correct answer
buttons.forEach((btn) => {
if (btn.dataset.word === this._currentCorrectAnswer) {
btn.classList.add('correct');
}
});
}
setTimeout(() => {
this._practiceRound++;
this._showPracticeChallenge();
}, 1500);
}
_endPractice() {
const accuracy = Math.round((this._practiceCorrectAnswers / this._config.maxPracticeRounds) * 100);
// Store best score
const gameKey = 'letter-discovery';
const currentScore = this._score;
const bestScore = parseInt(localStorage.getItem(`${gameKey}-best-score`) || '0');
const isNewBest = currentScore > bestScore;
if (isNewBest) {
localStorage.setItem(`${gameKey}-best-score`, currentScore.toString());
}
// Show victory popup
this._showVictoryPopup({
gameTitle: 'Letter Discovery',
currentScore,
bestScore: isNewBest ? currentScore : bestScore,
isNewBest,
stats: {
'Letters Found': this._discoveredLetters.length,
'Words Learned': this._discoveredWords.length,
'Practice Accuracy': `${accuracy}%`,
'Correct Answers': `${this._practiceCorrectAnswers}/${this._config.maxPracticeRounds}`
}
});
// Emit game completion event
this._eventBus.emit('game:completed', {
gameId: 'letter-discovery',
instanceId: this.name,
score: this._score,
lettersDiscovered: this._discoveredLetters.length,
wordsLearned: this._discoveredWords.length,
practiceAccuracy: accuracy
}, this.name);
this._updateHUD();
}
_restart() {
// Reset all game state
this._currentPhase = 'letter-discovery';
this._currentLetterIndex = 0;
this._discoveredLetters = [];
this._currentLetter = null;
this._currentWordIndex = 0;
this._discoveredWords = [];
this._score = 0;
this._lives = 3;
this._practiceLevel = 1;
this._practiceRound = 0;
this._practiceCorrectAnswers = 0;
this._practiceErrors = 0;
this._currentPracticeItems = [];
this._currentCorrectAnswer = null;
this._showLetterCard();
}
_shuffleArray(array) {
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;
}
_speakText(text, options = {}) {
if (!text) return;
try {
if ('speechSynthesis' in window) {
speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = this._getContentLanguage();
utterance.rate = options.rate || this._config.ttsSpeed;
utterance.volume = 1.0;
speechSynthesis.speak(utterance);
}
} catch (error) {
console.warn('TTS error:', error);
}
}
_getContentLanguage() {
if (this._content.language) {
const langMap = {
'chinese': 'zh-CN',
'english': 'en-US',
'french': 'fr-FR',
'spanish': 'es-ES'
};
return langMap[this._content.language] || this._content.language;
}
return 'en-US';
}
_showError(message) {
if (this._config.container) {
this._config.container.innerHTML = `
<div class="letter-discovery-wrapper">
<div class="completion-message" style="margin-top: 50px;">
<div style="font-size: 3em; margin-bottom: 20px;">❌</div>
<h3>Letter Discovery Error</h3>
<p>${message}</p>
<div class="letter-controls">
<button class="discovery-btn" onclick="history.back()">Go Back</button>
</div>
</div>
</div>
`;
}
}
_handlePause() {
this._eventBus.emit('game:paused', { instanceId: this.name }, this.name);
}
_handleResume() {
this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name);
}
_showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) {
const popup = document.createElement('div');
popup.className = 'victory-popup';
popup.innerHTML = `
<div class="victory-content">
<div class="victory-header">
<div class="victory-icon">🔤</div>
<h2 class="victory-title">${gameTitle} Complete!</h2>
${isNewBest ? '<div class="new-best-badge">🎉 New Best Score!</div>' : ''}
</div>
<div class="victory-scores">
<div class="score-display">
<div class="score-label">Your Score</div>
<div class="score-value">${currentScore}</div>
</div>
<div class="score-display best-score">
<div class="score-label">Best Score</div>
<div class="score-value">${bestScore}</div>
</div>
</div>
<div class="victory-stats">
${Object.entries(stats).map(([key, value]) => `
<div class="stat-item">
<div class="stat-label">${key}</div>
<div class="stat-value">${value}</div>
</div>
`).join('')}
</div>
<div class="victory-actions">
<button class="victory-btn victory-btn-primary" id="play-again-btn">
<span class="btn-icon">🔄</span>
<span class="btn-text">Play Again</span>
</button>
<button class="victory-btn victory-btn-secondary" id="different-game-btn">
<span class="btn-icon">🎮</span>
<span class="btn-text">Different Game</span>
</button>
<button class="victory-btn victory-btn-outline" id="main-menu-btn">
<span class="btn-icon">🏠</span>
<span class="btn-text">Main Menu</span>
</button>
</div>
</div>
`;
document.body.appendChild(popup);
// Animate in
requestAnimationFrame(() => {
popup.classList.add('show');
});
// Add event listeners
popup.querySelector('#play-again-btn').addEventListener('click', () => {
popup.remove();
this._restart();
});
popup.querySelector('#different-game-btn').addEventListener('click', () => {
popup.remove();
if (window.app && window.app.getCore().router) {
window.app.getCore().router.navigate('/games');
// Force content reload by re-emitting navigation event
setTimeout(() => {
const chapterId = window.currentChapterId || 'sbs';
this._eventBus.emit('navigation:games', {
path: `/games/${chapterId}`,
data: { path: `/games/${chapterId}` }
}, 'Application');
}, 100);
} else {
window.location.href = '/#/games';
}
});
popup.querySelector('#main-menu-btn').addEventListener('click', () => {
popup.remove();
if (window.app && window.app.getCore().router) {
window.app.getCore().router.navigate('/');
} else {
window.location.href = '/';
}
});
// Close on backdrop click
popup.addEventListener('click', (e) => {
if (e.target === popup) {
popup.remove();
if (window.app && window.app.getCore().router) {
window.app.getCore().router.navigate('/games');
// Force content reload by re-emitting navigation event
setTimeout(() => {
const chapterId = window.currentChapterId || 'sbs';
this._eventBus.emit('navigation:games', {
path: `/games/${chapterId}`,
data: { path: `/games/${chapterId}` }
}, 'Application');
}, 100);
} else {
window.location.href = '/#/games';
}
}
});
}
// Helper method to convert vocabulary to letters format
static _createLettersFromVocabulary(vocabulary) {
const letters = {};
Object.entries(vocabulary).forEach(([word, data]) => {
const firstLetter = word.charAt(0).toUpperCase();
if (!letters[firstLetter]) {
letters[firstLetter] = [];
}
letters[firstLetter].push({
word: word,
translation: data.user_language || data.translation || data,
type: data.type || 'word',
pronunciation: data.pronunciation
});
});
return letters;
}
}
export default LetterDiscovery;