- Enhanced Story Reader with text-to-story conversion methods - Added support for simple texts and sentences in Story Reader - Removed Text Reader game file (js/games/text-reader.js) - Updated all configuration files to remove Text Reader references - Modified game compatibility system to use Story Reader instead - Updated test fixtures to reflect game changes - Cleaned up debug/test HTML files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
685 lines
24 KiB
JavaScript
685 lines
24 KiB
JavaScript
// === MODULE WHACK-A-MOLE ===
|
||
|
||
class WhackAMoleGame {
|
||
constructor(options) {
|
||
this.container = options.container;
|
||
this.content = options.content;
|
||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||
this.onGameEnd = options.onGameEnd || (() => {});
|
||
|
||
// Game state
|
||
this.score = 0;
|
||
this.errors = 0;
|
||
this.maxErrors = 3;
|
||
this.gameTime = 60; // 60 secondes
|
||
this.timeLeft = this.gameTime;
|
||
this.isRunning = false;
|
||
this.gameMode = 'translation'; // 'translation', 'image', 'sound'
|
||
this.showPronunciation = false; // Track pronunciation display state
|
||
|
||
// Mole configuration
|
||
this.holes = [];
|
||
this.activeMoles = [];
|
||
this.moleAppearTime = 2000; // 2 seconds display time
|
||
this.spawnRate = 1500; // New mole every 1.5 seconds
|
||
|
||
// Timers
|
||
this.gameTimer = null;
|
||
this.spawnTimer = null;
|
||
|
||
// Vocabulary for this game - adapted for the new system
|
||
this.vocabulary = this.extractVocabulary(this.content);
|
||
this.currentWords = [];
|
||
this.targetWord = null;
|
||
|
||
// Target word guarantee system
|
||
this.spawnsSinceTarget = 0;
|
||
this.maxSpawnsWithoutTarget = 3; // Target word must appear in the next 3 moles
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
// Check that we have vocabulary
|
||
if (!this.vocabulary || this.vocabulary.length === 0) {
|
||
logSh('No vocabulary available for Whack-a-Mole', 'ERROR');
|
||
this.showInitError();
|
||
return;
|
||
}
|
||
|
||
this.createGameBoard();
|
||
this.createGameUI();
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
showInitError() {
|
||
this.container.innerHTML = `
|
||
<div class="game-error">
|
||
<h3>❌ Loading Error</h3>
|
||
<p>This content does not contain vocabulary compatible with Whack-a-Mole.</p>
|
||
<p>The game requires words with their translations.</p>
|
||
<button onclick="AppNavigation.navigateTo('games')" class="back-btn">← Back</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="pronunciation-btn" title="Toggle pronunciation">🔊 Pronunciation</button>
|
||
<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" id="game-board">
|
||
<!-- Holes will be generated here -->
|
||
</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 < 9; i++) {
|
||
const hole = document.createElement('div');
|
||
hole.className = 'whack-hole';
|
||
hole.dataset.holeId = i;
|
||
|
||
hole.innerHTML = `
|
||
<div class="whack-mole" data-hole="${i}">
|
||
<div class="pronunciation" style="display: none; font-size: 0.8em; color: #2563eb; font-style: italic; margin-bottom: 5px; font-weight: 500;"></div>
|
||
<div class="word"></div>
|
||
</div>
|
||
`;
|
||
|
||
gameBoard.appendChild(hole);
|
||
this.holes.push({
|
||
element: hole,
|
||
mole: hole.querySelector('.whack-mole'),
|
||
wordElement: hole.querySelector('.word'),
|
||
pronunciationElement: hole.querySelector('.pronunciation'),
|
||
isActive: false,
|
||
word: null,
|
||
timer: null
|
||
});
|
||
}
|
||
}
|
||
|
||
createGameUI() {
|
||
// UI elements are already created in 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('pronunciation-btn').addEventListener('click', () => this.togglePronunciation());
|
||
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.translation}"`, '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(); // Stop without triggering game end
|
||
this.resetGame();
|
||
setTimeout(() => this.start(), 100);
|
||
}
|
||
|
||
togglePronunciation() {
|
||
this.showPronunciation = !this.showPronunciation;
|
||
const btn = document.getElementById('pronunciation-btn');
|
||
|
||
if (this.showPronunciation) {
|
||
btn.textContent = '🔊 Pronunciation ON';
|
||
btn.classList.add('active');
|
||
} else {
|
||
btn.textContent = '🔊 Pronunciation OFF';
|
||
btn.classList.remove('active');
|
||
}
|
||
|
||
// Update currently visible moles
|
||
this.updateMoleDisplay();
|
||
}
|
||
|
||
updateMoleDisplay() {
|
||
// Update pronunciation display for all active moles
|
||
this.holes.forEach(hole => {
|
||
if (hole.isActive && hole.word) {
|
||
if (this.showPronunciation && hole.word.pronunciation) {
|
||
hole.pronunciationElement.textContent = hole.word.pronunciation;
|
||
hole.pronunciationElement.style.display = 'block';
|
||
} else {
|
||
hole.pronunciationElement.style.display = 'none';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
stop() {
|
||
this.stopWithoutEnd();
|
||
this.onGameEnd(this.score); // Trigger game end only here
|
||
}
|
||
|
||
stopWithoutEnd() {
|
||
this.isRunning = false;
|
||
this.stopTimers();
|
||
this.hideAllMoles();
|
||
|
||
document.getElementById('start-btn').disabled = false;
|
||
document.getElementById('pause-btn').disabled = true;
|
||
}
|
||
|
||
resetGame() {
|
||
// Ensure everything is completely stopped
|
||
this.stopWithoutEnd();
|
||
|
||
// Reset all state variables
|
||
this.score = 0;
|
||
this.errors = 0;
|
||
this.timeLeft = this.gameTime;
|
||
this.isRunning = false;
|
||
this.targetWord = null;
|
||
this.activeMoles = [];
|
||
this.spawnsSinceTarget = 0; // Reset guarantee counter
|
||
|
||
// Ensure all timers are properly stopped
|
||
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 with verification
|
||
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.pronunciationElement) {
|
||
hole.pronunciationElement.textContent = '';
|
||
hole.pronunciationElement.style.display = 'none';
|
||
}
|
||
if (hole.mole) {
|
||
hole.mole.classList.remove('active', 'hit');
|
||
}
|
||
});
|
||
|
||
logSh('🔄 Game completely reset', 'INFO');
|
||
}
|
||
|
||
startTimers() {
|
||
// Main game timer
|
||
this.gameTimer = setInterval(() => {
|
||
this.timeLeft--;
|
||
this.updateUI();
|
||
|
||
if (this.timeLeft <= 0 && this.isRunning) {
|
||
this.stop();
|
||
}
|
||
}, 1000);
|
||
|
||
// Mole spawn timer
|
||
this.spawnTimer = setInterval(() => {
|
||
if (this.isRunning) {
|
||
this.spawnMole();
|
||
}
|
||
}, this.spawnRate);
|
||
|
||
// First immediate mole
|
||
setTimeout(() => this.spawnMole(), 500);
|
||
}
|
||
|
||
stopTimers() {
|
||
if (this.gameTimer) {
|
||
clearInterval(this.gameTimer);
|
||
this.gameTimer = null;
|
||
}
|
||
if (this.spawnTimer) {
|
||
clearInterval(this.spawnTimer);
|
||
this.spawnTimer = null;
|
||
}
|
||
}
|
||
|
||
spawnMole() {
|
||
// Find a free hole
|
||
const availableHoles = this.holes.filter(hole => !hole.isActive);
|
||
if (availableHoles.length === 0) return;
|
||
|
||
const randomHole = availableHoles[Math.floor(Math.random() * availableHoles.length)];
|
||
const holeIndex = this.holes.indexOf(randomHole);
|
||
|
||
// Choose a word according to guarantee strategy
|
||
const word = this.getWordWithTargetGuarantee();
|
||
|
||
// Activate the mole
|
||
this.activateMole(holeIndex, word);
|
||
}
|
||
|
||
getWordWithTargetGuarantee() {
|
||
// Increment spawn counter since last target word
|
||
this.spawnsSinceTarget++;
|
||
|
||
// If we've reached the limit, force the target word
|
||
if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) {
|
||
logSh(`🎯 Forced target word spawn after ${this.spawnsSinceTarget} attempts`, 'INFO');
|
||
this.spawnsSinceTarget = 0;
|
||
return this.targetWord;
|
||
}
|
||
|
||
// Otherwise, 50% chance for target word, 50% random word
|
||
if (Math.random() < 0.5) {
|
||
logSh('🎯 Natural target word spawn', '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.original;
|
||
|
||
// Show pronunciation if enabled and available
|
||
if (this.showPronunciation && word.pronunciation) {
|
||
hole.pronunciationElement.textContent = word.pronunciation;
|
||
hole.pronunciationElement.style.display = 'block';
|
||
} else {
|
||
hole.pronunciationElement.style.display = 'none';
|
||
}
|
||
|
||
hole.mole.classList.add('active');
|
||
|
||
// Add to active moles list
|
||
this.activeMoles.push(holeIndex);
|
||
|
||
// Timer to make the mole disappear
|
||
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.pronunciationElement.textContent = '';
|
||
hole.pronunciationElement.style.display = 'none';
|
||
hole.mole.classList.remove('active');
|
||
|
||
if (hole.timer) {
|
||
clearTimeout(hole.timer);
|
||
hole.timer = null;
|
||
}
|
||
|
||
// Remove from active moles list
|
||
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.translation === this.targetWord.translation;
|
||
|
||
if (isCorrect) {
|
||
// Correct answer
|
||
this.score += 10;
|
||
this.deactivateMole(holeIndex);
|
||
this.setNewTarget();
|
||
this.showScorePopup(holeIndex, '+10', true);
|
||
this.showFeedback(`Well done! Now find: "${this.targetWord.translation}"`, '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.translation}" ≠ "${this.targetWord.translation}"`, '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() {
|
||
// Choose a new target word
|
||
const availableWords = this.vocabulary.filter(word =>
|
||
!this.activeMoles.some(moleIndex =>
|
||
this.holes[moleIndex].word &&
|
||
this.holes[moleIndex].word.original === word.original
|
||
)
|
||
);
|
||
|
||
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 counter for new target word
|
||
this.spawnsSinceTarget = 0;
|
||
logSh(`🎯 New target word: ${this.targetWord.original} -> ${this.targetWord.translation}`, 'INFO');
|
||
|
||
document.getElementById('target-word').textContent = this.targetWord.translation;
|
||
}
|
||
|
||
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('🔍 Extracting vocabulary from:', content?.name || 'content', 'INFO');
|
||
|
||
// Priority 1: Use raw module content (simple format)
|
||
if (content.rawContent) {
|
||
logSh('📦 Using raw module content', 'INFO');
|
||
return this.extractVocabularyFromRaw(content.rawContent);
|
||
}
|
||
|
||
// Priority 2: 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 and new centralized vocabulary format
|
||
if (typeof data === 'object' && (data.user_language || data.translation)) {
|
||
const translationText = data.user_language || data.translation;
|
||
return {
|
||
original: word, // Clé = original_language
|
||
translation: translationText.split(';')[0], // First translation
|
||
fullTranslation: translationText, // 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 and new centralized vocabulary format
|
||
if (typeof data === 'object' && (data.user_language || data.translation)) {
|
||
const translationText = data.user_language || data.translation;
|
||
return {
|
||
original: word, // Clé = original_language
|
||
translation: translationText.split(';')[0], // First translation
|
||
fullTranslation: translationText, // 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) {
|
||
// Validation and cleanup for ultra-modular format
|
||
vocabulary = vocabulary.filter(word =>
|
||
word &&
|
||
typeof word.original === 'string' &&
|
||
typeof word.translation === 'string' &&
|
||
word.original.trim() !== '' &&
|
||
word.translation.trim() !== ''
|
||
);
|
||
|
||
if (vocabulary.length === 0) {
|
||
logSh('❌ No valid vocabulary found', 'ERROR');
|
||
// Demo vocabulary as last resort
|
||
vocabulary = [
|
||
{ original: 'hello', translation: 'bonjour', category: 'greetings' },
|
||
{ original: 'goodbye', translation: 'au revoir', category: 'greetings' },
|
||
{ original: 'thank you', translation: 'merci', category: 'greetings' },
|
||
{ original: 'cat', translation: 'chat', category: 'animals' },
|
||
{ original: 'dog', translation: 'chien', category: 'animals' }
|
||
];
|
||
logSh('🚨 Using demo vocabulary', 'WARN');
|
||
}
|
||
|
||
logSh(`✅ Whack-a-Mole: ${vocabulary.length} vocabulary words finalized`, '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 = '';
|
||
}
|
||
}
|
||
|
||
// Module registration
|
||
window.GameModules = window.GameModules || {};
|
||
window.GameModules.WhackAMole = WhackAMoleGame; |