- Complete SPA architecture with dynamic module loading - 9 different educational games (whack-a-mole, memory, quiz, etc.) - Rich content system supporting multimedia (audio, images, video) - Chinese study mode with character recognition - Adaptive game system based on available content - Content types: vocabulary, grammar, poems, fill-blanks, corrections - AI-powered text evaluation for open-ended answers - Flexible content schema with backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
419 lines
16 KiB
JavaScript
419 lines
16 KiB
JavaScript
// === MODULE FILL THE BLANK ===
|
|
|
|
class FillTheBlankGame {
|
|
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.currentSentenceIndex = 0;
|
|
this.isRunning = false;
|
|
|
|
// Données de jeu
|
|
this.sentences = this.extractSentences(this.content);
|
|
this.currentSentence = null;
|
|
this.blanks = [];
|
|
this.userAnswers = [];
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Vérifier que nous avons des phrases
|
|
if (!this.sentences || this.sentences.length === 0) {
|
|
console.error('Aucune phrase disponible pour Fill the Blank');
|
|
this.showInitError();
|
|
return;
|
|
}
|
|
|
|
this.createGameBoard();
|
|
this.setupEventListeners();
|
|
// Le jeu démarrera quand start() sera appelé
|
|
}
|
|
|
|
showInitError() {
|
|
this.container.innerHTML = `
|
|
<div class="game-error">
|
|
<h3>❌ Erreur de chargement</h3>
|
|
<p>Ce contenu ne contient pas de phrases compatibles avec Fill the Blank.</p>
|
|
<p>Le jeu nécessite des phrases avec leurs traductions.</p>
|
|
<button onclick="AppNavigation.goBack()" class="back-btn">← Retour</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
extractSentences(content) {
|
|
let sentences = [];
|
|
|
|
console.log('🔍 Extraction phrases depuis:', content?.name || 'contenu');
|
|
|
|
// Utiliser le contenu brut du module si disponible
|
|
if (content.rawContent) {
|
|
console.log('📦 Utilisation du contenu brut du module');
|
|
return this.extractSentencesFromRaw(content.rawContent);
|
|
}
|
|
|
|
// Format avec sentences array
|
|
if (content.sentences && Array.isArray(content.sentences)) {
|
|
console.log('📝 Format sentences détecté');
|
|
sentences = content.sentences.filter(sentence =>
|
|
sentence.english && sentence.english.trim() !== ''
|
|
);
|
|
}
|
|
// Format moderne avec contentItems
|
|
else if (content.contentItems && Array.isArray(content.contentItems)) {
|
|
console.log('🆕 Format contentItems détecté');
|
|
sentences = content.contentItems
|
|
.filter(item => item.type === 'sentence' && item.english)
|
|
.map(item => ({
|
|
english: item.english,
|
|
french: item.french || item.translation,
|
|
chinese: item.chinese
|
|
}));
|
|
}
|
|
|
|
return this.finalizeSentences(sentences);
|
|
}
|
|
|
|
extractSentencesFromRaw(rawContent) {
|
|
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
|
|
let sentences = [];
|
|
|
|
// Format simple (sentences array)
|
|
if (rawContent.sentences && Array.isArray(rawContent.sentences)) {
|
|
sentences = rawContent.sentences.filter(sentence =>
|
|
sentence.english && sentence.english.trim() !== ''
|
|
);
|
|
console.log(`📝 ${sentences.length} phrases extraites depuis sentences array`);
|
|
}
|
|
// Format contentItems
|
|
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
|
|
sentences = rawContent.contentItems
|
|
.filter(item => item.type === 'sentence' && item.english)
|
|
.map(item => ({
|
|
english: item.english,
|
|
french: item.french || item.translation,
|
|
chinese: item.chinese
|
|
}));
|
|
console.log(`🆕 ${sentences.length} phrases extraites depuis contentItems`);
|
|
}
|
|
|
|
return this.finalizeSentences(sentences);
|
|
}
|
|
|
|
finalizeSentences(sentences) {
|
|
// Validation et nettoyage
|
|
sentences = sentences.filter(sentence =>
|
|
sentence &&
|
|
typeof sentence.english === 'string' &&
|
|
sentence.english.trim() !== '' &&
|
|
sentence.english.split(' ').length >= 3 // Au moins 3 mots pour créer des blanks
|
|
);
|
|
|
|
if (sentences.length === 0) {
|
|
console.error('❌ Aucune phrase valide trouvée');
|
|
// Phrases de démonstration en dernier recours
|
|
sentences = [
|
|
{ english: "I am learning English.", chinese: "我正在学英语。" },
|
|
{ english: "She goes to school every day.", chinese: "她每天都去学校。" },
|
|
{ english: "We like to play games together.", chinese: "我们喜欢一起玩游戏。" }
|
|
];
|
|
console.warn('🚨 Utilisation de phrases de démonstration');
|
|
}
|
|
|
|
// Mélanger les phrases
|
|
sentences = this.shuffleArray(sentences);
|
|
|
|
console.log(`✅ Fill the Blank: ${sentences.length} phrases finalisées`);
|
|
return sentences;
|
|
}
|
|
|
|
createGameBoard() {
|
|
this.container.innerHTML = `
|
|
<div class="fill-blank-wrapper">
|
|
<!-- Game Info -->
|
|
<div class="game-info">
|
|
<div class="game-stats">
|
|
<div class="stat-item">
|
|
<span class="stat-value" id="current-question">${this.currentSentenceIndex + 1}</span>
|
|
<span class="stat-label">/ ${this.sentences.length}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value" id="errors-count">${this.errors}</span>
|
|
<span class="stat-label">Erreurs</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value" id="score-display">${this.score}</span>
|
|
<span class="stat-label">Score</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Translation hint -->
|
|
<div class="translation-hint" id="translation-hint">
|
|
<!-- La traduction apparaîtra ici -->
|
|
</div>
|
|
|
|
<!-- Sentence with blanks -->
|
|
<div class="sentence-container" id="sentence-container">
|
|
<!-- La phrase avec les blanks apparaîtra ici -->
|
|
</div>
|
|
|
|
<!-- Input area -->
|
|
<div class="input-area" id="input-area">
|
|
<!-- Les inputs apparaîtront ici -->
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="game-controls">
|
|
<button class="control-btn secondary" id="hint-btn">💡 Indice</button>
|
|
<button class="control-btn primary" id="check-btn">✓ Vérifier</button>
|
|
<button class="control-btn secondary" id="skip-btn">→ Suivant</button>
|
|
</div>
|
|
|
|
<!-- Feedback Area -->
|
|
<div class="feedback-area" id="feedback-area">
|
|
<div class="instruction">
|
|
Complète la phrase en remplissant les blancs !
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
document.getElementById('check-btn').addEventListener('click', () => this.checkAnswer());
|
|
document.getElementById('hint-btn').addEventListener('click', () => this.showHint());
|
|
document.getElementById('skip-btn').addEventListener('click', () => this.skipSentence());
|
|
|
|
// Enter key to check answer
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && this.isRunning) {
|
|
this.checkAnswer();
|
|
}
|
|
});
|
|
}
|
|
|
|
start() {
|
|
console.log('🎮 Fill the Blank: Démarrage du jeu');
|
|
this.loadNextSentence();
|
|
}
|
|
|
|
restart() {
|
|
console.log('🔄 Fill the Blank: Redémarrage du jeu');
|
|
this.reset();
|
|
this.start();
|
|
}
|
|
|
|
reset() {
|
|
this.score = 0;
|
|
this.errors = 0;
|
|
this.currentSentenceIndex = 0;
|
|
this.isRunning = false;
|
|
this.currentSentence = null;
|
|
this.blanks = [];
|
|
this.userAnswers = [];
|
|
this.onScoreUpdate(0);
|
|
}
|
|
|
|
loadNextSentence() {
|
|
// Si on a fini toutes les phrases, recommencer depuis le début
|
|
if (this.currentSentenceIndex >= this.sentences.length) {
|
|
this.currentSentenceIndex = 0;
|
|
this.sentences = this.shuffleArray(this.sentences); // Mélanger à nouveau
|
|
this.showFeedback(`🎉 Toutes les phrases terminées ! On recommence avec un nouvel ordre.`, 'success');
|
|
setTimeout(() => {
|
|
this.loadNextSentence();
|
|
}, 1500);
|
|
return;
|
|
}
|
|
|
|
this.isRunning = true;
|
|
this.currentSentence = this.sentences[this.currentSentenceIndex];
|
|
this.createBlanks();
|
|
this.displaySentence();
|
|
this.updateUI();
|
|
}
|
|
|
|
createBlanks() {
|
|
const words = this.currentSentence.english.split(' ');
|
|
this.blanks = [];
|
|
|
|
// Créer 1-3 blanks selon la longueur de la phrase
|
|
const numBlanks = Math.min(Math.max(1, Math.floor(words.length / 4)), 3);
|
|
const blankIndices = new Set();
|
|
|
|
// Sélectionner des mots aléatoires (pas les articles/prépositions courtes)
|
|
const candidateWords = words.map((word, index) => ({ word, index }))
|
|
.filter(item => item.word.length > 2 && !['the', 'and', 'but', 'for', 'nor', 'or', 'so', 'yet'].includes(item.word.toLowerCase()));
|
|
|
|
// Si pas assez de candidats, prendre n'importe quels mots
|
|
if (candidateWords.length < numBlanks) {
|
|
candidateWords = words.map((word, index) => ({ word, index }));
|
|
}
|
|
|
|
// Sélectionner aléatoirement les indices des blanks
|
|
const shuffledCandidates = this.shuffleArray(candidateWords);
|
|
for (let i = 0; i < Math.min(numBlanks, shuffledCandidates.length); i++) {
|
|
blankIndices.add(shuffledCandidates[i].index);
|
|
}
|
|
|
|
// Créer la structure des blanks
|
|
words.forEach((word, index) => {
|
|
if (blankIndices.has(index)) {
|
|
this.blanks.push({
|
|
index: index,
|
|
word: word.replace(/[.,!?;:]$/, ''), // Retirer la ponctuation
|
|
punctuation: word.match(/[.,!?;:]$/) ? word.match(/[.,!?;:]$/)[0] : '',
|
|
userAnswer: ''
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
displaySentence() {
|
|
const words = this.currentSentence.english.split(' ');
|
|
let sentenceHTML = '';
|
|
let blankCounter = 0;
|
|
|
|
words.forEach((word, index) => {
|
|
const blank = this.blanks.find(b => b.index === index);
|
|
if (blank) {
|
|
sentenceHTML += `<span class="blank-wrapper">
|
|
<input type="text" class="blank-input"
|
|
id="blank-${blankCounter}"
|
|
placeholder="___"
|
|
maxlength="${blank.word.length + 2}">
|
|
${blank.punctuation}
|
|
</span> `;
|
|
blankCounter++;
|
|
} else {
|
|
sentenceHTML += `<span class="word">${word}</span> `;
|
|
}
|
|
});
|
|
|
|
document.getElementById('sentence-container').innerHTML = sentenceHTML;
|
|
|
|
// Afficher la traduction si disponible
|
|
const translation = this.currentSentence.chinese || this.currentSentence.french || '';
|
|
document.getElementById('translation-hint').innerHTML = translation ?
|
|
`<em>💭 ${translation}</em>` : '';
|
|
|
|
// Focus sur le premier input
|
|
const firstInput = document.getElementById('blank-0');
|
|
if (firstInput) {
|
|
setTimeout(() => firstInput.focus(), 100);
|
|
}
|
|
}
|
|
|
|
checkAnswer() {
|
|
if (!this.isRunning) return;
|
|
|
|
let allCorrect = true;
|
|
let correctCount = 0;
|
|
|
|
// Vérifier chaque blank
|
|
this.blanks.forEach((blank, index) => {
|
|
const input = document.getElementById(`blank-${index}`);
|
|
const userAnswer = input.value.trim().toLowerCase();
|
|
const correctAnswer = blank.word.toLowerCase();
|
|
|
|
blank.userAnswer = input.value.trim();
|
|
|
|
if (userAnswer === correctAnswer) {
|
|
input.classList.remove('incorrect');
|
|
input.classList.add('correct');
|
|
correctCount++;
|
|
} else {
|
|
input.classList.remove('correct');
|
|
input.classList.add('incorrect');
|
|
allCorrect = false;
|
|
}
|
|
});
|
|
|
|
if (allCorrect) {
|
|
// Toutes les réponses sont correctes
|
|
this.score += 10 * this.blanks.length;
|
|
this.showFeedback(`🎉 Parfait ! +${10 * this.blanks.length} points`, 'success');
|
|
setTimeout(() => {
|
|
this.currentSentenceIndex++;
|
|
this.loadNextSentence();
|
|
}, 1500);
|
|
} else {
|
|
// Quelques erreurs
|
|
this.errors++;
|
|
if (correctCount > 0) {
|
|
this.score += 5 * correctCount;
|
|
this.showFeedback(`✨ ${correctCount}/${this.blanks.length} correct ! +${5 * correctCount} points. Essaye encore.`, 'partial');
|
|
} else {
|
|
this.showFeedback(`❌ Essaye encore ! (${this.errors} erreurs)`, 'error');
|
|
}
|
|
}
|
|
|
|
this.updateUI();
|
|
this.onScoreUpdate(this.score);
|
|
}
|
|
|
|
showHint() {
|
|
// Afficher la première lettre de chaque blank vide
|
|
this.blanks.forEach((blank, index) => {
|
|
const input = document.getElementById(`blank-${index}`);
|
|
if (!input.value.trim()) {
|
|
input.value = blank.word[0];
|
|
input.focus();
|
|
}
|
|
});
|
|
|
|
this.showFeedback('💡 Première lettre ajoutée !', 'info');
|
|
}
|
|
|
|
skipSentence() {
|
|
// Révéler les bonnes réponses
|
|
this.blanks.forEach((blank, index) => {
|
|
const input = document.getElementById(`blank-${index}`);
|
|
input.value = blank.word;
|
|
input.classList.add('revealed');
|
|
});
|
|
|
|
this.showFeedback('📖 Réponses révélées ! Phrase suivante...', 'info');
|
|
setTimeout(() => {
|
|
this.currentSentenceIndex++;
|
|
this.loadNextSentence();
|
|
}, 2000);
|
|
}
|
|
|
|
// Méthode endGame supprimée - le jeu continue indéfiniment
|
|
|
|
showFeedback(message, type = 'info') {
|
|
const feedbackArea = document.getElementById('feedback-area');
|
|
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
|
}
|
|
|
|
updateUI() {
|
|
document.getElementById('current-question').textContent = this.currentSentenceIndex + 1;
|
|
document.getElementById('errors-count').textContent = this.errors;
|
|
document.getElementById('score-display').textContent = this.score;
|
|
}
|
|
|
|
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.isRunning = false;
|
|
this.container.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Enregistrement du module
|
|
window.GameModules = window.GameModules || {};
|
|
window.GameModules.FillTheBlank = FillTheBlankGame; |