up
All checks were successful
Deploy to Production / deploy (push) Successful in 6s

This commit is contained in:
lubukhu
2026-01-27 17:18:17 +07:00
parent 2d450da1b0
commit b68589e782
250 changed files with 448 additions and 37793 deletions

View File

@@ -1,28 +0,0 @@
{
"name": "G102-sequence",
"short_name": "G102-sequence",
"start_url": "index.html",
"display": "fullscreen",
"orientation": "any",
"icons": [{
"src": "icon-16.png",
"sizes": "16x16",
"type": "image/png"
}, {
"src": "icon-32.png",
"sizes": "32x32",
"type": "image/png"
}, {
"src": "icon-128.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "icon-256.png",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "icon-256.png",
"sizes": "256x256",
"type": "image/png"
}]
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 683 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -1,235 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>G102-sequence</title>
<!-- Standardised web app manifest -->
<link rel="manifest" href="appmanifest.json" />
<!-- Allow fullscreen mode on iOS devices. (These are Apple specific meta tags.) -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="apple-touch-icon" sizes="256x256" href="icon-256.png" />
<meta name="HandheldFriendly" content="true" />
<!-- Chrome for Android web app tags -->
<meta name="mobile-web-app-capable" content="yes" />
<link rel="shortcut icon" sizes="256x256" href="icon-256.png" />
<!-- All margins and padding must be zero for the canvas to fill the screen. -->
<style type="text/css">
* {
padding: 0;
margin: 0;
}
html, body {
color: #fff;
overflow: hidden;
touch-action: none;
-ms-touch-action: none;
}
canvas {
touch-action-delay: none;
touch-action: none;
-ms-touch-action: none;
}
</style>
</head>
<body>
<div id="fb-root"></div>
<div style="width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: -1;">
<!--<img src = "./bg.jpg" style="width:100%; height: 100%; object-fit: cover;"/> -->
<video autoplay="" loop="" muted="" style="width : 100%; height: 100%;object-fit: cover;" src="bg.mp4">
</video>
</div>
<script>
// Issue a warning if trying to preview an exported project on disk.
(function(){
// Check for running exported on file protocol
if (window.location.protocol.substr(0, 4) === "file")
{
alert("Exported games won't work until you upload them. (When running on the file:/// protocol, browsers block many features from working for security reasons.)");
}
})();
</script>
<!-- The canvas must be inside a div called c2canvasdiv -->
<div id="c2canvasdiv">
<!-- The canvas the project will render to. If you change its ID, don't forget to change the
ID the runtime looks for in the jQuery events above (ready() and cr_sizeCanvas()). -->
<canvas id="c2canvas" width="1200" height="1200">
<!-- This text is displayed if the visitor's browser does not support HTML5.
You can change it, but it is a good idea to link to a description of a browser
and provide some links to download some popular HTML5-compatible browsers. -->
<h1>Your browser does not appear to support HTML5. Try upgrading your browser to the latest version. <a href="http://www.whatbrowser.org">What is a browser?</a>
<br/><br/><a href="http://www.microsoft.com/windows/internet-explorer/default.aspx">Microsoft Internet Explorer</a><br/>
<a href="http://www.mozilla.com/firefox/">Mozilla Firefox</a><br/>
<a href="http://www.google.com/chrome/">Google Chrome</a><br/>
<a href="http://www.apple.com/safari/download/">Apple Safari</a></h1>
</canvas>
</div>
<!-- Pages load faster with scripts at the bottom -->
<!-- Construct 2 exported games require jQuery. -->
<script src="jquery-3.4.1.min.js"></script>
<script src="tdv_sdk.js"></script>
<!-- The runtime script. You can rename it, but don't forget to rename the reference here as well.
This file will have been minified and obfuscated if you enabled "Minify script" during export. -->
<script src="c2runtime.js"></script>
<script>
// Start the Construct 2 project running on window load.
jQuery(document).ready(function ()
{
// Create new runtime using the c2canvas
cr_createRuntime("c2canvas");
});
// Pause and resume on page becoming visible/invisible
function onVisibilityChanged() {
if (document.hidden || document.mozHidden || document.webkitHidden || document.msHidden)
cr_setSuspended(true);
else
cr_setSuspended(false);
};
document.addEventListener("visibilitychange", onVisibilityChanged, false);
document.addEventListener("mozvisibilitychange", onVisibilityChanged, false);
document.addEventListener("webkitvisibilitychange", onVisibilityChanged, false);
document.addEventListener("msvisibilitychange", onVisibilityChanged, false);
function OnRegisterSWError(e)
{
console.warn("Failed to register service worker: ", e);
};
// Runtime calls this global method when ready to start caching (i.e. after startup).
// This registers the service worker which caches resources for offline support.
window.C2_RegisterSW = function C2_RegisterSW()
{
if (!navigator.serviceWorker)
return; // no SW support, ignore call
try {
navigator.serviceWorker.register("sw.js", { scope: "./" })
.then(function (reg)
{
console.log("Registered service worker on " + reg.scope);
})
.catch(OnRegisterSWError);
}
catch (e)
{
OnRegisterSWError(e);
}
};
</script>
<script src="./sdk/package/dist/sena-game-sdk.js"></script>
<script>
/**
* ==============================
* SDK BRIDGE G120 (SEQUENCE)
* ==============================
* - Chỉ làm cầu nối
* - Không xử lý game logic
* - Không chấm điểm
*/
window.SDK_BRIDGE = {
sdk: null,
gameData: [],
currentIndex: 0,
gameCode: "G120",
mode: "preview"
};
// ===== INIT SDK =====
function initSDKBridge() {
var urlParams = new URLSearchParams(window.location.search);
SDK_BRIDGE.mode = urlParams.get("mode") || "preview";
SDK_BRIDGE.sdk = new SenaGameSDK({
iframePath: "./sdk/package/dist/sdk-iframe/index.html",
gameCode: SDK_BRIDGE.gameCode,
mode: SDK_BRIDGE.mode,
debug: true,
onReady: function (sdk) {
console.log("✅ [SDK BRIDGE] SDK Ready");
},
onDataReady: function (data) {
console.log("📥 [SDK BRIDGE] Data Ready:", data);
SDK_BRIDGE.gameData = data.items || [];
SDK_BRIDGE.currentIndex = 0;
// Đẩy data cho Construct 2
if (window.tdv_sdk) {
window.tdv_sdk.gameData = SDK_BRIDGE.gameData;
window.tdv_sdk.currentQuestionIndex = 0;
window.tdv_sdk.loadQuestions();
}
if (window.c2_callFunction) {
window.c2_callFunction("OnSDKDataReady", []);
}
},
onAnswerResult: function (result) {
console.log("📝 [SDK BRIDGE] Answer Result:", result);
window.answerResult = result.correct ? 1 : 0;
if (window.TDVTriger) {
window.TDVTriger.runtime.trigger(
cr.plugins_.TDVplugin.prototype.cnds.OnAnswerChecked,
window.TDVTriger
);
}
},
onGameComplete: function (result) {
console.log("🏁 [SDK BRIDGE] Game Complete:", result);
},
onError: function (err) {
console.error("❌ [SDK BRIDGE] SDK Error:", err);
}
});
}
// ===== HELPERS CHO CONSTRUCT 2 =====
function submitSequenceAnswer(sequenceArray) {
if (!SDK_BRIDGE.sdk) return;
SDK_BRIDGE.sdk.submitAnswer({
selectedAnswer: sequenceArray
});
}
function nextQuestion() {
SDK_BRIDGE.currentIndex++;
if (window.tdv_sdk) {
window.tdv_sdk.currentQuestionIndex = SDK_BRIDGE.currentIndex;
window.tdv_sdk.loadQuestions();
}
}
// Auto init
window.addEventListener("load", initSDKBridge);
</script>
</body>
</html>

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,66 +0,0 @@
{
"version": 1769236279,
"fileList": [
"data.js",
"c2runtime.js",
"jquery-3.4.1.min.js",
"offlineClient.js",
"images/sena_ui_frame_result-sheet0.png",
"images/sena_btn_replay-sheet0.png",
"images/asset3-sheet0.png",
"images/asset3-sheet1.png",
"images/answers-sheet0.png",
"images/txt_answerss.png",
"images/checker_frame-sheet0.png",
"images/btn_submit-sheet0.png",
"images/txt_instructions.png",
"images/btn_submit2-sheet0.png",
"images/btn_next-sheet0.png",
"images/btn_play-sheet0.png",
"images/btn_play-sheet1.png",
"images/bg-sheet0.png",
"images/frame_door-sheet0.png",
"images/frame_door_left-sheet0.png",
"images/frame_door_right-sheet0.png",
"images/hand_right-sheet0.png",
"images/hand_left-sheet0.png",
"images/ao_vang_quan_xanh-sheet0.png",
"images/sprite-sheet0.png",
"images/khung_thoai-sheet0.png",
"images/khung_thoai2-sheet0.png",
"images/khung_diem-sheet0.png",
"images/avatar-sheet0.png",
"images/star_-sheet0.png",
"images/frame_score-sheet0.png",
"images/dong_ho-sheet0.png",
"images/coin-sheet0.png",
"images/sena_ui_frame_intro-sheet0.png",
"images/sena_title_leaderboard-sheet0.png",
"images/sena_ui_frame_leaderboard-sheet0.png",
"images/sena_btn_exit-sheet0.png",
"images/sena_ui_item_bg-sheet0.png",
"images/sena_ui_item_bg_user_rank-sheet0.png",
"images/sena_btn_leaderboard-sheet0.png",
"images/sena_ui_number_rank-sheet0.png",
"images/sena_ui_number_rank-sheet1.png",
"images/sena_ui_number_rank-sheet2.png",
"images/sena_fx_wrong_mark-sheet0.png",
"images/sena_fx_correct_mark-sheet0.png",
"media/bg.m4a",
"media/bg.ogg",
"media/click.m4a",
"media/click.ogg",
"media/correct.m4a",
"media/correct.ogg",
"media/fail.m4a",
"media/fail.ogg",
"icon-16.png",
"icon-32.png",
"icon-114.png",
"icon-128.png",
"icon-256.png",
"loading-logo.png",
"tdv_sdk.js",
"bg.mp4"
]
}

View File

@@ -1,506 +0,0 @@
# SDK Message Protocol
Game giao tiếp với SDK thông qua **postMessage** trong hidden iframe. Game chỉ cần biết các message types, payloads, và khi nào gửi/nhận.
---
## 🔌 Architecture
```
┌──────────────────────────────┐
│ Game (React/Vue/etc) │
│ │
│ window.parent.postMessage() │
│ window.addEventListener() │
└──────────────┬───────────────┘
│ postMessage
│ (JSON)
┌──────────────────────┐
│ Hidden Iframe │
│ (sdk-iframe.html) │
│ │
│ - Sanitize data │
│ - Verify answers │
│ - Call API │
│ - Send responses │
└──────────────────────┘
```
---
## 📨 Message Types
### 1⃣ **SDK_INIT** (Game → SDK)
Game khởi tạo SDK với mode và game_code.
**Gửi:**
```javascript
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode: 'dev' | 'preview' | 'live',
game_code: 'G001' | 'G002' | ... | 'G123',
// LIVE mode only:
assignment_id?: string,
student_id?: string,
api_base_url?: string,
auth_token?: string
}
}, '*');
```
**Ví dụ DEV mode:**
```javascript
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode: 'dev',
game_code: 'G001'
}
}, '*');
```
**Ví dụ LIVE mode:**
```javascript
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode: 'live',
game_code: 'G001',
assignment_id: 'ASSIGN_123',
student_id: 'STU_456',
api_base_url: 'https://api.sena.tech',
auth_token: 'token_xyz'
}
}, '*');
```
**Nhận (SDK gửi lại khi ready):**
```javascript
{
type: 'SDK_READY',
payload: {
mode: 'dev' | 'preview' | 'live',
game_code: 'G001'
}
}
```
---
### 2⃣ **SDK_DATA_READY** (SDK → Game)
SDK gửi sanitized data cho game render. Game phải listen sự kiện này.
**Nhận:**
```javascript
window.addEventListener('message', (event) => {
if (event.data.type === 'SDK_DATA_READY') {
const {
items, // Sanitized items (NO answers!)
total_questions,
completed_count,
resume_data // Optional: previous results
} = event.data.payload;
// Render game
renderGame(items);
}
});
```
**Payload items (tùy game_code):**
#### Quiz Games (G001-G004):
```json
{
"id": "q1",
"question": "What is 2+2?",
"options": [
{"text": "5"},
{"text": "4"},
{"text": "3"}
]
}
```
#### Sequence Games (G110-G123):
```json
{
"id": "seq1",
"question": ["H", "", "L", "", "O"],
"options": ["L", "E"],
"audio_url": "https://..." // optional
}
```
---
### 3⃣ **SDK_CHECK_ANSWER** (Game → SDK)
Game gửi user's answer để SDK verify.
**Gửi:**
```javascript
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: 'q1',
choice: any, // Index (quiz) hoặc Array (sequence)
time_spent?: number // Milliseconds
}
}, '*');
```
**Quiz example (choice = index):**
```javascript
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: 'q1',
choice: 1, // User clicked option index 1
time_spent: 5000
}
}, '*');
```
**Sequence example (choice = reordered array):**
```javascript
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: 'seq1',
choice: ["H", "e", "l", "l", "o"], // Reordered
time_spent: 8000
}
}, '*');
```
---
### 4⃣ **SDK_ANSWER_RESULT** (SDK → Game)
SDK gửi kết quả verify.
**Nhận:**
```javascript
window.addEventListener('message', (event) => {
if (event.data.type === 'SDK_ANSWER_RESULT') {
const {
question_id,
correct, // true/false
score, // 0-1 hoặc custom
synced, // true = already synced to server
feedback // Optional: "✅ Correct!" or "❌ Wrong"
} = event.data.payload;
if (correct) {
showCorrectFeedback(question_id);
} else {
showWrongFeedback(question_id);
}
}
});
```
---
### 5⃣ **SDK_PUSH_DATA** (Game → SDK, PREVIEW only)
Nếu PREVIEW mode, game có thể push data thay vì SDK fetch.
**Gửi:**
```javascript
window.parent.postMessage({
type: 'SDK_PUSH_DATA',
payload: {
items: [
{
id: 'q1',
question: 'What is 2+2?',
options: [
{text: '3'},
{text: '4'},
{text: '5'}
],
answer: '4' // Server data (with answer)
},
// ... more items
]
}
}, '*');
```
SDK sẽ sanitize (remove answer, shuffle options) rồi gửi SDK_DATA_READY.
---
### 6⃣ **SDK_ERROR** (SDK → Game)
SDK gửi error notification.
**Nhận:**
```javascript
window.addEventListener('message', (event) => {
if (event.data.type === 'SDK_ERROR') {
const {
code, // Error code
message, // Error message
details // Optional: more info
} = event.data.payload;
console.error(`[SDK Error] ${code}: ${message}`);
}
});
```
---
## 🎮 Data Structures by Game Type
### Quiz Games (G001-G004)
#### Sanitized (Game receives):
```json
{
"id": "q1",
"question": "Audio URL or text",
"image_url": "Image URL (optional)",
"options": [
{"text": "Option A"} or {"audio": "Audio URL"}
]
}
```
#### User Answer Format:
```javascript
choice = 0 // Index of selected option
```
---
### Sequence Word (G110-G113)
#### Sanitized (Game receives):
```json
{
"id": "seq_word_1",
"question": ["H", "", "L", "", "O"],
"options": ["L", "E"],
"audio_url": "URL (optional)"
}
```
#### User Answer Format:
```javascript
choice = ["H", "e", "l", "l", "o"] // Reordered array
```
---
### Sequence Sentence (G120-G123)
#### Sanitized (Game receives):
```json
{
"id": "seq_sent_1",
"question": ["I", "", "reading", ""],
"options": ["love", "books"],
"audio_url": "URL (optional)"
}
```
#### User Answer Format:
```javascript
choice = ["I", "love", "reading", "books"] // Reordered
```
---
## 💻 Complete Game Implementation Example
```javascript
// ============ INITIALIZE ============
// Listen for SDK messages
window.addEventListener('message', (event) => {
handleSdkMessage(event.data);
});
// Initialize SDK
function initGame() {
const mode = 'live';
const gameCode = 'G001';
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode,
game_code: gameCode,
assignment_id: 'ASSIGN_123',
student_id: 'STU_456',
api_base_url: 'https://api.sena.tech'
}
}, '*');
}
// Handle all SDK messages
function handleSdkMessage(data) {
switch (data.type) {
case 'SDK_READY':
console.log('SDK initialized:', data.payload);
break;
case 'SDK_DATA_READY':
const items = data.payload.items;
renderGameItems(items);
break;
case 'SDK_ANSWER_RESULT':
const result = data.payload;
if (result.correct) {
showCorrectFeedback(result.question_id);
} else {
showWrongFeedback(result.question_id);
}
break;
case 'SDK_ERROR':
console.error('SDK Error:', data.payload);
break;
}
}
// ============ RENDER GAME ============
function renderGameItems(items) {
items.forEach(item => {
if (item.options) {
// Quiz game
renderQuizQuestion(item);
} else if (Array.isArray(item.question)) {
// Sequence game
renderSequenceGame(item);
}
});
}
// ============ HANDLE USER ANSWER ============
function submitAnswer(questionId, userChoice) {
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: questionId,
choice: userChoice, // Index for quiz, array for sequence
time_spent: 5000
}
}, '*');
}
// ============ GAME START ============
// Start when page loads
window.addEventListener('load', () => {
initGame();
});
```
---
## 🔄 Message Flow Examples
### DEV Mode (Local Verify)
```
Game: SDK_INIT
SDK: SDK_READY
SDK: SDK_DATA_READY (mock items)
Game: SDK_CHECK_ANSWER
SDK: SDK_ANSWER_RESULT (instantly)
```
### LIVE Mode (Server Verify)
```
Game: SDK_INIT
SDK: SDK_READY
SDK: fetch from API → SDK_DATA_READY
Game: SDK_CHECK_ANSWER
SDK: POST to server
SDK: SDK_ANSWER_RESULT (wait for server)
```
### PREVIEW Mode (Push Data)
```
Game: SDK_INIT (mode='preview')
SDK: SDK_READY
Game: SDK_PUSH_DATA (items with answers)
SDK: sanitize → SDK_DATA_READY
Game: SDK_CHECK_ANSWER
SDK: SDK_ANSWER_RESULT (local verify)
```
---
## ⚠️ Important Notes
**SDK Always Sanitizes**
- Removes `answer` field
- Removes `word`, `sentence`, `parts`, `missing_letter_count`
- Shuffles options (for quiz)
**Game Never Gets Answer**
- Game receives only question + options
- Answer is stored server-side
**Shuffled Options**
- Game receives shuffled options array
- Game user clicks index
- SDK internally resolves index → text
**Sequence Games**
- Random positions are blanked (based on `missing_letter_count`)
- Game receives question with blanks + missing items
- User reorders missing items
---
## 🚀 Testing
Test locally without SDK:
```javascript
// Simulate SDK messages for testing
function simulateSdkMessage(type, payload) {
const event = new MessageEvent('message', {
data: { type, payload }
});
window.dispatchEvent(event);
}
// Simulate SDK_DATA_READY
simulateSdkMessage('SDK_DATA_READY', {
items: [
{
id: 'q1',
question: 'What is 2+2?',
options: [{text: '4'}, {text: '5'}, {text: '3'}]
}
]
});
```

View File

@@ -1,29 +0,0 @@
/**
* Game Iframe SDK - Event Emitter
* Simple typed event emitter for SDK
*/
export type EventHandler<T = any> = (data: T) => void;
export declare class EventEmitter<Events extends Record<string, any>> {
private handlers;
/**
* Subscribe to an event
*/
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void;
/**
* Subscribe to an event (once)
*/
once<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void;
/**
* Unsubscribe from an event
*/
off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void;
/**
* Emit an event
*/
emit<K extends keyof Events>(event: K, data: Events[K]): void;
/**
* Remove all handlers for an event (or all events)
*/
removeAllListeners(event?: keyof Events): void;
}
//# sourceMappingURL=EventEmitter.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"EventEmitter.d.ts","sourceRoot":"","sources":["../src/EventEmitter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAEtD,qBAAa,YAAY,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IACxD,OAAO,CAAC,QAAQ,CAAmD;IAEnE;;OAEG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAUlF;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAQpF;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAI7E;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI;IAU7D;;OAEG;IACH,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI;CAOjD"}

View File

@@ -1,65 +0,0 @@
"use strict";
/**
* Game Iframe SDK - Event Emitter
* Simple typed event emitter for SDK
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventEmitter = void 0;
class EventEmitter {
constructor() {
this.handlers = new Map();
}
/**
* Subscribe to an event
*/
on(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event).add(handler);
// Return unsubscribe function
return () => this.off(event, handler);
}
/**
* Subscribe to an event (once)
*/
once(event, handler) {
const wrappedHandler = (data) => {
this.off(event, wrappedHandler);
handler(data);
};
return this.on(event, wrappedHandler);
}
/**
* Unsubscribe from an event
*/
off(event, handler) {
this.handlers.get(event)?.delete(handler);
}
/**
* Emit an event
*/
emit(event, data) {
this.handlers.get(event)?.forEach(handler => {
try {
handler(data);
}
catch (err) {
console.error(`[EventEmitter] Error in handler for "${String(event)}":`, err);
}
});
}
/**
* Remove all handlers for an event (or all events)
*/
removeAllListeners(event) {
if (event) {
this.handlers.delete(event);
}
else {
this.handlers.clear();
}
}
}
exports.EventEmitter = EventEmitter;
//# sourceMappingURL=EventEmitter.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"EventEmitter.js","sourceRoot":"","sources":["../src/EventEmitter.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAIH,MAAa,YAAY;IAAzB;QACY,aAAQ,GAAyC,IAAI,GAAG,EAAE,CAAC;IAwDvE,CAAC;IAtDG;;OAEG;IACH,EAAE,CAAyB,KAAQ,EAAE,OAAgC;QACjE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEvC,8BAA8B;QAC9B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,OAAgC;QACnE,MAAM,cAAc,GAAG,CAAC,IAAe,EAAE,EAAE;YACvC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,GAAG,CAAyB,KAAQ,EAAE,OAAgC;QAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,IAAe;QAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;YACxC,IAAI,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,wCAAwC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAClF,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;OAEG;IACH,kBAAkB,CAAC,KAAoB;QACnC,IAAI,KAAK,EAAE,CAAC;YACR,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;IACL,CAAC;CACJ;AAzDD,oCAyDC"}

