// === MODULE FILL THE BLANK === class FillTheBlankGame { 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.currentSentenceIndex = 0; this.isRunning = false; // Game data this.vocabulary = this.extractVocabulary(this.content); this.sentences = this.extractRealSentences(); this.currentSentence = null; this.blanks = []; this.userAnswers = []; this.init(); } init() { // Check that we have vocabulary if (!this.vocabulary || this.vocabulary.length === 0) { logSh('No vocabulary available for Fill the Blank', 'ERROR'); this.showInitError(); return; } this.createGameBoard(); this.setupEventListeners(); // The game will start when start() is called } showInitError() { this.container.innerHTML = `

โŒ Loading Error

This content does not contain vocabulary compatible with Fill the Blank.

The game requires words with their translations in ultra-modular format.

`; } extractVocabulary(content) { let vocabulary = []; logSh('๐Ÿ” Extracting vocabulary from:', content?.name || 'content', 'INFO'); // Priority 1: Use raw module content (ultra-modular 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 ONLY if (typeof data === 'object' && data.user_language) { return { original: word, // Clรฉ = original_language translation: data.user_language.split('๏ผ›')[0], // First translation fullTranslation: data.user_language, // 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 ONLY if (typeof data === 'object' && data.user_language) { return { original: word, // Clรฉ = original_language translation: data.user_language.split('๏ผ›')[0], // First translation fullTranslation: data.user_language, // 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' }, { original: 'house', translation: 'maison', category: 'objects' }, { original: 'school', translation: 'รฉcole', category: 'places' }, { original: 'book', translation: 'livre', category: 'objects' } ]; logSh('๐Ÿšจ Using demo vocabulary', 'WARN'); } logSh(`โœ… Fill the Blank: ${vocabulary.length} words finalized`, 'INFO'); return vocabulary; } extractRealSentences() { let sentences = []; logSh('๐Ÿ” Extracting real sentences from content...', 'INFO'); // Priority 1: Extract from story chapters if (this.content.story?.chapters) { this.content.story.chapters.forEach(chapter => { if (chapter.sentences) { chapter.sentences.forEach(sentence => { if (sentence.original && sentence.translation) { sentences.push({ original: sentence.original, translation: sentence.translation, source: 'story' }); } }); } }); } // Priority 2: Extract from rawContent story if (this.content.rawContent?.story?.chapters) { this.content.rawContent.story.chapters.forEach(chapter => { if (chapter.sentences) { chapter.sentences.forEach(sentence => { if (sentence.original && sentence.translation) { sentences.push({ original: sentence.original, translation: sentence.translation, source: 'rawContent.story' }); } }); } }); } // Priority 3: Extract from sentences array const directSentences = this.content.sentences || this.content.rawContent?.sentences; if (directSentences && Array.isArray(directSentences)) { directSentences.forEach(sentence => { if (sentence.english && sentence.chinese) { sentences.push({ original: sentence.english, translation: sentence.chinese, source: 'sentences' }); } else if (sentence.original && sentence.translation) { sentences.push({ original: sentence.original, translation: sentence.translation, source: 'sentences' }); } }); } // Filter sentences that are suitable for fill-the-blank (min 3 words) sentences = sentences.filter(sentence => sentence.original && sentence.original.split(' ').length >= 3 && sentence.original.trim().length > 0 ); // Shuffle and limit sentences = this.shuffleArray(sentences); logSh(`๐Ÿ“ Extracted ${sentences.length} real sentences for fill-the-blank`, 'INFO'); if (sentences.length === 0) { logSh('โŒ No suitable sentences found for fill-the-blank', 'ERROR'); return this.createFallbackSentences(); } return sentences.slice(0, 20); // Limit to 20 sentences max } createFallbackSentences() { // Simple fallback using vocabulary words in basic sentences const fallback = []; this.vocabulary.slice(0, 10).forEach(vocab => { fallback.push({ original: `This is a ${vocab.original}.`, translation: `่ฟ™ๆ˜ฏไธ€ไธช ${vocab.translation}ใ€‚`, source: 'fallback' }); }); return fallback; } createGameBoard() { this.container.innerHTML = `
${this.currentSentenceIndex + 1} / ${this.sentences.length}
${this.errors} Errors
${this.score} Score
Complete the sentence by filling in the blanks!
`; } 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() { logSh('๐ŸŽฎ Fill the Blank: Starting game', 'INFO'); this.loadNextSentence(); } restart() { logSh('๐Ÿ”„ Fill the Blank: Restarting game', 'INFO'); 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() { // If we've finished all sentences, restart from the beginning if (this.currentSentenceIndex >= this.sentences.length) { this.currentSentenceIndex = 0; this.sentences = this.shuffleArray(this.sentences); // Shuffle again this.showFeedback(`๐ŸŽ‰ All sentences completed! Starting over with a new order.`, '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.original.split(' '); this.blanks = []; // Create 1-2 blanks randomly (readable sentences) const numBlanks = Math.random() < 0.5 ? 1 : 2; const blankIndices = new Set(); // PRIORITY 1: Words from vocabulary (educational value) const vocabularyWords = []; const otherWords = []; words.forEach((word, index) => { const cleanWord = word.replace(/[.,!?;:"'()[\]{}\-โ€“โ€”]/g, '').toLowerCase(); const isVocabularyWord = this.vocabulary.some(vocab => vocab.original.toLowerCase() === cleanWord ); if (isVocabularyWord) { vocabularyWords.push({ word, index, priority: 'vocabulary' }); } else { otherWords.push({ word, index, priority: 'other', length: cleanWord.length }); } }); // Select blanks: vocabulary first, then longest words const selectedWords = []; // Take vocabulary words first (shuffled) const shuffledVocab = this.shuffleArray(vocabularyWords); for (let i = 0; i < Math.min(numBlanks, shuffledVocab.length); i++) { selectedWords.push(shuffledVocab[i]); } // If need more blanks, take longest other words if (selectedWords.length < numBlanks) { const sortedOthers = otherWords.sort((a, b) => b.length - a.length); const needed = numBlanks - selectedWords.length; for (let i = 0; i < Math.min(needed, sortedOthers.length); i++) { selectedWords.push(sortedOthers[i]); } } // Add selected indices to blanks selectedWords.forEach(item => blankIndices.add(item.index)); // Create blank structure words.forEach((word, index) => { if (blankIndices.has(index)) { this.blanks.push({ index: index, word: word.replace(/[.,!?;:]$/, ''), // Remove punctuation punctuation: word.match(/[.,!?;:]$/) ? word.match(/[.,!?;:]$/)[0] : '', userAnswer: '' }); } }); } displaySentence() { const words = this.currentSentence.original.split(' '); let sentenceHTML = ''; let blankCounter = 0; words.forEach((word, index) => { const blank = this.blanks.find(b => b.index === index); if (blank) { sentenceHTML += ` ${blank.punctuation} `; blankCounter++; } else { sentenceHTML += `${word} `; } }); document.getElementById('sentence-container').innerHTML = sentenceHTML; // Display translation if available const translation = this.currentSentence.translation || ''; document.getElementById('translation-hint').innerHTML = translation ? `๐Ÿ’ญ ${translation}` : ''; // Focus on first input const firstInput = document.getElementById('blank-0'); if (firstInput) { setTimeout(() => firstInput.focus(), 100); } } checkAnswer() { if (!this.isRunning) return; let allCorrect = true; let correctCount = 0; // Check each 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) { // All answers are correct this.score += 10 * this.blanks.length; this.showFeedback(`๐ŸŽ‰ Perfect! +${10 * this.blanks.length} points`, 'success'); setTimeout(() => { this.currentSentenceIndex++; this.loadNextSentence(); }, 1500); } else { // Some errors this.errors++; if (correctCount > 0) { this.score += 5 * correctCount; this.showFeedback(`โœจ ${correctCount}/${this.blanks.length} correct! +${5 * correctCount} points. Try again.`, 'partial'); } else { this.showFeedback(`โŒ Try again! (${this.errors} errors)`, 'error'); } } this.updateUI(); this.onScoreUpdate(this.score); } showHint() { // Show first letter of each empty blank this.blanks.forEach((blank, index) => { const input = document.getElementById(`blank-${index}`); if (!input.value.trim()) { input.value = blank.word[0]; input.focus(); } }); this.showFeedback('๐Ÿ’ก First letter added!', 'info'); } skipSentence() { // Reveal correct answers this.blanks.forEach((blank, index) => { const input = document.getElementById(`blank-${index}`); input.value = blank.word; input.classList.add('revealed'); }); this.showFeedback('๐Ÿ“– Answers revealed! Next sentence...', 'info'); setTimeout(() => { this.currentSentenceIndex++; this.loadNextSentence(); }, 2000); } // endGame method removed - game continues indefinitely showFeedback(message, type = 'info') { const feedbackArea = document.getElementById('feedback-area'); feedbackArea.innerHTML = `
${message}
`; } 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 = ''; } } // Module registration window.GameModules = window.GameModules || {}; window.GameModules.FillTheBlank = FillTheBlankGame;