Class_generator/Legacy/js/games/memory-match.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
Major Changes:
- Moved legacy system to Legacy/ folder for archival
- Built new modular architecture with strict separation of concerns
- Created core system: Module, EventBus, ModuleLoader, Router
- Added Application bootstrap with auto-start functionality
- Implemented development server with ES6 modules support
- Created comprehensive documentation and project context
- Converted SBS-7-8 content to JSON format
- Copied all legacy games and content to new structure

New Architecture Features:
- Sealed modules with WeakMap private data
- Strict dependency injection system
- Event-driven communication only
- Inviolable responsibility patterns
- Auto-initialization without commands
- Component-based UI foundation ready

Technical Stack:
- Vanilla JS/HTML/CSS only
- ES6 modules with proper imports/exports
- HTTP development server (no file:// protocol)
- Modular CSS with component scoping
- Comprehensive error handling and debugging

Ready for Phase 2: Converting legacy modules to new architecture

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 07:08:39 +08:00

495 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === 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 = `
<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.navigateTo('games')" class="back-btn">← Back</button>
</div>
`;
}
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 = `
<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.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 = `
<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');
// 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 = `<div class="instruction ${type}">${message}</div>`;
}
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;