Class_generator/js/games/fill-the-blank.js
StillHammer 30fb6cd46c Fix Fill the Blank with intelligent word selection and real sentences
🚨 MAJOR FIXES:
- Remove all hardcoded French templates (60+ lines of garbage)
- Replace with real sentence extraction from content
- Support story.chapters, rawContent.story, and sentences arrays
- Universal language support (English/Chinese, not French-only)

🎯 INTELLIGENT WORD SELECTION:
- Priority 1: Words from content vocabulary (educational value)
- Priority 2: Longest words if vocabulary not available
- Max 1-2 blanks per sentence (random) for readability
- Universal logic works for all languages (Chinese, English, etc.)

🔧 TECHNICAL IMPROVEMENTS:
- Clean punctuation before vocabulary matching
- Case-insensitive word comparison
- Proper fallback sentences with correct target language
- Better sentence filtering (min 3 words for blanks)

 RESULT:
- WTA1B1 now shows English sentences with Chinese translations
- Targets vocabulary words (turtle, umbrella, violet, etc.)
- No more "Je vois un..." French garbage
- Works universally for any language content

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 12:51:18 +08:00

569 lines
22 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 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 = `
<div class="game-error">
<h3>❌ Loading Error</h3>
<p>This content does not contain vocabulary compatible with Fill the Blank.</p>
<p>The game requires words with their translations in ultra-modular format.</p>
<button onclick="AppNavigation.navigateTo('games')" class="back-btn">← Back to Games</button>
</div>
`;
}
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 = `
<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">Errors</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">
<!-- Translation will appear here -->
</div>
<!-- Sentence with blanks -->
<div class="sentence-container" id="sentence-container">
<!-- Sentence with blanks will appear here -->
</div>
<!-- Input area -->
<div class="input-area" id="input-area">
<!-- Inputs will appear here -->
</div>
<!-- Controls -->
<div class="game-controls">
<button class="control-btn secondary" id="hint-btn">💡 Hint</button>
<button class="control-btn primary" id="check-btn">✓ Check</button>
<button class="control-btn secondary" id="skip-btn">→ Next</button>
</div>
<!-- Feedback Area -->
<div class="feedback-area" id="feedback-area">
<div class="instruction">
Complete the sentence by filling in the blanks!
</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() {
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 += `<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;
// Display translation if available
const translation = this.currentSentence.translation || '';
document.getElementById('translation-hint').innerHTML = translation ?
`<em>💭 ${translation}</em>` : '';
// 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 = `<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 = '';
}
}
// Module registration
window.GameModules = window.GameModules || {};
window.GameModules.FillTheBlank = FillTheBlankGame;