// === 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 = `
โ 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;
}
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 = `
${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-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 += `
${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;