import Module from '../core/Module.js';
/**
* QuizGame - Educational vocabulary quiz with multiple choice questions
* Supports bidirectional quizzing (English to translation or translation to English)
*/
class QuizGame extends Module {
constructor(name, dependencies, config = {}) {
super(name, ['eventBus']);
// Validate dependencies
if (!dependencies.eventBus || !dependencies.content) {
throw new Error('QuizGame requires eventBus and content dependencies');
}
this._eventBus = dependencies.eventBus;
this._content = dependencies.content;
this._config = {
container: null,
questionCount: 10,
optionsCount: 6,
timeLimit: 30, // seconds per question
...config
};
// Game state
this._vocabulary = null;
this._currentQuestion = 0;
this._score = 0;
this._questions = [];
this._currentQuestionData = null;
this._isAnswering = false;
this._gameStartTime = null;
this._questionStartTime = null;
this._quizDirection = 'en-to-translation'; // or 'translation-to-en'
this._timeRemaining = 0;
this._timer = null;
Object.seal(this);
}
/**
* Get game metadata
* @returns {Object} Game metadata
*/
static getMetadata() {
return {
name: 'Vocabulary Quiz',
description: 'Multiple choice vocabulary quiz with bidirectional testing',
difficulty: 'intermediate',
category: 'quiz',
estimatedTime: 8, // minutes
skills: ['vocabulary', 'comprehension', 'recognition', 'speed']
};
}
/**
* Calculate compatibility score with content
* @param {Object} content - Content to check compatibility with
* @returns {Object} Compatibility score and details
*/
static getCompatibilityScore(content) {
const vocab = content?.vocabulary || {};
const vocabCount = Object.keys(vocab).length;
if (vocabCount < 6) {
return {
score: 0,
reason: `Insufficient vocabulary (${vocabCount}/6 required)`,
requirements: ['vocabulary'],
minWords: 6,
details: 'Quiz Game needs at least 6 vocabulary words for multiple choice options'
};
}
// Perfect score at 20+ words, partial score for 6-19
const score = Math.min(vocabCount / 20, 1);
return {
score,
reason: `${vocabCount} vocabulary words available`,
requirements: ['vocabulary'],
minWords: 6,
optimalWords: 20,
details: `Can create quiz with ${Math.min(vocabCount, 10)} questions`
};
}
async init() {
this._validateNotDestroyed();
try {
// Validate container
if (!this._config.container) {
throw new Error('Game container is required');
}
// Extract and validate vocabulary
this._vocabulary = this._extractVocabulary();
if (this._vocabulary.length < this._config.optionsCount) {
throw new Error(`Insufficient vocabulary: need ${this._config.optionsCount}, got ${this._vocabulary.length}`);
}
// 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);
// Inject CSS
this._injectCSS();
// Initialize game interface
this._createGameInterface();
this._generateQuestions();
this._setupEventListeners();
// Start the game
this._gameStartTime = Date.now();
this._showQuizDirectionChoice();
// Emit game ready event
this._eventBus.emit('game:ready', {
gameId: 'quiz-game',
instanceId: this.name,
vocabulary: this._vocabulary.length,
questionsCount: this._questions.length
}, this.name);
this._setInitialized();
} catch (error) {
this._showError(error.message);
throw error;
}
}
async destroy() {
this._validateNotDestroyed();
// Clear timer
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
// Remove CSS
this._removeCSS();
// Clean up event listeners
if (this._config.container) {
this._config.container.innerHTML = '';
}
// Emit game end event
this._eventBus.emit('game:ended', {
gameId: 'quiz-game',
instanceId: this.name,
score: this._score,
questionsAnswered: this._currentQuestion,
totalQuestions: this._questions.length,
duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0
}, this.name);
this._setDestroyed();
}
/**
* Get current game state
* @returns {Object} Current game state
*/
getGameState() {
this._validateInitialized();
return {
score: this._score,
currentQuestion: this._currentQuestion,
totalQuestions: this._questions.length,
isComplete: this._currentQuestion >= this._questions.length,
duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0,
timeRemaining: this._timeRemaining
};
}
// Private methods
_extractVocabulary() {
const vocab = this._content?.vocabulary || {};
const vocabulary = [];
for (const [word, data] of Object.entries(vocab)) {
if (data.user_language) {
vocabulary.push({
english: word,
translation: data.user_language,
type: data.type || 'unknown'
});
}
}
return this._shuffleArray(vocabulary);
}
_injectCSS() {
const cssId = `quiz-game-styles-${this.name}`;
if (document.getElementById(cssId)) return;
const style = document.createElement('style');
style.id = cssId;
style.textContent = `
.quiz-game {
padding: 20px;
max-width: 800px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e0 100%);
border-radius: 12px;
color: #2d3748;
border: 2px solid #4299e1;
font-weight: 600;
}
.quiz-stats {
display: flex;
gap: 30px;
}
.quiz-stat {
text-align: center;
}
.stat-label {
display: block;
font-size: 0.8rem;
opacity: 0.9;
margin-bottom: 5px;
}
.stat-value {
display: block;
font-size: 1.5rem;
font-weight: bold;
}
.quiz-direction-choice {
text-align: center;
padding: 40px;
background: #f8f9fa;
border-radius: 12px;
margin-bottom: 30px;
}
.quiz-direction-choice h3 {
margin-bottom: 20px;
color: #333;
}
.direction-buttons {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.direction-btn {
padding: 15px 30px;
border: none;
border-radius: 8px;
background: #007bff;
color: white;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
}
.direction-btn:hover {
background: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.question-container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
.question-number {
font-size: 1.1rem;
color: #6c757d;
font-weight: 500;
}
.question-timer {
display: flex;
align-items: center;
gap: 10px;
color: #dc3545;
font-weight: bold;
}
.timer-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: #dc3545;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
}
.timer-circle.warning {
background: #ffc107;
color: #000;
}
.timer-circle.safe {
background: #28a745;
}
.question-text {
font-size: 1.5rem;
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
color: #333;
font-weight: 500;
}
.quiz-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.quiz-option {
padding: 20px;
border: 2px solid #e9ecef;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
font-size: 1.1rem;
position: relative;
}
.quiz-option:hover {
border-color: #007bff;
background: #f8f9fa;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
.quiz-option.selected {
border-color: #007bff;
background: #e3f2fd;
}
.quiz-option.correct {
border-color: #28a745;
background: #d4edda;
color: #155724;
}
.quiz-option.incorrect {
border-color: #dc3545;
background: #f8d7da;
color: #721c24;
}
.quiz-option.disabled {
pointer-events: none;
opacity: 0.7;
}
.quiz-feedback {
text-align: center;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
font-size: 1.1rem;
font-weight: 500;
}
.quiz-feedback.correct {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.quiz-feedback.incorrect {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.next-question-btn {
display: block;
margin: 20px auto;
padding: 12px 30px;
background: #28a745;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.next-question-btn:hover {
background: #218838;
transform: translateY(-2px);
}
.quiz-complete {
text-align: center;
padding: 40px;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
border-radius: 12px;
}
.quiz-complete h2 {
margin-bottom: 20px;
}
.final-score {
font-size: 2rem;
font-weight: bold;
margin: 20px 0;
}
.quiz-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin: 30px 0;
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 8px;
}
.summary-stat {
text-align: center;
}
.summary-stat-value {
display: block;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 5px;
}
.summary-stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
.restart-btn, .exit-btn {
margin: 10px;
padding: 12px 25px;
border: 2px solid white;
border-radius: 8px;
background: transparent;
color: white;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.restart-btn:hover, .exit-btn:hover {
background: white;
color: #28a745;
}
.quiz-error {
text-align: center;
padding: 40px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 12px;
color: #721c24;
}
.error-icon {
font-size: 3rem;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.quiz-header {
flex-direction: column;
gap: 20px;
}
.quiz-stats {
gap: 20px;
}
.quiz-options {
grid-template-columns: 1fr;
}
.direction-buttons {
flex-direction: column;
align-items: center;
}
}
`;
document.head.appendChild(style);
}
_removeCSS() {
const cssId = `quiz-game-styles-${this.name}`;
const existingStyle = document.getElementById(cssId);
if (existingStyle) {
existingStyle.remove();
}
}
_createGameInterface() {
this._config.container.innerHTML = `
`;
}
_generateQuestions() {
this._questions = [];
const questionCount = Math.min(this._config.questionCount, this._vocabulary.length);
const selectedVocab = this._shuffleArray([...this._vocabulary]).slice(0, questionCount);
selectedVocab.forEach((vocab, index) => {
this._questions.push({
id: index,
vocabulary: vocab,
options: this._generateOptions(vocab),
correctAnswer: null, // Will be set based on quiz direction
userAnswer: null,
timeSpent: 0,
answered: false
});
});
}
_generateOptions(correctVocab) {
const options = [];
const otherVocab = this._vocabulary.filter(v => v !== correctVocab);
const shuffledOthers = this._shuffleArray(otherVocab);
// Add correct answer
options.push(correctVocab);
// Add incorrect options
for (let i = 0; i < this._config.optionsCount - 1 && i < shuffledOthers.length; i++) {
options.push(shuffledOthers[i]);
}
return this._shuffleArray(options);
}
_showQuizDirectionChoice() {
const content = document.getElementById('quiz-content');
content.innerHTML = `
Choose Quiz Direction
How would you like to be tested?
`;
}
_startQuiz(direction) {
this._quizDirection = direction;
this._currentQuestion = 0;
this._score = 0;
// Set correct answers based on direction
this._questions.forEach(question => {
if (direction === 'en-to-translation') {
question.correctAnswer = question.vocabulary.translation;
} else {
question.correctAnswer = question.vocabulary.english;
}
});
this._showQuestion();
}
_showQuestion() {
if (this._currentQuestion >= this._questions.length) {
this._showResults();
return;
}
const question = this._questions[this._currentQuestion];
const content = document.getElementById('quiz-content');
// Determine question text based on direction
const questionText = this._quizDirection === 'en-to-translation'
? question.vocabulary.english
: question.vocabulary.translation;
content.innerHTML = `
${questionText}
${this._generateOptionHTML(question)}
`;
this._startQuestionTimer();
this._questionStartTime = Date.now();
}
_generateOptionHTML(question) {
return question.options.map((vocab, index) => {
const optionText = this._quizDirection === 'en-to-translation'
? vocab.translation
: vocab.english;
return `
${optionText}
`;
}).join('');
}
_startQuestionTimer() {
this._timeRemaining = this._config.timeLimit;
this._timer = setInterval(() => {
this._timeRemaining--;
this._updateTimer();
if (this._timeRemaining <= 0) {
this._handleTimeout();
}
}, 1000);
}
_updateTimer() {
const timerValue = document.getElementById('timer-value');
const timerCircle = document.getElementById('timer-circle');
if (timerValue) {
timerValue.textContent = this._timeRemaining;
}
if (timerCircle) {
timerCircle.className = 'timer-circle';
if (this._timeRemaining <= 5) {
timerCircle.classList.add('warning');
} else if (this._timeRemaining > 15) {
timerCircle.classList.add('safe');
}
}
}
_setupEventListeners() {
// Direction choice listeners
this._config.container.addEventListener('click', (event) => {
if (event.target.matches('.direction-btn')) {
const direction = event.target.dataset.direction;
this._startQuiz(direction);
}
if (event.target.matches('.quiz-option')) {
this._handleAnswerClick(event.target);
}
if (event.target.matches('.next-question-btn')) {
this._nextQuestion();
}
if (event.target.matches('.restart-btn')) {
this._restartQuiz();
}
});
// Exit button
const exitButton = this._config.container.querySelector('#exit-quiz');
if (exitButton) {
exitButton.addEventListener('click', () => {
this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name);
});
}
}
_handleAnswerClick(optionElement) {
if (this._isAnswering || optionElement.classList.contains('disabled')) {
return;
}
this._isAnswering = true;
const question = this._questions[this._currentQuestion];
const selectedAnswer = optionElement.dataset.value;
const isCorrect = selectedAnswer === question.correctAnswer;
// Clear timer
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
// Record answer
question.userAnswer = selectedAnswer;
question.answered = true;
question.timeSpent = this._questionStartTime ? Date.now() - this._questionStartTime : 0;
// Update score
if (isCorrect) {
const timeBonus = Math.max(0, this._timeRemaining * 2);
this._score += 100 + timeBonus;
}
// Show feedback
this._showAnswerFeedback(isCorrect, question);
this._updateStats();
// Emit answer event
this._eventBus.emit('quiz:answer', {
gameId: 'quiz-game',
instanceId: this.name,
questionNumber: this._currentQuestion + 1,
isCorrect,
score: this._score,
timeSpent: question.timeSpent
}, this.name);
}
_handleTimeout() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
const question = this._questions[this._currentQuestion];
question.answered = true;
question.timeSpent = this._config.timeLimit * 1000;
this._showAnswerFeedback(false, question, true);
this._updateStats();
}
_showAnswerFeedback(isCorrect, question, timeout = false) {
const optionsContainer = document.getElementById('quiz-options');
const feedbackContainer = document.getElementById('quiz-feedback');
// Disable all options
optionsContainer.querySelectorAll('.quiz-option').forEach(option => {
option.classList.add('disabled');
const optionValue = option.dataset.value;
if (optionValue === question.correctAnswer) {
option.classList.add('correct');
} else if (optionValue === question.userAnswer) {
option.classList.add('incorrect');
}
});
// Show feedback message
let feedbackMessage;
if (timeout) {
feedbackMessage = `⏰ Time's up! The correct answer was: ${question.correctAnswer}`;
} else if (isCorrect) {
feedbackMessage = `🎉 Correct! +${100 + Math.max(0, this._timeRemaining * 2)} points`;
} else {
feedbackMessage = `❌ Incorrect. The correct answer was: ${question.correctAnswer}`;
}
feedbackContainer.innerHTML = `
${feedbackMessage}
`;
}
_nextQuestion() {
this._currentQuestion++;
this._isAnswering = false;
this._showQuestion();
}
_showResults() {
const correctAnswers = this._questions.filter(q => q.userAnswer === q.correctAnswer).length;
const accuracy = Math.round((correctAnswers / this._questions.length) * 100);
const totalTime = this._gameStartTime ? Date.now() - this._gameStartTime : 0;
const avgTimePerQuestion = Math.round(totalTime / this._questions.length / 1000);
// Store best score
const gameKey = 'quiz-game';
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: 'Vocabulary Quiz',
currentScore,
bestScore: isNewBest ? currentScore : bestScore,
isNewBest,
stats: {
'Questions': `${correctAnswers}/${this._questions.length}`,
'Accuracy': `${accuracy}%`,
'Avg Time': `${avgTimePerQuestion}s`,
'Total Time': `${Math.round(totalTime / 1000)}s`
}
});
// Emit completion event
this._eventBus.emit('game:completed', {
gameId: 'quiz-game',
instanceId: this.name,
score: this._score,
correctAnswers,
totalQuestions: this._questions.length,
accuracy,
duration: totalTime
}, this.name);
}
_restartQuiz() {
this._currentQuestion = 0;
this._score = 0;
this._isAnswering = false;
this._generateQuestions();
this._showQuizDirectionChoice();
this._updateStats();
}
_updateStats() {
const scoreElement = document.getElementById('quiz-score');
const questionElement = document.getElementById('current-question');
const accuracyElement = document.getElementById('quiz-accuracy');
if (scoreElement) scoreElement.textContent = this._score;
if (questionElement) questionElement.textContent = this._currentQuestion + 1;
if (accuracyElement && this._currentQuestion > 0) {
const answeredQuestions = this._questions.slice(0, this._currentQuestion + 1).filter(q => q.answered);
const correctAnswers = answeredQuestions.filter(q => q.userAnswer === q.correctAnswer).length;
const accuracy = answeredQuestions.length > 0 ? Math.round((correctAnswers / answeredQuestions.length) * 100) : 0;
accuracyElement.textContent = `${accuracy}%`;
}
}
_showError(message) {
if (this._config.container) {
this._config.container.innerHTML = `
❌
Quiz Error
${message}
`;
}
}
_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;
}
_handlePause() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
this._eventBus.emit('game:paused', { instanceId: this.name }, this.name);
}
_handleResume() {
if (this._timeRemaining > 0 && !this._isAnswering) {
this._startQuestionTimer();
}
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 = `
Your Score
${currentScore}
${Object.entries(stats).map(([key, value]) => `
`).join('')}
`;
document.body.appendChild(popup);
// Animate in
requestAnimationFrame(() => {
popup.classList.add('show');
});
// Add event listeners
popup.querySelector('#play-again-btn').addEventListener('click', () => {
popup.remove();
this._restartQuiz();
});
popup.querySelector('#different-game-btn').addEventListener('click', () => {
popup.remove();
if (window.app && window.app.getCore().router) {
window.app.getCore().router.navigate('/games');
} 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');
} else {
window.location.href = '/#/games';
}
}
});
}
}
export default QuizGame;