// === MODULE MEMORY MATCH === class MemoryMatchGame { constructor(options) { this.container = options.container; this.content = options.content; this.onScoreUpdate = options.onScoreUpdate || (() => {}); this.onGameEnd = options.onGameEnd || (() => {}); // Game state this.cards = []; this.flippedCards = []; this.matchedPairs = 0; this.totalPairs = 8; // 4x4 grid = 16 cards = 8 pairs this.moves = 0; this.score = 0; this.isFlipping = false; // Extract vocabulary this.vocabulary = this.extractVocabulary(this.content); this.init(); } init() { // Check if we have enough vocabulary if (!this.vocabulary || this.vocabulary.length < this.totalPairs) { console.error('Not enough vocabulary for Memory Match'); this.showInitError(); return; } this.createGameInterface(); this.generateCards(); this.setupEventListeners(); } showInitError() { this.container.innerHTML = `

❌ Error loading

This content doesn't have enough vocabulary for Memory Match.

The game needs at least ${this.totalPairs} vocabulary pairs.

`; } extractVocabulary(content) { let vocabulary = []; console.log('📝 Extracting vocabulary from:', content?.name || 'content'); // Use raw module content if available if (content.rawContent) { console.log('📦 Using raw module content'); return this.extractVocabularyFromRaw(content.rawContent); } // Modern format with contentItems if (content.contentItems && Array.isArray(content.contentItems)) { console.log('🆕 ContentItems format detected'); const vocabItems = content.contentItems.filter(item => item.type === 'vocabulary'); if (vocabItems.length > 0) { vocabulary = vocabItems[0].items || []; } } // Legacy format with vocabulary array else if (content.vocabulary && Array.isArray(content.vocabulary)) { console.log('📚 Vocabulary array format detected'); vocabulary = content.vocabulary; } return this.finalizeVocabulary(vocabulary); } extractVocabularyFromRaw(rawContent) { console.log('🔧 Extracting from raw content:', rawContent.name || 'Module'); let vocabulary = []; // Check vocabulary object format (key-value pairs) if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({ english: english, french: translation })); console.log(`📝 ${vocabulary.length} vocabulary pairs extracted from object`); } // Check vocabulary array format else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) { vocabulary = rawContent.vocabulary; console.log(`📚 ${vocabulary.length} vocabulary items extracted from array`); } return this.finalizeVocabulary(vocabulary); } finalizeVocabulary(vocabulary) { // Filter and validate vocabulary vocabulary = vocabulary.filter(item => item && item.english && (item.french || item.translation || item.chinese) ).map(item => ({ english: item.english, french: item.french || item.translation || item.chinese })); if (vocabulary.length === 0) { console.error('❌ No valid vocabulary found'); // Demo vocabulary as fallback vocabulary = [ { english: "cat", french: "chat" }, { english: "dog", french: "chien" }, { english: "house", french: "maison" }, { english: "car", french: "voiture" }, { english: "book", french: "livre" }, { english: "water", french: "eau" }, { english: "food", french: "nourriture" }, { english: "friend", french: "ami" } ]; console.warn('🚨 Using demo vocabulary'); } console.log(`✅ Memory Match: ${vocabulary.length} vocabulary items finalized`); return vocabulary; } createGameInterface() { this.container.innerHTML = `
Moves: 0
Pairs: 0 / ${this.totalPairs}
Score: 0
Click cards to flip them and find matching pairs!
`; } generateCards() { // Select random vocabulary pairs const selectedVocab = this.vocabulary .sort(() => Math.random() - 0.5) .slice(0, this.totalPairs); // Create card pairs this.cards = []; selectedVocab.forEach((item, index) => { // English card this.cards.push({ id: `en_${index}`, content: item.english, type: 'english', pairId: index, isFlipped: false, isMatched: false }); // French card this.cards.push({ id: `fr_${index}`, content: item.french, type: 'french', pairId: index, isFlipped: false, isMatched: false }); }); // Shuffle cards this.cards.sort(() => Math.random() - 0.5); // Render cards this.renderCards(); } renderCards() { const grid = document.getElementById('memory-grid'); grid.innerHTML = ''; this.cards.forEach((card, index) => { const cardElement = document.createElement('div'); cardElement.className = 'memory-card'; cardElement.dataset.cardIndex = index; cardElement.innerHTML = `
🎯
${card.content}
`; cardElement.addEventListener('click', () => this.flipCard(index)); grid.appendChild(cardElement); }); } setupEventListeners() { document.getElementById('restart-btn').addEventListener('click', () => this.restart()); document.getElementById('hint-btn').addEventListener('click', () => this.showHint()); } flipCard(cardIndex) { if (this.isFlipping) return; const card = this.cards[cardIndex]; if (card.isFlipped || card.isMatched) return; // Flip the card card.isFlipped = true; this.updateCardDisplay(cardIndex); this.flippedCards.push(cardIndex); if (this.flippedCards.length === 2) { this.moves++; this.updateStats(); this.checkMatch(); } } updateCardDisplay(cardIndex) { const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`); const card = this.cards[cardIndex]; if (card.isFlipped || card.isMatched) { cardElement.classList.add('flipped'); } else { cardElement.classList.remove('flipped'); } if (card.isMatched) { cardElement.classList.add('matched'); } } checkMatch() { this.isFlipping = true; setTimeout(() => { const [firstIndex, secondIndex] = this.flippedCards; const firstCard = this.cards[firstIndex]; const secondCard = this.cards[secondIndex]; if (firstCard.pairId === secondCard.pairId) { // Match found! firstCard.isMatched = true; secondCard.isMatched = true; this.updateCardDisplay(firstIndex); this.updateCardDisplay(secondIndex); this.matchedPairs++; this.score += 100; this.showFeedback('Great match! 🎉', 'success'); // Trigger success animation this.triggerSuccessAnimation(firstIndex, secondIndex); if (this.matchedPairs === this.totalPairs) { setTimeout(() => this.gameComplete(), 800); } } else { // No match, flip back and apply penalty firstCard.isFlipped = false; secondCard.isFlipped = false; this.updateCardDisplay(firstIndex); this.updateCardDisplay(secondIndex); // Apply penalty but don't go below 0 this.score = Math.max(0, this.score - 10); this.showFeedback('Try again! (-10 points)', 'warning'); } this.flippedCards = []; this.isFlipping = false; this.updateStats(); }, 1000); } showHint() { if (this.flippedCards.length > 0) { this.showFeedback('Finish your current move first!', 'warning'); return; } // Find first unmatched pair const unmatchedCards = this.cards.filter(card => !card.isMatched); if (unmatchedCards.length === 0) return; // Group by pairId const pairs = {}; unmatchedCards.forEach((card, index) => { const actualIndex = this.cards.indexOf(card); if (!pairs[card.pairId]) { pairs[card.pairId] = []; } pairs[card.pairId].push(actualIndex); }); // Find first complete pair const completePair = Object.values(pairs).find(pair => pair.length === 2); if (completePair) { // Briefly show the pair completePair.forEach(cardIndex => { const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`); cardElement.classList.add('hint'); }); setTimeout(() => { completePair.forEach(cardIndex => { const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`); cardElement.classList.remove('hint'); }); }, 2000); this.showFeedback('Hint shown for 2 seconds!', 'info'); } } updateStats() { document.getElementById('moves-counter').textContent = this.moves; document.getElementById('pairs-counter').textContent = `${this.matchedPairs} / ${this.totalPairs}`; document.getElementById('score-counter').textContent = this.score; this.onScoreUpdate(this.score); } gameComplete() { // Calculate bonus based on moves const perfectMoves = this.totalPairs; if (this.moves <= perfectMoves + 5) { this.score += 200; // Efficiency bonus } this.updateStats(); this.showFeedback('🎉 Congratulations! All pairs found!', 'success'); setTimeout(() => { this.onGameEnd(this.score); }, 2000); } showFeedback(message, type = 'info') { const feedbackArea = document.getElementById('feedback-area'); feedbackArea.innerHTML = `
${message}
`; } start() { console.log('🧠 Memory Match: Starting'); this.showFeedback('Find matching English-French pairs!', 'info'); } restart() { console.log('🔄 Memory Match: Restarting'); this.reset(); this.start(); } reset() { this.flippedCards = []; this.matchedPairs = 0; this.moves = 0; this.score = 0; this.isFlipping = false; this.generateCards(); this.updateStats(); } triggerSuccessAnimation(cardIndex1, cardIndex2) { // Get card elements const card1 = document.querySelector(`[data-card-index="${cardIndex1}"]`); const card2 = document.querySelector(`[data-card-index="${cardIndex2}"]`); if (!card1 || !card2) return; // Add success animation class card1.classList.add('success-animation'); card2.classList.add('success-animation'); // Create sparkle particles for both cards this.createSparkleParticles(card1); this.createSparkleParticles(card2); // Remove animation class after animation completes setTimeout(() => { card1.classList.remove('success-animation'); card2.classList.remove('success-animation'); }, 800); } createSparkleParticles(cardElement) { const rect = cardElement.getBoundingClientRect(); // Create 4 sparkle particles around the card for (let i = 1; i <= 4; i++) { const particle = document.createElement('div'); particle.className = `success-particle particle-${i}`; // Position relative to card particle.style.position = 'fixed'; particle.style.left = (rect.left + rect.width / 2) + 'px'; particle.style.top = (rect.top + rect.height / 2) + 'px'; particle.style.pointerEvents = 'none'; particle.style.zIndex = '1000'; document.body.appendChild(particle); // Remove particle after animation setTimeout(() => { if (particle.parentNode) { particle.parentNode.removeChild(particle); } }, 1200); } } destroy() { this.container.innerHTML = ''; } } // Module registration window.GameModules = window.GameModules || {}; window.GameModules.MemoryMatch = MemoryMatchGame;