View File

@@ -1,93 +0,0 @@
/**
* Game Iframe SDK - Core
* SDK chính - compose các layers: MessageHandler, MessageSender
*/
import { EventEmitter } from './EventEmitter';
import { MessageHandler } from './MessageHandler';
import { MessageSender } from './MessageSender';
import { GameIframeSDKConfig, SDKEvents, PushDataPayload, LeaderboardData } from './types';
/**
* GameIframeSDK - Main SDK class
* Composes MessageHandler và MessageSender
*/
export declare class GameIframeSDK extends EventEmitter<SDKEvents> {
private config;
private messageHandler;
private messageSender;
private pendingData;
private isReady;
constructor(config: GameIframeSDKConfig);
/**
* Set iframe element reference
*/
setIframe(iframe: HTMLIFrameElement | null): this;
/**
* Get current iframe
*/
getIframe(): HTMLIFrameElement | null;
/**
* Check if game is ready
*/
isGameReady(): boolean;
/**
* Check if sender is ready (iframe available)
*/
isSenderReady(): boolean;
/**
* Send game data to iframe
*/
sendGameData(data: PushDataPayload): boolean;
/**
* Send leaderboard data to iframe
*/
sendLeaderboard(data: LeaderboardData): boolean;
/**
* Queue data to be sent when game is ready
*/
queueGameData(data: PushDataPayload): this;
/**
* Clear queued data
*/
clearQueuedData(): this;
/**
* Force reload iframe
*/
reloadIframe(): boolean;
/**
* Cleanup and destroy SDK
*/
destroy(): void;
/**
* Get MessageHandler instance for advanced usage
*/
getMessageHandler(): MessageHandler;
/**
* Get MessageSender instance for advanced usage
*/
getMessageSender(): MessageSender;
/**
* Setup event forwarding from MessageHandler to SDK events
*/
private setupEventForwarding;
/**
* Send queued data
*/
private sendQueuedData;
/**
* Internal logging
*/
private log;
}
/**
* Create SDK instance
*/
export declare function createGameIframeSDK(config: GameIframeSDKConfig): GameIframeSDK;
/**
* Get or create default SDK instance
*/
export declare function getGameIframeSDK(config?: GameIframeSDKConfig): GameIframeSDK;
/**
* Destroy default instance
*/
export declare function destroyGameIframeSDK(): void;
//# sourceMappingURL=GameIframeSDK.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"GameIframeSDK.d.ts","sourceRoot":"","sources":["../src/GameIframeSDK.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EACH,mBAAmB,EAEnB,SAAS,EACT,eAAe,EACf,eAAe,EAClB,MAAM,SAAS,CAAC;AAEjB;;;GAGG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,SAAS,CAAC;IACtD,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,OAAO,CAAkB;gBAErB,MAAM,EAAE,mBAAmB;IA4BvC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IAOjD;;OAEG;IACH,SAAS,IAAI,iBAAiB,GAAG,IAAI;IAIrC;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,aAAa,IAAI,OAAO;IAQxB;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO;IAa5C;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO;IAiB/C;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAY1C;;OAEG;IACH,eAAe,IAAI,IAAI;IASvB;;OAEG;IACH,YAAY,IAAI,OAAO;IASvB;;OAEG;IACH,OAAO,IAAI,IAAI;IAYf;;OAEG;IACH,iBAAiB,IAAI,cAAc;IAInC;;OAEG;IACH,gBAAgB,IAAI,aAAa;IAQjC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAmC5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;IACH,OAAO,CAAC,GAAG;CAkBd;AAQD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAE9E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,aAAa,CAQ5E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}

View File

