// === 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) { logSh('Not enough vocabulary for Memory Match', 'ERROR'); 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 = []; logSh('📝 Extracting vocabulary from:', content?.name || 'content', 'INFO'); // Use raw module content if available if (content.rawContent) { logSh('📦 Using raw module content', 'INFO'); return this.extractVocabularyFromRaw(content.rawContent); } // 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) { // Filter and validate vocabulary for ultra-modular format vocabulary = vocabulary.filter(item => item && typeof item.original === 'string' && typeof item.translation === 'string' && item.original.trim() !== '' && item.translation.trim() !== '' ); if (vocabulary.length === 0) { logSh('❌ No valid vocabulary found', 'ERROR'); // Demo vocabulary as fallback vocabulary = [ { original: "cat", translation: "chat" }, { original: "dog", translation: "chien" }, { original: "house", translation: "maison" }, { original: "car", translation: "voiture" }, { original: "book", translation: "livre" }, { original: "water", translation: "eau" }, { original: "food", translation: "nourriture" }, { original: "friend", translation: "ami" } ]; logSh('🚨 Using demo vocabulary', 'WARN'); } logSh(`✅ Memory Match: ${vocabulary.length} vocabulary items finalized`, 'INFO'); 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.original, type: 'english', pairId: index, isFlipped: false, isMatched: false }); // French card this.cards.push({ id: `fr_${index}`, content: item.translation, 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() { logSh('🧠 Memory Match: Starting', 'INFO'); this.showFeedback('Find matching English-French pairs!', 'info'); } restart() { logSh('🔄 Memory Match: Restarting', 'INFO'); 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;