Files
sentence1_teacher/image_spelling_sequence_sentence_teacher/sena_sdk.js
lubukhu 21e4ea5631
All checks were successful
Deploy to Production / deploy (push) Successful in 7s
up
2026-02-25 15:03:28 +07:00

1313 lines
40 KiB
JavaScript

/**
* ========== SENA SDK - MERGED COMPLETE VERSION ==========
* Combines all features from root, update_1, and update_2
* - Advanced game type support (G1-G9, G26)
* - PostMessage handling with late arrival support
* - Multi-question and single-question modes
* - G4, G5, G9 specific implementations
* - Matching, WordSearch, GroupSort, Crossword, Puzzle support
*/
/**
* ========== GAME TYPES CONFIG ==========
*/
var SENA_GAME_TYPES = {
1: { name: 'Quiz', description: 'Trắc nghiệm' },
2: { name: 'Sort', description: 'Sắp xếp' },
3: { name: 'Match', description: 'Nối cặp', sdk: 'tdv_sdk' },
4: { name: 'Fill', description: 'Điền từ' },
5: { name: 'Custom', description: 'Tùy chỉnh' },
6: { name: 'OddOneOut', description: 'Chọn khác loại' },
7: { name: 'WordSearch', description: 'Tìm từ ẩn', sdk: 'tdv_wordsearch' },
8: { name: 'GroupSort', description: 'Phân loại nhóm', sdk: 'tdv_groupsort' },
9: { name: 'Crossword', description: 'Ô chữ', sdk: 'tdv_crossword' },
26: { name: 'Puzzle', description: 'Ghép hình', sdk: 'tdv_puzzle' }
};
// Helper: Get valid game type range
function getSenaGameTypeRange() {
var types = Object.keys(SENA_GAME_TYPES).map(Number);
return { min: Math.min.apply(null, types), max: Math.max.apply(null, types) };
}
// ========== STUBS FOR LAZY LOADING ==========
window.tdv_sdk = window.tdv_sdk || {
isStub: true,
init: function (mode) {
console.log('⚠️ tdv_sdk.init() called on Stub. Mode saved:', mode);
this._pendingMode = mode;
},
prepareIDs: function () { },
getItemCount: function () { return 0; },
loadFromPostMessage: function () { console.warn('Stub loadFromPostMessage called'); },
isCorrect: function () { return 0; },
getCorrectCount: function () { return 0; },
getTimeSpent: function () { return 0; }
};
// Helper: Regex for game code
function getSenaGameCodeRegex(strict) {
var range = getSenaGameTypeRange();
var typePattern = '[' + range.min + '-' + range.max + ']';
var qoPattern = strict ? '[0-2]' : '[0-9]';
return new RegExp('^G(' + typePattern + ')([2-9])(' + qoPattern + ')(' + qoPattern + ')(?:S([0-1]))?(?:T(\\d+))?$');
}
/**
* Sena SDK Constructor
*/
function SenaSDK(gid = 'G2510S1T30') {
// Core data
this.data = null;
this.correctAnswer = null;
this.gameCode = gid;
this.timeLimit = 0;
this.shuffle = true;
this.uuid = Date.now() + '_' + Math.floor(Math.random() * 100000);
// Time tracking
this.startTime = 0;
this.endTime = 0;
// TTS (Web Speech API)
this.speechSynthesis = window.speechSynthesis;
this.currentUtterance = null;
this.voiceSettings = {
lang: 'en-US',
rate: 1.0,
pitch: 1.0,
volume: 1.0
};
// Multi-question support
this.list = [];
this.currentQuestion = null;
this.level = 0;
this.totalQuestions = 0;
this.userResults = [];
this.gameID = null;
this.userId = null;
this.postMessageDataLoaded = false;
// Game type flags
this.isMatchingGame = false;
this.isOddOneOutGame = false;
this.isWordSearchGame = false;
this.isGroupSortGame = false;
// PostMessage tracking
this._postMessageListenerRegistered = false;
this._waitingForPostMessage = false;
this._postMessageTimeout = null;
this._loadCallback = null;
// Interaction tracking
this._gameStartedByUser = false;
this._dataLoadedFromServer = false;
// G5 specific
this.masterList = [];
this.currentLevel = 0;
this.totalLevels = 1;
this.timePerCard = 0;
// Mode: 'live' | 'preview' | 'dev'
this.mode = 'preview';
}
/**
* Shuffle array using Fisher-Yates algorithm
*/
SenaSDK.prototype.shuffleArray = function (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]];
}
};
/**
* Load data from postMessage (SERVER_PUSH_DATA)
*/
SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
let self = this;
if (!inputJson) {
console.error('🎮 Sena SDK: No data in postMessage');
if (callback) callback(false);
return false;
}
console.log('📦 Sena SDK: Loading from PostMessage:', inputJson);
// Update gameCode if provided
if (inputJson.gameCode) {
self.gameCode = inputJson.gameCode;
}
let gameCode = self.gameCode;
let gameCategory = gameCode.charAt(1);
console.log('🎮 Sena SDK: GameCode:', gameCode, '| Category:', gameCategory);
// ========== FORWARD TO SPECIALIZED SDKs ==========
// Matching (G3xxx)
if (gameCategory === '3') {
console.log('🎯 Sena SDK: Detected MATCHING GAME (G3xxx), forwarding to tdv_sdk...');
if (window.tdv_sdk && typeof window.tdv_sdk.loadFromPostMessage === 'function') {
self.isMatchingGame = true;
return window.tdv_sdk.loadFromPostMessage(inputJson, callback);
} else {
console.error('❌ tdv_sdk not loaded!');
if (callback) callback(false);
return false;
}
}
// WordSearch (G7xxx)
if (gameCategory === '7') {
console.log('🎯 Sena SDK: Detected WORDSEARCH GAME (G7xxx), forwarding to tdv_wordsearch...');
if (window.tdv_wordsearch && typeof window.tdv_wordsearch.loadFromPostMessage === 'function') {
self.isWordSearchGame = true;
return window.tdv_wordsearch.loadFromPostMessage(inputJson, callback);
} else {
console.error('❌ tdv_wordsearch not loaded!');
if (callback) callback(false);
return false;
}
}
// GroupSort (G8xxx)
if (gameCategory === '8') {
console.log('🎯 Sena SDK: Detected GROUPSORT GAME (G8xxx), forwarding to tdv_groupsort...');
if (window.tdv_groupsort && typeof window.tdv_groupsort.loadFromPostMessage === 'function') {
self.isGroupSortGame = true;
return window.tdv_groupsort.loadFromPostMessage(inputJson, callback);
} else {
console.error('❌ tdv_groupsort not loaded!');
if (callback) callback(false);
return false;
}
}
// Crossword (G9xxx)
if (gameCategory === '9') {
console.log('🎯 Sena SDK: Detected CROSSWORD GAME (G9xxx), forwarding to tdv_crossword...');
if (window.tdv_crossword && typeof window.tdv_crossword.loadFromPostMessage === 'function') {
return window.tdv_crossword.loadFromPostMessage(inputJson, callback);
} else {
console.error('❌ tdv_crossword not loaded!');
if (callback) callback(false);
return false;
}
}
// Puzzle (G26xxx)
if (gameCategory === '2' && gameCode.charAt(2) === '6') {
console.log('🎯 Sena SDK: Detected PUZZLE GAME (G26xxx), forwarding to tdv_puzzle...');
if (window.tdv_puzzle && typeof window.tdv_puzzle.loadFromPostMessage === 'function') {
return window.tdv_puzzle.loadFromPostMessage(inputJson, callback);
} else {
console.error('❌ tdv_puzzle not loaded!');
if (callback) callback(false);
return false;
}
}
// ========== BASIC GAMES (Quiz, Sort, OddOneOut, Fill, Custom) ==========
let items = inputJson.list || [inputJson];
// Cancel timeout if waiting
if (self._postMessageTimeout) {
clearTimeout(self._postMessageTimeout);
self._postMessageTimeout = null;
}
self._waitingForPostMessage = false;
// Parse game code
self._parseGameCode();
if (items.length === 1) {
// Single question
let item = items[0];
self.data = {
question: item.question || item.data?.question || '',
request: item.request || item.data?.request || '',
options: item.options || item.data?.options || [],
image: item.image || item.data?.image || '',
audio: item.audio || item.data?.audio || '',
hint: item.hint || item.data?.hint || null,
reason: item.reason || item.data?.reason || ''
};
self.correctAnswer = item.answer;
self.gameID = item.id || null;
// G5 specific: Initialize master list
if (self.gameType === 5 && self.data?.options) {
self.masterList = [...self.data.options];
self.totalLevels = Math.ceil(self.masterList.length / self.itemCount);
self.currentLevel = 0;
self.loadLevelG5(1);
}
// G4 specific: Process data
if (self.gameType === 4) {
self._processG4Data();
}
} else {
// Multi-question
self.list = items.map((item, idx) => ({
id: item.id || idx,
question: item.question || '',
request: item.request || '',
options: item.options || [],
answer: item.answer,
image: item.image || '',
audio: item.audio || '',
reason: item.reason || ''
}));
self.totalQuestions = items.length;
self.level = 0;
self._loadCurrentQuestionToData();
}
self.postMessageDataLoaded = true;
console.log('✅ Sena SDK: Data processed -', items.length, 'item(s)');
if (self._loadCallback) {
self._loadCallback(true);
self._loadCallback = null;
}
if (callback) callback(true);
return true;
};
/**
* Parse game code to get settings
*/
SenaSDK.prototype._parseGameCode = function () {
let self = this;
const gameCode = self.gameCode || 'G2510S1T30';
const regex = getSenaGameCodeRegex(true);
let match = gameCode.match(regex);
if (match) {
self.gameType = parseInt(match[1], 10);
self.itemCount = parseInt(match[2], 10);
self.questionType = parseInt(match[3], 10);
self.optionType = parseInt(match[4], 10);
const shuffleFlag = match[5] !== undefined ? match[5] : '1';
const timeStr = match[6] !== undefined ? match[6] : '0';
self.shuffle = (shuffleFlag === '1');
// G5 uses T for time per card
if (self.gameType === 5) {
self.timePerCard = parseInt(timeStr, 10);
self.timeLimit = 0;
} else {
self.timeLimit = parseInt(timeStr, 10);
self.timePerCard = 0;
}
}
};
/**
* Load current question to this.data
*/
SenaSDK.prototype._loadCurrentQuestionToData = function () {
let self = this;
if (self.list.length > 0 && self.level < self.list.length) {
self.currentQuestion = self.list[self.level];
self.data = {
question: self.currentQuestion.question || '',
request: self.currentQuestion.request || '',
options: self.currentQuestion.options || [],
image: self.currentQuestion.image || '',
audio: self.currentQuestion.audio || '',
reason: self.currentQuestion.reason || ''
};
self.correctAnswer = self.currentQuestion.answer;
}
};
/**
* Move to next question
*/
SenaSDK.prototype.nextQuestion = function () {
let self = this;
if (self.level < self.totalQuestions - 1) {
self.level++;
self._loadCurrentQuestionToData();
console.log('🎮 Sena SDK: Next Question:', self.level + 1, '/', self.totalQuestions);
return true;
}
console.log('🎮 Sena SDK: No more questions');
return false;
};
/**
* Get current question number (1-indexed)
*/
SenaSDK.prototype.getCurrentNumber = function () {
return this.level + 1;
};
/**
* Get total questions
*/
SenaSDK.prototype.getTotalQuestions = function () {
return this.totalQuestions || 1;
};
// PostMessage timeout setting
SenaSDK.prototype.POSTMESSAGE_TIMEOUT_MS = 5000; // 5 seconds
/**
* Load data (with mode support)
*/
SenaSDK.prototype.load = function (callback, template = 'G2510S1T30') {
let self = this;
// Auto-register postMessage listener
if (!self._postMessageListenerRegistered) {
self.registerPostMessageListener();
self._postMessageListenerRegistered = true;
}
// Already loaded from postMessage
if (self.postMessageDataLoaded && self.list.length > 0) {
console.log('🎮 Sena SDK: Data already loaded from postMessage');
if (callback) callback(true);
return;
}
// Already have data
if (self.data && self.data.options) {
console.log('🎮 Sena SDK: Data already available');
if (callback) callback(true);
return;
}
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
const LID = urlParams.get('LID');
if (LID) {
self.gameCode = LID;
}
const urlMode = urlParams.get('mode');
if (urlMode && ['live', 'preview', 'dev'].includes(urlMode.toLowerCase())) {
self.mode = urlMode.toLowerCase();
}
console.log('🎮 Sena SDK: Mode =', self.mode.toUpperCase(), '| GameCode =', self.gameCode);
self._loadCallback = callback;
// Mode-based loading
switch (self.mode) {
case 'live':
self._waitingForPostMessage = true;
console.log('⏳ Sena SDK: [LIVE MODE] Waiting for server data (no timeout)...');
self._sendGameReady();
break;
case 'dev':
console.log('🔧 Sena SDK: [DEV MODE] Loading sample data immediately...');
self._waitingForPostMessage = false;
self._loadFromServer(callback, template);
break;
case 'preview':
default:
self._waitingForPostMessage = true;
console.log('⏳ Sena SDK: [PREVIEW MODE] Waiting for postMessage (5s timeout)...');
self._postMessageTimeout = setTimeout(function () {
if (self._waitingForPostMessage && !self.postMessageDataLoaded) {
console.warn('⚠️ Sena SDK: No postMessage received, fallback to sample data...');
self._loadFromServer(self._loadCallback, template);
}
}, self.POSTMESSAGE_TIMEOUT_MS);
break;
}
};
/**
* Send GAME_READY message
*/
SenaSDK.prototype._sendGameReady = function () {
let self = this;
window.parent.postMessage({
type: "GAME_READY",
uuid: self.uuid,
payload: {
game_id: self.gameID || self.gameCode,
game_code: self.gameCode,
mode: self.mode
}
}, "*");
console.log('📤 Sena SDK: Sent GAME_READY to parent');
console.log('🔑 UUID:', self.uuid);
};
/**
* Load from server (fallback)
*/
SenaSDK.prototype._loadFromServer = function (callback, template = 'G2510S1T30') {
let self = this;
self._waitingForPostMessage = false;
let url = `https://senaai.tech/sample/${self.gameCode}.json`;
console.log('📡 Sena SDK: Fetching sample from:', url);
fetch(url)
.then(response => {
if (!response.ok) throw new Error("HTTP " + response.status);
return response.json();
})
.then(jsonData => {
console.log('✅ Sena SDK: Fetched data success');
self.loadFromPostMessage(jsonData);
self._dataLoadedFromServer = true;
})
.catch(error => {
console.error('❌ Sena SDK: Error loading data:', error);
if (self.mode === 'dev') {
self._loadHardcodedFallback(callback);
} else {
if (callback) callback(false);
}
});
};
/**
* Hardcoded fallback (for dev mode when fetch fails)
*/
SenaSDK.prototype._loadHardcodedFallback = function (callback) {
let self = this;
console.warn('⚠️ Sena SDK: Using HARDCODED fallback data');
let fallbackJson = {
gameCode: self.gameCode,
data: {
question: "What is 2 + 2?",
options: ["3", "4", "5", "6"]
},
answer: "4"
};
self.loadFromPostMessage(fallbackJson, callback);
};
/**
* Validate game code
*/
SenaSDK.prototype.validateGameCode = function (code) {
code = code || this.gameCode;
const regex = getSenaGameCodeRegex(true);
const match = code.match(regex);
const range = getSenaGameTypeRange();
if (!match) {
return { valid: false, error: 'Invalid game code format', code: code };
}
return {
valid: true,
code: code,
type: parseInt(match[1], 10),
count: parseInt(match[2], 10),
qType: parseInt(match[3], 10),
oType: parseInt(match[4], 10),
shuffle: match[5] !== '0',
time: match[6] ? parseInt(match[6], 10) : 0,
description: this.getGameCodeDescription(match)
};
};
/**
* Get game code description
*/
SenaSDK.prototype.getGameCodeDescription = function (match) {
if (!match) return '';
const gameType = SENA_GAME_TYPES[match[1]]?.name || 'Unknown';
const contentTypes = ['Text', 'Image', 'Audio'];
const qType = contentTypes[parseInt(match[3])] || 'Unknown';
const oType = contentTypes[parseInt(match[4])] || 'Unknown';
return `${gameType}: ${match[2]} items, ${qType}${oType}`;
};
/**
* Generate developer guide
*/
SenaSDK.prototype.guide = function () {
let self = this;
const gameCode = self.gameCode || 'G2510S1T30';
return `╔════════════════════════════════════════════════════════════════════════════╗
║ SENA SDK - DEVELOPER GUIDE: ${gameCode.padEnd(37)}
╚════════════════════════════════════════════════════════════════════════════╝
📚 MERGED VERSION
This SDK combines all game types (G1-G9, G26) with full postMessage support.
🎮 SUPPORTED GAME TYPES:
${Object.entries(SENA_GAME_TYPES).map(([key, val]) => ` G${key}: ${val.name} - ${val.description}`).join('\n')}
📖 For detailed documentation, visit: https://senaai.tech/docs
`;
};
// ========== GETTERS ==========
SenaSDK.prototype.getQuestionValue = function () {
var q = String(this.data?.question || '').trim();
if (q.toLowerCase().startsWith('http')) return '';
return q;
};
SenaSDK.prototype.getQuestionType = function () {
let self = this;
const gameCode = self.gameCode || 'G2510S1T30';
const regex = getSenaGameCodeRegex(true);
const match = gameCode.match(regex);
if (match) {
const qIdx = match[3];
const types = { '0': 'text', '1': 'image', '2': 'audio' };
return types[qIdx] || 'text';
}
return 'text';
};
SenaSDK.prototype.getQuestionImage = function () {
return String(this.data?.question || '').trim();
};
SenaSDK.prototype.getRequestValue = function () {
return String(this.data?.request || '').trim();
};
SenaSDK.prototype.getRequestType = function () {
return this.getQuestionType();
};
SenaSDK.prototype.getOptionsCount = function () {
return this.data?.options?.length || 0;
};
SenaSDK.prototype.getOptionsType = function () {
let self = this;
const gameCode = self.gameCode || 'G2510S1T30';
const regex = getSenaGameCodeRegex(true);
const match = gameCode.match(regex);
if (match) {
const oIdx = match[4];
const types = { '0': 'text', '1': 'image', '2': 'audio' };
return types[oIdx] || 'text';
}
return 'text';
};
SenaSDK.prototype.getOptionsValue = function (index) {
return this.data?.options?.[index];
};
SenaSDK.prototype.getHintType = function () {
return this.data?.hint?.type || '';
};
SenaSDK.prototype.getHintCount = function () {
const hintValue = this.getHintValue();
if (hintValue === null) return 0;
if (Array.isArray(hintValue)) return hintValue.length;
return 1;
};
SenaSDK.prototype.getHintValue = function (index) {
if (this.data?.hint?.value !== undefined) {
const hintValue = this.data.hint.value;
const hintType = this.getHintType();
if (hintType === 'display' && Array.isArray(hintValue)) {
if (index !== undefined && index >= 0 && index < hintValue.length) {
return hintValue[index];
}
return hintValue;
}
return hintValue;
}
return null;
};
SenaSDK.prototype.getReason = function () {
return this.data?.reason || this.currentQuestion?.reason || '';
};
SenaSDK.prototype.hasReason = function () {
let reason = this.getReason();
return (reason && reason.length > 0) ? 1 : 0;
};
// ========== G9 MEMORY FUNCTIONS ==========
SenaSDK.prototype.getTargetName = function () {
if (this.correctAnswer) {
if (typeof this.correctAnswer === 'object' && this.correctAnswer.text) {
return this.correctAnswer.text;
}
return String(this.correctAnswer);
}
return '';
};
SenaSDK.prototype.getCardName = function (index) {
if (this.data?.options?.[index]) {
let opt = this.data.options[index];
return opt.name || opt.text || '';
}
return '';
};
SenaSDK.prototype.getCardImage = function (index) {
return this.data?.options?.[index]?.image || '';
};
SenaSDK.prototype.getCardAudio = function (index) {
return this.data?.options?.[index]?.audio || '';
};
SenaSDK.prototype.getCardID = function (index) {
return this.data?.options?.[index]?.id || '';
};
SenaSDK.prototype.getCardType = function (index) {
if (this.data?.options?.[index]) {
return this.data.options[index].type || 'text';
}
if (this.masterList?.[index]) {
return this.masterList[index].type || 'text';
}
return 'text';
};
// ========== G5 VOICE RUSH FUNCTIONS ==========
SenaSDK.prototype.loadLevelG5 = function (levelIndex) {
let self = this;
if (self.gameType !== 5 || !self.masterList) return false;
self.currentLevel = levelIndex;
let count = self.itemCount;
window.Sena_TotalLevels = Math.ceil(self.masterList.length / count);
let startIndex = (levelIndex - 1) * count;
let endIndex = Math.min(startIndex + count, self.masterList.length);
let levelOptions = self.masterList.slice(startIndex, endIndex);
self.data.options = levelOptions;
console.log(`🎮 Sena SDK: Loaded G5 Level ${levelIndex} with ${levelOptions.length} cards`);
return true;
};
SenaSDK.prototype.getTotalLevels = function () {
return this.totalLevels || 1;
};
SenaSDK.prototype.getTimePerCard = function () {
if (this.timePerCard === undefined) {
this._parseGameCode();
}
return this.timePerCard > 0 ? this.timePerCard : 5;
};
// ========== G4 MEMORY CARD FUNCTIONS ==========
SenaSDK.prototype._processG4Data = function () {
let self = this;
if (!self.data.options) self.data.options = [];
// Count occurrences
let counts = {};
self.data.options.forEach(item => {
if (item.type !== 'blank' && item.name) {
counts[item.name] = (counts[item.name] || 0) + 1;
}
});
// Remove orphans (items that appear less than 2 times)
self.data.options.forEach(item => {
if (item.type !== 'blank' && item.name && counts[item.name] < 2) {
console.log('🎮 Sena SDK: Orphan card detected:', item.name);
item.type = 'blank';
item.name = 'blank';
item.image = '';
item.id = 'blank_sanitized';
}
});
// Fill with blank cards to make 9 total
while (self.data.options.length < 9) {
self.data.options.push({
id: 'blank_' + self.data.options.length,
type: 'blank',
name: 'blank',
value: -1,
image: ''
});
}
// Shuffle
if (self.shuffle) {
self.shuffleArray(self.data.options);
}
};
SenaSDK.prototype.checkPair = function (idx1, idx2, callback) {
let self = this;
let card1 = self.data?.options?.[idx1];
let card2 = self.data?.options?.[idx2];
if (!card1 || !card2) {
if (callback) callback(false);
return;
}
let isMatch = false;
if (card1.type !== 'blank' && card2.type !== 'blank') {
if (card1.id !== card2.id) {
if (card1.name && card2.name && card1.name.toLowerCase() === card2.name.toLowerCase()) {
isMatch = true;
}
}
}
console.log(`🎮 Sena SDK: Check Pair [${idx1}] vs [${idx2}] -> ${isMatch ? 'MATCH' : 'WRONG'}`);
if (callback) callback(isMatch);
};
// ========== GAME ACTIONS ==========
SenaSDK.prototype.start = function () {
let self = this;
// Forward to specialized SDKs
if (self.isMatchingGame && window.tdv_sdk?.start) {
console.log('🎮 Sena SDK: Forwarding start() to tdv_sdk');
window.tdv_sdk.start();
return;
}
// Standard games
self.curIndex = 0;
if (self.shuffle && self.data?.options) {
self.shuffleArray(self.data.options);
}
self.startTime = Date.now();
};
SenaSDK.prototype.markUserInteraction = function () {
if (!this._gameStartedByUser) {
this._gameStartedByUser = true;
console.log('🎮 Sena SDK: User interaction detected');
if (this.isMatchingGame && window.tdv_sdk) {
window.tdv_sdk._gameStartedByUser = true;
}
}
};
SenaSDK.prototype.canReloadData = function () {
return !this._gameStartedByUser;
};
SenaSDK.prototype.end = function (answer, callback) {
let self = this;
const category = self.gameCode ? self.gameCode.charAt(1) : '';
// ========== FORWARD TO SPECIALIZED SDKs ==========
// Crossword (G9)
if (category === '9' && window.tdv_crossword) {
let isCorrect = (window.tdv_crossword.placedCount >= window.tdv_crossword.totalCells);
let duration = window.tdv_crossword.getTimeSpent?.() || 0;
console.log(`🎮 Sena SDK: Crossword Result: ${isCorrect ? 'WIN' : 'LOSE'}`);
if (callback) callback(isCorrect);
return { isCorrect, duration };
}
// Matching/GroupSort (G3/G8)
if ((category === '8' || category === '3' || self.isMatchingGame) && window.tdv_sdk) {
console.log('🎮 Sena SDK: Forwarding end() to tdv_sdk');
let isCorrect = false;
let duration = 0;
if (String(answer || '').includes(':') && category === '8') {
isCorrect = self.validateCheck(answer);
} else {
isCorrect = window.tdv_sdk.isCorrect?.() === 1;
}
duration = window.tdv_sdk.getTimeSpent?.() || 0;
if (callback) callback(isCorrect);
return { isCorrect, duration };
}
// ========== STANDARD GAMES (Quiz/Sort/Fill) ==========
self.endTime = Date.now();
const duration = (self.endTime - self.startTime) / 1000;
const answerStr = String(answer || '');
const userAnswers = answerStr.includes('|')
? answerStr.split('|').map(a => a.trim().toLowerCase()).filter(a => a)
: [answerStr.trim().toLowerCase()].filter(a => a);
let correctAnswers = [];
if (self.correctAnswer) {
if (Array.isArray(self.correctAnswer)) {
correctAnswers = self.correctAnswer.map(a => {
if (typeof a === 'string') return a.toLowerCase();
if (a.text) return a.text.toLowerCase();
return '';
});
} else if (typeof self.correctAnswer === 'string') {
correctAnswers = self.correctAnswer.includes('|')
? self.correctAnswer.split('|').map(a => a.trim().toLowerCase())
: [self.correctAnswer.toLowerCase()];
} else if (self.correctAnswer.text) {
correctAnswers = [self.correctAnswer.text.toLowerCase()];
}
}
// Compare answers
const isStrictOrder = self.gameType === 2; // Sort game
const finalUser = isStrictOrder ? [...userAnswers] : [...userAnswers].sort();
const finalCorrect = isStrictOrder ? [...correctAnswers] : [...correctAnswers].sort();
let isCorrect = false;
const getFileName = (url) => {
if (!url.startsWith('http')) return url;
return url.split('/').pop().split('?')[0];
};
if (finalUser.length === finalCorrect.length) {
isCorrect = finalUser.every((uVal, index) => {
let cVal = finalCorrect[index];
if (uVal === cVal) return true;
if (uVal.startsWith('http') || cVal.startsWith('http')) {
return getFileName(uVal) === getFileName(cVal);
}
return false;
});
}
// Check time limit
if (self.timeLimit > 0 && duration > self.timeLimit) {
isCorrect = false;
console.log('🎮 Sena SDK: Time limit exceeded');
}
const result = {
isCorrect: isCorrect,
duration: duration,
correctAnswer: correctAnswers.join(' | '),
userAnswer: userAnswers.join(' | ')
};
console.log(`🎮 Sena SDK: Result: ${isCorrect ? 'CORRECT' : 'INCORRECT'} (${duration}s)`);
if (callback) callback(result.isCorrect);
return result;
};
SenaSDK.prototype.validateCheck = function (answer) {
let self = this;
const answerStr = String(answer || '');
const userAnswers = answerStr.includes('|')
? answerStr.split('|').map(a => a.trim().toLowerCase())
: [answerStr.trim().toLowerCase()];
let correctAnswers = [];
if (self.correctAnswer) {
if (Array.isArray(self.correctAnswer)) {
correctAnswers = self.correctAnswer.map(a => {
if (typeof a === 'string') return a.toLowerCase();
if (a.hasOwnProperty('left') && a.hasOwnProperty('right')) {
return (String(a.left) + ':' + String(a.right)).toLowerCase();
}
return '';
});
}
}
// Partial check for GroupSort
if (userAnswers.length === 1 && correctAnswers.length > 1) {
return correctAnswers.includes(userAnswers[0]);
}
// Full check
if (userAnswers.length === correctAnswers.length) {
return userAnswers.every((ans, index) => ans === correctAnswers[index]);
}
return false;
};
// ========== AUDIO ==========
SenaSDK.prototype.playVoice = function (type) {
let self = this;
let textToSpeak = '';
if (type.startsWith('option')) {
const optionIndex = parseInt(type.slice(6)) - 1;
textToSpeak = self.getOptionsValue(optionIndex);
} else if (type === 'question') {
textToSpeak = self.getQuestionValue();
} else if (type === 'request') {
textToSpeak = self.getRequestValue();
} else if (type === 'hint') {
const hintValue = self.getHintValue();
if (typeof hintValue === 'string') {
textToSpeak = hintValue;
}
}
if (textToSpeak === '') return;
if (window['audioPlayer']) {
window['audioPlayer'].pause();
window['audioPlayer'].src = textToSpeak;
window['audioPlayer'].play();
} else {
window['audioPlayer'] = new Audio(textToSpeak);
window['audioPlayer'].play();
}
};
// ========== POSTMESSAGE ==========
SenaSDK.prototype.registerPostMessageListener = function () {
let self = this;
window.addEventListener("message", function (event) {
if (!event.data || !event.data.type) return;
switch (event.data.type) {
case "SERVER_PUSH_DATA":
console.log('📥 Sena SDK: Received SERVER_PUSH_DATA');
if (self._postMessageTimeout) {
clearTimeout(self._postMessageTimeout);
self._postMessageTimeout = null;
}
self._waitingForPostMessage = false;
// Nếu trước đó load bằng server rồi
if (self._dataLoadedFromServer && !self._gameStartedByUser) {
console.log('🔄 PostMessage late, reloading data');
self.postMessageDataLoaded = false;
self._dataLoadedFromServer = false;
}
if (self._gameStartedByUser) {
console.log('⚠️ Sena SDK: PostMessage ignored (user already playing)');
return;
}
self.loadFromPostMessage(event.data.jsonData);
if (self._loadCallback) {
let cb = self._loadCallback;
self._loadCallback = null;
cb(true);
}
break;
case "SYNC_TIME":
if (event.data.end_time_iso) {
let endTimeDate = new Date(event.data.end_time_iso);
let now = new Date();
self.timeLimit = Math.max(0, Math.floor((endTimeDate - now) / 1000));
console.log('🎮 Sena SDK: Time synced:', self.timeLimit, 's');
}
break;
case "SDK_DATA_READY":
console.log('📥 Sena SDK: Received SDK_DATA_READY');
self._handleSdkDataReady(event.data.payload);
break;
case "SDK_PUSH_DATA":
console.log('📥 Sena SDK: Received SDK_PUSH_DATA');
if (event.data.payload?.items) {
self._handleSdkPushData(event.data.payload.items);
}
break;
case "SEQUENCE_SYNC": // Đổi ở đây
console.log("📥 Sena SDK: Received SEQUENCE_SYNC", event.data);
if (event.data.uuid === self.uuid) {
console.log("🔄 Sena SDK: Own message echoed back, processing...");
}
if (typeof self.onCustomMessage === "function") {
self.onCustomMessage(event.data.data, event.data.uuid);
}
break;
case "SDK_ERROR":
console.error('❌ Sena SDK: Received SDK_ERROR', event.data.payload);
break;
}
});
console.log('🎮 Sena SDK: PostMessage listener registered');
};
/**
* Send custom message to parent window with UUID
* @param {Object} data - Custom JSON data to send
*/
SenaSDK.prototype.sendMessageToParent = function (data) {
let self = this;
if (!data || typeof data !== 'object') {
console.error('❌ Sena SDK: sendMessageToParent requires data object');
return false;
}
let payload = {
type: "Sequence_Sync",
uuid: self.uuid,
data: data,
timestamp: Date.now()
};
window.parent.postMessage(payload, "*");
console.log('📤 Sena SDK: Sent Sequence_Sync to parent');
console.log('🔑 UUID:', self.uuid);
console.log('📦 Data:', data);
return true;
};
SenaSDK.prototype._handleSdkDataReady = function (payload) {
let self = this;
if (!payload?.items) {
console.error('🎮 Sena SDK: SDK_DATA_READY missing items');
return;
}
if (self._postMessageTimeout) {
clearTimeout(self._postMessageTimeout);
self._postMessageTimeout = null;
}
self._waitingForPostMessage = false;
let items = payload.items;
if (items.length === 1) {
let item = items[0];
self.data = {
question: item.question || '',
request: item.question || '',
options: item.options?.map(o => o.text || o.audio || o) || []
};
self.gameID = item.id;
} else {
self.list = items.map((item, idx) => ({
id: item.id || idx,
question: item.question || '',
options: item.options?.map(o => o.text || o) || [],
answer: null
}));
self.totalQuestions = items.length;
self.level = payload.completed_count || 0;
self._loadCurrentQuestionToData();
}
self.postMessageDataLoaded = true;
console.log('✅ Sena SDK: SDK_DATA_READY processed');
if (self._loadCallback) {
self._loadCallback(true);
self._loadCallback = null;
}
};
SenaSDK.prototype._handleSdkPushData = function (items) {
let self = this;
if (!items || items.length === 0) {
console.error('🎮 Sena SDK: SDK_PUSH_DATA missing items');
return;
}
if (self._postMessageTimeout) {
clearTimeout(self._postMessageTimeout);
self._postMessageTimeout = null;
}
self._waitingForPostMessage = false;
if (items.length === 1) {
let item = items[0];
self.data = {
question: item.question || '',
options: item.options || []
};
self.correctAnswer = item.answer;
self.gameID = item.id;
} else {
self.list = items.map((item, idx) => ({
id: item.id || idx,
question: item.question || '',
options: item.options || [],
answer: item.answer
}));
self.totalQuestions = items.length;
self.level = 0;
self._loadCurrentQuestionToData();
}
self.postMessageDataLoaded = true;
console.log('✅ Sena SDK: SDK_PUSH_DATA processed');
if (self._loadCallback) {
self._loadCallback(true);
self._loadCallback = null;
}
};
SenaSDK.prototype.getTimeSpent = function () {
if (!this.startTime) return 0;
return Math.floor((Date.now() - this.startTime) / 1000);
};
// ========== MULTI-QUESTION SUPPORT ==========
SenaSDK.prototype.play = function (selectedText, isTimeout) {
let self = this;
if (!self.currentQuestion && self.data) {
let result = { isCorrect: false, result: 0 };
self.end(selectedText, function (isCorrect) {
result.isCorrect = isCorrect;
result.result = isCorrect ? 1 : 0;
});
return result.result;
}
if (!self.currentQuestion) return 0;
let alreadyAnswered = self.userResults.find(r => r.id === self.currentQuestion.id);
if (alreadyAnswered) return alreadyAnswered.result;
let isCorrect = false;
let userChoice = null;
if (isTimeout === true || String(isTimeout) === 'true') {
isCorrect = false;
userChoice = null;
} else {
userChoice = String(selectedText).trim();
let correctAnswer = String(self.currentQuestion.answer).trim();
isCorrect = (userChoice.toLowerCase() === correctAnswer.toLowerCase());
}
let resultValue = isCorrect ? 1 : 0;
self.userResults.push({ id: self.currentQuestion.id, result: resultValue });
console.log('🎮 Sena SDK: Answer Result:', resultValue);
return resultValue;
};
SenaSDK.prototype.submitResults = function () {
let self = this;
let uniqueResults = [];
let seenIds = {};
for (let i = self.userResults.length - 1; i >= 0; i--) {
if (!seenIds[self.userResults[i].id]) {
uniqueResults.unshift(self.userResults[i]);
seenIds[self.userResults[i].id] = true;
}
}
let correctCount = uniqueResults.filter(r => r.result === 1).length;
let totalScore = self.totalQuestions > 0
? Math.round((correctCount / self.totalQuestions) * 100) / 10
: 0;
let timeSpent = Math.floor((Date.now() - self.startTime) / 1000);
let finalData = {
game_id: self.gameID,
score: totalScore,
time_spent: timeSpent,
correct_count: correctCount,
total_questions: self.totalQuestions,
details: uniqueResults
};
console.log('🎮 Sena SDK: Final Score:', totalScore);
window.parent.postMessage({
type: "FINAL_RESULT",
uuid: self.uuid,
data: finalData
}, "*");
return finalData;
};
// ========== HELPER ==========
SenaSDK.prototype.helper = {};
SenaSDK.prototype.helper.CalcObjectPositions = function (n, objectWidth, margin, maxWidth) {
let self = this;
self.positions = [];
const totalWidth = n * objectWidth + (n - 1) * margin;
const startX = (maxWidth - totalWidth) / 2;
for (let i = 0; i < n; i++) {
self.positions.push(startX + i * (objectWidth + margin) + objectWidth / 2);
}
};
SenaSDK.prototype.helper.getPosXbyIndex = function (index) {
if (index < 0 || index >= this.positions.length) return null;
return this.positions[index];
};
// ========== EXPORT ==========
if (typeof module !== 'undefined' && module.exports) {
module.exports = SenaSDK;
} else if (typeof define === 'function' && define.amd) {
define([], function () { return SenaSDK; });
} else {
window.SenaSDK = SenaSDK;
}