@@ -1,254 +0,0 @@
"use strict";
/**
* Game Iframe SDK - Core
* SDK chính - compose các layers: MessageHandler, MessageSender
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameIframeSDK = void 0;
exports.createGameIframeSDK = createGameIframeSDK;
exports.getGameIframeSDK = getGameIframeSDK;
exports.destroyGameIframeSDK = destroyGameIframeSDK;
const EventEmitter_1 = require("./EventEmitter");
const MessageHandler_1 = require("./MessageHandler");
const MessageSender_1 = require("./MessageSender");
const types_1 = require("./types");
/**
* GameIframeSDK - Main SDK class
* Composes MessageHandler và MessageSender
*/
class GameIframeSDK extends EventEmitter_1.EventEmitter {
constructor(config) {
super();
this.pendingData = null;
this.isReady = false;
this.config = { ...types_1.DEFAULT_CONFIG, ...config };
// Initialize layers
this.messageHandler = new MessageHandler_1.MessageHandler({
acceptedOrigin: this.config.iframeOrigin,
debug: this.config.debug,
});
this.messageSender = new MessageSender_1.MessageSender({
targetOrigin: this.config.iframeOrigin,
debug: this.config.debug,
});
// Setup event forwarding
this.setupEventForwarding();
// Start listening
this.messageHandler.start();
this.log('info', 'SDK initialized', { config: this.config });
}
// ==========================================================================
// PUBLIC API - Iframe Management
// ==========================================================================
/**
* Set iframe element reference
*/
setIframe(iframe) {
this.messageSender.setIframe(iframe);
this.isReady = false;
this.log('info', 'Iframe set', { hasIframe: !!iframe });
return this;
}
/**
* Get current iframe
*/
getIframe() {
return this.messageSender.getIframe();
}
/**
* Check if game is ready
*/
isGameReady() {
return this.isReady;
}
/**
* Check if sender is ready (iframe available)
*/
isSenderReady() {
return this.messageSender.isReady();
}
// ==========================================================================
// PUBLIC API - Send Data
// ==========================================================================
/**
* Send game data to iframe
*/
sendGameData(data) {
const result = this.messageSender.sendGameData(data);
if (!result.success) {
this.emit('error', {
message: 'Failed to send game data',
error: result.error,
});
}
return result.success;
}
/**
* Send leaderboard data to iframe
*/
sendLeaderboard(data) {
const result = this.messageSender.sendLeaderboard(data);
if (!result.success) {
this.emit('error', {
message: 'Failed to send leaderboard',
error: result.error,
});
}
return result.success;
}
// ==========================================================================
// PUBLIC API - Queue & Auto-send
// ==========================================================================
/**
* Queue data to be sent when game is ready
*/
queueGameData(data) {
this.pendingData = data;
this.log('info', 'Data queued for when game is ready');
// If already ready, send immediately
if (this.isReady) {
this.sendQueuedData();
}
return this;
}
/**
* Clear queued data
*/
clearQueuedData() {
this.pendingData = null;
return this;
}
// ==========================================================================
// PUBLIC API - Iframe Control
// ==========================================================================
/**
* Force reload iframe
*/
reloadIframe() {
this.isReady = false;
return this.messageSender.reloadIframe();
}
// ==========================================================================
// PUBLIC API - Lifecycle
// ==========================================================================
/**
* Cleanup and destroy SDK
*/
destroy() {
this.messageHandler.destroy();
this.removeAllListeners();
this.pendingData = null;
this.isReady = false;
this.log('info', 'SDK destroyed');
}
// ==========================================================================
// PUBLIC API - Direct Layer Access (Advanced)
// ==========================================================================
/**
* Get MessageHandler instance for advanced usage
*/
getMessageHandler() {
return this.messageHandler;
}
/**
* Get MessageSender instance for advanced usage
*/
getMessageSender() {
return this.messageSender;
}
// ==========================================================================
// PRIVATE METHODS
// ==========================================================================
/**
* Setup event forwarding from MessageHandler to SDK events
*/
setupEventForwarding() {
// Forward gameReady
this.messageHandler.on('gameReady', () => {
this.isReady = true;
this.emit('gameReady', undefined);
// Auto-send queued data if enabled
if (this.config.autoSendOnReady && this.pendingData) {
setTimeout(() => {
this.sendQueuedData();
}, this.config.readyDelay);
}
});
// Forward answerReport
this.messageHandler.on('answerReport', (data) => {
this.emit('answerReport', data);
});
// Forward finalResult
this.messageHandler.on('finalResult', (data) => {
this.emit('finalResult', data);
});
// Forward leaderboardRequest
this.messageHandler.on('leaderboardRequest', (data) => {
this.emit('leaderboardRequest', data);
});
// Forward errors
this.messageHandler.on('error', (error) => {
this.emit('error', error);
});
}
/**
* Send queued data
*/
sendQueuedData() {
if (this.pendingData) {
this.sendGameData(this.pendingData);
this.pendingData = null;
}
}
/**
* Internal logging
*/
log(level, message, data) {
if (this.config.debug) {
const prefix = '[GameIframeSDK]';
switch (level) {
case 'info':
console.log(prefix, message, data ?? '');
break;
case 'warn':
console.warn(prefix, message, data ?? '');
break;
case 'error':
console.error(prefix, message, data ?? '');
break;
}
}
this.emit('log', { level, message, data });
}
}
exports.GameIframeSDK = GameIframeSDK;
// ==========================================================================
// FACTORY / SINGLETON HELPERS
// ==========================================================================
let defaultInstance = null;
/**
* Create SDK instance
*/
function createGameIframeSDK(config) {
return new GameIframeSDK(config);
}
/**
* Get or create default SDK instance
*/
function getGameIframeSDK(config) {
if (!defaultInstance && config) {
defaultInstance = new GameIframeSDK(config);
}
if (!defaultInstance) {
throw new Error('GameIframeSDK not initialized. Call with config first.');
}
return defaultInstance;
}
/**
* Destroy default instance
*/
function destroyGameIframeSDK() {
defaultInstance?.destroy();
defaultInstance = null;
}
//# sourceMappingURL=GameIframeSDK.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,70 +0,0 @@
/**
* Game Iframe SDK - Message Handler
* Xử lý message từ iframe
*/
import { AnswerReportData, FinalResultData } from './types';
import { EventEmitter } from './EventEmitter';
export interface MessageHandlerEvents {
gameReady: void;
answerReport: AnswerReportData;
finalResult: FinalResultData;
leaderboardRequest: {
top: number;
};
unknownMessage: {
type: string;
data: any;
};
error: {
message: string;
error?: any;
};
}
export interface MessageHandlerConfig {
/**
* Accepted origin (use '*' to accept all - not recommended for production)
*/
acceptedOrigin: string;
/**
* Enable debug logging
*/
debug?: boolean;
}
/**
* MessageHandler - Xử lý incoming messages từ iframe
*/
export declare class MessageHandler extends EventEmitter<MessageHandlerEvents> {
private config;
private boundHandler;
private isListening;
constructor(config: MessageHandlerConfig);
/**
* Start listening for messages
*/
start(): this;
/**
* Stop listening for messages
*/
stop(): this;
/**
* Check if handler is listening
*/
isActive(): boolean;
/**
* Handle incoming message
*/
private handleMessage;
/**
* Check if origin is allowed
*/
private isOriginAllowed;
/**
* Debug log
*/
private log;
/**
* Cleanup
*/
destroy(): void;
}
//# sourceMappingURL=MessageHandler.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAiB,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACjC,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,WAAW,EAAE,eAAe,CAAC;IAC7B,kBAAkB,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC;IAC5C,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;CAC3C;AAED,MAAM,WAAW,oBAAoB;IACjC;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAClE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,EAAE,oBAAoB;IAKxC;;OAEG;IACH,KAAK,IAAI,IAAI;IAab;;OAEG;IACH,IAAI,IAAI,IAAI;IAWZ;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,OAAO,CAAC,aAAa;IAyCrB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,OAAO,IAAI,IAAI;CAIlB"}

View File

@@ -1,115 +0,0 @@
"use strict";
/**
* Game Iframe SDK - Message Handler
* Xử lý message từ iframe
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageHandler = void 0;
const types_1 = require("./types");
const EventEmitter_1 = require("./EventEmitter");
/**
* MessageHandler - Xử lý incoming messages từ iframe
*/
class MessageHandler extends EventEmitter_1.EventEmitter {
constructor(config) {
super();
this.boundHandler = null;
this.isListening = false;
this.config = config;
}
/**
* Start listening for messages
*/
start() {
if (this.isListening) {
return this;
}
this.boundHandler = this.handleMessage.bind(this);
window.addEventListener('message', this.boundHandler);
this.isListening = true;
this.log('MessageHandler started');
return this;
}
/**
* Stop listening for messages
*/
stop() {
if (this.boundHandler) {
window.removeEventListener('message', this.boundHandler);
this.boundHandler = null;
}
this.isListening = false;
this.log('MessageHandler stopped');
return this;
}
/**
* Check if handler is listening
*/
isActive() {
return this.isListening;
}
/**
* Handle incoming message
*/
handleMessage(event) {
// Origin check
if (!this.isOriginAllowed(event.origin)) {
return;
}
const { type, data } = event.data || {};
if (!type)
return;
this.log(`Received: ${type}`, data);
try {
switch (type) {
case types_1.MESSAGE_TYPES.GAME_READY:
this.emit('gameReady', undefined);
break;
case types_1.MESSAGE_TYPES.ANSWER_REPORT:
// Raw data pass-through
this.emit('answerReport', data);
break;
case types_1.MESSAGE_TYPES.FINAL_RESULT:
// Raw data pass-through
this.emit('finalResult', data);
break;
case types_1.MESSAGE_TYPES.GET_LEADERBOARD:
this.emit('leaderboardRequest', { top: data?.top || 10 });
break;
default:
this.emit('unknownMessage', { type, data });
break;
}
}
catch (error) {
const err = error;
this.emit('error', { message: `Error handling ${type}`, error: err });
}
}
/**
* Check if origin is allowed
*/
isOriginAllowed(origin) {
if (this.config.acceptedOrigin === '*') {
return true;
}
return origin === this.config.acceptedOrigin;
}
/**
* Debug log
*/
log(message, data) {
if (this.config.debug) {
console.log('[MessageHandler]', message, data ?? '');
}
}
/**
* Cleanup
*/
destroy() {
this.stop();
this.removeAllListeners();
}
}
exports.MessageHandler = MessageHandler;
//# sourceMappingURL=MessageHandler.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"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"}

View File

@@ -1,60 +0,0 @@
/**
* Game Iframe SDK - Message Sender
* Gửi message đến iframe
*/
import { PushDataPayload, LeaderboardData } from './types';
export interface MessageSenderConfig {
/**
* Target origin for postMessage
*/
targetOrigin: string;
/**
* Enable debug logging
*/
debug?: boolean;
}
export interface SendResult {
success: boolean;
error?: Error;
}
/**
* MessageSender - Gửi messages đến iframe
*/
export declare class MessageSender {
private config;
private iframe;
constructor(config: MessageSenderConfig);
/**
* Set iframe element
*/
setIframe(iframe: HTMLIFrameElement | null): this;
/**
* Get current iframe
*/
getIframe(): HTMLIFrameElement | null;
/**
* Check if iframe is available
*/
isReady(): boolean;
/**
* Send raw message to iframe
*/
sendRaw(message: any): SendResult;
/**
* Send game data (SERVER_PUSH_DATA)
*/
sendGameData(payload: PushDataPayload): SendResult;
/**
* Send leaderboard (SERVER_PUSH_LEADERBOARD)
*/
sendLeaderboard(data: LeaderboardData): SendResult;
/**
* Reload iframe
*/
reloadIframe(): boolean;
/**
* Debug log
*/
private log;
}
//# sourceMappingURL=MessageSender.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageSender.d.ts","sourceRoot":"","sources":["../src/MessageSender.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAiB,MAAM,SAAS,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAChC;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAkC;gBAEpC,MAAM,EAAE,mBAAmB;IAIvC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IAKjD;;OAEG;IACH,SAAS,IAAI,iBAAiB,GAAG,IAAI;IAIrC;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;OAEG;IACH,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU;IAmBjC;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,UAAU;IAoBlD;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,UAAU;IAmBlD;;OAEG;IACH,YAAY,IAAI,OAAO;IAqBvB;;OAEG;IACH,OAAO,CAAC,GAAG;CAYd"}

View File

@@ -1,132 +0,0 @@
"use strict";
/**
* Game Iframe SDK - Message Sender
* Gửi message đến iframe
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageSender = void 0;
const types_1 = require("./types");
/**
* MessageSender - Gửi messages đến iframe
*/
class MessageSender {
constructor(config) {
this.iframe = null;
this.config = config;
}
/**
* Set iframe element
*/
setIframe(iframe) {
this.iframe = iframe;
return this;
}
/**
* Get current iframe
*/
getIframe() {
return this.iframe;
}
/**
* Check if iframe is available
*/
isReady() {
return !!this.iframe?.contentWindow;
}
/**
* Send raw message to iframe
*/
sendRaw(message) {
if (!this.iframe?.contentWindow) {
return {
success: false,
error: new Error('Iframe not available'),
};
}
try {
this.iframe.contentWindow.postMessage(message, this.config.targetOrigin);
this.log('Sent message', { type: message.type });
return { success: true };
}
catch (error) {
const err = error;
this.log('Send failed', { error: err.message });
return { success: false, error: err };
}
}
/**
* Send game data (SERVER_PUSH_DATA)
*/
sendGameData(payload) {
// Inline message creation
const message = {
type: types_1.MESSAGE_TYPES.SERVER_PUSH_DATA,
jsonData: payload,
};
const result = this.sendRaw(message);
if (result.success) {
const dataLength = payload.data?.length || 0;
this.log('Sent game data', {
game_id: payload.game_id,
items: dataLength,
});
}
return result;
}
/**
* Send leaderboard (SERVER_PUSH_LEADERBOARD)
*/
sendLeaderboard(data) {
// Inline message creation
const message = {
type: types_1.MESSAGE_TYPES.SERVER_PUSH_LEADERBOARD,
leaderboardData: data,
};
const result = this.sendRaw(message);
if (result.success) {
this.log('Sent leaderboard', {
players: data.top_players?.length || 0,
hasUserRank: !!data.user_rank,
});
}
return result;
}
/**
* Reload iframe
*/
reloadIframe() {
if (!this.iframe) {
return false;
}
const currentSrc = this.iframe.src;
if (!currentSrc || currentSrc === 'about:blank') {
return false;
}
this.iframe.src = '';
setTimeout(() => {
if (this.iframe) {
this.iframe.src = currentSrc;
this.log('Iframe reloaded');
}
}, 100);
return true;
}
/**
* Debug log
*/
log(message, data) {
if (this.config.debug) {
console.log('[MessageSender]', message);
if (data) {
try {
console.log(JSON.stringify(data, null, 2));
}
catch (e) {
console.log(data);
}
}
}
}
}
exports.MessageSender = MessageSender;
//# sourceMappingURL=MessageSender.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../src/MessageSender.ts"],"names":[],"mappings":"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"}

