Major architectural update to replace fixed 50%/100% scoring with true dynamic percentages based on content volume: • Replace old interpolation system with Math.min(100, (count/optimal)*100) formula • Add embedded compatibility methods to all 14 game modules with static requirements • Remove compatibility cache system for real-time calculation • Fix content loading to pass complete modules with vocabulary (not just metadata) • Clean up duplicate syntax errors in adventure-reader and grammar-discovery • Update navigation.js module mapping to match actual exported class names Examples of new dynamic scoring: - 15 words / 20 optimal = 75% (was 87.5% with old interpolation) - 5 words / 10 minimum = 50% (was 25% with old linear system) - 30 words / 20 optimal = 100% (unchanged) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
791 lines
30 KiB
JavaScript
791 lines
30 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 || (() => {});
|
||
|
||
// 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 = '';
|
||
}
|
||
|
||
// === COMPATIBILITY SYSTEM ===
|
||
static getCompatibilityRequirements() {
|
||
return {
|
||
minimum: {
|
||
vocabulary: 5,
|
||
sentences: 3
|
||
},
|
||
optimal: {
|
||
vocabulary: 12,
|
||
sentences: 8
|
||
},
|
||
name: "Fill the Blank",
|
||
description: "Needs vocabulary and sentences/texts to create meaningful cloze exercises"
|
||
};
|
||
}
|
||
|
||
static checkContentCompatibility(content) {
|
||
const requirements = FillTheBlankGame.getCompatibilityRequirements();
|
||
|
||
// Extract vocabulary and sentences using same method as instance
|
||
const vocabulary = FillTheBlankGame.extractVocabularyStatic(content);
|
||
const sentences = FillTheBlankGame.extractSentencesStatic(content);
|
||
|
||
const vocabCount = vocabulary.length;
|
||
const sentenceCount = sentences.length;
|
||
|
||
// Dynamic percentage based on optimal volumes (5→12 vocab, 3→8 sentences)
|
||
// Vocabulary: 0=0%, 6=50%, 12=100%
|
||
// Sentences: 0=0%, 4=50%, 8=100%
|
||
const vocabScore = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100);
|
||
const sentenceScore = Math.min(100, (sentenceCount / requirements.optimal.sentences) * 100);
|
||
|
||
// Combined score (weighted average: 60% vocabulary, 40% sentences)
|
||
const finalScore = (vocabScore * 0.6) + (sentenceScore * 0.4);
|
||
|
||
const recommendations = [];
|
||
if (vocabCount < requirements.optimal.vocabulary) {
|
||
recommendations.push(`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words`);
|
||
}
|
||
if (sentenceCount < requirements.optimal.sentences) {
|
||
recommendations.push(`Add ${requirements.optimal.sentences - sentenceCount} more sentences/texts`);
|
||
}
|
||
|
||
return {
|
||
score: Math.round(finalScore),
|
||
details: {
|
||
vocabulary: {
|
||
found: vocabCount,
|
||
minimum: requirements.minimum.vocabulary,
|
||
optimal: requirements.optimal.vocabulary,
|
||
status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient'
|
||
},
|
||
sentences: {
|
||
found: sentenceCount,
|
||
minimum: requirements.minimum.sentences,
|
||
optimal: requirements.optimal.sentences,
|
||
status: sentenceCount >= requirements.minimum.sentences ? 'sufficient' : 'insufficient'
|
||
}
|
||
},
|
||
recommendations: recommendations
|
||
};
|
||
}
|
||
|
||
static extractVocabularyStatic(content) {
|
||
let vocabulary = [];
|
||
|
||
// Priority 1: Use raw module content (simple format)
|
||
if (content.rawContent) {
|
||
return FillTheBlankGame.extractVocabularyFromRawStatic(content.rawContent);
|
||
}
|
||
|
||
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
|
||
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
|
||
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
|
||
// Support ultra-modular format ONLY
|
||
if (typeof data === 'object' && data.user_language) {
|
||
return {
|
||
original: word,
|
||
translation: data.user_language.split(';')[0],
|
||
fullTranslation: data.user_language,
|
||
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);
|
||
}
|
||
|
||
return FillTheBlankGame.finalizeVocabularyStatic(vocabulary);
|
||
}
|
||
|
||
static extractVocabularyFromRawStatic(rawContent) {
|
||
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,
|
||
translation: data.user_language.split(';')[0],
|
||
fullTranslation: data.user_language,
|
||
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);
|
||
}
|
||
|
||
return FillTheBlankGame.finalizeVocabularyStatic(vocabulary);
|
||
}
|
||
|
||
static finalizeVocabularyStatic(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() !== ''
|
||
);
|
||
|
||
return vocabulary;
|
||
}
|
||
|
||
static extractSentencesStatic(content) {
|
||
let sentences = [];
|
||
|
||
// Priority 1: Use raw module content
|
||
if (content.rawContent) {
|
||
// Extract from sentences array
|
||
if (content.rawContent.sentences && Array.isArray(content.rawContent.sentences)) {
|
||
content.rawContent.sentences.forEach(sentence => {
|
||
if (sentence.english) {
|
||
sentences.push(sentence.english);
|
||
}
|
||
if (sentence.chinese) {
|
||
sentences.push(sentence.chinese);
|
||
}
|
||
if (sentence.french) {
|
||
sentences.push(sentence.french);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Extract from texts array
|
||
if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) {
|
||
content.rawContent.texts.forEach(text => {
|
||
if (text.content) {
|
||
// Split text content into sentences
|
||
const textSentences = text.content.split(/[.!?]+/).filter(s => s.trim().length > 10);
|
||
sentences.push(...textSentences);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Priority 2: Direct content properties
|
||
if (content.sentences && Array.isArray(content.sentences)) {
|
||
content.sentences.forEach(sentence => {
|
||
if (sentence.english) {
|
||
sentences.push(sentence.english);
|
||
}
|
||
if (sentence.chinese) {
|
||
sentences.push(sentence.chinese);
|
||
}
|
||
if (sentence.french) {
|
||
sentences.push(sentence.french);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (content.texts && Array.isArray(content.texts)) {
|
||
content.texts.forEach(text => {
|
||
if (text.content) {
|
||
const textSentences = text.content.split(/[.!?]+/).filter(s => s.trim().length > 10);
|
||
sentences.push(...textSentences);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Filter and validate sentences
|
||
sentences = sentences.filter(sentence =>
|
||
sentence &&
|
||
typeof sentence === 'string' &&
|
||
sentence.trim().length > 5 &&
|
||
sentence.split(' ').length >= 3
|
||
);
|
||
|
||
return sentences;
|
||
}
|
||
}
|
||
|
||
// Module registration
|
||
window.GameModules = window.GameModules || {};
|
||
window.GameModules.FillTheBlank = FillTheBlankGame; |