// === 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 = `
`;
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;