View File

@@ -1,49 +0,0 @@
/**
* Data Validator
* Verify data structure cho từng game code
*
* Usage:
* ```typescript
* import { validateGameData, DataValidator } from 'game-iframe-sdk/client';
*
* const result = validateGameData('G001', receivedData);
* if (!result.valid) {
* console.error('Invalid data:', result.errors);
* }
* ```
*/
import { GameCode } from '../kit/GameDataHandler';
export interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
export interface FieldSchema {
type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any';
required: boolean;
arrayItemType?: 'string' | 'number' | 'object' | 'any';
description?: string;
}
export interface ItemSchema {
[field: string]: FieldSchema;
}
/**
* Validate game data payload
*/
export declare function validateGameData(gameCode: GameCode, payload: any): ValidationResult;
/**
* Get schema for a game code
*/
export declare function getSchema(gameCode: GameCode): ItemSchema | null;
/**
* Get schema documentation for a game code
*/
export declare function getSchemaDoc(gameCode: GameCode): string;
export declare class DataValidator {
private gameCode;
constructor(gameCode: GameCode);
validate(payload: any): ValidationResult;
getSchema(): ItemSchema | null;
getSchemaDoc(): string;
}
//# sourceMappingURL=DataValidator.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"DataValidator.d.ts","sourceRoot":"","sources":["../../src/client/DataValidator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAc,MAAM,wBAAwB,CAAC;AAM9D,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnE,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACvB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC;CAChC;AAoJD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,GAAG,gBAAgB,CAmDnF;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI,CAE/D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAyBvD;AAMD,qBAAa,aAAa;IACtB,OAAO,CAAC,QAAQ,CAAW;gBAEf,QAAQ,EAAE,QAAQ;IAI9B,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,gBAAgB;IAIxC,SAAS,IAAI,UAAU,GAAG,IAAI;IAI9B,YAAY,IAAI,MAAM;CAGzB"}

View File

