// === 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;