Class_generator/js/games/fill-the-blank.js
StillHammer 30a2028da6 Remove Text Reader game and enhance Story Reader
- 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>
2025-09-20 11:22:56 +08:00

516 lines
20 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.generateSentencesFromVocabulary();
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;
}
generateSentencesFromVocabulary() {
// Generate sentences based on word types
const nounTemplates = [
{ pattern: 'I see a {word}.', translation: 'Je vois un {translation}.' },
{ pattern: 'The {word} is here.', translation: 'Le {translation} est ici.' },
{ pattern: 'I like the {word}.', translation: 'J\'aime le {translation}.' },
{ pattern: 'Where is the {word}?', translation: 'Où est le {translation}?' },
{ pattern: 'This is a {word}.', translation: 'C\'est un {translation}.' },
{ pattern: 'I have a {word}.', translation: 'J\'ai un {translation}.' }
];
const verbTemplates = [
{ pattern: 'I {word} every day.', translation: 'Je {translation} tous les jours.' },
{ pattern: 'We {word} together.', translation: 'Nous {translation} ensemble.' },
{ pattern: 'They {word} quickly.', translation: 'Ils {translation} rapidement.' },
{ pattern: 'I like to {word}.', translation: 'J\'aime {translation}.' }
];
const adjectiveTemplates = [
{ pattern: 'The cat is {word}.', translation: 'Le chat est {translation}.' },
{ pattern: 'This house is {word}.', translation: 'Cette maison est {translation}.' },
{ pattern: 'I am {word}.', translation: 'Je suis {translation}.' },
{ pattern: 'The weather is {word}.', translation: 'Le temps est {translation}.' }
];
let sentences = [];
// Generate sentences for each vocabulary word based on type
this.vocabulary.forEach(vocab => {
let templates;
// Choose templates based on word type
if (vocab.type === 'verb') {
templates = verbTemplates;
} else if (vocab.type === 'adjective') {
templates = adjectiveTemplates;
} else {
// Default to noun templates for nouns and unknown types
templates = nounTemplates;
}
const template = templates[Math.floor(Math.random() * templates.length)];
const sentence = {
original: template.pattern.replace('{word}', vocab.original),
translation: template.translation.replace('{translation}', vocab.translation),
targetWord: vocab.original,
wordType: vocab.type || 'noun'
};
// Ensure sentence has at least 3 words for blanks
if (sentence.original.split(' ').length >= 3) {
sentences.push(sentence);
}
});
// Shuffle and limit sentences
sentences = this.shuffleArray(sentences);
logSh(`✅ Generated ${sentences.length} sentences from vocabulary`, 'INFO');
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">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-3 blanks depending on sentence length
const numBlanks = Math.min(Math.max(1, Math.floor(words.length / 4)), 3);
const blankIndices = new Set();
// Select random words (not articles/short prepositions)
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()));
// If not enough candidates, take any words
if (candidateWords.length < numBlanks) {
candidateWords = words.map((word, index) => ({ word, index }));
}
// Randomly select blank indices
const shuffledCandidates = this.shuffleArray(candidateWords);
for (let i = 0; i < Math.min(numBlanks, shuffledCandidates.length); i++) {
blankIndices.add(shuffledCandidates[i].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;