@@ -1,252 +0,0 @@
"use strict";
/**
* Data Validator
* Verify data structure cho từng game code
*
* Usage:
* ```typescript
* import { validateGameData, DataValidator } from 'game-iframe-sdk/client';
*
* const result = validateGameData('G001', receivedData);
* if (!result.valid) {
* console.error('Invalid data:', result.errors);
* }
* ```
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataValidator = void 0;
exports.validateGameData = validateGameData;
exports.getSchema = getSchema;
exports.getSchemaDoc = getSchemaDoc;
const GameDataHandler_1 = require("../kit/GameDataHandler");
// =============================================================================
// SCHEMAS FOR EACH GAME CODE
// =============================================================================
const QUIZ_BASE_SCHEMA = {
id: { type: 'string', required: true, description: 'Unique question ID' },
options: { type: 'array', required: true, arrayItemType: 'string', description: 'Answer options' },
answer: { type: 'number', required: true, description: 'Correct answer index (0-based)' },
};
const SCHEMAS = {
// Quiz variants
G001: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
},
G002: {
...QUIZ_BASE_SCHEMA,
question_audio: { type: 'string', required: true, description: 'Audio URL for question' },
},
G003: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
// options are audio URLs
},
G004: {
...QUIZ_BASE_SCHEMA,
question_image: { type: 'string', required: true, description: 'Image URL for question' },
question: { type: 'string', required: false, description: 'Optional text hint' },
},
// G005: Quiz Text-Image (options are image URLs, client picks index)
G005: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
// options are image URLs, answer is index pointing to correct image
},
// Sequence Word variants
G110: {
id: { type: 'string', required: true },
word: { type: 'string', required: true, description: 'The word to arrange' },
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Letters/parts to arrange' },
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct order' },
},
G111: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true, description: 'Audio hint URL' },
},
G112: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G113: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
// Sequence Sentence variants
G120: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false, description: 'Full sentence (hint)' },
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Words to arrange' },
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct word order' },
},
G121: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G122: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G123: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
}
};
// =============================================================================
// VALIDATOR
// =============================================================================
/**
* Validate a single item against schema
*/
function validateItem(item, schema, itemIndex) {
const errors = [];
if (!item || typeof item !== 'object') {
errors.push(`Item [${itemIndex}]: Must be an object`);
return errors;
}
for (const [field, fieldSchema] of Object.entries(schema)) {
const value = item[field];
// Check required
if (fieldSchema.required && (value === undefined || value === null)) {
errors.push(`Item [${itemIndex}].${field}: Required field is missing`);
continue;
}
// Skip validation if optional and not present
if (!fieldSchema.required && (value === undefined || value === null)) {
continue;
}
// Check type
const actualType = Array.isArray(value) ? 'array' : typeof value;
if (fieldSchema.type !== 'any' && actualType !== fieldSchema.type) {
errors.push(`Item [${itemIndex}].${field}: Expected ${fieldSchema.type}, got ${actualType}`);
continue;
}
// Check array items
if (fieldSchema.type === 'array' && fieldSchema.arrayItemType && fieldSchema.arrayItemType !== 'any') {
for (let i = 0; i < value.length; i++) {
const itemType = typeof value[i];
if (itemType !== fieldSchema.arrayItemType) {
errors.push(`Item [${itemIndex}].${field}[${i}]: Expected ${fieldSchema.arrayItemType}, got ${itemType}`);
}
}
}
}
return errors;
}
/**
* Validate game data payload
*/
function validateGameData(gameCode, payload) {
const errors = [];
const warnings = [];
// Check game code
if (!GameDataHandler_1.GAME_CODES[gameCode]) {
errors.push(`Unknown game code: ${gameCode}`);
return { valid: false, errors, warnings };
}
// Check payload structure
if (!payload || typeof payload !== 'object') {
errors.push('Payload must be an object');
return { valid: false, errors, warnings };
}
// Check data array
const items = payload.data || payload.items || payload.questions;
if (!items) {
errors.push('Missing data array (expected "data", "items", or "questions")');
return { valid: false, errors, warnings };
}
if (!Array.isArray(items)) {
errors.push('"data" must be an array');
return { valid: false, errors, warnings };
}
if (items.length === 0) {
warnings.push('Data array is empty');
}
// Validate each item
const schema = SCHEMAS[gameCode];
for (let i = 0; i < items.length; i++) {
const itemErrors = validateItem(items[i], schema, i);
errors.push(...itemErrors);
}
// Check for duplicate IDs
const ids = items.map((item) => item.id).filter(Boolean);
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
if (duplicates.length > 0) {
warnings.push(`Duplicate IDs found: ${[...new Set(duplicates)].join(', ')}`);
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Get schema for a game code
*/
function getSchema(gameCode) {
return SCHEMAS[gameCode] ?? null;
}
/**
* Get schema documentation for a game code
*/
function getSchemaDoc(gameCode) {
const schema = SCHEMAS[gameCode];
if (!schema)
return `Unknown game code: ${gameCode}`;
const gameInfo = GameDataHandler_1.GAME_CODES[gameCode];
const lines = [
`## ${gameCode}: ${gameInfo.name}`,
`Category: ${gameInfo.category}`,
'',
'### Fields:',
];
for (const [field, fieldSchema] of Object.entries(schema)) {
const required = fieldSchema.required ? '(required)' : '(optional)';
let type = fieldSchema.type;
if (fieldSchema.arrayItemType) {
type = `${fieldSchema.type}<${fieldSchema.arrayItemType}>`;
}
lines.push(`- **${field}**: ${type} ${required}`);
if (fieldSchema.description) {
lines.push(` - ${fieldSchema.description}`);
}
}
return lines.join('\n');
}
// =============================================================================
// DATA VALIDATOR CLASS
// =============================================================================
class DataValidator {
constructor(gameCode) {
this.gameCode = gameCode;
}
validate(payload) {
return validateGameData(this.gameCode, payload);
}
getSchema() {
return getSchema(this.gameCode);
}
getSchemaDoc() {
return getSchemaDoc(this.gameCode);
}
}
exports.DataValidator = DataValidator;
//# sourceMappingURL=DataValidator.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,146 +0,0 @@
/**
* GameClientSDK - SDK dành cho Game Iframe
*
* Sử dụng trong game để:
* - Tự động xác định mode (preview/live) từ URL
* - Nhận data từ parent (preview) hoặc fetch API (live)
* - Verify answers locally
* - Report results về parent
*/
import { GameCode } from '../kit/GameDataHandler';
import { ValidationResult } from './DataValidator';
export type ClientMode = 'preview' | 'live' | 'dev';
export interface ClientSDKConfig {
debug?: boolean;
apiBaseUrl?: string;
getAuthHeaders?: () => Record<string, string>;
}
export interface URLParams {
mode: ClientMode;
gameCode: GameCode;
gameId?: string;
lid?: string;
studentId?: string;
}
export interface GameDataPayload {
game_id: string;
game_code: GameCode;
data: any[];
completed_question_ids?: Array<{
id: string;
result: 0 | 1;
}>;
}
export interface AnswerResult {
isCorrect: boolean;
score: number;
feedback?: string;
}
export interface FinalResult {
score: number;
total: number;
correct: number;
wrong: number;
details: Array<{
question_id: string;
choice: any;
result: 0 | 1;
time_spent: number;
}>;
}
export interface ClientSDKEvents {
ready: void;
dataReceived: {
items: any[];
resumeData?: any[];
validation?: ValidationResult;
};
error: {
message: string;
error?: any;
};
modeDetected: {
mode: ClientMode;
params: URLParams;
};
validationError: {
validation: ValidationResult;
};
}
type EventHandler<T> = (data: T) => void;
declare class SimpleEventEmitter<Events extends Record<string, any>> {
private handlers;
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void;
off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void;
protected emit<K extends keyof Events>(event: K, data: Events[K]): void;
}
export declare class GameClientSDK extends SimpleEventEmitter<ClientSDKEvents> {
private config;
private params;
private mode;
private originalItems;
private sanitizedItems;
private userAnswers;
private isInitialized;
private startTime;
constructor(config?: ClientSDKConfig);
/**
* Get current mode
*/
getMode(): ClientMode;
/**
* Get URL params
*/
getParams(): URLParams;
/**
* Get game code
*/
getGameCode(): GameCode;
/**
* Get sanitized items (safe for rendering)
*/
getItems(): any[];
/**
* Submit an answer and get verification result
*/
submitAnswer(questionId: string, choice: any): AnswerResult;
/**
* Get final result
*/
getFinalResult(): FinalResult;
/**
* Report final result to parent
*/
reportFinalResult(result?: FinalResult): void;
/**
* Request leaderboard from parent
*/
requestLeaderboard(top?: number): void;
/**
* Cleanup
*/
destroy(): void;
private parseURLParams;
private setupMessageListener;
private handleMessage;
private initialize;
/**
* Load mock data for dev mode
*/
private loadMockData;
private sendGameReady;
private fetchLiveData;
private handleDataReceived;
private sendAnswerReport;
private log;
}
/**
* Get or create GameClientSDK instance
*/
export declare function getGameClientSDK(config?: ClientSDKConfig): GameClientSDK;
/**
* Destroy client instance
*/
export declare function destroyGameClientSDK(): void;
export {};
//# sourceMappingURL=GameClientSDK.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"GameClientSDK.d.ts","sourceRoot":"","sources":["../../src/client/GameClientSDK.ts"],"names":[],"mappings":"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"}

View File

@@ -1,365 +0,0 @@
"use strict";
/**
* GameClientSDK - SDK dành cho Game Iframe
*
* Sử dụng trong game để:
* - Tự động xác định mode (preview/live) từ URL
* - Nhận data từ parent (preview) hoặc fetch API (live)
* - Verify answers locally
* - Report results về parent
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameClientSDK = void 0;
exports.getGameClientSDK = getGameClientSDK;
exports.destroyGameClientSDK = destroyGameClientSDK;
const GameDataHandler_1 = require("../kit/GameDataHandler");
const MockData_1 = require("./MockData");
const DataValidator_1 = require("./DataValidator");
class SimpleEventEmitter {
constructor() {
this.handlers = new Map();
}
on(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event).add(handler);
return () => this.off(event, handler);
}
off(event, handler) {
this.handlers.get(event)?.delete(handler);
}
emit(event, data) {
this.handlers.get(event)?.forEach(handler => {
try {
handler(data);
}
catch (err) {
console.error(`[GameClientSDK] Error in ${String(event)} handler:`, err);
}
});
}
}
// =============================================================================
// GAME CLIENT SDK
// =============================================================================
class GameClientSDK extends SimpleEventEmitter {
constructor(config = {}) {
super();
// Data storage
this.originalItems = new Map(); // Có đáp án
this.sanitizedItems = []; // Không có đáp án
this.userAnswers = new Map();
this.isInitialized = false;
this.startTime = 0;
this.config = {
debug: config.debug ?? false,
apiBaseUrl: config.apiBaseUrl ?? '',
getAuthHeaders: config.getAuthHeaders ?? (() => ({})),
};
// Parse URL params
this.params = this.parseURLParams();
this.mode = this.params.mode;
this.log('info', 'SDK created', { mode: this.mode, params: this.params });
// Emit mode detected
this.emit('modeDetected', { mode: this.mode, params: this.params });
// Setup message listener
this.setupMessageListener();
// Auto-initialize based on mode
this.initialize();
}
// ==========================================================================
// PUBLIC API
// ==========================================================================
/**
* Get current mode
*/
getMode() {
return this.mode;
}
/**
* Get URL params
*/
getParams() {
return { ...this.params };
}
/**
* Get game code
*/
getGameCode() {
return this.params.gameCode;
}
/**
* Get sanitized items (safe for rendering)
*/
getItems() {
return this.sanitizedItems;
}
/**
* Submit an answer and get verification result
*/
submitAnswer(questionId, choice) {
const originalItem = this.originalItems.get(questionId);
if (!originalItem) {
this.log('warn', `Item not found: ${questionId}`);
return { isCorrect: false, score: 0, feedback: 'Question not found' };
}
// Verify using GameDataHandler
const result = (0, GameDataHandler_1.checkAnswer)(this.params.gameCode, originalItem, choice);
// Store user answer
const timeSpent = Date.now() - (this.userAnswers.size === 0 ? this.startTime : Date.now());
this.userAnswers.set(questionId, {
choice,
result: result.isCorrect ? 1 : 0,
time: timeSpent,
});
// Report to parent
this.sendAnswerReport(questionId, choice, result.isCorrect ? 1 : 0, timeSpent);
this.log('info', `Answer submitted: ${questionId}`, { choice, result });
return result;
}
/**
* Get final result
*/
getFinalResult() {
const details = Array.from(this.userAnswers.entries()).map(([id, data]) => ({
question_id: id,
choice: data.choice,
result: data.result,
time_spent: data.time,
}));
const correct = details.filter(d => d.result === 1).length;
const total = this.originalItems.size;
return {
score: total > 0 ? Math.round((correct / total) * 100) : 0,
total,
correct,
wrong: total - correct,
details,
};
}
/**
* Report final result to parent
*/
reportFinalResult(result) {
const finalResult = result ?? this.getFinalResult();
window.parent.postMessage({
type: 'FINAL_RESULT',
data: finalResult,
}, '*');
this.log('info', 'Final result reported', finalResult);
}
/**
* Request leaderboard from parent
*/
requestLeaderboard(top = 10) {
window.parent.postMessage({
type: 'GET_LEADERBOARD',
data: { top },
}, '*');
}
/**
* Cleanup
*/
destroy() {
window.removeEventListener('message', this.handleMessage);
this.originalItems.clear();
this.sanitizedItems = [];
this.userAnswers.clear();
this.log('info', 'SDK destroyed');
}
// ==========================================================================
// PRIVATE METHODS
// ==========================================================================
parseURLParams() {
const searchParams = new URLSearchParams(window.location.search);
const mode = (searchParams.get('mode') || 'preview');
const gameCode = (searchParams.get('game_code') || 'G001');
const gameId = searchParams.get('game_id') || undefined;
const lid = searchParams.get('lid') || undefined;
const studentId = searchParams.get('student_id') || undefined;
// Validate mode
if (mode !== 'preview' && mode !== 'live' && mode !== 'dev') {
this.log('warn', `Invalid mode: ${mode}, defaulting to preview`);
}
// Validate game code
if (!GameDataHandler_1.GAME_CODES[gameCode]) {
this.log('warn', `Unknown game code: ${gameCode}`);
}
return { mode, gameCode, gameId, lid, studentId };
}
setupMessageListener() {
this.handleMessage = this.handleMessage.bind(this);
window.addEventListener('message', this.handleMessage);
}
handleMessage(event) {
const { type, jsonData, leaderboardData } = event.data || {};
this.log('debug', 'Message received', { type, hasData: !!jsonData });
switch (type) {
case 'SERVER_PUSH_DATA':
if (jsonData) {
this.handleDataReceived(jsonData);
}
break;
case 'SERVER_PUSH_LEADERBOARD':
if (leaderboardData) {
this.log('info', 'Leaderboard received', leaderboardData);
// Could emit event here
}
break;
}
}
async initialize() {
// Send GAME_READY immediately
this.sendGameReady();
if (this.mode === 'dev') {
// Dev mode: load mock data immediately
this.log('info', 'DEV MODE: Loading mock data...');
this.loadMockData();
}
else if (this.mode === 'live') {
// Live mode: fetch data from API
await this.fetchLiveData();
}
else {
// Preview mode: wait for postMessage
this.log('info', 'Preview mode: waiting for SERVER_PUSH_DATA...');
}
}
/**
* Load mock data for dev mode
*/
loadMockData() {
const mockData = (0, MockData_1.getMockData)(this.params.gameCode);
if (!mockData) {
this.emit('error', {
message: `No mock data available for game code: ${this.params.gameCode}`
});
return;
}
this.log('info', `Loaded mock data for ${this.params.gameCode}`);
this.handleDataReceived(mockData);
}
sendGameReady() {
window.parent.postMessage({ type: 'GAME_READY' }, '*');
this.emit('ready', undefined);
this.log('info', 'GAME_READY sent');
}
async fetchLiveData() {
const { gameId, lid } = this.params;
if (!gameId || !lid) {
this.emit('error', { message: 'Live mode requires game_id and lid' });
return;
}
if (!this.config.apiBaseUrl) {
this.emit('error', { message: 'Live mode requires apiBaseUrl' });
return;
}
try {
const url = `${this.config.apiBaseUrl}/games/${gameId}?lid=${lid}`;
const headers = {
'Content-Type': 'application/json',
...this.config.getAuthHeaders(),
};
this.log('info', `Fetching live data: ${url}`);
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
this.handleDataReceived(data);
}
catch (error) {
this.log('error', 'Failed to fetch live data', error);
this.emit('error', { message: 'Failed to fetch game data', error });
}
}
handleDataReceived(payload) {
this.startTime = Date.now();
// Update game code if provided
if (payload.game_code && GameDataHandler_1.GAME_CODES[payload.game_code]) {
this.params.gameCode = payload.game_code;
}
// Validate data structure
const validation = (0, DataValidator_1.validateGameData)(this.params.gameCode, payload);
if (!validation.valid) {
this.log('error', 'Data validation failed', validation.errors);
this.emit('validationError', { validation });
// Continue anyway to allow partial rendering
}
if (validation.warnings.length > 0) {
this.log('warn', 'Data validation warnings', validation.warnings);
}
// Extract items from various payload formats
const items = payload.data || payload.items || payload.questions || [];
const resumeData = payload.completed_question_ids || [];
// Store original items (with answers)
this.originalItems.clear();
items.forEach((item) => {
if (item.id) {
this.originalItems.set(item.id, item);
}
});
// Sanitize for client (remove answers)
this.sanitizedItems = (0, GameDataHandler_1.sanitizeForClient)(this.params.gameCode, items);
this.isInitialized = true;
this.log('info', `Data received: ${items.length} items, ${resumeData.length} completed`);
// Emit event with validation result
this.emit('dataReceived', {
items: this.sanitizedItems,
resumeData,
validation,
});
}
sendAnswerReport(questionId, choice, result, timeSpent) {
window.parent.postMessage({
type: 'ANSWER_REPORT',
data: {
question_id: questionId,
question_index: Array.from(this.originalItems.keys()).indexOf(questionId),
choice,
result,
time_spent: timeSpent,
},
}, '*');
}
log(level, message, data) {
if (!this.config.debug && level === 'debug')
return;
const prefix = `[GameClientSDK:${this.mode}]`;
switch (level) {
case 'debug':
case 'info':
console.log(prefix, message, data ?? '');
break;
case 'warn':
console.warn(prefix, message, data ?? '');
break;
case 'error':
console.error(prefix, message, data ?? '');
break;
}
}
}
exports.GameClientSDK = GameClientSDK;
// =============================================================================
// FACTORY
// =============================================================================
let clientInstance = null;
/**
* Get or create GameClientSDK instance
*/
function getGameClientSDK(config) {
if (!clientInstance) {
clientInstance = new GameClientSDK(config);
}
return clientInstance;
}
/**
* Destroy client instance
*/
function destroyGameClientSDK() {
clientInstance?.destroy();
clientInstance = null;
}
//# sourceMappingURL=GameClientSDK.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,182 +0,0 @@
/**
* Mock Data cho từng Game Code
* Game developers dùng để test game standalone
*
* Usage:
* ```typescript
* import { MockData } from 'game-iframe-sdk/client';
*
* // Lấy sample data cho Quiz Text-Text
* const quizData = MockData.G001;
*
* // Lấy sample data cho Sequence Word
* const seqData = MockData.G110;
* ```
*/
import { GameCode } from '../kit/GameDataHandler';
/** G001: Quiz Text-Text */
export declare const MOCK_G001: {
game_code: GameCode;
game_id: string;
data: {
id: string;
question: string;
options: string[];
answer: number;
}[];
};
/** G002: Quiz Audio-Text */
export declare const MOCK_G002: {
game_code: GameCode;
game_id: string;
data: {
id: string;
question_audio: string;
options: string[];
answer: number;
}[];
};
/** G003: Quiz Text-Audio */
export declare const MOCK_G003: {
game_code: GameCode;
game_id: string;
data: {
id: string;
question: string;
options: string[];
answer: number;
}[];
};
/** G004: Quiz Image-Text */
export declare const MOCK_G004: {
game_code: GameCode;
game_id: string;
data: ({
id: string;
question_image: string;
question: string;
options: string[];
answer: number;
} | {
id: string;
question_image: string;
options: string[];
answer: number;
question?: undefined;
})[];
};
/** G005: Quiz Text-Image */
export declare const MOCK_G005: {
game_code: GameCode;
game_id: string;
data: {
id: string;
question: string;
options: string[];
answer: number;
}[];
};
/** G110: Sequence Word - no audio */
export declare const MOCK_G110: {
game_code: GameCode;
game_id: string;
data: {
id: string;
word: string;
parts: string[];
answer: string[];
}[];
};
/** G111: Sequence Word - audio, hide 2 */
export declare const MOCK_G111: {
game_code: GameCode;
game_id: string;
data: {
id: string;
word: string;
parts: string[];
answer: string[];
audio_url: string;
}[];
};
/** G112: Sequence Word - audio, hide 4 */
export declare const MOCK_G112: {
game_code: GameCode;
game_id: string;
data: {
id: string;
word: string;
parts: string[];
answer: string[];
audio_url: string;
}[];
};
/** G113: Sequence Word - audio, hide all */
export declare const MOCK_G113: {
game_code: GameCode;
game_id: string;
data: {
id: string;
word: string;
parts: string[];
answer: string[];
audio_url: string;
}[];
};
/** G120: Sequence Sentence - no audio */
export declare const MOCK_G120: {
game_code: GameCode;
game_id: string;
data: {
id: string;
sentence: string;
parts: string[];
answer: string[];
}[];
};
/** G121: Sequence Sentence - audio, hide 2 */
export declare const MOCK_G121: {
game_code: GameCode;
game_id: string;
data: {
id: string;
sentence: string;
parts: string[];
answer: string[];
audio_url: string;
}[];
};
/** G122: Sequence Sentence - audio, hide 4 */
export declare const MOCK_G122: {
game_code: GameCode;
game_id: string;
data: {
id: string;
sentence: string;
parts: string[];
answer: string[];
audio_url: string;
}[];
};
/** G123: Sequence Sentence - audio, hide all */
export declare const MOCK_G123: {
game_code: GameCode;
game_id: string;
data: {
id: string;
sentence: string;
parts: string[];
answer: string[];
audio_url: string;
}[];
};
export declare const MockData: Record<GameCode, any>;
/**
* Get mock data for a game code
*/
export declare function getMockData(code: GameCode): any;
/**
* Get all available game codes
*/
export declare function getAvailableGameCodes(): GameCode[];
//# sourceMappingURL=MockData.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MockData.d.ts","sourceRoot":"","sources":["../../src/client/MockData.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAMlD,2BAA2B;AAC3B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAsBhC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAgBhC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAchC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;;;;;;;CAiBhC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAchC,CAAC;AAMF,qCAAqC;AACrC,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAsBhC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,4CAA4C;AAC5C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAMF,yCAAyC;AACzC,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAgBhC,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAMF,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,GAAG,CAiB1C,CAAC;AAEF;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,CAE/C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,EAAE,CAElD"}

View File

@@ -1,289 +0,0 @@
"use strict";
/**
* Mock Data cho từng Game Code
* Game developers dùng để test game standalone
*
* Usage:
* ```typescript
* import { MockData } from 'game-iframe-sdk/client';
*
* // Lấy sample data cho Quiz Text-Text
* const quizData = MockData.G001;
*
* // Lấy sample data cho Sequence Word
* const seqData = MockData.G110;
* ```
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockData = exports.MOCK_G123 = exports.MOCK_G122 = exports.MOCK_G121 = exports.MOCK_G120 = exports.MOCK_G113 = exports.MOCK_G112 = exports.MOCK_G111 = exports.MOCK_G110 = exports.MOCK_G005 = exports.MOCK_G004 = exports.MOCK_G003 = exports.MOCK_G002 = exports.MOCK_G001 = void 0;
exports.getMockData = getMockData;
exports.getAvailableGameCodes = getAvailableGameCodes;
// =============================================================================
// QUIZ MOCK DATA
// =============================================================================
/** G001: Quiz Text-Text */
exports.MOCK_G001 = {
game_code: 'G001',
game_id: 'mock-quiz-text-text',
data: [
{
id: 'q1',
question: 'Thủ đô của Việt Nam là gì?',
options: ['Hà Nội', 'Hồ Chí Minh', 'Đà Nẵng', 'Huế'],
answer: 0, // Index của đáp án đúng
},
{
id: 'q2',
question: '2 + 2 = ?',
options: ['3', '4', '5', '6'],
answer: 1,
},
{
id: 'q3',
question: 'Con vật nào biết bay?',
options: ['Chó', 'Mèo', 'Chim', 'Cá'],
answer: 2,
},
],
};
/** G002: Quiz Audio-Text */
exports.MOCK_G002 = {
game_code: 'G002',
game_id: 'mock-quiz-audio-text',
data: [
{
id: 'q1',
question_audio: 'https://example.com/audio/question1.mp3',
options: ['Apple', 'Banana', 'Orange', 'Grape'],
answer: 0,
},
{
id: 'q2',
question_audio: 'https://example.com/audio/question2.mp3',
options: ['Dog', 'Cat', 'Bird', 'Fish'],
answer: 2,
},
],
};
/** G003: Quiz Text-Audio */
exports.MOCK_G003 = {
game_code: 'G003',
game_id: 'mock-quiz-text-audio',
data: [
{
id: 'q1',
question: 'Chọn phát âm đúng của từ "Hello"',
options: [
'https://example.com/audio/hello1.mp3',
'https://example.com/audio/hello2.mp3',
'https://example.com/audio/hello3.mp3',
],
answer: 0,
},
],
};
/** G004: Quiz Image-Text */
exports.MOCK_G004 = {
game_code: 'G004',
game_id: 'mock-quiz-image-text',
data: [
{
id: 'q1',
question_image: 'https://example.com/images/apple.jpg',
question: 'Đây là quả gì?', // Optional hint
options: ['Táo', 'Cam', 'Chuối', 'Nho'],
answer: 0,
},
{
id: 'q2',
question_image: 'https://example.com/images/cat.jpg',
options: ['Chó', 'Mèo', 'Thỏ', 'Chuột'],
answer: 1,
},
],
};
/** G005: Quiz Text-Image */
exports.MOCK_G005 = {
game_code: 'G005',
game_id: 'mock-quiz-text-image',
data: [
{
id: 'q1',
question: 'Chọn hình ảnh con mèo',
options: [
'https://example.com/images/dog.jpg',
'https://example.com/images/cat.jpg',
'https://example.com/images/bird.jpg',
],
answer: 1,
},
],
};
// =============================================================================
// SEQUENCE WORD MOCK DATA
// =============================================================================
/** G110: Sequence Word - no audio */
exports.MOCK_G110 = {
game_code: 'G110',
game_id: 'mock-sequence-word',
data: [
{
id: 'sw1',
word: 'APPLE',
parts: ['A', 'P', 'P', 'L', 'E'], // Đáp án đúng theo thứ tự
answer: ['A', 'P', 'P', 'L', 'E'], // SDK sẽ shuffle parts, giữ answer để verify
},
{
id: 'sw2',
word: 'HELLO',
parts: ['H', 'E', 'L', 'L', 'O'],
answer: ['H', 'E', 'L', 'L', 'O'],
},
{
id: 'sw3',
word: 'WORLD',
parts: ['W', 'O', 'R', 'L', 'D'],
answer: ['W', 'O', 'R', 'L', 'D'],
},
],
};
/** G111: Sequence Word - audio, hide 2 */
exports.MOCK_G111 = {
game_code: 'G111',
game_id: 'mock-sequence-word-audio-2',
data: [
{
id: 'sw1',
word: 'BANANA',
parts: ['B', 'A', 'N', 'A', 'N', 'A'],
answer: ['B', 'A', 'N', 'A', 'N', 'A'],
audio_url: 'https://example.com/audio/banana.mp3',
},
],
};
/** G112: Sequence Word - audio, hide 4 */
exports.MOCK_G112 = {
game_code: 'G112',
game_id: 'mock-sequence-word-audio-4',
data: [
{
id: 'sw1',
word: 'COMPUTER',
parts: ['C', 'O', 'M', 'P', 'U', 'T', 'E', 'R'],
answer: ['C', 'O', 'M', 'P', 'U', 'T', 'E', 'R'],
audio_url: 'https://example.com/audio/computer.mp3',
},
],
};
/** G113: Sequence Word - audio, hide all */
exports.MOCK_G113 = {
game_code: 'G113',
game_id: 'mock-sequence-word-audio-all',
data: [
{
id: 'sw1',
word: 'ELEPHANT',
parts: ['E', 'L', 'E', 'P', 'H', 'A', 'N', 'T'],
answer: ['E', 'L', 'E', 'P', 'H', 'A', 'N', 'T'],
audio_url: 'https://example.com/audio/elephant.mp3',
},
],
};
// =============================================================================
// SEQUENCE SENTENCE MOCK DATA
// =============================================================================
/** G120: Sequence Sentence - no audio */
exports.MOCK_G120 = {
game_code: 'G120',
game_id: 'mock-sequence-sentence',
data: [
{
id: 'ss1',
sentence: 'I love learning English.',
parts: ['I', 'love', 'learning', 'English.'],
answer: ['I', 'love', 'learning', 'English.'],
},
{
id: 'ss2',
sentence: 'The cat is sleeping.',
parts: ['The', 'cat', 'is', 'sleeping.'],
answer: ['The', 'cat', 'is', 'sleeping.'],
},
],
};
/** G121: Sequence Sentence - audio, hide 2 */
exports.MOCK_G121 = {
game_code: 'G121',
game_id: 'mock-sequence-sentence-audio-2',
data: [
{
id: 'ss1',
sentence: 'She goes to school every day.',
parts: ['She', 'goes', 'to', 'school', 'every', 'day.'],
answer: ['She', 'goes', 'to', 'school', 'every', 'day.'],
audio_url: 'https://example.com/audio/sentence1.mp3',
},
],
};
/** G122: Sequence Sentence - audio, hide 4 */
exports.MOCK_G122 = {
game_code: 'G122',
game_id: 'mock-sequence-sentence-audio-4',
data: [
{
id: 'ss1',
sentence: 'My brother plays football in the park.',
parts: ['My', 'brother', 'plays', 'football', 'in', 'the', 'park.'],
answer: ['My', 'brother', 'plays', 'football', 'in', 'the', 'park.'],
audio_url: 'https://example.com/audio/sentence2.mp3',
},
],
};
/** G123: Sequence Sentence - audio, hide all */
exports.MOCK_G123 = {
game_code: 'G123',
game_id: 'mock-sequence-sentence-audio-all',
data: [
{
id: 'ss1',
sentence: 'The quick brown fox jumps over the lazy dog.',
parts: ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog.'],
answer: ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog.'],
audio_url: 'https://example.com/audio/sentence3.mp3',
},
],
};
// =============================================================================
// MOCK DATA MAP
// =============================================================================
exports.MockData = {
// Quiz
G001: exports.MOCK_G001,
G002: exports.MOCK_G002,
G003: exports.MOCK_G003,
G004: exports.MOCK_G004,
// Sequence Word
G110: exports.MOCK_G110,
G111: exports.MOCK_G111,
G112: exports.MOCK_G112,
G113: exports.MOCK_G113,
// Sequence Sentence
G120: exports.MOCK_G120,
G121: exports.MOCK_G121,
G122: exports.MOCK_G122,
G123: exports.MOCK_G123,
G005: exports.MOCK_G005,
};
/**
* Get mock data for a game code
*/
function getMockData(code) {
return exports.MockData[code] ?? null;
}
/**
* Get all available game codes
*/
function getAvailableGameCodes() {
return Object.keys(exports.MockData);
}
//# sourceMappingURL=MockData.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
/**
* Client SDK exports
* SDK dành cho game developers sử dụng trong game iframe
*/
export { GameClientSDK, getGameClientSDK, destroyGameClientSDK, type ClientMode, type ClientSDKConfig, type URLParams, type GameDataPayload, type AnswerResult, type FinalResult, type ClientSDKEvents, } from './GameClientSDK';
export { MockData, getMockData, getAvailableGameCodes, MOCK_G001, MOCK_G002, MOCK_G003, MOCK_G004, MOCK_G110, MOCK_G111, MOCK_G112, MOCK_G113, MOCK_G120, MOCK_G121, MOCK_G122, MOCK_G123, } from './MockData';
export { validateGameData, getSchema, getSchemaDoc, DataValidator, type ValidationResult, type FieldSchema, type ItemSchema, } from './DataValidator';
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,aAAa,EACb,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,eAAe,GACvB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACH,QAAQ,EACR,WAAW,EACX,qBAAqB,EACrB,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,GACZ,MAAM,YAAY,CAAC;AAGpB,OAAO,EACH,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,aAAa,EACb,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,UAAU,GAClB,MAAM,iBAAiB,CAAC"}

View File

@@ -1,35 +0,0 @@
"use strict";
/**
* Client SDK exports
* SDK dành cho game developers sử dụng trong game iframe
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataValidator = exports.getSchemaDoc = exports.getSchema = exports.validateGameData = exports.MOCK_G123 = exports.MOCK_G122 = exports.MOCK_G121 = exports.MOCK_G120 = exports.MOCK_G113 = exports.MOCK_G112 = exports.MOCK_G111 = exports.MOCK_G110 = exports.MOCK_G004 = exports.MOCK_G003 = exports.MOCK_G002 = exports.MOCK_G001 = exports.getAvailableGameCodes = exports.getMockData = exports.MockData = exports.destroyGameClientSDK = exports.getGameClientSDK = exports.GameClientSDK = void 0;
var GameClientSDK_1 = require("./GameClientSDK");
Object.defineProperty(exports, "GameClientSDK", { enumerable: true, get: function () { return GameClientSDK_1.GameClientSDK; } });
Object.defineProperty(exports, "getGameClientSDK", { enumerable: true, get: function () { return GameClientSDK_1.getGameClientSDK; } });
Object.defineProperty(exports, "destroyGameClientSDK", { enumerable: true, get: function () { return GameClientSDK_1.destroyGameClientSDK; } });
// Mock Data - sample data cho từng game code
var MockData_1 = require("./MockData");
Object.defineProperty(exports, "MockData", { enumerable: true, get: function () { return MockData_1.MockData; } });
Object.defineProperty(exports, "getMockData", { enumerable: true, get: function () { return MockData_1.getMockData; } });
Object.defineProperty(exports, "getAvailableGameCodes", { enumerable: true, get: function () { return MockData_1.getAvailableGameCodes; } });
Object.defineProperty(exports, "MOCK_G001", { enumerable: true, get: function () { return MockData_1.MOCK_G001; } });
Object.defineProperty(exports, "MOCK_G002", { enumerable: true, get: function () { return MockData_1.MOCK_G002; } });
Object.defineProperty(exports, "MOCK_G003", { enumerable: true, get: function () { return MockData_1.MOCK_G003; } });
Object.defineProperty(exports, "MOCK_G004", { enumerable: true, get: function () { return MockData_1.MOCK_G004; } });
Object.defineProperty(exports, "MOCK_G110", { enumerable: true, get: function () { return MockData_1.MOCK_G110; } });
Object.defineProperty(exports, "MOCK_G111", { enumerable: true, get: function () { return MockData_1.MOCK_G111; } });
Object.defineProperty(exports, "MOCK_G112", { enumerable: true, get: function () { return MockData_1.MOCK_G112; } });
Object.defineProperty(exports, "MOCK_G113", { enumerable: true, get: function () { return MockData_1.MOCK_G113; } });
Object.defineProperty(exports, "MOCK_G120", { enumerable: true, get: function () { return MockData_1.MOCK_G120; } });
Object.defineProperty(exports, "MOCK_G121", { enumerable: true, get: function () { return MockData_1.MOCK_G121; } });
Object.defineProperty(exports, "MOCK_G122", { enumerable: true, get: function () { return MockData_1.MOCK_G122; } });
Object.defineProperty(exports, "MOCK_G123", { enumerable: true, get: function () { return MockData_1.MOCK_G123; } });
// Data Validator - verify data structure
var DataValidator_1 = require("./DataValidator");
Object.defineProperty(exports, "validateGameData", { enumerable: true, get: function () { return DataValidator_1.validateGameData; } });
Object.defineProperty(exports, "getSchema", { enumerable: true, get: function () { return DataValidator_1.getSchema; } });
Object.defineProperty(exports, "getSchemaDoc", { enumerable: true, get: function () { return DataValidator_1.getSchemaDoc; } });
Object.defineProperty(exports, "DataValidator", { enumerable: true, get: function () { return DataValidator_1.DataValidator; } });
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,iDAWyB;AAVrB,8GAAA,aAAa,OAAA;AACb,iHAAA,gBAAgB,OAAA;AAChB,qHAAA,oBAAoB,OAAA;AAUxB,6CAA6C;AAC7C,uCAgBoB;AAfhB,oGAAA,QAAQ,OAAA;AACR,uGAAA,WAAW,OAAA;AACX,iHAAA,qBAAqB,OAAA;AACrB,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AAGb,yCAAyC;AACzC,iDAQyB;AAPrB,iHAAA,gBAAgB,OAAA;AAChB,0GAAA,SAAS,OAAA;AACT,6GAAA,YAAY,OAAA;AACZ,8GAAA,aAAa,OAAA"}

View File

@@ -1,61 +0,0 @@
/**
* Game Iframe SDK - Event Emitter
* Simple typed event emitter for SDK
*/
export class EventEmitter {
constructor() {
this.handlers = new Map();
}
/**
* Subscribe to an event
*/
on(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event).add(handler);
// Return unsubscribe function
return () => this.off(event, handler);
}
/**
* Subscribe to an event (once)
*/
once(event, handler) {
const wrappedHandler = (data) => {
this.off(event, wrappedHandler);
handler(data);
};
return this.on(event, wrappedHandler);
}
/**
* Unsubscribe from an event
*/
off(event, handler) {
this.handlers.get(event)?.delete(handler);
}
/**
* Emit an event
*/
emit(event, data) {
this.handlers.get(event)?.forEach(handler => {
try {
handler(data);
}
catch (err) {
console.error(`[EventEmitter] Error in handler for "${String(event)}":`, err);
}
});
}
/**
* Remove all handlers for an event (or all events)
*/
removeAllListeners(event) {
if (event) {
this.handlers.delete(event);
}
else {
this.handlers.clear();
}
}
}
//# sourceMappingURL=EventEmitter.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"EventEmitter.js","sourceRoot":"","sources":["../../src/EventEmitter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,OAAO,YAAY;IAAzB;QACY,aAAQ,GAAyC,IAAI,GAAG,EAAE,CAAC;IAwDvE,CAAC;IAtDG;;OAEG;IACH,EAAE,CAAyB,KAAQ,EAAE,OAAgC;QACjE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEvC,8BAA8B;QAC9B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,OAAgC;QACnE,MAAM,cAAc,GAAG,CAAC,IAAe,EAAE,EAAE;YACvC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,GAAG,CAAyB,KAAQ,EAAE,OAAgC;QAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,IAAe;QAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;YACxC,IAAI,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,wCAAwC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAClF,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;OAEG;IACH,kBAAkB,CAAC,KAAoB;QACnC,IAAI,KAAK,EAAE,CAAC;YACR,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;IACL,CAAC;CACJ"}

