"use strict"; /** * GameDataHandler - Data Sanitizer & Scorer * * Game Codes: * - G001: Quiz text-text * - G002: Quiz audio-text (audio question, text answer) * - G003: Quiz text-audio (text question, audio answer) * - G004: Quiz image-text (image question, text answer) * - G005: Quiz text-image (text question, image answer) * * - G110: Sequence Word - no audio * - G111: Sequence Word - có audio, missing_letter_count từ item * - G112: Sequence Word - có audio, missing_letter_count từ item * - G113: Sequence Word - có audio, missing_letter_count từ item * * - G120: Sequence Sentence - no audio * - G121: Sequence Sentence - có audio, missing_letter_count từ item * - G122: Sequence Sentence - có audio, missing_letter_count từ item * - G123: Sequence Sentence - có audio, missing_letter_count từ item */ Object.defineProperty(exports, "__esModule", { value: true }); exports.GAME_CODES = void 0; exports.getGameCategory = getGameCategory; exports.sanitizeForClient = sanitizeForClient; exports.checkAnswer = checkAnswer; // Game code metadata exports.GAME_CODES = { // Quiz G001: { name: 'Quiz Text-Text', category: 'quiz', hasAudio: false, hasImage: false }, G002: { name: 'Quiz Audio-Text', category: 'quiz', hasAudio: true, hasImage: false }, G003: { name: 'Quiz Text-Audio', category: 'quiz', hasAudio: true, hasImage: false }, G004: { name: 'Quiz Image-Text', category: 'quiz', hasAudio: false, hasImage: true }, G005: { name: 'Quiz Text-Image', category: 'quiz', hasAudio: false, hasImage: true }, // Sequence Word G110: { name: 'Sequence Word', category: 'sequence_word', hasAudio: false }, G111: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true }, G112: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true }, G113: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true }, // Sequence Sentence G120: { name: 'Sequence Sentence', category: 'sequence_sentence', hasAudio: false }, G121: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true }, G122: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true }, G123: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true }, }; function getGameCategory(code) { return exports.GAME_CODES[code]?.category || 'unknown'; } // ============================================================================= // SANITIZE DATA FOR CLIENT (REMOVE ANSWERS) // ============================================================================= /** * Sanitize game data before sending to iframe * CRITICAL: Never send answers/correct data to client */ function sanitizeForClient(code, items) { if (!Array.isArray(items)) return []; switch (code) { // ===== QUIZ VARIANTS ===== case 'G001': // Quiz text-text return sanitizeQuizTextText(items); case 'G002': // Quiz audio-text return sanitizeQuizAudioText(items); case 'G003': // Quiz text-audio return sanitizeQuizTextAudio(items); case 'G004': // Quiz image-text return sanitizeQuizImageText(items); case 'G005': // Quiz text-image return sanitizeQuizTextImage(items); // ===== SEQUENCE WORD VARIANTS ===== case 'G110': // Sequence word return sanitizeSequenceWord(items); case 'G111': // Sequence word return sanitizeSequenceWord(items); case 'G112': // Sequence word return sanitizeSequenceWord(items); case 'G113': // Sequence word return sanitizeSequenceWord(items); // ===== SEQUENCE SENTENCE VARIANTS ===== case 'G120': // Sequence sentence return sanitizeSequenceSentence(items); case 'G121': // Sequence sentence return sanitizeSequenceSentence(items); case 'G122': // Sequence sentence return sanitizeSequenceSentence(items); case 'G123': // Sequence sentence return sanitizeSequenceSentence(items); default: console.warn(`[GameDataHandler] Unknown game code: ${code}, returning raw data`); return items; } } // ============================================================================= // QUIZ SANITIZERS // ============================================================================= /** * G001: Quiz Text-Text * Client receives: id, question, options (shuffled) * Client does NOT receive: answer */ function sanitizeQuizTextText(items) { return items.map(item => { // Normalize options to {text: string} const options = (item.options || []).map((o) => { if (typeof o === 'string') { return { text: o }; } if (o && typeof o === 'object') { return { text: String(o.text ?? '') }; } return { text: String(o ?? '') }; }); // Shuffle to hide answer position shuffleArray(options); // Save shuffled text order for SDK to resolve index const shuffledTexts = options.map((o) => String(o.text ?? '')); return { id: item.id, question: item.question, options: options, __shuffledOptions: shuffledTexts, // SDK internal }; }); } /** * G002: Quiz Audio-Text * Client receives: id, question (audio URL), options (shuffled) * Client does NOT receive: answer */ function sanitizeQuizAudioText(items) { return items.map(item => { const options = (item.options || []).map((o) => { if (typeof o === 'string') { return { text: o }; } if (o && typeof o === 'object') { return { text: String(o.text ?? '') }; } return { text: String(o ?? '') }; }); shuffleArray(options); const shuffledTexts = options.map((o) => String(o.text ?? '')); return { id: item.id, question: item.audio || item.audio_url, options: options, __shuffledOptions: shuffledTexts, }; }); } /** * G003: Quiz Text-Audio * Client receives: id, question (text), options (audio URLs shuffled) * Client does NOT receive: answer */ function sanitizeQuizTextAudio(items) { return items.map(item => { const options = (item.options || []).map((o) => { if (typeof o === 'string') { return { audio: o }; } if (o && typeof o === 'object') { const audioUrl = o.audio || o.audio_url || ''; return { audio: String(audioUrl) }; } return { audio: String(o ?? '') }; }); shuffleArray(options); const shuffledAudios = options.map((o) => String(o.audio ?? '')); return { id: item.id, question: item.question, options: options, __shuffledOptions: shuffledAudios, }; }); } /** * G004: Quiz Image-Text * Client receives: id, image_url, question (hint), options (shuffled) * Client does NOT receive: answer */ function sanitizeQuizImageText(items) { return items.map(item => { const options = (item.options || []).map((o) => { if (typeof o === 'string') { return { text: o }; } if (o && typeof o === 'object') { return { text: String(o.text ?? '') }; } return { text: String(o ?? '') }; }); shuffleArray(options); const shuffledTexts = options.map((o) => String(o.text ?? '')); return { id: item.id, image_url: item.image_url, question: item.question, options: options, __shuffledOptions: shuffledTexts, }; }); } /** * G005: Quiz Text-Image * Client receives: id, question (text), options (image URLs shuffled) * Client does NOT receive: answer */ function sanitizeQuizTextImage(items) { return items.map(item => { const options = (item.options || []).map((o) => { if (typeof o === 'string') { return { image_url: o }; } if (o && typeof o === 'object') { const imageUrl = o.image_url || o.image || ''; return { image_url: String(imageUrl) }; } return { image_url: String(o ?? '') }; }); shuffleArray(options); const shuffledUrls = options.map((o) => String(o.image_url ?? '')); return { id: item.id, question: item.question, options: options, // Each option has {image_url: ...} __shuffledOptions: shuffledUrls, }; }); } // ============================================================================= // SEQUENCE WORD SANITIZERS // ============================================================================= /** * Sequence Word (G110-G113) * Client receives: id, question (array with blanks), options (missing letters shuffled), audio_url (optional) * Client does NOT receive: word, parts, answer, missing_letter_count * * Logic: * 1. Read missing_letter_count from item (count of letters to blank out) * 2. Randomly select positions to blank * 3. question: array with blanks at selected positions * 4. options: extracted missing letters (shuffled) */ function sanitizeSequenceWord(items) { return items.map(item => { const parts = item.answer || item.parts || []; const missingCount = item.missing_letter_count || 0; if (missingCount === 0 || parts.length === 0) { // No missing - all visible return { id: item.id, question: [...parts], options: [], ...(item.audio_url && { audio_url: item.audio_url }) }; } // Randomly select which positions to blank const allIndices = Array.from({ length: parts.length }, (_, i) => i); const blankIndices = new Set(); const count = Math.min(missingCount, parts.length); while (blankIndices.size < count) { const randomIdx = Math.floor(Math.random() * allIndices.length); const actualIdx = allIndices[randomIdx]; blankIndices.add(actualIdx); allIndices.splice(randomIdx, 1); } // Build question array with blanks at random positions const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p)); // Extract missing letters and shuffle const missingLetters = Array.from(blankIndices).map(i => String(parts[i])); shuffleArray(missingLetters); const result = { id: item.id, question, // e.g. ["H", "", "L", "", "O"] options: missingLetters, // e.g. ["L", "E"] - shuffled __shuffledOptions: [...missingLetters] // SDK internal: to resolve indices }; if (item.audio_url) { result.audio_url = item.audio_url; } // CRITICAL: Do NOT send word, parts, answer, missing_letter_count return result; }); } // ============================================================================= // SEQUENCE SENTENCE SANITIZERS // ============================================================================= /** * Sequence Sentence (G120-G123) * Client receives: id, question (array with blanks), options (missing words shuffled), audio_url (optional) * Client does NOT receive: sentence, parts, answer, missing_letter_count * * Logic: Same as Sequence Word * 1. Read missing_letter_count from item * 2. Randomly select positions to blank * 3. question: array with blanks * 4. options: extracted missing words (shuffled) */ function sanitizeSequenceSentence(items) { return items.map(item => { const parts = item.answer || item.parts || []; const missingCount = item.missing_letter_count || 0; if (missingCount === 0 || parts.length === 0) { // No missing - all visible return { id: item.id, question: [...parts], options: [], ...(item.audio_url && { audio_url: item.audio_url }) }; } // Randomly select which positions to blank const allIndices = Array.from({ length: parts.length }, (_, i) => i); const blankIndices = new Set(); const count = Math.min(missingCount, parts.length); while (blankIndices.size < count) { const randomIdx = Math.floor(Math.random() * allIndices.length); const actualIdx = allIndices[randomIdx]; blankIndices.add(actualIdx); allIndices.splice(randomIdx, 1); } // Build question array with blanks at random positions const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p)); // Extract missing words and shuffle const missingWords = Array.from(blankIndices).map(i => String(parts[i])); shuffleArray(missingWords); const result = { id: item.id, question, // e.g. ["I", "", "reading", ""] options: missingWords, // e.g. ["love", "books"] - shuffled __shuffledOptions: [...missingWords] // SDK internal }; if (item.audio_url) { result.audio_url = item.audio_url; } // CRITICAL: Do NOT send sentence, parts, answer, missing_letter_count return result; }); } /** * Check if player's answer is correct * * @param code - Game code (G001, G110, etc.) * @param originalItem - Original item from server (has answer field!) * @param playerAnswer - Player's answer (text for quiz, array for sequence) */ function checkAnswer(code, originalItem, playerAnswer) { switch (code) { // ===== QUIZ VARIANTS (G001-G005) ===== case 'G001': // Quiz Text-Text case 'G002': // Quiz Audio-Text case 'G003': // Quiz Text-Audio case 'G004': // Quiz Image-Text case 'G005': // Quiz Text-Image return checkQuizAnswer(originalItem, playerAnswer); // ===== SEQUENCE WORD VARIANTS (G110-G113) ===== case 'G110': // Sequence Word case 'G111': // Sequence Word Audio case 'G112': // Sequence Word Audio case 'G113': // Sequence Word Audio return checkSequenceAnswer(originalItem, playerAnswer); // ===== SEQUENCE SENTENCE VARIANTS (G120-G123) ===== case 'G120': // Sequence Sentence case 'G121': // Sequence Sentence Audio case 'G122': // Sequence Sentence Audio case 'G123': // Sequence Sentence Audio return checkSequenceAnswer(originalItem, playerAnswer); default: console.warn(`[GameDataHandler] Unknown game code for scoring: ${code}`); return { isCorrect: false, score: 0 }; } } // Quiz Scoring function checkQuizAnswer(item, answerChoice) { const correctAnswer = String(item.answer || ''); if (!correctAnswer) { return { isCorrect: false, score: 0, feedback: 'No correct answer defined' }; } let playerAnswerText; if (typeof answerChoice === 'number') { // Index: resolve from original options if (Array.isArray(item.options)) { const v = item.options[answerChoice]; if (typeof v === 'string') { playerAnswerText = v; } else if (v && typeof v === 'object' && 'text' in v) { playerAnswerText = String(v.text ?? ''); } else { playerAnswerText = String(v ?? ''); } } else { return { isCorrect: false, score: 0, feedback: 'Invalid question options' }; } } else { // Direct text answer if (answerChoice && typeof answerChoice === 'object' && 'text' in answerChoice) { playerAnswerText = String(answerChoice.text ?? ''); } else { playerAnswerText = String(answerChoice ?? ''); } } const isCorrect = playerAnswerText.toLowerCase().trim() === correctAnswer.toLowerCase().trim(); return { isCorrect, score: isCorrect ? 1 : 0, feedback: isCorrect ? `✅ Correct! "${playerAnswerText}" matches answer "${correctAnswer}"` : `❌ Wrong. You chose "${playerAnswerText}" but correct answer is "${correctAnswer}"` }; } // Sequence Scoring function checkSequenceAnswer(item, answer) { const correctOrder = item.answer || item.parts; if (!Array.isArray(answer) || !Array.isArray(correctOrder)) { return { isCorrect: false, score: 0 }; } const isCorrect = arraysEqual(answer, correctOrder); return { isCorrect, score: isCorrect ? 1 : 0, }; } // ============================================================================= // UTILITIES // ============================================================================= function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } function arraysEqual(a, b) { if (a.length !== b.length) return false; return a.every((val, idx) => { if (typeof val === 'string' && typeof b[idx] === 'string') { return val.toLowerCase().trim() === b[idx].toLowerCase().trim(); } return val === b[idx]; }); } //# sourceMappingURL=GameDataHandler.js.map