Class_generator/js/games/memory-match.js

404 lines
14 KiB
JavaScript

// === 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 = `
<div class="game-error">
<h3>❌ Error loading</h3>
<p>This content doesn't have enough vocabulary for Memory Match.</p>
<p>The game needs at least ${this.totalPairs} vocabulary pairs.</p>
<button onclick="AppNavigation.goBack()" class="back-btn">← Back</button>
</div>
`;
}
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 = `
<div class="memory-match-wrapper">
<!-- Game Stats -->
<div class="game-stats">
<div class="stat-item">
<span class="stat-label">Moves:</span>
<span id="moves-counter">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Pairs:</span>
<span id="pairs-counter">0 / ${this.totalPairs}</span>
</div>
<div class="stat-item">
<span class="stat-label">Score:</span>
<span id="score-counter">0</span>
</div>
</div>
<!-- Game Grid -->
<div class="memory-grid" id="memory-grid">
<!-- Cards will be generated here -->
</div>
<!-- Game Controls -->
<div class="game-controls">
<button class="control-btn secondary" id="restart-btn">🔄 Restart</button>
<button class="control-btn secondary" id="hint-btn">💡 Hint</button>
</div>
<!-- Feedback Area -->
<div class="feedback-area" id="feedback-area">
<div class="instruction">
Click cards to flip them and find matching pairs!
</div>
</div>
</div>
`;
}
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 = `
<div class="card-inner">
<div class="card-front">
<span class="card-icon">🎯</span>
</div>
<div class="card-back">
<span class="card-content">${card.content}</span>
</div>
</div>
`;
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');
if (this.matchedPairs === this.totalPairs) {
this.gameComplete();
}
} 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 = `<div class="instruction ${type}">${message}</div>`;
}
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();
}
destroy() {
this.container.innerHTML = '';
}
}
// Module registration
window.GameModules = window.GameModules || {};
window.GameModules.MemoryMatch = MemoryMatchGame;