View File

@@ -1,247 +0,0 @@
/**
* Game Iframe SDK - Core
* SDK chính - compose các layers: MessageHandler, MessageSender
*/
import { EventEmitter } from './EventEmitter';
import { MessageHandler } from './MessageHandler';
import { MessageSender } from './MessageSender';
import { DEFAULT_CONFIG, } from './types';
/**
* GameIframeSDK - Main SDK class
* Composes MessageHandler và MessageSender
*/
export class GameIframeSDK extends EventEmitter {
constructor(config) {
super();
this.pendingData = null;
this.isReady = false;
this.config = { ...DEFAULT_CONFIG, ...config };
// Initialize layers
this.messageHandler = new MessageHandler({
acceptedOrigin: this.config.iframeOrigin,
debug: this.config.debug,
});
this.messageSender = new MessageSender({
targetOrigin: this.config.iframeOrigin,
debug: this.config.debug,
});
// Setup event forwarding
this.setupEventForwarding();
// Start listening
this.messageHandler.start();
this.log('info', 'SDK initialized', { config: this.config });
}
// ==========================================================================
// PUBLIC API - Iframe Management
// ==========================================================================
/**
* Set iframe element reference
*/
setIframe(iframe) {
this.messageSender.setIframe(iframe);
this.isReady = false;
this.log('info', 'Iframe set', { hasIframe: !!iframe });
return this;
}
/**
* Get current iframe
*/
getIframe() {
return this.messageSender.getIframe();
}
/**
* Check if game is ready
*/
isGameReady() {
return this.isReady;
}
/**
* Check if sender is ready (iframe available)
*/
isSenderReady() {
return this.messageSender.isReady();
}
// ==========================================================================
// PUBLIC API - Send Data
// ==========================================================================
/**
* Send game data to iframe
*/
sendGameData(data) {
const result = this.messageSender.sendGameData(data);
if (!result.success) {
this.emit('error', {
message: 'Failed to send game data',
error: result.error,
});
}
return result.success;
}
/**
* Send leaderboard data to iframe
*/
sendLeaderboard(data) {
const result = this.messageSender.sendLeaderboard(data);
if (!result.success) {
this.emit('error', {
message: 'Failed to send leaderboard',
error: result.error,
});
}
return result.success;
}
// ==========================================================================
// PUBLIC API - Queue & Auto-send
// ==========================================================================
/**
* Queue data to be sent when game is ready
*/
queueGameData(data) {
this.pendingData = data;
this.log('info', 'Data queued for when game is ready');
// If already ready, send immediately
if (this.isReady) {
this.sendQueuedData();
}
return this;
}
/**
* Clear queued data
*/
clearQueuedData() {
this.pendingData = null;
return this;
}
// ==========================================================================
// PUBLIC API - Iframe Control
// ==========================================================================
/**
* Force reload iframe
*/
reloadIframe() {
this.isReady = false;
return this.messageSender.reloadIframe();
}
// ==========================================================================
// PUBLIC API - Lifecycle
// ==========================================================================
/**
* Cleanup and destroy SDK
*/
destroy() {
this.messageHandler.destroy();
this.removeAllListeners();
this.pendingData = null;
this.isReady = false;
this.log('info', 'SDK destroyed');
}
// ==========================================================================
// PUBLIC API - Direct Layer Access (Advanced)
// ==========================================================================
/**
* Get MessageHandler instance for advanced usage
*/
getMessageHandler() {
return this.messageHandler;
}
/**
* Get MessageSender instance for advanced usage
*/
getMessageSender() {
return this.messageSender;
}
// ==========================================================================
// PRIVATE METHODS
// ==========================================================================
/**
* Setup event forwarding from MessageHandler to SDK events
*/
setupEventForwarding() {
// Forward gameReady
this.messageHandler.on('gameReady', () => {
this.isReady = true;
this.emit('gameReady', undefined);
// Auto-send queued data if enabled
if (this.config.autoSendOnReady && this.pendingData) {
setTimeout(() => {
this.sendQueuedData();
}, this.config.readyDelay);
}
});
// Forward answerReport
this.messageHandler.on('answerReport', (data) => {
this.emit('answerReport', data);
});
// Forward finalResult
this.messageHandler.on('finalResult', (data) => {
this.emit('finalResult', data);
});
// Forward leaderboardRequest
this.messageHandler.on('leaderboardRequest', (data) => {
this.emit('leaderboardRequest', data);
});
// Forward errors
this.messageHandler.on('error', (error) => {
this.emit('error', error);
});
}
/**
* Send queued data
*/
sendQueuedData() {
if (this.pendingData) {
this.sendGameData(this.pendingData);
this.pendingData = null;
}
}
/**
* Internal logging
*/
log(level, message, data) {
if (this.config.debug) {
const prefix = '[GameIframeSDK]';
switch (level) {
case 'info':
console.log(prefix, message, data ?? '');
break;
case 'warn':
console.warn(prefix, message, data ?? '');
break;
case 'error':
console.error(prefix, message, data ?? '');
break;
}
}
this.emit('log', { level, message, data });
}
}
// ==========================================================================
// FACTORY / SINGLETON HELPERS
// ==========================================================================
let defaultInstance = null;
/**
* Create SDK instance
*/
export function createGameIframeSDK(config) {
return new GameIframeSDK(config);
}
/**
* Get or create default SDK instance
*/
export function getGameIframeSDK(config) {
if (!defaultInstance && config) {
defaultInstance = new GameIframeSDK(config);
}
if (!defaultInstance) {
throw new Error('GameIframeSDK not initialized. Call with config first.');
}
return defaultInstance;
}
/**
* Destroy default instance
*/
export function destroyGameIframeSDK() {
defaultInstance?.destroy();
defaultInstance = null;
}
//# sourceMappingURL=GameIframeSDK.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,111 +0,0 @@
/**
* Game Iframe SDK - Message Handler
* Xử lý message từ iframe
*/
import { MESSAGE_TYPES } from './types';
import { EventEmitter } from './EventEmitter';
/**
* MessageHandler - Xử lý incoming messages từ iframe
*/
export class MessageHandler extends EventEmitter {
constructor(config) {
super();
this.boundHandler = null;
this.isListening = false;
this.config = config;
}
/**
* Start listening for messages
*/
start() {
if (this.isListening) {
return this;
}
this.boundHandler = this.handleMessage.bind(this);
window.addEventListener('message', this.boundHandler);
this.isListening = true;
this.log('MessageHandler started');
return this;
}
/**
* Stop listening for messages
*/
stop() {
if (this.boundHandler) {
window.removeEventListener('message', this.boundHandler);
this.boundHandler = null;
}
this.isListening = false;
this.log('MessageHandler stopped');
return this;
}
/**
* Check if handler is listening
*/
isActive() {
return this.isListening;
}
/**
* Handle incoming message
*/
handleMessage(event) {
// Origin check
if (!this.isOriginAllowed(event.origin)) {
return;
}
const { type, data } = event.data || {};
if (!type)
return;
this.log(`Received: ${type}`, data);
try {
switch (type) {
case MESSAGE_TYPES.GAME_READY:
this.emit('gameReady', undefined);
break;
case MESSAGE_TYPES.ANSWER_REPORT:
// Raw data pass-through
this.emit('answerReport', data);
break;
case MESSAGE_TYPES.FINAL_RESULT:
// Raw data pass-through
this.emit('finalResult', data);
break;
case MESSAGE_TYPES.GET_LEADERBOARD:
this.emit('leaderboardRequest', { top: data?.top || 10 });
break;
default:
this.emit('unknownMessage', { type, data });
break;
}
}
catch (error) {
const err = error;
this.emit('error', { message: `Error handling ${type}`, error: err });
}
}
/**
* Check if origin is allowed
*/
isOriginAllowed(origin) {
if (this.config.acceptedOrigin === '*') {
return true;
}
return origin === this.config.acceptedOrigin;
}
/**
* Debug log
*/
log(message, data) {
if (this.config.debug) {
console.log('[MessageHandler]', message, data ?? '');
}
}
/**
* Cleanup
*/
destroy() {
this.stop();
this.removeAllListeners();
}
}
//# sourceMappingURL=MessageHandler.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../../src/MessageHandler.ts"],"names":[],"mappings":"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"}

