Compare commits

..

8 Commits

Author SHA1 Message Date
Đặng Minh Quang
2e5c6a83dc Merge main into master to update to the latest version, preferring main's changes
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-27 15:52:31 +07:00
Đặng Minh Quang
70c98caede fix bug timer live mode
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-27 15:47:41 +07:00
Đặng Minh Quang
683fe53ddd deploy cicd
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-27 15:32:51 +07:00
Đặng Minh Quang
ff8ec77f40 up lai game
All checks were successful
Deploy to Production / deploy (push) Successful in 9s
2026-01-21 15:50:54 +07:00
Đặng Minh Quang
92657bf568 Rename Source to source
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-01-21 15:37:07 +07:00
Đặng Minh Quang
3841e1da96 Update index.html
All checks were successful
Deploy to Production / deploy (push) Successful in 7s
2026-01-21 15:19:57 +07:00
silverpro89
dc642a3328 update CICD
All checks were successful
Deploy to Production / deploy (push) Successful in 7s
2026-01-21 15:17:09 +07:00
Đặng Minh Quang
bb7b411c60 Initial upload: Source and Game Build 2026-01-21 15:10:24 +07:00
41 changed files with 744 additions and 88 deletions

View File

@@ -0,0 +1,90 @@
name: Deploy to Production
on:
push:
branches:
- main
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Deploy to server
run: |
echo "🚀 Deploying to /var/www/html/games"
echo "📁 Current directory: $(pwd)"
echo "📁 GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
# Read game info from readme.md
if [ -f "$GITHUB_WORKSPACE/readme.md" ]; then
GAME_TITLE=$(sed -n '1p' "$GITHUB_WORKSPACE/readme.md")
GAME_DESC=$(sed -n '2p' "$GITHUB_WORKSPACE/readme.md")
GAME_TYPE=$(sed -n '3p' "$GITHUB_WORKSPACE/readme.md")
else
GAME_TITLE="Untitled Game"
GAME_DESC="No description"
GAME_TYPE="quiz"
fi
# Create base directory
mkdir -p /var/www/html/games/
# Find and deploy all game folders (exclude source, .git, .gitea, etc.)
DEPLOYED_FOLDERS=""
DEPLOYED_URLS=""
for folder in $GITHUB_WORKSPACE/*/; do
folder_name=$(basename "$folder")
# Skip excluded folders
if [[ "$folder_name" == "source" ]] || \
[[ "$folder_name" == ".git" ]] || \
[[ "$folder_name" == ".gitea" ]] || \
[[ "$folder_name" == "node_modules" ]] || \
[[ "$folder_name" == "logs" ]] || \
[[ "$folder_name" == "uploads" ]]; then
echo "⏭️ Skipping: $folder_name"
continue
fi
# Deploy the folder
echo "📦 Deploying: $folder_name"
rsync -av --delete "$folder" "/var/www/html/games/$folder_name/"
DEPLOYED_FOLDERS="$DEPLOYED_FOLDERS $folder_name"
GAME_URL="https://senaai.tech/games/$folder_name/"
DEPLOYED_URLS="$DEPLOYED_URLS\n 🔗 $GAME_URL"
# Get thumbnail (first image found or default)
THUMBNAIL=$(find "$folder" -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \) 2>/dev/null | head -n 1 || true)
if [ -z "$THUMBNAIL" ]; then
THUMBNAIL="https://senaai.tech/games/$folder_name/images/default.png"
else
THUMBNAIL="https://senaai.tech/games/$folder_name/$(basename "$THUMBNAIL")"
fi
# Submit game info to API
echo "📤 Submitting game info to API..."
curl --location 'http://senaai.tech:10000/api/games/save-with-check' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "title=$GAME_TITLE" \
--data-urlencode "description=$GAME_DESC" \
--data-urlencode "url=$GAME_URL" \
--data-urlencode "thumbnail=$THUMBNAIL" \
--data-urlencode "type=$GAME_TYPE" || true
echo ""
done
# Show deployment summary
echo ""
echo "=========================================="
echo "🎉 Deployment Completed Successfully!"
echo "=========================================="
echo ""
echo "📍 Deployed URLs:"
echo -e "$DEPLOYED_URLS"
echo ""
echo "=========================================="

View File

@@ -19347,6 +19347,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
this.bgMusicPlaying = false;
this.bgMusicPaused = false;
this.calculatedPositions = [];
this.customData = {
data1: "",
data2: "",
data3: "",
data4: "",
data5: ""
};
this.lastMessageData = null;
this.lastSenderUUID = "";
};
instanceProto.onDestroy = function () {
if (this.sdk) {
@@ -19376,6 +19385,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
Cnds.prototype.OnResume = function () {
return true;
};
Cnds.prototype.OnPairCorrect = function () {
return true;
};
Cnds.prototype.OnPairWrong = function () {
return true;
};
Cnds.prototype.OnMessage = function () {
return true;
};
pluginProto.cnds = new Cnds();
function Acts() {}
Acts.prototype.Load = function () {
@@ -19383,6 +19401,16 @@ cr.plugins_.SenaPlugin = function (runtime) {
var gameCode = this.properties[0] || "G2510S1T30";
if (window["SenaSDK"]) {
this.sdk = new window["SenaSDK"](gameCode);
this.sdk.onCustomMessage = function (data, senderUuid) {
console.log('Runtime received custom message from:', senderUuid);
console.log('Data:', data);
self.lastMessageData = data;
self.lastSenderUUID = senderUuid;
window["SenaTrigger"].runtime.trigger(
cr.plugins_.SenaPlugin.prototype.cnds.OnMessage,
window["SenaTrigger"]
);
};
this.sdk.load(function (success) {
if (success) {
console.log("SDK loaded successfully");
@@ -19506,16 +19534,92 @@ cr.plugins_.SenaPlugin = function (runtime) {
objectWidth,
margin,
maxWidth,
rowBreak,
rowGap,
type,
groupGap
) {
var self = this;
this.calculatedPositions = [];
var totalWidth = count * objectWidth + (count - 1) * margin;
var startX = (maxWidth - totalWidth) / 2;
for (var i = 0; i < count; i++) {
var posX = startX + i * (objectWidth + margin) + objectWidth / 2;
this.calculatedPositions.push(posX);
if (count <= 0) return;
var rows = [];
if (rowBreak > 0) {
for (var i = 0; i < count; i += rowBreak) {
rows.push(Math.min(rowBreak, count - i));
}
} else {
if (count <= 5) {
rows.push(count);
} else {
var top = Math.ceil((count + 1) / 2);
var bottom = count - top;
rows.push(top);
rows.push(bottom);
}
}
var baseY = 0;
if (type === "word") {
baseY = groupGap || (rowGap * rows.length); // word always below slot
}
var index = 0;
for (var r = 0; r < rows.length; r++) {
var itemsInRow = rows[r];
var rowWidth = itemsInRow * objectWidth + (itemsInRow - 1) * margin;
var startX = (maxWidth - rowWidth) / 2;
for (var i = 0; i < itemsInRow; i++) {
this.calculatedPositions.push({
x: startX + i * (objectWidth + margin) + objectWidth / 2,
y: baseY + r * rowGap
});
index++;
}
}
console.log("Calculated positions (multi-row):", this.calculatedPositions);
};
Acts.prototype.LoadLevelG5 = function (levelIndex) {
if (this.sdk && this.sdk.loadLevelG5) {
this.sdk.loadLevelG5(levelIndex);
}
};
Acts.prototype.CheckPair = function (idx1, idx2) {
var self = this;
if (this.sdk && this.sdk.checkPair) {
this.sdk.checkPair(idx1, idx2, function (isMatch) {
if (isMatch) {
window["SenaTrigger"].runtime.trigger(
cr.plugins_.SenaPlugin.prototype.cnds.OnPairCorrect,
window["SenaTrigger"],
);
} else {
window["SenaTrigger"].runtime.trigger(
cr.plugins_.SenaPlugin.prototype.cnds.OnPairWrong,
window["SenaTrigger"],
);
}
});
}
};
Acts.prototype.SetData = function (data1, data2, data3, data4, data5) {
this.customData.data1 = data1 || "";
this.customData.data2 = data2 || "";
this.customData.data3 = data3 || "";
this.customData.data4 = data4 || "";
this.customData.data5 = data5 || "";
console.log('Custom data set:', this.customData);
};
Acts.prototype.PostMessage = function () {
if (this.sdk && this.sdk.sendMessageToParent) {
var dataToSend = {
data1: this.customData.data1,
data2: this.customData.data2,
data3: this.customData.data3,
data4: this.customData.data4,
data5: this.customData.data5
};
this.sdk.sendMessageToParent(dataToSend);
console.log('Posted message to parent:', dataToSend);
} else {
console.error('SDK not initialized or sendMessageToParent not available');
}
console.log("Calculated positions:", this.calculatedPositions);
};
pluginProto.acts = new Acts();
function Exps() {}
@@ -19658,12 +19762,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
}
};
Exps.prototype.getPosXbyIndex = function (ret, index) {
if (
this.calculatedPositions &&
index >= 0 &&
index < this.calculatedPositions.length
) {
ret.set_float(this.calculatedPositions[index]);
if (this.calculatedPositions[index]) {
ret.set_float(this.calculatedPositions[index].x);
} else {
ret.set_float(0);
}
};
Exps.prototype.getPosYbyIndex = function (ret, index) {
if (this.calculatedPositions[index]) {
ret.set_float(this.calculatedPositions[index].y);
} else {
ret.set_float(0);
}
@@ -19703,6 +19810,59 @@ cr.plugins_.SenaPlugin = function (runtime) {
ret.set_string("");
}
};
Exps.prototype.getTimePerCard = function (ret) {
if (this.sdk && this.sdk.getTimePerCard) {
ret.set_int(this.sdk.getTimePerCard());
} else {
ret.set_int(0);
}
};
Exps.prototype.getTotalLevels = function (ret) {
if (this.sdk && this.sdk.getTotalLevels) {
ret.set_int(this.sdk.getTotalLevels());
} else {
ret.set_int(1);
}
};
Exps.prototype.getCurrentLevel = function (ret) {
if (this.sdk) {
ret.set_int(this.sdk.currentLevel || 1);
} else {
ret.set_int(1);
}
};
Exps.prototype.GetCardType = function (ret, index) {
if (this.sdk && this.sdk.getCardType) {
ret.set_string(this.sdk.getCardType(index));
} else {
ret.set_string("");
}
};
Exps.prototype.getCardID = function (ret, index) {
if (this.sdk && this.sdk.getCardID) {
ret.set_string(this.sdk.getCardID(index) || "");
} else {
ret.set_string("");
}
};
Exps.prototype.getData = function (ret, dataIndex) {
var dataKey = "data" + dataIndex;
if (this.customData && this.customData[dataKey] !== undefined) {
ret.set_string(this.customData[dataKey]);
} else {
ret.set_string("");
}
};
Exps.prototype.getLastSenderUUID = function (ret) {
ret.set_string(this.lastSenderUUID || "");
};
Exps.prototype.getLastMessageJSON = function (ret) {
if (this.lastMessageData) {
ret.set_string(JSON.stringify(this.lastMessageData));
} else {
ret.set_string("{}");
}
};
pluginProto.exps = new Exps();
})();
;
@@ -25462,8 +25622,8 @@ cr.getObjectRefTable = function () { return [
cr.plugins_.Audio,
cr.plugins_.Browser,
cr.plugins_.Function,
cr.plugins_.SenaPlugin,
cr.plugins_.Sprite,
cr.plugins_.SenaPlugin,
cr.plugins_.Text,
cr.plugins_.Touch,
cr.behaviors.Fade,
@@ -25489,13 +25649,13 @@ cr.getObjectRefTable = function () { return [
cr.plugins_.Function.prototype.cnds.OnFunction,
cr.plugins_.Sprite.prototype.acts.SetInstanceVar,
cr.plugins_.SenaPlugin.prototype.cnds.OnLoad,
cr.plugins_.SenaPlugin.prototype.acts.Start,
cr.plugins_.Sprite.prototype.acts.SetAnimFrame,
cr.plugins_.SenaPlugin.prototype.acts.CalcObjectPositions,
cr.behaviors.Rex_MoveTo.prototype.acts.SetTargetPos,
cr.plugins_.Sprite.prototype.acts.SetVisible,
cr.behaviors.Fade.prototype.acts.RestartFade,
cr.plugins_.SenaPlugin.prototype.acts.Start,
cr.plugins_.SenaPlugin.prototype.cnds.OnStart,
cr.plugins_.SenaPlugin.prototype.acts.CalcObjectPositions,
cr.system_object.prototype.cnds.Compare,
cr.plugins_.Browser.prototype.exps.ExecJS,
cr.plugins_.Function.prototype.exps.Param,

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

View File

@@ -81,7 +81,7 @@
<!-- Construct 2 exported games require jQuery. -->
<script src="jquery-3.4.1.min.js"></script>
<script src="tdv_sdk.js"></script>
<script src="sena_sdk.js"></script>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,5 @@
{
"version": 1770267014,
"version": 1772181987,
"fileList": [
"data.js",
"c2runtime.js",

View File

@@ -50,6 +50,7 @@ function SenaSDK(gid = "G2510S1T30") {
// 'preview' - Timeout 5s rồi fallback sample (testing với data thật)
// 'dev' - Load sample ngay lập tức (development)
this.mode = "preview"; // Default mode
this.role = "student"; // Default role
}
/**
@@ -118,6 +119,7 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
console.warn("⚠️ Sena SDK: tdv_sdk not found, storing data locally");
self.data = inputJson.data;
self.correctAnswer = inputJson.answer;
self._parseGameCode(); // Thêm dòng này để SDK đọc đuôi thời gian T30
if (callback) callback(true);
}
return true;
@@ -147,6 +149,19 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
audio: inputJson.data.audio || "",
hint: inputJson.data.hint || null,
};
// --- [UPDATE G5] Khởi tạo Master List cho G5 ---
if (self.gameType === 5 && self.data && self.data.options) {
// Lưu trữ danh sách gốc
self.masterList = [...self.data.options];
// Tính tổng số level
self.totalLevels = Math.ceil(self.masterList.length / 6);
self.currentLevel = 0;
// Load Level 1 ngay lập tức để self.data.options chỉ chứa 6 card đầu
self.loadLevelG5(1);
}
self.correctAnswer = inputJson.answer;
// Cũng set vào list để hỗ trợ multi-question API
@@ -166,6 +181,9 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
self.currentQuestion = self.list[0];
self.userResults = [];
// [UPDATE G4] Process G4 Data
if (self.gameType === 4) self._processG4Data();
console.log(
"✅ Sena SDK: Single question loaded -",
inputJson.description || inputJson.data.question,
@@ -234,9 +252,9 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
*/
SenaSDK.prototype._parseGameCode = function () {
let self = this;
const gameCode = self.gameCode || "G2510S1T30";
const gameCode = self.gameCode || "G4410S1T30"; // G4 mẫu
// FIX: Regex chấp nhận 2 chữ số cho Count (\d{1,2})
// Regex hỗ trợ G1-G9
const regex = /^G([1-9])(\d{1,2})([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
let match = gameCode.match(regex);
@@ -248,7 +266,14 @@ SenaSDK.prototype._parseGameCode = function () {
const shuffleFlag = match[5] !== undefined ? match[5] : "1";
const timeStr = match[6] !== undefined ? match[6] : "0";
self.shuffle = shuffleFlag === "1";
self.timeLimit = parseInt(timeStr, 10);
// --- [UPDATE G5] Logic Time Per Card ---
if (self.gameType === 5) {
self.timePerCard = parseInt(timeStr, 10); // T5 = 5s mỗi card
self.timeLimit = 0; // G5 không giới hạn tổng thời gian
} else {
self.timeLimit = parseInt(timeStr, 10);
self.timePerCard = 0;
}
}
};
@@ -350,9 +375,15 @@ SenaSDK.prototype.load = function (callback, template = "G2510S1T30") {
self.mode = urlMode.toLowerCase();
}
// THÊM 2 DÒNG NÀY: Lấy role từ URL
const urlRole = urlParams.get("role");
if (urlRole) self.role = urlRole.toLowerCase();
console.log(
"🎮 Sena SDK: Mode =",
self.mode.toUpperCase(),
"| Role =",
self.role || "student",
"| GameCode =",
self.gameCode,
);
@@ -483,6 +514,21 @@ SenaSDK.prototype._loadFromServer = function (
console.log("🎮 Sena SDK: Data shuffled immediately on load");
}
// --- [UPDATE G5] Khởi tạo Master List cho G5 ---
if (self.gameType === 5 && self.data && self.data.options) {
// Lưu trữ danh sách gốc
self.masterList = [...self.data.options];
// Tính tổng số level
self.totalLevels = Math.ceil(self.masterList.length / 6);
self.currentLevel = 0;
// Load Level 1 ngay lập tức để self.data.options chỉ chứa 6 card đầu
self.loadLevelG5(1);
}
// [UPDATE G4] Process G4 Data
if (self.gameType === 4) self._processG4Data();
console.log("🎮 Sena SDK: Data loaded for", self.gameCode);
self._dataLoadedFromServer = true;
if (callback) callback(true);
@@ -840,14 +886,12 @@ SenaSDK.prototype.guide = function () {
return guide;
};
/**
* Get the question text
* @returns {string} Question or request text
* Get the question text/url
* @returns {string} Question, request text, or URL
*/
SenaSDK.prototype.getQuestionValue = function () {
var q = String(this.data.question || "").trim();
if (q.toLowerCase().startsWith("http")) {
return "";
}
// Đã bỏ chặn URL để có thể lấy link ảnh/audio
return q;
};
/**
@@ -905,7 +949,10 @@ SenaSDK.prototype.getRequestType = function () {
* @returns {number} Number of options
*/
SenaSDK.prototype.getOptionsCount = function () {
return this.data.options.length;
if (this.data && this.data.options) {
return this.data.options.length;
}
return 0;
};
/**
@@ -991,6 +1038,9 @@ SenaSDK.prototype.getHintValue = function (index) {
SenaSDK.prototype.start = function () {
let self = this;
// LUÔN GÁN THỜI GIAN BẮT ĐẦU Ở ĐÂY
self.startTime = Date.now();
// Nếu là matching game, forward sang tdv_sdk.start()
if (self.isMatchingGame) {
console.log("🎮 Sena SDK: Matching game - forwarding start() to tdv_sdk");
@@ -1005,7 +1055,6 @@ SenaSDK.prototype.start = function () {
if (self.shuffle && self.data && self.data.options) {
self.shuffleArray(self.data.options);
}
self.startTime = Date.now();
// Additional logic for tracking can be added here if needed
};
@@ -1037,8 +1086,7 @@ SenaSDK.prototype.canReloadData = function () {
/**
* End the game and check answer
* @param {string} answer - User's answer (single text or multiple answers separated by |)
* @returns {Object} Result object with isCorrect, duration, correctAnswer, and userAnswer
* [UPDATE] Support Unordered Answers & Auto-cleanup empty strings
*/
SenaSDK.prototype.end = function (answer, callback) {
let self = this;
@@ -1071,88 +1119,98 @@ SenaSDK.prototype.end = function (answer, callback) {
self.endTime = Date.now();
const duration = (self.endTime - self.startTime) / 1000;
// Parse user answer - split by | for multiple answers
// 1. CLEANUP INPUT: Tách chuỗi, Xóa khoảng trắng, Chuyển thường, LỌC BỎ RỖNG
// .filter(a => a) sẽ loại bỏ ngay cái đuôi "" do dấu | thừa tạo ra
const userAnswers = answer.includes("|")
? answer.split("|").map((a) => a.trim().toLowerCase())
: [answer.trim().toLowerCase()];
? answer
.split("|")
.map((a) => a.trim().toLowerCase())
.filter((a) => a)
: [answer.trim().toLowerCase()].filter((a) => a);
// Get correct answer(s) from data
// 2. GET CORRECT ANSWERS
let correctAnswers = [];
if (self.correctAnswer) {
// Check if answer is an array (multiple answers) or single answer
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.toLowerCase()];
} else if (self.correctAnswer.text) {
correctAnswers = [self.correctAnswer.text.toLowerCase()];
correctAnswers = self.correctAnswer.map((a) =>
(typeof a === "object" ? a.text || "" : String(a)).trim().toLowerCase(),
);
} else {
let str =
typeof self.correctAnswer === "object"
? self.correctAnswer.text
: String(self.correctAnswer);
correctAnswers = str.includes("|")
? str.split("|").map((a) => a.trim().toLowerCase())
: [str.trim().toLowerCase()];
}
}
// Check if answer is correct
// 3. COMPARE
// Nếu là Game Type 2 (Sort) thì giữ nguyên thứ tự, nếu không thì sort (unordered)
const isStrictOrder = self.gameType === 2;
const finalUser = isStrictOrder ? [...userAnswers] : [...userAnswers].sort();
const finalCorrect = isStrictOrder
? [...correctAnswers]
: [...correctAnswers].sort();
let isCorrect = false;
// Helper to normalize and strip proxy from URL
const normalizeAnswer = (str) => {
let val = String(str || "")
.trim()
.toLowerCase();
if (val.includes("corsproxy.io/?")) {
try {
let decoded = decodeURIComponent(val.split("corsproxy.io/?")[1]);
if (decoded) val = decoded.toLowerCase().trim();
} catch (e) {}
}
return val;
};
// Helper to get filename from URL
// Helper check file name for URL matching
const getFileName = (url) => {
if (!url.startsWith("http")) return url;
const parts = url.split("/");
return parts[parts.length - 1].split("?")[0];
try {
return url.split("/").pop().split("?")[0];
} catch (e) {
return url;
}
};
const normUser = userAnswers.map(normalizeAnswer);
const normCorrect = correctAnswers.map(normalizeAnswer);
if (normUser.length === normCorrect.length) {
// For ordered multiple answers
isCorrect = normUser.every((ans, index) => {
if (ans === normCorrect[index]) return true;
// Fuzzy match for URLs
if (ans.startsWith("http") || normCorrect[index].startsWith("http")) {
return getFileName(ans) === getFileName(normCorrect[index]);
if (finalUser.length === finalCorrect.length) {
isCorrect = finalUser.every((uVal, index) => {
let cVal = finalCorrect[index];
if (uVal === cVal) return true;
// Fuzzy match cho URL (so sánh tên file ảnh)
if (uVal.startsWith("http") || cVal.startsWith("http")) {
return getFileName(uVal) === getFileName(cVal);
}
return false;
});
} else if (normUser.length === 1 && normCorrect.length === 1) {
// For single answer
const u = normUser[0];
const c = normCorrect[0];
isCorrect = u === c;
if (!isCorrect && (u.startsWith("http") || c.startsWith("http"))) {
isCorrect = getFileName(u) === getFileName(c);
}
}
// -----------------------------------------------------------
// [BƯỚC 1] Kiểm tra Time Limit TRƯỚC (Sửa biến isCorrect)
// -----------------------------------------------------------
// THÊM ĐIỀU KIỆN: Nếu là teacher thì bỏ qua kiểm tra thời gian
if (
self.role !== "teacher" &&
self.timeLimit > 0 &&
duration > self.timeLimit
) {
isCorrect = false; // CHỈ sửa biến boolean, KHÔNG gọi result.isCorrect
console.log("🎮 Sena SDK: Time Limit Exceeded -> Result set to False");
}
// -----------------------------------------------------------
// [BƯỚC 2] Sau đó mới tạo biến result (Dùng isCorrect đã chốt)
// -----------------------------------------------------------
const result = {
isCorrect: isCorrect,
isCorrect: isCorrect, // Lúc này isCorrect đã được xử lý xong xuôi
duration: duration,
correctAnswer: correctAnswers.join(" | "),
userAnswer: userAnswers.join(" | "),
};
// if time spent more than time limit, mark as incorrect
if (self.timeLimit > 0 && duration > self.timeLimit) {
result.isCorrect = false;
}
console.log(`Time spent in game: ${duration} seconds`);
console.log(`Result: ${isCorrect ? "CORRECT" : "INCORRECT"}`);
// -----------------------------------------------------------
// [BƯỚC 3] Log và Return
// -----------------------------------------------------------
console.log(`Time spent: ${duration}s`);
console.log(
`Result: ${isCorrect ? "CORRECT" : "INCORRECT"} (User: ${result.userAnswer} vs Correct: ${result.correctAnswer})`,
);
if (callback) callback(result.isCorrect);
return result; // Return full object for debug
};
SenaSDK.prototype.playVoice = function (type) {
@@ -1331,6 +1389,17 @@ SenaSDK.prototype.registerPostMessageListener = function () {
}
break;
case "SEQUENCE_SYNC":
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":
// Server gửi error
console.error("❌ Sena SDK: Received SDK_ERROR", event.data.payload);
@@ -1657,6 +1726,189 @@ SenaSDK.prototype.getCardAudio = function (index) {
}
return "";
};
/**
* [UPDATE G5] Load data cho level cụ thể (Phân trang tự nhiên)
* Logic mới: Lấy vừa đủ số lượng còn lại, không lặp lại (wrap-around) data cũ.
*/
SenaSDK.prototype.loadLevelG5 = function (levelIndex) {
let self = this;
if (self.gameType !== 5 || !self.masterList) return false;
self.currentLevel = levelIndex;
let count = 6; // Khóa cứng max 6 card/trang cho G5
window.Sena_TotalLevels = Math.ceil(self.masterList.length / count);
let startIndex = (levelIndex - 1) * count;
// --- LOGIC MỚI: CẮT DATA (SLICING) ---
// Tính điểm kết thúc: Nếu vượt quá độ dài list thì lấy độ dài list (không wrap)
let endIndex = Math.min(startIndex + count, self.masterList.length);
// Cắt danh sách card cho level hiện tại
let levelOptions = self.masterList.slice(startIndex, endIndex);
// Gán vào data.options để C2 render
self.data.options = levelOptions;
console.log(
`🎮 Sena SDK: Loaded Level ${levelIndex} (G5) with ${levelOptions.length} cards`,
);
return true;
};
/**
* [NEW G5] Lấy thông tin Level
*/
SenaSDK.prototype.getTotalLevels = function () {
return this.totalLevels || 1;
};
SenaSDK.prototype.getTimePerCard = function () {
if (this.timePerCard === undefined) {
this._parseGameCode();
}
if (this.timePerCard && this.timePerCard > 0) {
return this.timePerCard;
}
return 5;
};
SenaSDK.prototype.getCardType = function (index) {
// Ưu tiên 1: Lấy từ data.options (G4, G1, G2 đang chạy trên grid hiện tại)
if (this.data && this.data.options && this.data.options[index]) {
return this.data.options[index].type || "text";
}
// Ưu tiên 2: Fallback cho G5 (Master List)
if (this.masterList && this.masterList[index]) {
return this.masterList[index].type || "text";
}
// Mặc định
return "text";
};
// [UPDATE G4] Xử lý data đặc thù cho Memory Card: Fill blank và Xử lý thẻ lẻ (Orphan)
SenaSDK.prototype._processG4Data = function () {
let self = this;
if (!self.data.options) self.data.options = [];
// BƯỚC 1: Xử lý thẻ lẻ (Sanitize Data) ngay tại nguồn
// Đếm số lượng xuất hiện của từng cặp tên
let counts = {};
self.data.options.forEach((item) => {
if (item.type !== "blank" && item.name) {
counts[item.name] = (counts[item.name] || 0) + 1;
}
});
// Duyệt lại và biến những thẻ có số lượng < 2 thành blank
self.data.options.forEach((item) => {
if (item.type !== "blank" && item.name) {
if (counts[item.name] < 2) {
console.log("🎮 Sena SDK: Orphan card detected & removed:", item.name);
item.type = "blank";
item.name = "blank"; // Xóa tên để tránh logic game bắt nhầm
item.image = ""; // Xóa ảnh
item.id = "blank_sanitized";
}
}
});
// BƯỚC 2: Fill thêm thẻ blank cho đủ 9 ô (Logic cũ)
while (self.data.options.length < 9) {
self.data.options.push({
id: "blank_" + self.data.options.length,
type: "blank",
name: "blank",
value: -1,
image: "",
});
}
// BƯỚC 3: Shuffle (Trộn bài)
if (self.shuffle) {
self.shuffleArray(self.data.options);
}
};
// [UPDATE G4] Hàm lấy ID
SenaSDK.prototype.getCardID = function (index) {
if (this.data && this.data.options && this.data.options[index]) {
return this.data.options[index].id || "";
}
return "";
};
// [UPDATE G4] Hàm Check Pair (Logic tạm thời ở Client cho Mock)
SenaSDK.prototype.checkPair = function (idx1, idx2, callback) {
let self = this;
// Validate index
let card1 = self.data.options[idx1];
let card2 = self.data.options[idx2];
if (!card1 || !card2) {
if (callback) callback(false);
return;
}
// Logic so sánh: Name giống nhau VÀ ID khác nhau (tránh click 2 lần 1 thẻ) VÀ không phải blank
let isMatch = false;
if (card1.type !== "blank" && card2.type !== "blank") {
if (card1.id !== card2.id) {
// Check ko phải chính nó
// So sánh name (ví dụ: "dog" == "dog")
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"}`,
);
// [TODO] Sau này sẽ thay đoạn này bằng postMessage lên Server verify
if (callback) callback(isMatch);
};
/**
* [NEW v2.2] Gửi Custom Data lên Parent Window
* @param {Object} data - Object chứa 5 trường data1 -> data5
*/
SenaSDK.prototype.sendMessageToParent = function (data) {
let self = this;
// Tự động tạo UUID cho session nếu chưa có
if (!self.uuid) {
self.uuid =
"session-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
}
// Đóng gói payload đúng chuẩn tài liệu v2.2
let payload = {
type: "SEQUENCE_SYNC",
uuid: self.uuid,
data: data,
timestamp: Date.now(),
};
console.log("📤 Sena SDK: Sending SEQUENCE_SYNC to parent:", payload);
// Gửi lên Parent Window (Backend/Iframe parent)
window.parent.postMessage(payload, "*");
};
if (typeof module !== "undefined" && module.exports) {
module.exports = SenaSDK;
} else if (typeof define === "function" && define.amd) {

150
PairOrNotPair/tdv_sdk.js Normal file
View File

@@ -0,0 +1,150 @@
var tdv_sdk = {};
tdv_sdk.list = [
{
"id": "1",
"name": "dog",
"image": "https://images.senaai.vn/images/dog4.jpg",
"audio": "https://audio.senaai.vn/audio/en_female_1_dog.mp3"
},
{
"id": "2",
"name": "cat",
"image": "https://images.senaai.vn/images/cat_1.jpg",
"audio": "https://audio.senaai.vn/audio/en_female_1_cat.mp3"
},
{
"id": "3",
"name": "cow",
"image": "https://images.senaai.vn/images/cow1.jpg",
"audio": "https://audio.senaai.vn/audio/en_female_1_cow.mp3"
},
{
"id": "4",
"name": "fish",
"image": "https://images.senaai.vn/images/blue_fish1.jpg",
"audio": "https://audio.senaai.vn/audio/en_female_1_fish.mp3"
},
{
"id": "5",
"name": "bird",
"image": "https://images.senaai.vn/images/brown_bird1.jpg",
"audio": "https://audio.senaai.vn/audio/en_female_1_bird.mp3"
},
{
"id": "6",
"name": "duck",
"image": "https://images.senaai.vn/images/duck2.jpg",
"audio": "https://audio.senaai.vn/audio/en_female_1_duck.mp3"
}
];
tdv_sdk.max_question = 5; // CẤU HÌNH SỐ LƯỢNG CÂU HỎI TẠI ĐÂY
tdv_sdk.gameList = [];
tdv_sdk.currentLevel = 0;
tdv_sdk.score = 0;
tdv_sdk.shuffleArray = function(array) {
var newArray = array.slice();
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
};
tdv_sdk.start = function() {
tdv_sdk.currentLevel = 0;
tdv_sdk.score = 0;
tdv_sdk.gameList = [];
var shuffledList = tdv_sdk.shuffleArray(tdv_sdk.list);
var selectedItems = shuffledList.slice(0, tdv_sdk.max_question);
selectedItems.forEach(function(item) {
var isPair = Math.random() < 0.5;
var questionObj = {
image: item.image,
audio_image: item.audio,
is_pair: isPair
};
if (isPair) {
questionObj.text = item.name;
questionObj.audio_text = item.audio;
} else {
var distractor;
do {
distractor = tdv_sdk.list[Math.floor(Math.random() * tdv_sdk.list.length)];
} while (distractor.id === item.id);
questionObj.text = distractor.name;
questionObj.audio_text = distractor.audio;
}
tdv_sdk.gameList.push(questionObj);
});
};
tdv_sdk.getCurrentLevel = function() {
return tdv_sdk.currentLevel + 1;
};
tdv_sdk.getTotalQuestion = function() {
return tdv_sdk.max_question;
};
tdv_sdk.getCurImage = function() {
if (tdv_sdk.gameList[tdv_sdk.currentLevel]) {
return tdv_sdk.gameList[tdv_sdk.currentLevel].image;
}
return "";
};
tdv_sdk.getCurText = function() {
if (tdv_sdk.gameList[tdv_sdk.currentLevel]) {
return tdv_sdk.gameList[tdv_sdk.currentLevel].text;
}
return "";
};
tdv_sdk.isPair = function() {
if (tdv_sdk.gameList[tdv_sdk.currentLevel]) {
return tdv_sdk.gameList[tdv_sdk.currentLevel].is_pair ? 1 : 0;
}
return 0;
};
tdv_sdk.playAudio = function(url) {
if (window.audio && !window.audio.paused) {
window.audio.pause();
}
window.audio = new Audio(url);
window.audio.play();
};
tdv_sdk.playAudioImage = function() {
if (tdv_sdk.gameList[tdv_sdk.currentLevel]) {
tdv_sdk.playAudio(tdv_sdk.gameList[tdv_sdk.currentLevel].audio_image);
}
};
tdv_sdk.playAudioText = function() {
if (tdv_sdk.gameList[tdv_sdk.currentLevel]) {
tdv_sdk.playAudio(tdv_sdk.gameList[tdv_sdk.currentLevel].audio_text);
}
};
tdv_sdk.nextLevel = function() {
if (tdv_sdk.currentLevel < tdv_sdk.max_question) {
tdv_sdk.currentLevel++;
}
};
tdv_sdk.addScore = function() {
tdv_sdk.score++;
};
tdv_sdk.getScore = function() {
return tdv_sdk.score;
};

4
readme.md Normal file
View File

@@ -0,0 +1,4 @@
PairOrNotPair
Mô tả của trò chơi
PairOrNotPair

Binary file not shown.