// === MODULE WHACK-A-MOLE HARD ===
class WhackAMoleHardGame {
constructor(options) {
this.container = options.container;
this.content = options.content;
this.onScoreUpdate = options.onScoreUpdate || (() => {});
this.onGameEnd = options.onGameEnd || (() => {});
// État du jeu
this.score = 0;
this.errors = 0;
this.maxErrors = 5;
this.gameTime = 60; // 60 secondes
this.timeLeft = this.gameTime;
this.isRunning = false;
this.gameMode = 'translation'; // 'translation', 'image', 'sound'
// Configuration des taupes
this.holes = [];
this.activeMoles = [];
this.moleAppearTime = 3000; // 3 secondes d'affichage (plus long)
this.spawnRate = 2000; // Nouvelle vague toutes les 2 secondes
this.molesPerWave = 3; // 3 taupes par vague
// Timers
this.gameTimer = null;
this.spawnTimer = null;
// Vocabulaire pour ce jeu - adapté pour le nouveau système
this.vocabulary = this.extractVocabulary(this.content);
this.currentWords = [];
this.targetWord = null;
// Système de garantie pour le mot cible
this.spawnsSinceTarget = 0;
this.maxSpawnsWithoutTarget = 10; // Le mot cible doit apparaître dans les 10 prochaines taupes (1/10 chance)
this.init();
}
init() {
// Vérifier que nous avons du vocabulaire
if (!this.vocabulary || this.vocabulary.length === 0) {
console.error('Aucun vocabulaire disponible pour Whack-a-Mole');
this.showInitError();
return;
}
this.createGameBoard();
this.createGameUI();
this.setupEventListeners();
}
showInitError() {
this.container.innerHTML = `
❌ Erreur de chargement
Ce contenu ne contient pas de vocabulaire compatible avec Whack-a-Mole.
Le jeu nécessite des mots avec leurs traductions.
`;
}
createGameBoard() {
this.container.innerHTML = `
${this.timeLeft}
Time
${this.errors}
Errors
---
Find
Select a mode and click Start!
`;
this.createHoles();
}
createHoles() {
const gameBoard = document.getElementById('game-board');
gameBoard.innerHTML = '';
for (let i = 0; i < 15; i++) { // 5x3 = 15 trous
const hole = document.createElement('div');
hole.className = 'whack-hole';
hole.dataset.holeId = i;
hole.innerHTML = `
`;
gameBoard.appendChild(hole);
this.holes.push({
element: hole,
mole: hole.querySelector('.whack-mole'),
wordElement: hole.querySelector('.word'),
isActive: false,
word: null,
timer: null
});
}
}
createGameUI() {
// Les éléments UI sont déjà créés dans createGameBoard
}
setupEventListeners() {
// Mode selection
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
if (this.isRunning) return;
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.gameMode = btn.dataset.mode;
if (this.gameMode !== 'translation') {
this.showFeedback('This mode will be available soon!', 'info');
// Return to translation mode
document.querySelector('.mode-btn[data-mode="translation"]').classList.add('active');
btn.classList.remove('active');
this.gameMode = 'translation';
}
});
});
// Game controls
document.getElementById('start-btn').addEventListener('click', () => this.start());
document.getElementById('pause-btn').addEventListener('click', () => this.pause());
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
// Mole clicks
this.holes.forEach((hole, index) => {
hole.mole.addEventListener('click', () => this.hitMole(index));
});
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.score = 0;
this.errors = 0;
this.timeLeft = this.gameTime;
this.updateUI();
this.setNewTarget();
this.startTimers();
document.getElementById('start-btn').disabled = true;
document.getElementById('pause-btn').disabled = false;
this.showFeedback(`Find the word: "${this.targetWord.french}"`, 'info');
// Show loaded content info
const contentName = this.content.name || 'Content';
console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`);
}
pause() {
if (!this.isRunning) return;
this.isRunning = false;
this.stopTimers();
this.hideAllMoles();
document.getElementById('start-btn').disabled = false;
document.getElementById('pause-btn').disabled = true;
this.showFeedback('Game paused', 'info');
}
restart() {
this.stopWithoutEnd(); // Arrêter sans déclencher la fin de jeu
this.resetGame();
setTimeout(() => this.start(), 100);
}
stop() {
this.stopWithoutEnd();
this.onGameEnd(this.score); // Déclencher la fin de jeu seulement ici
}
stopWithoutEnd() {
this.isRunning = false;
this.stopTimers();
this.hideAllMoles();
document.getElementById('start-btn').disabled = false;
document.getElementById('pause-btn').disabled = true;
}
resetGame() {
// S'assurer que tout est complètement arrêté
this.stopWithoutEnd();
// Reset de toutes les variables d'état
this.score = 0;
this.errors = 0;
this.timeLeft = this.gameTime;
this.isRunning = false;
this.targetWord = null;
this.activeMoles = [];
this.spawnsSinceTarget = 0; // Reset du compteur de garantie
// S'assurer que tous les timers sont bien arrêtés
this.stopTimers();
// Reset UI
this.updateUI();
this.onScoreUpdate(0);
// Clear feedback
document.getElementById('target-word').textContent = '---';
this.showFeedback('Select a mode and click Start!', 'info');
// Reset buttons
document.getElementById('start-btn').disabled = false;
document.getElementById('pause-btn').disabled = true;
// Clear all holes avec vérification
this.holes.forEach(hole => {
if (hole.timer) {
clearTimeout(hole.timer);
hole.timer = null;
}
hole.isActive = false;
hole.word = null;
if (hole.wordElement) {
hole.wordElement.textContent = '';
}
if (hole.mole) {
hole.mole.classList.remove('active', 'hit');
}
});
console.log('🔄 Game completely reset');
}
startTimers() {
// Timer principal du jeu
this.gameTimer = setInterval(() => {
this.timeLeft--;
this.updateUI();
if (this.timeLeft <= 0 && this.isRunning) {
this.stop();
}
}, 1000);
// Timer d'apparition des taupes
this.spawnTimer = setInterval(() => {
if (this.isRunning) {
this.spawnMole();
}
}, this.spawnRate);
// Première taupe immédiate
setTimeout(() => this.spawnMole(), 500);
}
stopTimers() {
if (this.gameTimer) {
clearInterval(this.gameTimer);
this.gameTimer = null;
}
if (this.spawnTimer) {
clearInterval(this.spawnTimer);
this.spawnTimer = null;
}
}
spawnMole() {
// Mode Hard: Spawn 3 taupes à la fois
this.spawnMultipleMoles();
}
spawnMultipleMoles() {
// Trouver tous les trous libres
const availableHoles = this.holes.filter(hole => !hole.isActive);
// Spawn jusqu'à 3 taupes (ou moins si pas assez de trous libres)
const molesToSpawn = Math.min(this.molesPerWave, availableHoles.length);
if (molesToSpawn === 0) return;
// Mélanger les trous disponibles
const shuffledHoles = this.shuffleArray(availableHoles);
// Spawn les taupes
for (let i = 0; i < molesToSpawn; i++) {
const hole = shuffledHoles[i];
const holeIndex = this.holes.indexOf(hole);
// Choisir un mot selon la stratégie de garantie
const word = this.getWordWithTargetGuarantee();
// Activer la taupe avec un petit délai pour un effet visuel
setTimeout(() => {
if (this.isRunning && !hole.isActive) {
this.activateMole(holeIndex, word);
}
}, i * 200); // Délai de 200ms entre chaque taupe
}
}
getWordWithTargetGuarantee() {
// Incrémenter le compteur de spawns depuis le dernier mot cible
this.spawnsSinceTarget++;
// Si on a atteint la limite, forcer le mot cible
if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) {
console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`);
this.spawnsSinceTarget = 0;
return this.targetWord;
}
// Sinon, 10% de chance d'avoir le mot cible (1/10 au lieu de 1/2)
if (Math.random() < 0.1) {
console.log('🎯 Spawn naturel du mot cible (1/10)');
this.spawnsSinceTarget = 0;
return this.targetWord;
} else {
return this.getRandomWord();
}
}
activateMole(holeIndex, word) {
const hole = this.holes[holeIndex];
if (hole.isActive) return;
hole.isActive = true;
hole.word = word;
hole.wordElement.textContent = word.english;
hole.mole.classList.add('active');
// Ajouter à la liste des taupes actives
this.activeMoles.push(holeIndex);
// Timer pour faire disparaître la taupe
hole.timer = setTimeout(() => {
this.deactivateMole(holeIndex);
}, this.moleAppearTime);
}
deactivateMole(holeIndex) {
const hole = this.holes[holeIndex];
if (!hole.isActive) return;
hole.isActive = false;
hole.word = null;
hole.wordElement.textContent = '';
hole.mole.classList.remove('active');
if (hole.timer) {
clearTimeout(hole.timer);
hole.timer = null;
}
// Retirer de la liste des taupes actives
const activeIndex = this.activeMoles.indexOf(holeIndex);
if (activeIndex > -1) {
this.activeMoles.splice(activeIndex, 1);
}
}
hitMole(holeIndex) {
if (!this.isRunning) return;
const hole = this.holes[holeIndex];
if (!hole.isActive || !hole.word) return;
const isCorrect = hole.word.french === this.targetWord.french;
if (isCorrect) {
// Bonne réponse
this.score += 10;
this.deactivateMole(holeIndex);
this.setNewTarget();
this.showScorePopup(holeIndex, '+10', true);
this.showFeedback(`Well done! Now find: "${this.targetWord.french}"`, 'success');
// Success animation
hole.mole.classList.add('hit');
setTimeout(() => hole.mole.classList.remove('hit'), 500);
} else {
// Wrong answer
this.errors++;
this.score = Math.max(0, this.score - 2);
this.showScorePopup(holeIndex, '-2', false);
this.showFeedback(`Oops! "${hole.word.french}" ≠ "${this.targetWord.french}"`, 'error');
}
this.updateUI();
this.onScoreUpdate(this.score);
// Check game end by errors
if (this.errors >= this.maxErrors) {
this.showFeedback('Too many errors! Game over.', 'error');
setTimeout(() => {
if (this.isRunning) { // Check if game is still running
this.stop();
}
}, 1500);
}
}
setNewTarget() {
// Choisir un nouveau mot cible
const availableWords = this.vocabulary.filter(word =>
!this.activeMoles.some(moleIndex =>
this.holes[moleIndex].word &&
this.holes[moleIndex].word.english === word.english
)
);
if (availableWords.length > 0) {
this.targetWord = availableWords[Math.floor(Math.random() * availableWords.length)];
} else {
this.targetWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)];
}
// Reset du compteur pour le nouveau mot cible
this.spawnsSinceTarget = 0;
console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`);
document.getElementById('target-word').textContent = this.targetWord.french;
}
getRandomWord() {
return this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)];
}
hideAllMoles() {
this.holes.forEach((hole, index) => {
if (hole.isActive) {
this.deactivateMole(index);
}
});
this.activeMoles = [];
}
showScorePopup(holeIndex, scoreText, isPositive) {
const hole = this.holes[holeIndex];
const popup = document.createElement('div');
popup.className = `score-popup ${isPositive ? 'correct-answer' : 'wrong-answer'}`;
popup.textContent = scoreText;
const rect = hole.element.getBoundingClientRect();
popup.style.left = rect.left + rect.width / 2 + 'px';
popup.style.top = rect.top + 'px';
document.body.appendChild(popup);
setTimeout(() => {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
}, 1000);
}
showFeedback(message, type = 'info') {
const feedbackArea = document.getElementById('feedback-area');
feedbackArea.innerHTML = `${message}
`;
}
updateUI() {
document.getElementById('time-left').textContent = this.timeLeft;
document.getElementById('errors-count').textContent = this.errors;
}
extractVocabulary(content) {
let vocabulary = [];
console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu');
// Priorité 1: Utiliser le contenu brut du module (format simple)
if (content.rawContent) {
console.log('📦 Utilisation du contenu brut du module');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Priorité 2: Format simple avec vocabulary object (nouveau format préféré)
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
console.log('✨ Format simple détecté (vocabulary object)');
vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({
english: english,
french: translation.split(';')[0], // Prendre la première traduction si plusieurs
chinese: translation, // Garder la traduction complète en chinois
category: 'general'
}));
}
// Priorité 3: Format legacy avec vocabulary array
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
console.log('📚 Format legacy détecté (vocabulary array)');
vocabulary = content.vocabulary.filter(word => word.english && word.french);
}
// Priorité 4: Format moderne avec contentItems
else if (content.contentItems && Array.isArray(content.contentItems)) {
console.log('🆕 Format contentItems détecté');
vocabulary = content.contentItems
.filter(item => item.type === 'vocabulary' && item.english && item.french)
.map(item => ({
english: item.english,
french: item.french,
image: item.image || null,
category: item.category || 'general'
}));
}
return this.finalizeVocabulary(vocabulary);
}
extractVocabularyFromRaw(rawContent) {
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
let vocabulary = [];
// Format simple avec vocabulary object (PRÉFÉRÉ)
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({
english: english,
french: translation.split(';')[0], // Première traduction pour le français
chinese: translation, // Traduction complète en chinois
category: 'general'
}));
console.log(`✨ ${vocabulary.length} mots extraits depuis vocabulary object (format simple)`);
}
// Format legacy (vocabulary array)
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
vocabulary = rawContent.vocabulary.filter(word => word.english && word.french);
console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`);
}
// Format contentItems (ancien format complexe)
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
vocabulary = rawContent.contentItems
.filter(item => item.type === 'vocabulary' && item.english && item.french)
.map(item => ({
english: item.english,
french: item.french,
image: item.image || null,
category: item.category || 'general'
}));
console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`);
}
// Fallback
else {
console.warn('⚠️ Format de contenu brut non reconnu');
}
return this.finalizeVocabulary(vocabulary);
}
finalizeVocabulary(vocabulary) {
// Validation et nettoyage
vocabulary = vocabulary.filter(word =>
word &&
typeof word.english === 'string' &&
typeof word.french === 'string' &&
word.english.trim() !== '' &&
word.french.trim() !== ''
);
if (vocabulary.length === 0) {
console.error('❌ Aucun vocabulaire valide trouvé');
// Vocabulaire de démonstration en dernier recours
vocabulary = [
{ english: 'hello', french: 'bonjour', category: 'greetings' },
{ english: 'goodbye', french: 'au revoir', category: 'greetings' },
{ english: 'thank you', french: 'merci', category: 'greetings' },
{ english: 'cat', french: 'chat', category: 'animals' },
{ english: 'dog', french: 'chien', category: 'animals' }
];
console.warn('🚨 Utilisation du vocabulaire de démonstration');
}
console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`);
return this.shuffleArray(vocabulary);
}
shuffleArray(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;
}
destroy() {
this.stop();
this.container.innerHTML = '';
}
}
// Enregistrement du module
window.GameModules = window.GameModules || {};
window.GameModules.WhackAMoleHard = WhackAMoleHardGame;