View File

@@ -1,128 +0,0 @@
/**
* Game Iframe SDK - Message Sender
* Gửi message đến iframe
*/
import { MESSAGE_TYPES } from './types';
/**
* MessageSender - Gửi messages đến iframe
*/
export class MessageSender {
constructor(config) {
this.iframe = null;
this.config = config;
}
/**
* Set iframe element
*/
setIframe(iframe) {
this.iframe = iframe;
return this;
}
/**
* Get current iframe
*/
getIframe() {
return this.iframe;
}
/**
* Check if iframe is available
*/
isReady() {
return !!this.iframe?.contentWindow;
}
/**
* Send raw message to iframe
*/
sendRaw(message) {
if (!this.iframe?.contentWindow) {
return {
success: false,
error: new Error('Iframe not available'),
};
}
try {
this.iframe.contentWindow.postMessage(message, this.config.targetOrigin);
this.log('Sent message', { type: message.type });
return { success: true };
}
catch (error) {
const err = error;
this.log('Send failed', { error: err.message });
return { success: false, error: err };
}
}
/**
* Send game data (SERVER_PUSH_DATA)
*/
sendGameData(payload) {
// Inline message creation
const message = {
type: MESSAGE_TYPES.SERVER_PUSH_DATA,
jsonData: payload,
};
const result = this.sendRaw(message);
if (result.success) {
const dataLength = payload.data?.length || 0;
this.log('Sent game data', {
game_id: payload.game_id,
items: dataLength,
});
}
return result;
}
/**
* Send leaderboard (SERVER_PUSH_LEADERBOARD)
*/
sendLeaderboard(data) {
// Inline message creation
const message = {
type: MESSAGE_TYPES.SERVER_PUSH_LEADERBOARD,
leaderboardData: data,
};
const result = this.sendRaw(message);
if (result.success) {
this.log('Sent leaderboard', {
players: data.top_players?.length || 0,
hasUserRank: !!data.user_rank,
});
}
return result;
}
/**
* Reload iframe
*/
reloadIframe() {
if (!this.iframe) {
return false;
}
const currentSrc = this.iframe.src;
if (!currentSrc || currentSrc === 'about:blank') {
return false;
}
this.iframe.src = '';
setTimeout(() => {
if (this.iframe) {
this.iframe.src = currentSrc;
this.log('Iframe reloaded');
}
}, 100);
return true;
}
/**
* Debug log
*/
log(message, data) {
if (this.config.debug) {
console.log('[MessageSender]', message);
if (data) {
try {
console.log(JSON.stringify(data, null, 2));
}
catch (e) {
console.log(data);
}
}
}
}
}
//# sourceMappingURL=MessageSender.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../../src/MessageSender.ts"],"names":[],"mappings":"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"}

View File

