Déplacement de fichiers et cohérence des header d'exercices
This commit is contained in:
527
exercices/memory-pictogrammes/assets/js/memory-game.js
Normal file
527
exercices/memory-pictogrammes/assets/js/memory-game.js
Normal file
@@ -0,0 +1,527 @@
|
||||
/* ============================================
|
||||
MEMORY PICTOGRAMMES - GAME ENGINE
|
||||
Logique principale du jeu
|
||||
============================================ */
|
||||
|
||||
class MemoryGameEngine {
|
||||
constructor(difficulty = 'normal') {
|
||||
this.difficulty = difficulty;
|
||||
this.mode = DIFFICULTY_MODES[difficulty];
|
||||
this.cards = [];
|
||||
this.gameState = {
|
||||
flipped: [],
|
||||
matched: 0,
|
||||
moves: 0,
|
||||
score: 0,
|
||||
isProcessing: false,
|
||||
isPaused: false,
|
||||
gameStarted: false
|
||||
};
|
||||
this.startTime = null;
|
||||
this.timerInterval = null;
|
||||
this.scoreEngine = new ScoringEngine();
|
||||
this.analytics = new AnalyticsManager();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialiser le jeu
|
||||
*/
|
||||
init() {
|
||||
this.cards = this.initializeCards();
|
||||
this.renderCards();
|
||||
this.setupEventListeners();
|
||||
this.updateHUD();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialiser le deck de cartes
|
||||
*/
|
||||
initializeCards() {
|
||||
const selectedPictogrammes = PICTOGRAMMES.slice(0, this.mode.paires);
|
||||
const pairs = [];
|
||||
|
||||
// Créer paires (image + texte pour chaque pictogramme)
|
||||
selectedPictogrammes.forEach(pic => {
|
||||
pairs.push({
|
||||
type: 'image',
|
||||
data: pic,
|
||||
id: pic.id,
|
||||
pairId: `${pic.id}-img`
|
||||
});
|
||||
pairs.push({
|
||||
type: 'text',
|
||||
data: pic,
|
||||
id: pic.id,
|
||||
pairId: `${pic.id}-txt`
|
||||
});
|
||||
});
|
||||
|
||||
// Mélanger les cartes
|
||||
return this.shuffle(pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithme de shuffling (Fisher-Yates)
|
||||
*/
|
||||
shuffle(array) {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendre les cartes en HTML
|
||||
*/
|
||||
renderCards() {
|
||||
const gameGrid = document.getElementById('game-grid');
|
||||
gameGrid.innerHTML = '';
|
||||
|
||||
this.cards.forEach((card, index) => {
|
||||
const cardElement = document.createElement('div');
|
||||
cardElement.className = 'memory-card';
|
||||
cardElement.setAttribute('data-card-index', index);
|
||||
|
||||
// Face avant (couverture)
|
||||
const frontFace = document.createElement('div');
|
||||
frontFace.className = 'card-face front';
|
||||
frontFace.textContent = '?';
|
||||
|
||||
// Face arrière (contenu)
|
||||
const backFace = document.createElement('div');
|
||||
backFace.className = 'card-face back';
|
||||
|
||||
if (card.type === 'image') {
|
||||
const img = document.createElement('img');
|
||||
img.src = `assets/pictogrammes/${card.data.filename}`;
|
||||
img.alt = card.data.nom;
|
||||
backFace.appendChild(img);
|
||||
} else {
|
||||
const text = document.createElement('span');
|
||||
text.textContent = card.data.texte;
|
||||
backFace.appendChild(text);
|
||||
}
|
||||
|
||||
cardElement.appendChild(frontFace);
|
||||
cardElement.appendChild(backFace);
|
||||
|
||||
cardElement.addEventListener('click', () => this.onCardClicked(index));
|
||||
gameGrid.appendChild(cardElement);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestion du clic sur une carte
|
||||
*/
|
||||
onCardClicked(cardIndex) {
|
||||
// Démarrer le timer au premier clic
|
||||
if (!this.gameState.gameStarted) {
|
||||
this.startGame();
|
||||
}
|
||||
|
||||
// Guard clauses
|
||||
if (this.gameState.isProcessing) return;
|
||||
if (this.gameState.flipped.length === 2) return;
|
||||
if (this.cards[cardIndex].isMatched) return;
|
||||
if (this.gameState.flipped.includes(cardIndex)) return;
|
||||
if (this.gameState.isPaused) return;
|
||||
|
||||
// Retourner la carte
|
||||
this.flipCard(cardIndex);
|
||||
this.gameState.flipped.push(cardIndex);
|
||||
|
||||
// Analytics
|
||||
this.analytics.trackFlip(cardIndex, this.gameState.moves);
|
||||
|
||||
// Vérifier si 2 cartes retournées
|
||||
if (this.gameState.flipped.length === 2) {
|
||||
this.gameState.isProcessing = true;
|
||||
this.gameState.moves++;
|
||||
this.updateHUD();
|
||||
|
||||
// Délai pour visualiser les 2 cartes
|
||||
setTimeout(() => this.checkMatch(), 1200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourner une carte (animation)
|
||||
*/
|
||||
flipCard(index) {
|
||||
const cardElement = document.querySelector(`[data-card-index="${index}"]`);
|
||||
cardElement.classList.add('flipped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourner une carte (inverse l'animation)
|
||||
*/
|
||||
unflipCard(index) {
|
||||
const cardElement = document.querySelector(`[data-card-index="${index}"]`);
|
||||
cardElement.classList.remove('flipped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifier si les 2 cartes retournées correspondent
|
||||
*/
|
||||
checkMatch() {
|
||||
const [idx1, idx2] = this.gameState.flipped;
|
||||
const card1 = this.cards[idx1];
|
||||
const card2 = this.cards[idx2];
|
||||
|
||||
if (card1.id === card2.id) {
|
||||
this.onMatchSuccess(idx1, idx2);
|
||||
} else {
|
||||
this.onMatchFailure(idx1, idx2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paire correctement matchée
|
||||
*/
|
||||
onMatchSuccess(idx1, idx2) {
|
||||
const card1Element = document.querySelector(`[data-card-index="${idx1}"]`);
|
||||
const card2Element = document.querySelector(`[data-card-index="${idx2}"]`);
|
||||
|
||||
// Marquer comme matched
|
||||
this.cards[idx1].isMatched = true;
|
||||
this.cards[idx2].isMatched = true;
|
||||
card1Element.classList.add('matched');
|
||||
card2Element.classList.add('matched');
|
||||
|
||||
this.gameState.matched++;
|
||||
|
||||
// Augmenter score
|
||||
const points = 100 + (50 * this.mode.scoreMultiplier);
|
||||
this.gameState.score += points;
|
||||
|
||||
// Analytics
|
||||
this.analytics.trackMatch(
|
||||
this.cards[idx1].id,
|
||||
this.gameState.matched,
|
||||
this.gameState.moves
|
||||
);
|
||||
|
||||
// Mettre à jour UI
|
||||
this.updateHUD();
|
||||
|
||||
// Ajouter pulse à la barre de progression
|
||||
const progressContainer = document.querySelector('.progress-container');
|
||||
progressContainer.classList.add('pulse');
|
||||
setTimeout(() => progressContainer.classList.remove('pulse'), 600);
|
||||
|
||||
// Vérifier victoire
|
||||
if (this.gameState.matched === this.mode.paires) {
|
||||
setTimeout(() => this.endGame(true), 600);
|
||||
} else {
|
||||
// Reset pour prochaine tentative
|
||||
this.gameState.flipped = [];
|
||||
this.gameState.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paire incorrecte
|
||||
*/
|
||||
onMatchFailure(idx1, idx2) {
|
||||
const card1Element = document.querySelector(`[data-card-index="${idx1}"]`);
|
||||
const card2Element = document.querySelector(`[data-card-index="${idx2}"]`);
|
||||
|
||||
// Animation shake
|
||||
card1Element.classList.add('error');
|
||||
card2Element.classList.add('error');
|
||||
|
||||
// Analytics
|
||||
this.analytics.trackMismatch(
|
||||
this.cards[idx1].id,
|
||||
this.cards[idx2].id,
|
||||
this.gameState.moves
|
||||
);
|
||||
|
||||
// Retourner après délai
|
||||
setTimeout(() => {
|
||||
this.unflipCard(idx1);
|
||||
this.unflipCard(idx2);
|
||||
card1Element.classList.remove('error');
|
||||
card2Element.classList.remove('error');
|
||||
this.gameState.flipped = [];
|
||||
this.gameState.isProcessing = false;
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarrer le timer du jeu
|
||||
*/
|
||||
startGame() {
|
||||
this.gameState.gameStarted = true;
|
||||
this.startTime = Date.now();
|
||||
|
||||
this.timerInterval = setInterval(() => {
|
||||
if (!this.gameState.isPaused) {
|
||||
this.updateTimer();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
this.analytics.trackGameStart(this.difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour le timer
|
||||
*/
|
||||
updateTimer() {
|
||||
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
|
||||
const timerDisplay = document.getElementById('timer-display');
|
||||
timerDisplay.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
|
||||
// Animation warning si peu de temps reste
|
||||
if (this.mode.timeLimit && elapsed >= this.mode.timeLimit * 0.8) {
|
||||
timerDisplay.style.color = '#FF9800';
|
||||
timerDisplay.style.animation = 'timerWarning 0.5s infinite';
|
||||
}
|
||||
|
||||
// Game over si temps limite dépassé
|
||||
if (this.mode.timeLimit && elapsed >= this.mode.timeLimit) {
|
||||
this.endGame(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour le HUD
|
||||
*/
|
||||
updateHUD() {
|
||||
// Score
|
||||
document.getElementById('score-display').textContent = this.gameState.score;
|
||||
|
||||
// Mouvements
|
||||
document.getElementById('moves-display').textContent = this.gameState.moves;
|
||||
|
||||
// Paires trouvées
|
||||
document.getElementById('pairs-display').textContent = `${this.gameState.matched}/${this.mode.paires}`;
|
||||
|
||||
// Barre de progression
|
||||
const progress = (this.gameState.matched / this.mode.paires) * 100;
|
||||
document.getElementById('progress-bar').style.width = `${progress}%`;
|
||||
|
||||
// Modifier le scroll progress bar en haut
|
||||
const scrollProgress = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
|
||||
document.getElementById('progress-fill').style.width = `${Math.min(scrollProgress, 100)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fin du jeu
|
||||
*/
|
||||
endGame(isWon) {
|
||||
clearInterval(this.timerInterval);
|
||||
|
||||
const endTime = Date.now();
|
||||
const gameData = {
|
||||
duration: Math.floor((endTime - this.startTime) / 1000),
|
||||
moves: this.gameState.moves,
|
||||
score: this.gameState.score,
|
||||
difficulty: this.difficulty,
|
||||
won: isWon
|
||||
};
|
||||
|
||||
// Calculer score final avec bonus
|
||||
const finalScore = this.scoreEngine.calculateFinalScore(
|
||||
gameData.moves,
|
||||
gameData.duration,
|
||||
this.mode.paires,
|
||||
this.difficulty
|
||||
);
|
||||
|
||||
// Détecter achievements
|
||||
const achievements = this.scoreEngine.detectAchievements(
|
||||
gameData.moves,
|
||||
gameData.duration,
|
||||
this.mode.paires,
|
||||
this.difficulty
|
||||
);
|
||||
|
||||
// Analytics
|
||||
this.analytics.trackGameEnd({
|
||||
...gameData,
|
||||
finalScore: finalScore.totalScore,
|
||||
achievements: achievements.map(a => a.id)
|
||||
});
|
||||
|
||||
// Sauvegarder score
|
||||
LeaderboardManager.addScore({
|
||||
...gameData,
|
||||
score: finalScore.totalScore
|
||||
});
|
||||
|
||||
// Afficher modal victoire
|
||||
this.showVictoryModal(finalScore, achievements, isWon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Afficher la modal de victoire
|
||||
*/
|
||||
showVictoryModal(finalScore, achievements, isWon) {
|
||||
const modal = document.getElementById('victory-modal');
|
||||
const minutes = Math.floor(this.gameState.score / 60);
|
||||
const seconds = this.gameState.score % 60;
|
||||
|
||||
// Remplir les données
|
||||
document.getElementById('victory-title').textContent = isWon ? 'Bravo! 🎉' : 'Temps écoulé! ⏰';
|
||||
document.getElementById('final-score').textContent = finalScore.totalScore;
|
||||
document.getElementById('modal-moves').textContent = this.gameState.moves;
|
||||
document.getElementById('modal-time').textContent = `${String(Math.floor(finalScore.duration / 60)).padStart(2, '0')}:${String(finalScore.duration % 60).padStart(2, '0')}`;
|
||||
document.getElementById('modal-difficulty').textContent = this.mode.name;
|
||||
document.getElementById('modal-bonus').textContent = `+${finalScore.timeBonus}`;
|
||||
|
||||
// Achievements
|
||||
const achievementsList = document.getElementById('achievements-list');
|
||||
achievementsList.innerHTML = '';
|
||||
|
||||
if (achievements.length > 0) {
|
||||
const title = document.createElement('div');
|
||||
title.style.fontSize = '14px';
|
||||
title.style.fontWeight = '600';
|
||||
title.style.marginBottom = '12px';
|
||||
title.textContent = '🏅 Achievements Débloqués';
|
||||
achievementsList.appendChild(title);
|
||||
|
||||
achievements.forEach((ach, index) => {
|
||||
const achievementEl = document.createElement('div');
|
||||
achievementEl.className = 'achievement';
|
||||
achievementEl.style.animationDelay = `${(index * 0.1)}s`;
|
||||
achievementEl.innerHTML = `
|
||||
<div class="achievement-icon">${ach.icon}</div>
|
||||
<div class="achievement-text">
|
||||
<div class="achievement-name">${ach.name}</div>
|
||||
<div class="achievement-desc">${ach.description}</div>
|
||||
</div>
|
||||
`;
|
||||
achievementsList.appendChild(achievementEl);
|
||||
});
|
||||
}
|
||||
|
||||
// Boutons d'action
|
||||
document.getElementById('play-again-btn').onclick = () => this.restart();
|
||||
document.getElementById('view-leaderboard-btn').onclick = () => {
|
||||
modal.classList.add('hidden');
|
||||
document.querySelector('.leaderboard-section').scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
// Afficher modal
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redémarrer le jeu
|
||||
*/
|
||||
restart() {
|
||||
document.getElementById('victory-modal').classList.add('hidden');
|
||||
clearInterval(this.timerInterval);
|
||||
|
||||
// Réinitialiser l'état
|
||||
this.gameState = {
|
||||
flipped: [],
|
||||
matched: 0,
|
||||
moves: 0,
|
||||
score: 0,
|
||||
isProcessing: false,
|
||||
isPaused: false,
|
||||
gameStarted: false
|
||||
};
|
||||
|
||||
// Réinitialiser les cartes
|
||||
this.cards = this.initializeCards();
|
||||
this.renderCards();
|
||||
this.updateHUD();
|
||||
|
||||
// Reset timer display
|
||||
document.getElementById('timer-display').textContent = '00:00';
|
||||
document.getElementById('timer-display').style.color = '';
|
||||
document.getElementById('timer-display').style.animation = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause/Reprise
|
||||
*/
|
||||
togglePause() {
|
||||
if (!this.gameState.gameStarted) return;
|
||||
|
||||
this.gameState.isPaused = !this.gameState.isPaused;
|
||||
const pauseBtn = document.getElementById('pause-btn');
|
||||
|
||||
if (this.gameState.isPaused) {
|
||||
pauseBtn.textContent = '▶ Reprendre';
|
||||
pauseBtn.classList.add('paused');
|
||||
} else {
|
||||
pauseBtn.textContent = '⏸ Pause';
|
||||
pauseBtn.classList.remove('paused');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Bouton restart
|
||||
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
||||
|
||||
// Bouton pause
|
||||
document.getElementById('pause-btn').addEventListener('click', () => this.togglePause());
|
||||
|
||||
// Sélecteur de difficulté
|
||||
document.querySelectorAll('.btn-difficulty').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const newDifficulty = btn.getAttribute('data-difficulty');
|
||||
if (newDifficulty !== this.difficulty) {
|
||||
this.difficulty = newDifficulty;
|
||||
this.mode = DIFFICULTY_MODES[newDifficulty];
|
||||
this.restart();
|
||||
|
||||
// Marquer comme actif
|
||||
document.querySelectorAll('.btn-difficulty').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
|
||||
this.analytics.trackDifficultyChange(newDifficulty);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Tabs du leaderboard
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const filter = btn.getAttribute('data-filter');
|
||||
LeaderboardManager.updateDisplay(filter);
|
||||
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INITIALISATION AU CHARGEMENT
|
||||
// ============================================
|
||||
let game;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialiser le jeu
|
||||
game = new MemoryGameEngine('normal');
|
||||
|
||||
// Afficher le leaderboard initial
|
||||
LeaderboardManager.updateDisplay('all');
|
||||
LeaderboardManager.updatePlayerStats();
|
||||
|
||||
// Scroll progress
|
||||
window.addEventListener('scroll', () => {
|
||||
game.updateHUD();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user