Class_generator/js/games/quiz-game.js
StillHammer 30a2028da6 Remove Text Reader game and enhance Story Reader
- Enhanced Story Reader with text-to-story conversion methods
- Added support for simple texts and sentences in Story Reader
- Removed Text Reader game file (js/games/text-reader.js)
- Updated all configuration files to remove Text Reader references
- Modified game compatibility system to use Story Reader instead
- Updated test fixtures to reflect game changes
- Cleaned up debug/test HTML files

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 11:22:56 +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;