@@ -1,245 +0,0 @@
/**
* Data Validator
* Verify data structure cho từng game code
*
* Usage:
* ```typescript
* import { validateGameData, DataValidator } from 'game-iframe-sdk/client';
*
* const result = validateGameData('G001', receivedData);
* if (!result.valid) {
* console.error('Invalid data:', result.errors);
* }
* ```
*/
import { GAME_CODES } from '../kit/GameDataHandler';
// =============================================================================
// SCHEMAS FOR EACH GAME CODE
// =============================================================================
const QUIZ_BASE_SCHEMA = {
id: { type: 'string', required: true, description: 'Unique question ID' },
options: { type: 'array', required: true, arrayItemType: 'string', description: 'Answer options' },
answer: { type: 'number', required: true, description: 'Correct answer index (0-based)' },
};
const SCHEMAS = {
// Quiz variants
G001: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
},
G002: {
...QUIZ_BASE_SCHEMA,
question_audio: { type: 'string', required: true, description: 'Audio URL for question' },
},
G003: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
// options are audio URLs
},
G004: {
...QUIZ_BASE_SCHEMA,
question_image: { type: 'string', required: true, description: 'Image URL for question' },
question: { type: 'string', required: false, description: 'Optional text hint' },
},
// G005: Quiz Text-Image (options are image URLs, client picks index)
G005: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
// options are image URLs, answer is index pointing to correct image
},
// Sequence Word variants
G110: {
id: { type: 'string', required: true },
word: { type: 'string', required: true, description: 'The word to arrange' },
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Letters/parts to arrange' },
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct order' },
},
G111: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true, description: 'Audio hint URL' },
},
G112: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G113: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
// Sequence Sentence variants
G120: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false, description: 'Full sentence (hint)' },
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Words to arrange' },
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct word order' },
},
G121: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G122: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G123: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
}
};
// =============================================================================
// VALIDATOR
// =============================================================================
/**
* Validate a single item against schema
*/
function validateItem(item, schema, itemIndex) {
const errors = [];
if (!item || typeof item !== 'object') {
errors.push(`Item [${itemIndex}]: Must be an object`);
return errors;
}
for (const [field, fieldSchema] of Object.entries(schema)) {
const value = item[field];
// Check required
if (fieldSchema.required && (value === undefined || value === null)) {
errors.push(`Item [${itemIndex}].${field}: Required field is missing`);
continue;
}
// Skip validation if optional and not present
if (!fieldSchema.required && (value === undefined || value === null)) {
continue;
}
// Check type
const actualType = Array.isArray(value) ? 'array' : typeof value;
if (fieldSchema.type !== 'any' && actualType !== fieldSchema.type) {
errors.push(`Item [${itemIndex}].${field}: Expected ${fieldSchema.type}, got ${actualType}`);
continue;
}
// Check array items
if (fieldSchema.type === 'array' && fieldSchema.arrayItemType && fieldSchema.arrayItemType !== 'any') {
for (let i = 0; i < value.length; i++) {
const itemType = typeof value[i];
if (itemType !== fieldSchema.arrayItemType) {
errors.push(`Item [${itemIndex}].${field}[${i}]: Expected ${fieldSchema.arrayItemType}, got ${itemType}`);
}
}
}
}
return errors;
}
/**
* Validate game data payload
*/
export function validateGameData(gameCode, payload) {
const errors = [];
const warnings = [];
// Check game code
if (!GAME_CODES[gameCode]) {
errors.push(`Unknown game code: ${gameCode}`);
return { valid: false, errors, warnings };
}
// Check payload structure
if (!payload || typeof payload !== 'object') {
errors.push('Payload must be an object');
return { valid: false, errors, warnings };
}
// Check data array
const items = payload.data || payload.items || payload.questions;
if (!items) {
errors.push('Missing data array (expected "data", "items", or "questions")');
return { valid: false, errors, warnings };
}
if (!Array.isArray(items)) {
errors.push('"data" must be an array');
return { valid: false, errors, warnings };
}
if (items.length === 0) {
warnings.push('Data array is empty');
}
// Validate each item
const schema = SCHEMAS[gameCode];
for (let i = 0; i < items.length; i++) {
const itemErrors = validateItem(items[i], schema, i);
errors.push(...itemErrors);
}
// Check for duplicate IDs
const ids = items.map((item) => item.id).filter(Boolean);
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
if (duplicates.length > 0) {
warnings.push(`Duplicate IDs found: ${[...new Set(duplicates)].join(', ')}`);
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Get schema for a game code
*/
export function getSchema(gameCode) {
return SCHEMAS[gameCode] ?? null;
}
/**
* Get schema documentation for a game code
*/
export function getSchemaDoc(gameCode) {
const schema = SCHEMAS[gameCode];
if (!schema)
return `Unknown game code: ${gameCode}`;
const gameInfo = GAME_CODES[gameCode];
const lines = [
`## ${gameCode}: ${gameInfo.name}`,
`Category: ${gameInfo.category}`,
'',
'### Fields:',
];
for (const [field, fieldSchema] of Object.entries(schema)) {
const required = fieldSchema.required ? '(required)' : '(optional)';
let type = fieldSchema.type;
if (fieldSchema.arrayItemType) {
type = `${fieldSchema.type}<${fieldSchema.arrayItemType}>`;
}
lines.push(`- **${field}**: ${type} ${required}`);
if (fieldSchema.description) {
lines.push(` - ${fieldSchema.description}`);
}
}
return lines.join('\n');
}
// =============================================================================
// DATA VALIDATOR CLASS
// =============================================================================
export class DataValidator {
constructor(gameCode) {
this.gameCode = gameCode;
}
validate(payload) {
return validateGameData(this.gameCode, payload);
}
getSchema() {
return getSchema(this.gameCode);
}
getSchemaDoc() {
return getSchemaDoc(this.gameCode);
}
}
//# sourceMappingURL=DataValidator.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,359 +0,0 @@
/**
* GameClientSDK - SDK dành cho Game Iframe
*
* Sử dụng trong game để:
* - Tự động xác định mode (preview/live) từ URL
* - Nhận data từ parent (preview) hoặc fetch API (live)
* - Verify answers locally
* - Report results về parent
*/
import { checkAnswer, sanitizeForClient, GAME_CODES } from '../kit/GameDataHandler';
import { getMockData } from './MockData';
import { validateGameData } from './DataValidator';
class SimpleEventEmitter {
constructor() {
this.handlers = new Map();
}
on(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event).add(handler);
return () => this.off(event, handler);
}
off(event, handler) {
this.handlers.get(event)?.delete(handler);
}
emit(event, data) {
this.handlers.get(event)?.forEach(handler => {
try {
handler(data);
}
catch (err) {
console.error(`[GameClientSDK] Error in ${String(event)} handler:`, err);
}
});
}
}
// =============================================================================
// GAME CLIENT SDK
// =============================================================================
export class GameClientSDK extends SimpleEventEmitter {
constructor(config = {}) {
super();
// Data storage
this.originalItems = new Map(); // Có đáp án
this.sanitizedItems = []; // Không có đáp án
this.userAnswers = new Map();
this.isInitialized = false;
this.startTime = 0;
this.config = {
debug: config.debug ?? false,
apiBaseUrl: config.apiBaseUrl ?? '',
getAuthHeaders: config.getAuthHeaders ?? (() => ({})),
};
// Parse URL params
this.params = this.parseURLParams();
this.mode = this.params.mode;
this.log('info', 'SDK created', { mode: this.mode, params: this.params });
// Emit mode detected
this.emit('modeDetected', { mode: this.mode, params: this.params });
// Setup message listener
this.setupMessageListener();
// Auto-initialize based on mode
this.initialize();
}
// ==========================================================================
// PUBLIC API
// ==========================================================================
/**
* Get current mode
*/
getMode() {
return this.mode;
}
/**
* Get URL params
*/
getParams() {
return { ...this.params };
}
/**
* Get game code
*/
getGameCode() {
return this.params.gameCode;
}
/**
* Get sanitized items (safe for rendering)
*/
getItems() {
return this.sanitizedItems;
}
/**
* Submit an answer and get verification result
*/
submitAnswer(questionId, choice) {
const originalItem = this.originalItems.get(questionId);
if (!originalItem) {
this.log('warn', `Item not found: ${questionId}`);
return { isCorrect: false, score: 0, feedback: 'Question not found' };
}
// Verify using GameDataHandler
const result = checkAnswer(this.params.gameCode, originalItem, choice);
// Store user answer
const timeSpent = Date.now() - (this.userAnswers.size === 0 ? this.startTime : Date.now());
this.userAnswers.set(questionId, {
choice,
result: result.isCorrect ? 1 : 0,
time: timeSpent,
});
// Report to parent
this.sendAnswerReport(questionId, choice, result.isCorrect ? 1 : 0, timeSpent);
this.log('info', `Answer submitted: ${questionId}`, { choice, result });
return result;
}
/**
* Get final result
*/
getFinalResult() {
const details = Array.from(this.userAnswers.entries()).map(([id, data]) => ({
question_id: id,
choice: data.choice,
result: data.result,
time_spent: data.time,
}));
const correct = details.filter(d => d.result === 1).length;
const total = this.originalItems.size;
return {
score: total > 0 ? Math.round((correct / total) * 100) : 0,
total,
correct,
wrong: total - correct,
details,
};
}
/**
* Report final result to parent
*/
reportFinalResult(result) {
const finalResult = result ?? this.getFinalResult();
window.parent.postMessage({
type: 'FINAL_RESULT',
data: finalResult,
}, '*');
this.log('info', 'Final result reported', finalResult);
}
/**
* Request leaderboard from parent
*/
requestLeaderboard(top = 10) {
window.parent.postMessage({
type: 'GET_LEADERBOARD',
data: { top },
}, '*');
}
/**
* Cleanup
*/
destroy() {
window.removeEventListener('message', this.handleMessage);
this.originalItems.clear();
this.sanitizedItems = [];
this.userAnswers.clear();
this.log('info', 'SDK destroyed');
}
// ==========================================================================
// PRIVATE METHODS
// ==========================================================================
parseURLParams() {
const searchParams = new URLSearchParams(window.location.search);
const mode = (searchParams.get('mode') || 'preview');
const gameCode = (searchParams.get('game_code') || 'G001');
const gameId = searchParams.get('game_id') || undefined;
const lid = searchParams.get('lid') || undefined;
const studentId = searchParams.get('student_id') || undefined;
// Validate mode
if (mode !== 'preview' && mode !== 'live' && mode !== 'dev') {
this.log('warn', `Invalid mode: ${mode}, defaulting to preview`);
}
// Validate game code
if (!GAME_CODES[gameCode]) {
this.log('warn', `Unknown game code: ${gameCode}`);
}
return { mode, gameCode, gameId, lid, studentId };
}
setupMessageListener() {
this.handleMessage = this.handleMessage.bind(this);
window.addEventListener('message', this.handleMessage);
}
handleMessage(event) {
const { type, jsonData, leaderboardData } = event.data || {};
this.log('debug', 'Message received', { type, hasData: !!jsonData });
switch (type) {
case 'SERVER_PUSH_DATA':
if (jsonData) {
this.handleDataReceived(jsonData);
}
break;
case 'SERVER_PUSH_LEADERBOARD':
if (leaderboardData) {
this.log('info', 'Leaderboard received', leaderboardData);
// Could emit event here
}
break;
}
}
async initialize() {
// Send GAME_READY immediately
this.sendGameReady();
if (this.mode === 'dev') {
// Dev mode: load mock data immediately
this.log('info', 'DEV MODE: Loading mock data...');
this.loadMockData();
}
else if (this.mode === 'live') {
// Live mode: fetch data from API
await this.fetchLiveData();
}
else {
// Preview mode: wait for postMessage
this.log('info', 'Preview mode: waiting for SERVER_PUSH_DATA...');
}
}
/**
* Load mock data for dev mode
*/
loadMockData() {
const mockData = getMockData(this.params.gameCode);
if (!mockData) {
this.emit('error', {
message: `No mock data available for game code: ${this.params.gameCode}`
});
return;
}
this.log('info', `Loaded mock data for ${this.params.gameCode}`);
this.handleDataReceived(mockData);
}
sendGameReady() {
window.parent.postMessage({ type: 'GAME_READY' }, '*');
this.emit('ready', undefined);
this.log('info', 'GAME_READY sent');
}
async fetchLiveData() {
const { gameId, lid } = this.params;
if (!gameId || !lid) {
this.emit('error', { message: 'Live mode requires game_id and lid' });
return;
}
if (!this.config.apiBaseUrl) {
this.emit('error', { message: 'Live mode requires apiBaseUrl' });
return;
}
try {
const url = `${this.config.apiBaseUrl}/games/${gameId}?lid=${lid}`;
const headers = {
'Content-Type': 'application/json',
...this.config.getAuthHeaders(),
};
this.log('info', `Fetching live data: ${url}`);
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
this.handleDataReceived(data);
}
catch (error) {
this.log('error', 'Failed to fetch live data', error);
this.emit('error', { message: 'Failed to fetch game data', error });
}
}
handleDataReceived(payload) {
this.startTime = Date.now();
// Update game code if provided
if (payload.game_code && GAME_CODES[payload.game_code]) {
this.params.gameCode = payload.game_code;
}
// Validate data structure
const validation = validateGameData(this.params.gameCode, payload);
if (!validation.valid) {
this.log('error', 'Data validation failed', validation.errors);
this.emit('validationError', { validation });
// Continue anyway to allow partial rendering
}
if (validation.warnings.length > 0) {
this.log('warn', 'Data validation warnings', validation.warnings);
}
// Extract items from various payload formats
const items = payload.data || payload.items || payload.questions || [];
const resumeData = payload.completed_question_ids || [];
// Store original items (with answers)
this.originalItems.clear();
items.forEach((item) => {
if (item.id) {
this.originalItems.set(item.id, item);
}
});
// Sanitize for client (remove answers)
this.sanitizedItems = sanitizeForClient(this.params.gameCode, items);
this.isInitialized = true;
this.log('info', `Data received: ${items.length} items, ${resumeData.length} completed`);
// Emit event with validation result
this.emit('dataReceived', {
items: this.sanitizedItems,
resumeData,
validation,
});
}
sendAnswerReport(questionId, choice, result, timeSpent) {
window.parent.postMessage({
type: 'ANSWER_REPORT',
data: {
question_id: questionId,
question_index: Array.from(this.originalItems.keys()).indexOf(questionId),
choice,
result,
time_spent: timeSpent,
},
}, '*');
}
log(level, message, data) {
if (!this.config.debug && level === 'debug')
return;
const prefix = `[GameClientSDK:${this.mode}]`;
switch (level) {
case 'debug':
case 'info':
console.log(prefix, message, data ?? '');
break;
case 'warn':
console.warn(prefix, message, data ?? '');
break;
case 'error':
console.error(prefix, message, data ?? '');
break;
}
}
}
// =============================================================================
// FACTORY
// =============================================================================
let clientInstance = null;
/**
* Get or create GameClientSDK instance
*/
export function getGameClientSDK(config) {
if (!clientInstance) {
clientInstance = new GameClientSDK(config);
}
return clientInstance;
}
/**
* Destroy client instance
*/
export function destroyGameClientSDK() {
clientInstance?.destroy();
clientInstance = null;
}
//# sourceMappingURL=GameClientSDK.js.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More