Class_generator/Legacy/js/games/quiz-game.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
Major Changes:
- Moved legacy system to Legacy/ folder for archival
- Built new modular architecture with strict separation of concerns
- Created core system: Module, EventBus, ModuleLoader, Router
- Added Application bootstrap with auto-start functionality
- Implemented development server with ES6 modules support
- Created comprehensive documentation and project context
- Converted SBS-7-8 content to JSON format
- Copied all legacy games and content to new structure

New Architecture Features:
- Sealed modules with WeakMap private data
- Strict dependency injection system
- Event-driven communication only
- Inviolable responsibility patterns
- Auto-initialization without commands
- Component-based UI foundation ready

Technical Stack:
- Vanilla JS/HTML/CSS only
- ES6 modules with proper imports/exports
- HTTP development server (no file:// protocol)
- Modular CSS with component scoping
- Comprehensive error handling and debugging

Ready for Phase 2: Converting legacy modules to new architecture

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 07:08:39 +08:00

529 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === MODULE QUIZ GAME ===
class QuizGame {
constructor(options) {
this.container = options.container;
this.content = options.content;
this.onScoreUpdate = options.onScoreUpdate || (() => {});
this.onGameEnd = options.onGameEnd || (() => {});
// Game state
this.vocabulary = [];
this.currentQuestion = 0;
this.totalQuestions = 10;
this.score = 0;
this.correctAnswers = 0;
this.currentQuestionData = null;
this.hasAnswered = false;
this.quizDirection = 'original_to_translation'; // 'original_to_translation' or 'translation_to_original'
// Extract vocabulary and additional words from texts/stories
this.vocabulary = this.extractVocabulary(this.content);
this.allWords = this.extractAllWords(this.content);
this.init();
}
init() {
// Check if we have enough vocabulary
if (!this.vocabulary || this.vocabulary.length < 6) {
logSh('Not enough vocabulary for Quiz Game', 'ERROR');
this.showInitError();
return;
}
// Adjust total questions based on available vocabulary
this.totalQuestions = Math.min(this.totalQuestions, this.vocabulary.length);
this.createGameInterface();
this.generateQuestion();
}
showInitError() {
this.container.innerHTML = `
<div class="game-error">
<h3>❌ Error loading</h3>
<p>This content doesn't have enough vocabulary for Quiz Game.</p>
<p>The game needs at least 6 vocabulary items.</p>
<button onclick="AppNavigation.navigateTo('games')" class="back-btn">← Back to Games</button>
</div>
`;
}
extractVocabulary(content) {
let vocabulary = [];
logSh('🔍 Extracting vocabulary from:', content?.name || 'content', 'INFO');
// Priority 1: Use raw module content (simple format)
if (content.rawContent) {
logSh('📦 Using raw module content', 'INFO');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
logSh('✨ Ultra-modular format detected (vocabulary object)', 'INFO');
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
original: word, // Clé = original_language
translation: data.user_language.split('')[0], // First translation
fullTranslation: data.user_language, // Complete translation
type: data.type || 'general',
audio: data.audio,
image: data.image,
examples: data.examples,
pronunciation: data.pronunciation,
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
original: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
}
// No other formats supported - ultra-modular only
return this.finalizeVocabulary(vocabulary);
}
extractVocabularyFromRaw(rawContent) {
logSh('🔧 Extracting from raw content:', rawContent.name || 'Module', 'INFO');
let vocabulary = [];
// Ultra-modular format (vocabulary object) - ONLY format supported
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
original: word, // Clé = original_language
translation: data.user_language.split('')[0], // First translation
fullTranslation: data.user_language, // Complete translation
type: data.type || 'general',
audio: data.audio,
image: data.image,
examples: data.examples,
pronunciation: data.pronunciation,
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
original: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
logSh(`${vocabulary.length} words extracted from ultra-modular vocabulary`, 'INFO');
}
// No other formats supported - ultra-modular only
else {
logSh('⚠️ Content format not supported - ultra-modular format required', 'WARN');
}
return this.finalizeVocabulary(vocabulary);
}
finalizeVocabulary(vocabulary) {
// Validation and cleanup for ultra-modular format
vocabulary = vocabulary.filter(word =>
word &&
typeof word.original === 'string' &&
typeof word.translation === 'string' &&
word.original.trim() !== '' &&
word.translation.trim() !== ''
);
if (vocabulary.length === 0) {
logSh('❌ No valid vocabulary found', 'ERROR');
// Demo vocabulary as last resort
vocabulary = [
{ original: 'hello', translation: 'bonjour', category: 'greetings' },
{ original: 'goodbye', translation: 'au revoir', category: 'greetings' },
{ original: 'thank you', translation: 'merci', category: 'greetings' },
{ original: 'cat', translation: 'chat', category: 'animals' },
{ original: 'dog', translation: 'chien', category: 'animals' },
{ original: 'house', translation: 'maison', category: 'objects' },
{ original: 'car', translation: 'voiture', category: 'objects' },
{ original: 'book', translation: 'livre', category: 'objects' }
];
logSh('🚨 Using demo vocabulary', 'WARN');
}
// Shuffle vocabulary for random questions
vocabulary = this.shuffleArray(vocabulary);
logSh(`✅ Quiz Game: ${vocabulary.length} vocabulary words finalized`, 'INFO');
return vocabulary;
}
extractAllWords(content) {
let allWords = [];
// Add vocabulary words first
allWords = [...this.vocabulary];
// Extract from stories/texts
if (content.rawContent?.story?.chapters) {
content.rawContent.story.chapters.forEach(chapter => {
if (chapter.sentences) {
chapter.sentences.forEach(sentence => {
if (sentence.words && Array.isArray(sentence.words)) {
sentence.words.forEach(wordObj => {
if (wordObj.word && wordObj.translation) {
allWords.push({
original: wordObj.word,
translation: wordObj.translation,
type: wordObj.type || 'word',
pronunciation: wordObj.pronunciation
});
}
});
}
});
}
});
}
// Extract from additional stories (like WTA1B1)
if (content.rawContent?.additionalStories) {
content.rawContent.additionalStories.forEach(story => {
if (story.chapters) {
story.chapters.forEach(chapter => {
if (chapter.sentences) {
chapter.sentences.forEach(sentence => {
if (sentence.words && Array.isArray(sentence.words)) {
sentence.words.forEach(wordObj => {
if (wordObj.word && wordObj.translation) {
allWords.push({
original: wordObj.word,
translation: wordObj.translation,
type: wordObj.type || 'word',
pronunciation: wordObj.pronunciation
});
}
});
}
});
}
});
}
});
}
// Remove duplicates based on original word
const uniqueWords = [];
const seenWords = new Set();
allWords.forEach(word => {
const key = word.original.toLowerCase();
if (!seenWords.has(key)) {
seenWords.add(key);
uniqueWords.push(word);
}
});
logSh(`📚 Extracted ${uniqueWords.length} total words for quiz options`, 'INFO');
return uniqueWords;
}
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;
}
createGameInterface() {
this.container.innerHTML = `
<div class="quiz-game-wrapper">
<!-- Top Controls - Restart button moved to top left -->
<div class="quiz-top-controls">
<button class="control-btn secondary restart-top" id="restart-btn">🔄 Restart</button>
</div>
<!-- Progress Bar -->
<div class="quiz-progress">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-text">
<span id="question-counter">1 / ${this.totalQuestions}</span>
<span id="score-display">Score: 0</span>
</div>
</div>
<!-- Question Area -->
<div class="question-area">
<div class="question-text" id="question-text">
Loading question...
</div>
</div>
<!-- Options Area -->
<div class="options-area" id="options-area">
<!-- Options will be generated here -->
</div>
<!-- Controls -->
<div class="quiz-controls">
<button class="control-btn primary" id="next-btn" style="display: none;">Next Question →</button>
</div>
<!-- Feedback Area -->
<div class="feedback-area" id="feedback-area">
<div class="instruction">
Choose the correct translation!
</div>
</div>
</div>
`;
// Add CSS for top controls
const style = document.createElement('style');
style.textContent = `
.quiz-top-controls {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
}
.restart-top {
background: rgba(255, 255, 255, 0.9) !important;
border: 2px solid #ccc !important;
color: #666 !important;
font-size: 12px !important;
padding: 8px 12px !important;
border-radius: 6px !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
.restart-top:hover {
background: rgba(255, 255, 255, 1) !important;
border-color: #999 !important;
color: #333 !important;
}
`;
document.head.appendChild(style);
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('next-btn').addEventListener('click', () => this.nextQuestion());
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
}
generateQuestion() {
if (this.currentQuestion >= this.totalQuestions) {
this.gameComplete();
return;
}
this.hasAnswered = false;
// Get current vocabulary item
const correctAnswer = this.vocabulary[this.currentQuestion];
// Randomly choose quiz direction
this.quizDirection = Math.random() < 0.5 ? 'original_to_translation' : 'translation_to_original';
let questionText, correctAnswerText, sourceForWrongAnswers;
if (this.quizDirection === 'original_to_translation') {
questionText = correctAnswer.original;
correctAnswerText = correctAnswer.translation;
sourceForWrongAnswers = 'translation';
} else {
questionText = correctAnswer.translation;
correctAnswerText = correctAnswer.original;
sourceForWrongAnswers = 'original';
}
// Generate 5 wrong answers from allWords (which includes story words)
const availableWords = this.allWords.length >= 6 ? this.allWords : this.vocabulary;
const wrongAnswers = availableWords
.filter(item => item !== correctAnswer)
.sort(() => Math.random() - 0.5)
.slice(0, 5)
.map(item => sourceForWrongAnswers === 'translation' ? item.translation : item.original);
// Combine and shuffle all options (1 correct + 5 wrong = 6 total)
const allOptions = [correctAnswerText, ...wrongAnswers].sort(() => Math.random() - 0.5);
this.currentQuestionData = {
question: questionText,
correctAnswer: correctAnswerText,
options: allOptions,
direction: this.quizDirection
};
this.renderQuestion();
this.updateProgress();
}
renderQuestion() {
const { question, options } = this.currentQuestionData;
// Update question text with direction indicator
const direction = this.currentQuestionData.direction;
const directionText = direction === 'original_to_translation' ?
'What is the translation of' : 'What is the original word for';
document.getElementById('question-text').innerHTML = `
${directionText} "<strong>${question}</strong>"?
`;
// Clear and generate options
const optionsArea = document.getElementById('options-area');
optionsArea.innerHTML = '';
options.forEach((option, index) => {
const optionButton = document.createElement('button');
optionButton.className = 'quiz-option';
optionButton.textContent = option;
optionButton.addEventListener('click', () => this.selectAnswer(option, optionButton));
optionsArea.appendChild(optionButton);
});
// Hide next button
document.getElementById('next-btn').style.display = 'none';
}
selectAnswer(selectedAnswer, buttonElement) {
if (this.hasAnswered) return;
this.hasAnswered = true;
const isCorrect = selectedAnswer === this.currentQuestionData.correctAnswer;
// Disable all option buttons and show results
const allOptions = document.querySelectorAll('.quiz-option');
allOptions.forEach(btn => {
btn.disabled = true;
if (btn.textContent === this.currentQuestionData.correctAnswer) {
btn.classList.add('correct');
} else if (btn === buttonElement && !isCorrect) {
btn.classList.add('wrong');
} else if (btn !== buttonElement && btn.textContent !== this.currentQuestionData.correctAnswer) {
btn.classList.add('disabled');
}
});
// Update score and feedback
if (isCorrect) {
this.correctAnswers++;
this.score += 10;
this.showFeedback('✅ Correct! Well done!', 'success');
} else {
this.score = Math.max(0, this.score - 5);
this.showFeedback(`❌ Wrong! Correct answer: "${this.currentQuestionData.correctAnswer}"`, 'error');
}
this.updateScore();
// Show next button or finish
if (this.currentQuestion < this.totalQuestions - 1) {
document.getElementById('next-btn').style.display = 'block';
} else {
setTimeout(() => this.gameComplete(), 250);
}
}
nextQuestion() {
this.currentQuestion++;
this.generateQuestion();
}
updateProgress() {
const progressFill = document.getElementById('progress-fill');
const progressPercent = ((this.currentQuestion + 1) / this.totalQuestions) * 100;
progressFill.style.width = `${progressPercent}%`;
document.getElementById('question-counter').textContent =
`${this.currentQuestion + 1} / ${this.totalQuestions}`;
}
updateScore() {
document.getElementById('score-display').textContent = `Score: ${this.score}`;
this.onScoreUpdate(this.score);
}
gameComplete() {
const accuracy = Math.round((this.correctAnswers / this.totalQuestions) * 100);
// Bonus for high accuracy
if (accuracy >= 90) {
this.score += 50; // Excellence bonus
} else if (accuracy >= 70) {
this.score += 20; // Good performance bonus
}
this.updateScore();
this.showFeedback(
`🎉 Quiz completed! ${this.correctAnswers}/${this.totalQuestions} correct (${accuracy}%)`,
'success'
);
setTimeout(() => {
this.onGameEnd(this.score);
}, 3000);
}
showFeedback(message, type = 'info') {
const feedbackArea = document.getElementById('feedback-area');
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
}
start() {
logSh('❓ Quiz Game: Starting', 'INFO');
this.showFeedback('Choose the correct translation for each word!', 'info');
}
restart() {
logSh('🔄 Quiz Game: Restarting', 'INFO');
this.reset();
this.start();
}
reset() {
this.currentQuestion = 0;
this.score = 0;
this.correctAnswers = 0;
this.hasAnswered = false;
this.currentQuestionData = null;
// Re-shuffle vocabulary
this.vocabulary = this.shuffleArray(this.vocabulary);
this.generateQuestion();
this.updateScore();
}
destroy() {
this.container.innerHTML = '';
}
}
// Module registration
window.GameModules = window.GameModules || {};
window.GameModules.QuizGame = QuizGame;