Class_generator/js/games/whack-a-mole-hard.js
StillHammer 1f8688c4aa Fix WebSocket logging system and add comprehensive network features
- Fix WebSocket server to properly broadcast logs to all connected clients
- Integrate professional logging system with real-time WebSocket interface
- Add network status indicator with DigitalOcean Spaces connectivity
- Implement AWS Signature V4 authentication for private bucket access
- Add JSON content loader with backward compatibility to JS modules
- Restore navigation breadcrumb system with comprehensive logging
- Add multiple content formats: JSON + JS with automatic discovery
- Enhance top bar with logger toggle and network status indicator
- Remove deprecated temp-games module and clean up unused files

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 23:05:14 +08:00

644 lines
23 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 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) {
logSh('Aucun vocabulaire disponible pour Whack-a-Mole', 'ERROR');
this.showInitError();
return;
}
this.createGameBoard();
this.createGameUI();
this.setupEventListeners();
}
showInitError() {
this.container.innerHTML = `
<div class="game-error">
<h3>❌ Erreur de chargement</h3>
<p>Ce contenu ne contient pas de vocabulaire compatible avec Whack-a-Mole.</p>
<p>Le jeu nécessite des mots avec leurs traductions.</p>
<button onclick="AppNavigation.goBack()" class="back-btn">← Retour</button>
</div>
`;
}
createGameBoard() {
this.container.innerHTML = `
<div class="whack-game-wrapper">
<!-- Mode Selection -->
<div class="mode-selector">
<button class="mode-btn active" data-mode="translation">
🔤 Translation
</button>
<button class="mode-btn" data-mode="image">
🖼️ Image (soon)
</button>
<button class="mode-btn" data-mode="sound">
🔊 Sound (soon)
</button>
</div>
<!-- Game Info -->
<div class="game-info">
<div class="game-stats">
<div class="stat-item">
<span class="stat-value" id="time-left">${this.timeLeft}</span>
<span class="stat-label">Time</span>
</div>
<div class="stat-item">
<span class="stat-value" id="errors-count">${this.errors}</span>
<span class="stat-label">Errors</span>
</div>
<div class="stat-item">
<span class="stat-value" id="target-word">---</span>
<span class="stat-label">Find</span>
</div>
</div>
<div class="game-controls">
<button class="control-btn" id="start-btn">🎮 Start</button>
<button class="control-btn" id="pause-btn" disabled>⏸️ Pause</button>
<button class="control-btn" id="restart-btn">🔄 Restart</button>
</div>
</div>
<!-- Game Board -->
<div class="whack-game-board hard-mode" id="game-board">
<!-- Les trous seront générés ici (5x3 = 15 trous) -->
</div>
<!-- Feedback Area -->
<div class="feedback-area" id="feedback-area">
<div class="instruction">
Select a mode and click Start!
</div>
</div>
</div>
`;
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 = `
<div class="whack-mole" data-hole="${i}">
<div class="word"></div>
</div>
`;
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';
logSh(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words, 'INFO');`);
}
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');
}
});
logSh('🔄 Game completely reset', 'INFO');
}
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) {
logSh(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`, 'INFO');
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) {
logSh('🎯 Spawn naturel du mot cible (1/10, 'INFO');');
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;
logSh(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`, 'INFO');
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 = `<div class="instruction ${type}">${message}</div>`;
}
updateUI() {
document.getElementById('time-left').textContent = this.timeLeft;
document.getElementById('errors-count').textContent = this.errors;
}
extractVocabulary(content) {
let vocabulary = [];
logSh('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu', 'INFO');
// Priorité 1: Utiliser le contenu brut du module (format simple)
if (content.rawContent) {
logSh('📦 Utilisation du contenu brut du module', 'INFO');
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)) {
logSh('✨ Format simple détecté (vocabulary object, 'INFO');');
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)) {
logSh('📚 Format legacy détecté (vocabulary array, 'INFO');');
vocabulary = content.vocabulary.filter(word => word.english && word.french);
}
// Priorité 4: Format moderne avec contentItems
else if (content.contentItems && Array.isArray(content.contentItems)) {
logSh('🆕 Format contentItems détecté', 'INFO');
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) {
logSh('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module', 'INFO');
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'
}));
logSh(`${vocabulary.length} mots extraits depuis vocabulary object (format simple, 'INFO');`);
}
// Format legacy (vocabulary array)
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
vocabulary = rawContent.vocabulary.filter(word => word.english && word.french);
logSh(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`, 'INFO');
}
// 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'
}));
logSh(`📝 ${vocabulary.length} mots extraits depuis contentItems`, 'INFO');
}
// Fallback
else {
logSh('⚠️ Format de contenu brut non reconnu', 'WARN');
}
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) {
logSh('❌ Aucun vocabulaire valide trouvé', 'ERROR');
// 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' }
];
logSh('🚨 Utilisation du vocabulaire de démonstration', 'WARN');
}
logSh(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`, 'INFO');
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;