This commit is contained in:
@@ -135,5 +135,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<script src="./sdk/package/dist/sena-game-sdk.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
506
G102-sequence/sdk/package/README.md
Normal file
506
G102-sequence/sdk/package/README.md
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
# 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'}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
29
G102-sequence/sdk/package/dist/EventEmitter.d.ts
vendored
Normal file
29
G102-sequence/sdk/package/dist/EventEmitter.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/EventEmitter.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/EventEmitter.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
65
G102-sequence/sdk/package/dist/EventEmitter.js
vendored
Normal file
65
G102-sequence/sdk/package/dist/EventEmitter.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/EventEmitter.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/EventEmitter.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
93
G102-sequence/sdk/package/dist/GameIframeSDK.d.ts
vendored
Normal file
93
G102-sequence/sdk/package/dist/GameIframeSDK.d.ts
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/GameIframeSDK.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/GameIframeSDK.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
254
G102-sequence/sdk/package/dist/GameIframeSDK.js
vendored
Normal file
254
G102-sequence/sdk/package/dist/GameIframeSDK.js
vendored
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/GameIframeSDK.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/GameIframeSDK.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
70
G102-sequence/sdk/package/dist/MessageHandler.d.ts
vendored
Normal file
70
G102-sequence/sdk/package/dist/MessageHandler.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/MessageHandler.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/MessageHandler.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
115
G102-sequence/sdk/package/dist/MessageHandler.js
vendored
Normal file
115
G102-sequence/sdk/package/dist/MessageHandler.js
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/MessageHandler.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/MessageHandler.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"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"}
|
||||||
60
G102-sequence/sdk/package/dist/MessageSender.d.ts
vendored
Normal file
60
G102-sequence/sdk/package/dist/MessageSender.d.ts
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/MessageSender.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/MessageSender.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
132
G102-sequence/sdk/package/dist/MessageSender.js
vendored
Normal file
132
G102-sequence/sdk/package/dist/MessageSender.js
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/MessageSender.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/MessageSender.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../src/MessageSender.ts"],"names":[],"mappings":"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"}
|
||||||
49
G102-sequence/sdk/package/dist/client/DataValidator.d.ts
vendored
Normal file
49
G102-sequence/sdk/package/dist/client/DataValidator.d.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/client/DataValidator.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/DataValidator.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
252
G102-sequence/sdk/package/dist/client/DataValidator.js
vendored
Normal file
252
G102-sequence/sdk/package/dist/client/DataValidator.js
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/client/DataValidator.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/DataValidator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
146
G102-sequence/sdk/package/dist/client/GameClientSDK.d.ts
vendored
Normal file
146
G102-sequence/sdk/package/dist/client/GameClientSDK.d.ts
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/client/GameClientSDK.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/GameClientSDK.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"GameClientSDK.d.ts","sourceRoot":"","sources":["../../src/client/GameClientSDK.ts"],"names":[],"mappings":"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"}
|
||||||
365
G102-sequence/sdk/package/dist/client/GameClientSDK.js
vendored
Normal file
365
G102-sequence/sdk/package/dist/client/GameClientSDK.js
vendored
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/client/GameClientSDK.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/GameClientSDK.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
182
G102-sequence/sdk/package/dist/client/MockData.d.ts
vendored
Normal file
182
G102-sequence/sdk/package/dist/client/MockData.d.ts
vendored
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/client/MockData.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/MockData.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
289
G102-sequence/sdk/package/dist/client/MockData.js
vendored
Normal file
289
G102-sequence/sdk/package/dist/client/MockData.js
vendored
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/client/MockData.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/MockData.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
G102-sequence/sdk/package/dist/client/index.d.ts
vendored
Normal file
8
G102-sequence/sdk/package/dist/client/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/client/index.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
35
G102-sequence/sdk/package/dist/client/index.js
vendored
Normal file
35
G102-sequence/sdk/package/dist/client/index.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"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
|
||||||
1
G102-sequence/sdk/package/dist/client/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/client/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
61
G102-sequence/sdk/package/dist/esm/EventEmitter.js
vendored
Normal file
61
G102-sequence/sdk/package/dist/esm/EventEmitter.js
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/esm/EventEmitter.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/EventEmitter.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
247
G102-sequence/sdk/package/dist/esm/GameIframeSDK.js
vendored
Normal file
247
G102-sequence/sdk/package/dist/esm/GameIframeSDK.js
vendored
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/esm/GameIframeSDK.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/GameIframeSDK.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
111
G102-sequence/sdk/package/dist/esm/MessageHandler.js
vendored
Normal file
111
G102-sequence/sdk/package/dist/esm/MessageHandler.js
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/esm/MessageHandler.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/MessageHandler.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../../src/MessageHandler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAqC,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAuB9C;;GAEG;AACH,MAAM,OAAO,cAAe,SAAQ,YAAkC;IAKlE,YAAY,MAA4B;QACpC,KAAK,EAAE,CAAC;QAJJ,iBAAY,GAA2C,IAAI,CAAC;QAC5D,gBAAW,GAAG,KAAK,CAAC;QAIxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK;QACD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QACtD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QAEnC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;OAEG;IACH,IAAI;QACA,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YACzD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,IAAI,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QAEnC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;OAEG;IACH,QAAQ;QACJ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC5B,CAAC;IAED;;OAEG;IACK,aAAa,CAAC,KAAmB;QACrC,eAAe;QACf,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YACtC,OAAO;QACX,CAAC;QAED,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;QACxC,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,IAAI,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;QAEpC,IAAI,CAAC;YACD,QAAQ,IAAI,EAAE,CAAC;gBACX,KAAK,aAAa,CAAC,UAAU;oBACzB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;oBAClC,MAAM;gBAEV,KAAK,aAAa,CAAC,aAAa;oBAC5B,wBAAwB;oBACxB,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,IAAwB,CAAC,CAAC;oBACpD,MAAM;gBAEV,KAAK,aAAa,CAAC,YAAY;oBAC3B,wBAAwB;oBACxB,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAuB,CAAC,CAAC;oBAClD,MAAM;gBAEV,KAAK,aAAa,CAAC,eAAe;oBAC9B,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;oBAC1D,MAAM;gBAEV;oBACI,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;oBAC5C,MAAM;YACd,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,KAAc,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,kBAAkB,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,MAAc;QAClC,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,GAAG,EAAE,CAAC;YACrC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;IACjD,CAAC;IAED;;OAEG;IACK,GAAG,CAAC,OAAe,EAAE,IAAU;QACnC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACpB,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACzD,CAAC;IACL,CAAC;IAED;;OAEG;IACH,OAAO;QACH,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC9B,CAAC;CACJ"}
|
||||||
128
G102-sequence/sdk/package/dist/esm/MessageSender.js
vendored
Normal file
128
G102-sequence/sdk/package/dist/esm/MessageSender.js
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/esm/MessageSender.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/MessageSender.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../../src/MessageSender.ts"],"names":[],"mappings":"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"}
|
||||||
245
G102-sequence/sdk/package/dist/esm/client/DataValidator.js
vendored
Normal file
245
G102-sequence/sdk/package/dist/esm/client/DataValidator.js
vendored
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/esm/client/DataValidator.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/client/DataValidator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
359
G102-sequence/sdk/package/dist/esm/client/GameClientSDK.js
vendored
Normal file
359
G102-sequence/sdk/package/dist/esm/client/GameClientSDK.js
vendored
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
1
G102-sequence/sdk/package/dist/esm/client/GameClientSDK.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/client/GameClientSDK.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
284
G102-sequence/sdk/package/dist/esm/client/MockData.js
vendored
Normal file
284
G102-sequence/sdk/package/dist/esm/client/MockData.js
vendored
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
// =============================================================================
|
||||||
|
// QUIZ MOCK DATA
|
||||||
|
// =============================================================================
|
||||||
|
/** G001: Quiz Text-Text */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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 */
|
||||||
|
export const 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
|
||||||
|
// =============================================================================
|
||||||
|
export const MockData = {
|
||||||
|
// Quiz
|
||||||
|
G001: MOCK_G001,
|
||||||
|
G002: MOCK_G002,
|
||||||
|
G003: MOCK_G003,
|
||||||
|
G004: MOCK_G004,
|
||||||
|
// Sequence Word
|
||||||
|
G110: MOCK_G110,
|
||||||
|
G111: MOCK_G111,
|
||||||
|
G112: MOCK_G112,
|
||||||
|
G113: MOCK_G113,
|
||||||
|
// Sequence Sentence
|
||||||
|
G120: MOCK_G120,
|
||||||
|
G121: MOCK_G121,
|
||||||
|
G122: MOCK_G122,
|
||||||
|
G123: MOCK_G123,
|
||||||
|
G005: MOCK_G005,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Get mock data for a game code
|
||||||
|
*/
|
||||||
|
export function getMockData(code) {
|
||||||
|
return MockData[code] ?? null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get all available game codes
|
||||||
|
*/
|
||||||
|
export function getAvailableGameCodes() {
|
||||||
|
return Object.keys(MockData);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=MockData.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/client/MockData.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/client/MockData.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
10
G102-sequence/sdk/package/dist/esm/client/index.js
vendored
Normal file
10
G102-sequence/sdk/package/dist/esm/client/index.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Client SDK exports
|
||||||
|
* SDK dành cho game developers sử dụng trong game iframe
|
||||||
|
*/
|
||||||
|
export { GameClientSDK, getGameClientSDK, destroyGameClientSDK, } from './GameClientSDK';
|
||||||
|
// Mock Data - sample data cho từng game code
|
||||||
|
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';
|
||||||
|
// Data Validator - verify data structure
|
||||||
|
export { validateGameData, getSchema, getSchemaDoc, DataValidator, } from './DataValidator';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/client/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/client/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,aAAa,EACb,gBAAgB,EAChB,oBAAoB,GAQvB,MAAM,iBAAiB,CAAC;AAEzB,6CAA6C;AAC7C,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;AAEpB,yCAAyC;AACzC,OAAO,EACH,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,aAAa,GAIhB,MAAM,iBAAiB,CAAC"}
|
||||||
292
G102-sequence/sdk/package/dist/esm/game-bridge/GameBridge.js
vendored
Normal file
292
G102-sequence/sdk/package/dist/esm/game-bridge/GameBridge.js
vendored
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* Game Bridge
|
||||||
|
* Wrapper đơn giản cho game developers để giao tiếp với SDK Iframe
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```typescript
|
||||||
|
* import { GameBridge } from 'game-iframe-sdk/game-bridge';
|
||||||
|
*
|
||||||
|
* const bridge = new GameBridge({
|
||||||
|
* sdkIframeUrl: 'https://sdk.sena.tech/sdk-iframe.html',
|
||||||
|
* debug: true,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Init
|
||||||
|
* await bridge.init({
|
||||||
|
* mode: 'preview',
|
||||||
|
* game_code: 'G001',
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Listen for data
|
||||||
|
* bridge.on('dataReady', (data) => {
|
||||||
|
* renderGame(data.items);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Check answer
|
||||||
|
* bridge.checkAnswer('q1', userChoice).then(result => {
|
||||||
|
* showFeedback(result.correct);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import { SDK_MESSAGE_TYPES, createSdkMessage, isSdkMessage, } from '../sdk-iframe/types';
|
||||||
|
// =============================================================================
|
||||||
|
// GAME BRIDGE
|
||||||
|
// =============================================================================
|
||||||
|
export class GameBridge {
|
||||||
|
constructor(config) {
|
||||||
|
this.sdkIframe = null;
|
||||||
|
this.sdkOrigin = '';
|
||||||
|
this.isReady = false;
|
||||||
|
// Event handlers
|
||||||
|
this.handlers = new Map();
|
||||||
|
// Pending requests (for promise-based API)
|
||||||
|
this.pendingRequests = new Map();
|
||||||
|
this.requestCounter = 0;
|
||||||
|
this.config = {
|
||||||
|
sdkIframeUrl: config.sdkIframeUrl,
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
timeout: config.timeout ?? 10000,
|
||||||
|
};
|
||||||
|
// Extract origin from SDK URL
|
||||||
|
try {
|
||||||
|
const url = new URL(this.config.sdkIframeUrl);
|
||||||
|
this.sdkOrigin = url.origin;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
this.sdkOrigin = '*';
|
||||||
|
}
|
||||||
|
this.setupMessageListener();
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - Init
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Create SDK Iframe and initialize
|
||||||
|
*/
|
||||||
|
async init(payload) {
|
||||||
|
// Create hidden iframe
|
||||||
|
this.sdkIframe = document.createElement('iframe');
|
||||||
|
this.sdkIframe.src = this.config.sdkIframeUrl;
|
||||||
|
this.sdkIframe.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden;';
|
||||||
|
this.sdkIframe.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(this.sdkIframe);
|
||||||
|
this.log('info', 'SDK Iframe created');
|
||||||
|
// Wait for iframe to load
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.sdkIframe.onload = () => resolve();
|
||||||
|
});
|
||||||
|
// Send init
|
||||||
|
return this.sendRequest(SDK_MESSAGE_TYPES.SDK_INIT, payload);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Push data (preview mode)
|
||||||
|
*/
|
||||||
|
async pushData(payload) {
|
||||||
|
return this.sendRequest(SDK_MESSAGE_TYPES.SDK_PUSH_DATA, payload);
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - Game Actions
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Check answer - returns local result immediately
|
||||||
|
* Also triggers server sync in background
|
||||||
|
*/
|
||||||
|
async checkAnswer(questionId, choice, timeSpent) {
|
||||||
|
const payload = {
|
||||||
|
question_id: questionId,
|
||||||
|
choice,
|
||||||
|
time_spent: timeSpent,
|
||||||
|
};
|
||||||
|
return this.sendRequest(SDK_MESSAGE_TYPES.SDK_CHECK_ANSWER, payload);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get final result
|
||||||
|
*/
|
||||||
|
async getFinalResult() {
|
||||||
|
return this.sendRequest(SDK_MESSAGE_TYPES.SDK_GET_RESULT, {});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Retry sync for a question
|
||||||
|
*/
|
||||||
|
async retrySync(questionId) {
|
||||||
|
return this.sendRequest(SDK_MESSAGE_TYPES.SDK_RETRY_SYNC, {
|
||||||
|
question_id: questionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - Events
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Subscribe to events
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unsubscribe from events
|
||||||
|
*/
|
||||||
|
off(event, handler) {
|
||||||
|
this.handlers.get(event)?.delete(handler);
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - State
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Check if SDK is ready
|
||||||
|
*/
|
||||||
|
isSdkReady() {
|
||||||
|
return this.isReady;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Destroy bridge and cleanup
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
// Clear pending requests
|
||||||
|
this.pendingRequests.forEach((pending) => {
|
||||||
|
clearTimeout(pending.timeout);
|
||||||
|
pending.reject(new Error('Bridge destroyed'));
|
||||||
|
});
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
// Remove iframe
|
||||||
|
if (this.sdkIframe && this.sdkIframe.parentNode) {
|
||||||
|
this.sdkIframe.parentNode.removeChild(this.sdkIframe);
|
||||||
|
}
|
||||||
|
this.sdkIframe = null;
|
||||||
|
// Clear handlers
|
||||||
|
this.handlers.clear();
|
||||||
|
this.log('info', 'Bridge destroyed');
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// INTERNAL - Message Handling
|
||||||
|
// ==========================================================================
|
||||||
|
setupMessageListener() {
|
||||||
|
window.addEventListener('message', this.handleMessage.bind(this));
|
||||||
|
}
|
||||||
|
handleMessage(event) {
|
||||||
|
// Validate origin (if not *)
|
||||||
|
if (this.sdkOrigin !== '*' && event.origin !== this.sdkOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = event.data;
|
||||||
|
if (!isSdkMessage(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.log('debug', `Received: ${data.type}`, data.payload);
|
||||||
|
// Handle pending request response
|
||||||
|
if (data.request_id && this.pendingRequests.has(data.request_id)) {
|
||||||
|
const pending = this.pendingRequests.get(data.request_id);
|
||||||
|
clearTimeout(pending.timeout);
|
||||||
|
this.pendingRequests.delete(data.request_id);
|
||||||
|
if (data.type === SDK_MESSAGE_TYPES.SDK_ERROR) {
|
||||||
|
pending.reject(data.payload);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pending.resolve(data.payload);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Handle events
|
||||||
|
switch (data.type) {
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_READY:
|
||||||
|
this.isReady = true;
|
||||||
|
this.emit('ready', data.payload);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_DATA_READY:
|
||||||
|
this.emit('dataReady', data.payload);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_ANSWER_RESULT:
|
||||||
|
this.emit('answerResult', data.payload);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_SYNC_STATUS:
|
||||||
|
this.emit('syncStatus', data.payload);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_SYNC_ERROR:
|
||||||
|
this.emit('syncError', data.payload);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_FINAL_RESULT:
|
||||||
|
this.emit('finalResult', data.payload);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_ERROR:
|
||||||
|
this.emit('error', data.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit(event, data) {
|
||||||
|
this.handlers.get(event)?.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler(data);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.log('error', `Error in ${event} handler`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// INTERNAL - Sending Messages
|
||||||
|
// ==========================================================================
|
||||||
|
sendRequest(type, payload) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.sdkIframe?.contentWindow) {
|
||||||
|
reject(new Error('SDK Iframe not ready'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const requestId = `req_${++this.requestCounter}_${Date.now()}`;
|
||||||
|
const message = createSdkMessage(type, payload, requestId);
|
||||||
|
// Setup timeout
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.pendingRequests.delete(requestId);
|
||||||
|
reject(new Error(`Request timeout: ${type}`));
|
||||||
|
}, this.config.timeout);
|
||||||
|
// Store pending request
|
||||||
|
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
||||||
|
// Send message
|
||||||
|
this.sdkIframe.contentWindow.postMessage(message, this.sdkOrigin);
|
||||||
|
this.log('debug', `Sent: ${type}`, payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log(level, message, data) {
|
||||||
|
if (!this.config.debug && level === 'debug')
|
||||||
|
return;
|
||||||
|
const prefix = '[GameBridge]';
|
||||||
|
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 bridgeInstance = null;
|
||||||
|
/**
|
||||||
|
* Get or create GameBridge instance
|
||||||
|
*/
|
||||||
|
export function getGameBridge(config) {
|
||||||
|
if (!bridgeInstance && config) {
|
||||||
|
bridgeInstance = new GameBridge(config);
|
||||||
|
}
|
||||||
|
if (!bridgeInstance) {
|
||||||
|
throw new Error('GameBridge not initialized. Call with config first.');
|
||||||
|
}
|
||||||
|
return bridgeInstance;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Destroy GameBridge instance
|
||||||
|
*/
|
||||||
|
export function destroyGameBridge() {
|
||||||
|
bridgeInstance?.destroy();
|
||||||
|
bridgeInstance = null;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=GameBridge.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/game-bridge/GameBridge.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/game-bridge/GameBridge.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
G102-sequence/sdk/package/dist/esm/game-bridge/index.js
vendored
Normal file
8
G102-sequence/sdk/package/dist/esm/game-bridge/index.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Game Bridge exports
|
||||||
|
* Dành cho game developers tích hợp vào game
|
||||||
|
*/
|
||||||
|
export { GameBridge, getGameBridge, destroyGameBridge, } from './GameBridge';
|
||||||
|
// Re-export types từ sdk-iframe
|
||||||
|
export { SDK_MESSAGE_TYPES, } from '../sdk-iframe/types';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/game-bridge/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/game-bridge/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/game-bridge/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,UAAU,EACV,aAAa,EACb,iBAAiB,GAGpB,MAAM,cAAc,CAAC;AAEtB,gCAAgC;AAChC,OAAO,EACH,iBAAiB,GAYpB,MAAM,qBAAqB,CAAC"}
|
||||||
94
G102-sequence/sdk/package/dist/esm/index.js
vendored
Normal file
94
G102-sequence/sdk/package/dist/esm/index.js
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Game Iframe SDK - Main Entry Point
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
* @module game-iframe-sdk
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - types.ts: Type definitions
|
||||||
|
* - mappers.ts: Data transformation/mapping
|
||||||
|
* - EventEmitter.ts: Simple typed event emitter
|
||||||
|
* - MessageHandler.ts: Handle incoming messages from iframe
|
||||||
|
* - MessageSender.ts: Send messages to iframe
|
||||||
|
* - GameIframeSDK.ts: Main SDK (composes above layers)
|
||||||
|
* - useGameIframeSDK.ts: React hook
|
||||||
|
*
|
||||||
|
* @example Browser/Vanilla JS
|
||||||
|
* ```typescript
|
||||||
|
* import { GameIframeSDK } from 'game-iframe-sdk';
|
||||||
|
*
|
||||||
|
* const sdk = new GameIframeSDK({
|
||||||
|
* iframeOrigin: 'http://senaai.vn:1357',
|
||||||
|
* debug: true
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* sdk.setIframe(document.getElementById('gameIframe'));
|
||||||
|
*
|
||||||
|
* sdk.on('gameReady', () => {
|
||||||
|
* sdk.sendGameData({ game_id: 'xxx', user_id: 'yyy', questions: [...] });
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example React
|
||||||
|
* ```tsx
|
||||||
|
* import { useGameIframeSDK } from 'game-iframe-sdk';
|
||||||
|
*
|
||||||
|
* function GamePlayer() {
|
||||||
|
* const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
*
|
||||||
|
* const { isReady, sendGameData } = useGameIframeSDK({
|
||||||
|
* iframeRef,
|
||||||
|
* iframeOrigin: 'http://senaai.vn:1357',
|
||||||
|
* onAnswerReport: (data) => submitToServer(data),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* return <iframe ref={iframeRef} src={gameUrl} />;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
// =============================================================================
|
||||||
|
// CORE SDK
|
||||||
|
// =============================================================================
|
||||||
|
export { GameIframeSDK, createGameIframeSDK, getGameIframeSDK, destroyGameIframeSDK, } from './GameIframeSDK';
|
||||||
|
// =============================================================================
|
||||||
|
// LAYERS (for advanced usage)
|
||||||
|
// =============================================================================
|
||||||
|
export { MessageHandler } from './MessageHandler';
|
||||||
|
export { MessageSender } from './MessageSender';
|
||||||
|
export { EventEmitter } from './EventEmitter';
|
||||||
|
// Mappers removed - mapped data should be handled by the application
|
||||||
|
// =============================================================================
|
||||||
|
// =============================================================================
|
||||||
|
// REACT HOOK
|
||||||
|
// =============================================================================
|
||||||
|
export { useGameIframeSDK } from './useGameIframeSDK';
|
||||||
|
// =============================================================================
|
||||||
|
// TYPES
|
||||||
|
// =============================================================================
|
||||||
|
export {
|
||||||
|
// Message Types
|
||||||
|
MESSAGE_TYPES, DEFAULT_CONFIG, } from './types';
|
||||||
|
// =============================================================================
|
||||||
|
// KIT (Helper Mappers, API, Components)
|
||||||
|
// =============================================================================
|
||||||
|
import * as Kit from './kit';
|
||||||
|
export { Kit };
|
||||||
|
// =============================================================================
|
||||||
|
// CLIENT SDK (for Game Iframe - used by game developers)
|
||||||
|
// =============================================================================
|
||||||
|
import * as Client from './client';
|
||||||
|
export { Client };
|
||||||
|
export { GameClientSDK, getGameClientSDK, destroyGameClientSDK, } from './client';
|
||||||
|
// =============================================================================
|
||||||
|
// SDK IFRAME (Hidden iframe - runs on separate domain for security)
|
||||||
|
// =============================================================================
|
||||||
|
import * as SdkIframe from './sdk-iframe';
|
||||||
|
export { SdkIframe };
|
||||||
|
export { SdkIframeCore, SDK_MESSAGE_TYPES, } from './sdk-iframe';
|
||||||
|
// =============================================================================
|
||||||
|
// GAME BRIDGE (For game developers - communicates with SDK Iframe)
|
||||||
|
// =============================================================================
|
||||||
|
import * as GameBridgeModule from './game-bridge';
|
||||||
|
export { GameBridgeModule };
|
||||||
|
export { GameBridge, getGameBridge, destroyGameBridge, } from './game-bridge';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,gFAAgF;AAChF,WAAW;AACX,gFAAgF;AAEhF,OAAO,EACH,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,GACvB,MAAM,iBAAiB,CAAC;AAEzB,gFAAgF;AAChF,8BAA8B;AAC9B,gFAAgF;AAEhF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,qEAAqE;AACrE,gFAAgF;AAEhF,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,gFAAgF;AAChF,QAAQ;AACR,gFAAgF;AAEhF,OAAO;AACH,gBAAgB;AAChB,aAAa,EAiBb,cAAc,GAMjB,MAAM,SAAS,CAAC;AAEjB,gFAAgF;AAChF,wCAAwC;AACxC,gFAAgF;AAEhF,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,GAAG,EAAE,CAAC;AAEf,gFAAgF;AAChF,yDAAyD;AACzD,gFAAgF;AAEhF,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EACH,aAAa,EACb,gBAAgB,EAChB,oBAAoB,GAQvB,MAAM,UAAU,CAAC;AAElB,gFAAgF;AAChF,oEAAoE;AACpE,gFAAgF;AAEhF,OAAO,KAAK,SAAS,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EACH,aAAa,EACb,iBAAiB,GAapB,MAAM,cAAc,CAAC;AAEtB,gFAAgF;AAChF,mEAAmE;AACnE,gFAAgF;AAEhF,OAAO,KAAK,gBAAgB,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,OAAO,EACH,UAAU,EACV,aAAa,EACb,iBAAiB,GAGpB,MAAM,eAAe,CAAC"}
|
||||||
444
G102-sequence/sdk/package/dist/esm/kit/GameDataHandler.js
vendored
Normal file
444
G102-sequence/sdk/package/dist/esm/kit/GameDataHandler.js
vendored
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
/**
|
||||||
|
* GameDataHandler - Data Sanitizer & Scorer
|
||||||
|
*
|
||||||
|
* Game Codes:
|
||||||
|
* - G001: Quiz text-text
|
||||||
|
* - G002: Quiz audio-text (audio question, text answer)
|
||||||
|
* - G003: Quiz text-audio (text question, audio answer)
|
||||||
|
* - G004: Quiz image-text (image question, text answer)
|
||||||
|
* - G005: Quiz text-image (text question, image answer)
|
||||||
|
*
|
||||||
|
* - G110: Sequence Word - no audio
|
||||||
|
* - G111: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
* - G112: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
* - G113: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
*
|
||||||
|
* - G120: Sequence Sentence - no audio
|
||||||
|
* - G121: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
* - G122: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
* - G123: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
*/
|
||||||
|
// Game code metadata
|
||||||
|
export const GAME_CODES = {
|
||||||
|
// Quiz
|
||||||
|
G001: { name: 'Quiz Text-Text', category: 'quiz', hasAudio: false, hasImage: false },
|
||||||
|
G002: { name: 'Quiz Audio-Text', category: 'quiz', hasAudio: true, hasImage: false },
|
||||||
|
G003: { name: 'Quiz Text-Audio', category: 'quiz', hasAudio: true, hasImage: false },
|
||||||
|
G004: { name: 'Quiz Image-Text', category: 'quiz', hasAudio: false, hasImage: true },
|
||||||
|
G005: { name: 'Quiz Text-Image', category: 'quiz', hasAudio: false, hasImage: true },
|
||||||
|
// Sequence Word
|
||||||
|
G110: { name: 'Sequence Word', category: 'sequence_word', hasAudio: false },
|
||||||
|
G111: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||||
|
G112: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||||
|
G113: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||||
|
// Sequence Sentence
|
||||||
|
G120: { name: 'Sequence Sentence', category: 'sequence_sentence', hasAudio: false },
|
||||||
|
G121: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||||
|
G122: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||||
|
G123: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||||
|
};
|
||||||
|
export function getGameCategory(code) {
|
||||||
|
return GAME_CODES[code]?.category || 'unknown';
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// SANITIZE DATA FOR CLIENT (REMOVE ANSWERS)
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* Sanitize game data before sending to iframe
|
||||||
|
* CRITICAL: Never send answers/correct data to client
|
||||||
|
*/
|
||||||
|
export function sanitizeForClient(code, items) {
|
||||||
|
if (!Array.isArray(items))
|
||||||
|
return [];
|
||||||
|
switch (code) {
|
||||||
|
// ===== QUIZ VARIANTS =====
|
||||||
|
case 'G001': // Quiz text-text
|
||||||
|
return sanitizeQuizTextText(items);
|
||||||
|
case 'G002': // Quiz audio-text
|
||||||
|
return sanitizeQuizAudioText(items);
|
||||||
|
case 'G003': // Quiz text-audio
|
||||||
|
return sanitizeQuizTextAudio(items);
|
||||||
|
case 'G004': // Quiz image-text
|
||||||
|
return sanitizeQuizImageText(items);
|
||||||
|
case 'G005': // Quiz text-image
|
||||||
|
return sanitizeQuizTextImage(items);
|
||||||
|
// ===== SEQUENCE WORD VARIANTS =====
|
||||||
|
case 'G110': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
case 'G111': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
case 'G112': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
case 'G113': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
// ===== SEQUENCE SENTENCE VARIANTS =====
|
||||||
|
case 'G120': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
case 'G121': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
case 'G122': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
case 'G123': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
default:
|
||||||
|
console.warn(`[GameDataHandler] Unknown game code: ${code}, returning raw data`);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// QUIZ SANITIZERS
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* G001: Quiz Text-Text
|
||||||
|
* Client receives: id, question, options (shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizTextText(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
// Normalize options to {text: string}
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { text: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
return { text: String(o.text ?? '') };
|
||||||
|
}
|
||||||
|
return { text: String(o ?? '') };
|
||||||
|
});
|
||||||
|
// Shuffle to hide answer position
|
||||||
|
shuffleArray(options);
|
||||||
|
// Save shuffled text order for SDK to resolve index
|
||||||
|
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.question,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledTexts, // SDK internal
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G002: Quiz Audio-Text
|
||||||
|
* Client receives: id, question (audio URL), options (shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizAudioText(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { text: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
return { text: String(o.text ?? '') };
|
||||||
|
}
|
||||||
|
return { text: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.audio || item.audio_url,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledTexts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G003: Quiz Text-Audio
|
||||||
|
* Client receives: id, question (text), options (audio URLs shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizTextAudio(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { audio: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
const audioUrl = o.audio || o.audio_url || '';
|
||||||
|
return { audio: String(audioUrl) };
|
||||||
|
}
|
||||||
|
return { audio: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledAudios = options.map((o) => String(o.audio ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.question,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledAudios,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G004: Quiz Image-Text
|
||||||
|
* Client receives: id, image_url, question (hint), options (shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizImageText(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { text: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
return { text: String(o.text ?? '') };
|
||||||
|
}
|
||||||
|
return { text: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
image_url: item.image_url,
|
||||||
|
question: item.question,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledTexts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G005: Quiz Text-Image
|
||||||
|
* Client receives: id, question (text), options (image URLs shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizTextImage(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { image_url: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
const imageUrl = o.image_url || o.image || '';
|
||||||
|
return { image_url: String(imageUrl) };
|
||||||
|
}
|
||||||
|
return { image_url: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledUrls = options.map((o) => String(o.image_url ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.question,
|
||||||
|
options: options, // Each option has {image_url: ...}
|
||||||
|
__shuffledOptions: shuffledUrls,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// SEQUENCE WORD SANITIZERS
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* Sequence Word (G110-G113)
|
||||||
|
* Client receives: id, question (array with blanks), options (missing letters shuffled), audio_url (optional)
|
||||||
|
* Client does NOT receive: word, parts, answer, missing_letter_count
|
||||||
|
*
|
||||||
|
* Logic:
|
||||||
|
* 1. Read missing_letter_count from item (count of letters to blank out)
|
||||||
|
* 2. Randomly select positions to blank
|
||||||
|
* 3. question: array with blanks at selected positions
|
||||||
|
* 4. options: extracted missing letters (shuffled)
|
||||||
|
*/
|
||||||
|
function sanitizeSequenceWord(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const parts = item.answer || item.parts || [];
|
||||||
|
const missingCount = item.missing_letter_count || 0;
|
||||||
|
if (missingCount === 0 || parts.length === 0) {
|
||||||
|
// No missing - all visible
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: [...parts],
|
||||||
|
options: [],
|
||||||
|
...(item.audio_url && { audio_url: item.audio_url })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Randomly select which positions to blank
|
||||||
|
const allIndices = Array.from({ length: parts.length }, (_, i) => i);
|
||||||
|
const blankIndices = new Set();
|
||||||
|
const count = Math.min(missingCount, parts.length);
|
||||||
|
while (blankIndices.size < count) {
|
||||||
|
const randomIdx = Math.floor(Math.random() * allIndices.length);
|
||||||
|
const actualIdx = allIndices[randomIdx];
|
||||||
|
blankIndices.add(actualIdx);
|
||||||
|
allIndices.splice(randomIdx, 1);
|
||||||
|
}
|
||||||
|
// Build question array with blanks at random positions
|
||||||
|
const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p));
|
||||||
|
// Extract missing letters and shuffle
|
||||||
|
const missingLetters = Array.from(blankIndices).map(i => String(parts[i]));
|
||||||
|
shuffleArray(missingLetters);
|
||||||
|
const result = {
|
||||||
|
id: item.id,
|
||||||
|
question, // e.g. ["H", "", "L", "", "O"]
|
||||||
|
options: missingLetters, // e.g. ["L", "E"] - shuffled
|
||||||
|
__shuffledOptions: [...missingLetters] // SDK internal: to resolve indices
|
||||||
|
};
|
||||||
|
if (item.audio_url) {
|
||||||
|
result.audio_url = item.audio_url;
|
||||||
|
}
|
||||||
|
// CRITICAL: Do NOT send word, parts, answer, missing_letter_count
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// SEQUENCE SENTENCE SANITIZERS
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* Sequence Sentence (G120-G123)
|
||||||
|
* Client receives: id, question (array with blanks), options (missing words shuffled), audio_url (optional)
|
||||||
|
* Client does NOT receive: sentence, parts, answer, missing_letter_count
|
||||||
|
*
|
||||||
|
* Logic: Same as Sequence Word
|
||||||
|
* 1. Read missing_letter_count from item
|
||||||
|
* 2. Randomly select positions to blank
|
||||||
|
* 3. question: array with blanks
|
||||||
|
* 4. options: extracted missing words (shuffled)
|
||||||
|
*/
|
||||||
|
function sanitizeSequenceSentence(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const parts = item.answer || item.parts || [];
|
||||||
|
const missingCount = item.missing_letter_count || 0;
|
||||||
|
if (missingCount === 0 || parts.length === 0) {
|
||||||
|
// No missing - all visible
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: [...parts],
|
||||||
|
options: [],
|
||||||
|
...(item.audio_url && { audio_url: item.audio_url })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Randomly select which positions to blank
|
||||||
|
const allIndices = Array.from({ length: parts.length }, (_, i) => i);
|
||||||
|
const blankIndices = new Set();
|
||||||
|
const count = Math.min(missingCount, parts.length);
|
||||||
|
while (blankIndices.size < count) {
|
||||||
|
const randomIdx = Math.floor(Math.random() * allIndices.length);
|
||||||
|
const actualIdx = allIndices[randomIdx];
|
||||||
|
blankIndices.add(actualIdx);
|
||||||
|
allIndices.splice(randomIdx, 1);
|
||||||
|
}
|
||||||
|
// Build question array with blanks at random positions
|
||||||
|
const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p));
|
||||||
|
// Extract missing words and shuffle
|
||||||
|
const missingWords = Array.from(blankIndices).map(i => String(parts[i]));
|
||||||
|
shuffleArray(missingWords);
|
||||||
|
const result = {
|
||||||
|
id: item.id,
|
||||||
|
question, // e.g. ["I", "", "reading", ""]
|
||||||
|
options: missingWords, // e.g. ["love", "books"] - shuffled
|
||||||
|
__shuffledOptions: [...missingWords] // SDK internal
|
||||||
|
};
|
||||||
|
if (item.audio_url) {
|
||||||
|
result.audio_url = item.audio_url;
|
||||||
|
}
|
||||||
|
// CRITICAL: Do NOT send sentence, parts, answer, missing_letter_count
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if player's answer is correct
|
||||||
|
*
|
||||||
|
* @param code - Game code (G001, G110, etc.)
|
||||||
|
* @param originalItem - Original item from server (has answer field!)
|
||||||
|
* @param playerAnswer - Player's answer (text for quiz, array for sequence)
|
||||||
|
*/
|
||||||
|
export function checkAnswer(code, originalItem, playerAnswer) {
|
||||||
|
switch (code) {
|
||||||
|
// ===== QUIZ VARIANTS (G001-G005) =====
|
||||||
|
case 'G001': // Quiz Text-Text
|
||||||
|
case 'G002': // Quiz Audio-Text
|
||||||
|
case 'G003': // Quiz Text-Audio
|
||||||
|
case 'G004': // Quiz Image-Text
|
||||||
|
case 'G005': // Quiz Text-Image
|
||||||
|
return checkQuizAnswer(originalItem, playerAnswer);
|
||||||
|
// ===== SEQUENCE WORD VARIANTS (G110-G113) =====
|
||||||
|
case 'G110': // Sequence Word
|
||||||
|
case 'G111': // Sequence Word Audio
|
||||||
|
case 'G112': // Sequence Word Audio
|
||||||
|
case 'G113': // Sequence Word Audio
|
||||||
|
return checkSequenceAnswer(originalItem, playerAnswer);
|
||||||
|
// ===== SEQUENCE SENTENCE VARIANTS (G120-G123) =====
|
||||||
|
case 'G120': // Sequence Sentence
|
||||||
|
case 'G121': // Sequence Sentence Audio
|
||||||
|
case 'G122': // Sequence Sentence Audio
|
||||||
|
case 'G123': // Sequence Sentence Audio
|
||||||
|
return checkSequenceAnswer(originalItem, playerAnswer);
|
||||||
|
default:
|
||||||
|
console.warn(`[GameDataHandler] Unknown game code for scoring: ${code}`);
|
||||||
|
return { isCorrect: false, score: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Quiz Scoring
|
||||||
|
function checkQuizAnswer(item, answerChoice) {
|
||||||
|
const correctAnswer = String(item.answer || '');
|
||||||
|
if (!correctAnswer) {
|
||||||
|
return { isCorrect: false, score: 0, feedback: 'No correct answer defined' };
|
||||||
|
}
|
||||||
|
let playerAnswerText;
|
||||||
|
if (typeof answerChoice === 'number') {
|
||||||
|
// Index: resolve from original options
|
||||||
|
if (Array.isArray(item.options)) {
|
||||||
|
const v = item.options[answerChoice];
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
playerAnswerText = v;
|
||||||
|
}
|
||||||
|
else if (v && typeof v === 'object' && 'text' in v) {
|
||||||
|
playerAnswerText = String(v.text ?? '');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
playerAnswerText = String(v ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { isCorrect: false, score: 0, feedback: 'Invalid question options' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Direct text answer
|
||||||
|
if (answerChoice && typeof answerChoice === 'object' && 'text' in answerChoice) {
|
||||||
|
playerAnswerText = String(answerChoice.text ?? '');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
playerAnswerText = String(answerChoice ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isCorrect = playerAnswerText.toLowerCase().trim() === correctAnswer.toLowerCase().trim();
|
||||||
|
return {
|
||||||
|
isCorrect,
|
||||||
|
score: isCorrect ? 1 : 0,
|
||||||
|
feedback: isCorrect
|
||||||
|
? `✅ Correct! "${playerAnswerText}" matches answer "${correctAnswer}"`
|
||||||
|
: `❌ Wrong. You chose "${playerAnswerText}" but correct answer is "${correctAnswer}"`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Sequence Scoring
|
||||||
|
function checkSequenceAnswer(item, answer) {
|
||||||
|
const correctOrder = item.answer || item.parts;
|
||||||
|
if (!Array.isArray(answer) || !Array.isArray(correctOrder)) {
|
||||||
|
return { isCorrect: false, score: 0 };
|
||||||
|
}
|
||||||
|
const isCorrect = arraysEqual(answer, correctOrder);
|
||||||
|
return {
|
||||||
|
isCorrect,
|
||||||
|
score: isCorrect ? 1 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// UTILITIES
|
||||||
|
// =============================================================================
|
||||||
|
function shuffleArray(array) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function arraysEqual(a, b) {
|
||||||
|
if (a.length !== b.length)
|
||||||
|
return false;
|
||||||
|
return a.every((val, idx) => {
|
||||||
|
if (typeof val === 'string' && typeof b[idx] === 'string') {
|
||||||
|
return val.toLowerCase().trim() === b[idx].toLowerCase().trim();
|
||||||
|
}
|
||||||
|
return val === b[idx];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=GameDataHandler.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/kit/GameDataHandler.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/kit/GameDataHandler.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
82
G102-sequence/sdk/package/dist/esm/kit/api.js
vendored
Normal file
82
G102-sequence/sdk/package/dist/esm/kit/api.js
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Game API Client Kit
|
||||||
|
* Standardized API client for communicating with Game Backend
|
||||||
|
*/
|
||||||
|
export class GameApiClient {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
async request(method, endpoint, body) {
|
||||||
|
const url = `${this.config.baseUrl}${endpoint}`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(this.config.getHeaders ? this.config.getHeaders() : {})
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorBody = await res.text();
|
||||||
|
let errorMessage = `API Error ${res.status}: ${res.statusText}`;
|
||||||
|
let errorCode;
|
||||||
|
try {
|
||||||
|
const jsonError = JSON.parse(errorBody);
|
||||||
|
// Capture error code from response
|
||||||
|
if (jsonError.code !== undefined) {
|
||||||
|
errorCode = jsonError.code;
|
||||||
|
}
|
||||||
|
if (jsonError.message)
|
||||||
|
errorMessage += ` - ${jsonError.message}`;
|
||||||
|
else if (jsonError.error)
|
||||||
|
errorMessage += ` - ${jsonError.error}`;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (errorBody && errorBody.length < 200)
|
||||||
|
errorMessage += ` - ${errorBody}`;
|
||||||
|
}
|
||||||
|
// Throw error object with code and message
|
||||||
|
const error = new Error(errorMessage);
|
||||||
|
error.code = errorCode;
|
||||||
|
error.httpStatus = res.status;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('[GameApiClient] Request failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getGameWithProgress(assignmentId, studentId, refresh = false) {
|
||||||
|
return this.request('GET', `/submissions/live/init/${assignmentId}/${studentId}${refresh ? '?refresh=1' : ''}`);
|
||||||
|
}
|
||||||
|
async startLiveSession(assignmentId, studentId, refresh = false) {
|
||||||
|
return this.request('POST', `/submissions/live/start${refresh ? '?refresh=1' : ''}`, {
|
||||||
|
assignment_id: assignmentId,
|
||||||
|
student_id: studentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async submitAnswer(assignmentId, studentId, questionId, answer, timeSpent = 5, isTimeout = false) {
|
||||||
|
return this.request('POST', '/submissions/live/answer', {
|
||||||
|
assignment_id: assignmentId,
|
||||||
|
student_id: studentId,
|
||||||
|
question_id: questionId,
|
||||||
|
selected_answer: answer,
|
||||||
|
time_spent: timeSpent,
|
||||||
|
is_timeout: isTimeout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async completeSession(assignmentId, studentId) {
|
||||||
|
return this.request('POST', '/submissions/live/complete', {
|
||||||
|
assignment_id: assignmentId,
|
||||||
|
student_id: studentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async getLeaderboard(assignmentId, studentId) {
|
||||||
|
return this.request('GET', `/submissions/leaderboard/${assignmentId}?student_id=${studentId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=api.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/kit/api.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/kit/api.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/kit/api.ts"],"names":[],"mappings":"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"}
|
||||||
6
G102-sequence/sdk/package/dist/esm/kit/index.js
vendored
Normal file
6
G102-sequence/sdk/package/dist/esm/kit/index.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './mappers';
|
||||||
|
export * from './api';
|
||||||
|
export * from './GameDataHandler';
|
||||||
|
export * from './react/GamePlayer';
|
||||||
|
export * from './react/GameTestPlayer';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/kit/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/kit/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/kit/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC"}
|
||||||
75
G102-sequence/sdk/package/dist/esm/kit/mappers.js
vendored
Normal file
75
G102-sequence/sdk/package/dist/esm/kit/mappers.js
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Mappers Kit
|
||||||
|
* Helper functions to transform data between Client App and Game Iframe
|
||||||
|
*/
|
||||||
|
export function prepareCompletedQuestions(answeredQuestions) {
|
||||||
|
return (answeredQuestions || []).map(a => ({
|
||||||
|
id: a.id || a.questionId,
|
||||||
|
result: (a.isCorrect || a.result === 1) ? 1 : 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function createGamePayload(options) {
|
||||||
|
const { gameId, userId, gameData, answeredQuestions = [], endTimeIso } = options;
|
||||||
|
const completed_question_ids = prepareCompletedQuestions(answeredQuestions);
|
||||||
|
// Ưu tiên lấy field .questions hoặc .data, hoặc dùng chính gameData nếu nó là mảng
|
||||||
|
let data = [];
|
||||||
|
if (Array.isArray(gameData)) {
|
||||||
|
data = gameData;
|
||||||
|
}
|
||||||
|
else if (gameData && Array.isArray(gameData.questions)) {
|
||||||
|
data = gameData.questions;
|
||||||
|
}
|
||||||
|
else if (gameData && Array.isArray(gameData.data)) {
|
||||||
|
data = gameData.data;
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
game_id: gameId,
|
||||||
|
user_id: userId,
|
||||||
|
data: data,
|
||||||
|
completed_question_ids: completed_question_ids,
|
||||||
|
// Merge các field metadata khác
|
||||||
|
...(typeof gameData === 'object' && !Array.isArray(gameData) ? gameData : {}),
|
||||||
|
// Merge extraData
|
||||||
|
...(options.extraData || {})
|
||||||
|
};
|
||||||
|
// Inject end_time_iso (absolute timestamp for accurate sync)
|
||||||
|
if (endTimeIso) {
|
||||||
|
payload.end_time_iso = endTimeIso;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
export function createLeaderboardPayload(apiData) {
|
||||||
|
const topPlayers = apiData.topPlayers || [];
|
||||||
|
const userRank = apiData.userRank || null;
|
||||||
|
return {
|
||||||
|
top_players: topPlayers.map((p) => ({
|
||||||
|
rank: p.rank,
|
||||||
|
name: p.name || p.studentName || p.user_id,
|
||||||
|
score: p.score ?? p.finalScore ?? 0,
|
||||||
|
student_id: p.studentId || p.userId,
|
||||||
|
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||||
|
completed_at: p.completedAt
|
||||||
|
})),
|
||||||
|
user_rank: userRank ? {
|
||||||
|
rank: userRank.rank,
|
||||||
|
name: userRank.name || userRank.studentName,
|
||||||
|
score: userRank.score ?? userRank.finalScore ?? 0,
|
||||||
|
student_id: userRank.studentId || userRank.userId,
|
||||||
|
time_spent: userRank.timeSpent ?? userRank.time_spent ?? 0,
|
||||||
|
completed_at: userRank.completedAt
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function normalizeAnswerReport(data) {
|
||||||
|
// Simplified per user request
|
||||||
|
// Input: { question_id: "Q1", result: 1, choice: "2" }
|
||||||
|
return {
|
||||||
|
question_id: data.question_id || data.questionId || data.id,
|
||||||
|
choice: data.choice ?? data.selected_answer ?? data.selectedAnswer,
|
||||||
|
result: data.result ?? (data.is_correct ? 1 : 0),
|
||||||
|
is_correct: !!(data.result === 1 || data.is_correct === true),
|
||||||
|
time_spent: data.time_spent ?? 5,
|
||||||
|
is_timeout: !!data.is_timeout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=mappers.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/kit/mappers.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/kit/mappers.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"mappers.js","sourceRoot":"","sources":["../../../src/kit/mappers.ts"],"names":[],"mappings":"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"}
|
||||||
579
G102-sequence/sdk/package/dist/esm/kit/react/GamePlayer.js
vendored
Normal file
579
G102-sequence/sdk/package/dist/esm/kit/react/GamePlayer.js
vendored
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
|
import { useGameIframeSDK } from '../../useGameIframeSDK';
|
||||||
|
import { GameApiClient } from '../api';
|
||||||
|
import { createGamePayload, normalizeAnswerReport } from '../mappers';
|
||||||
|
// Session Error Codes (synced with backend)
|
||||||
|
export const SessionErrorCodes = {
|
||||||
|
SESSION_NOT_STARTED: -60,
|
||||||
|
SESSION_ENDED: -61,
|
||||||
|
SESSION_COMPLETED: -62,
|
||||||
|
SESSION_TIMEOUT: -63,
|
||||||
|
SESSION_NOT_FOUND: -64,
|
||||||
|
SESSION_ALREADY_ANSWERED: -65,
|
||||||
|
};
|
||||||
|
export const GamePlayer = ({ apiConfig, assignmentId, studentId, className, style, debug = false, onComplete, onError, onGameLoaded, extraGameData, onLog, onSessionStatus, renderLoading, renderError, loadingTimeout = 30000 }) => {
|
||||||
|
const iframeRef = useRef(null);
|
||||||
|
const [gameUrl, setGameUrl] = useState('');
|
||||||
|
const [gameState, setGameState] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [hasLoadedGame, setHasLoadedGame] = useState(false);
|
||||||
|
const [apiClient] = useState(() => new GameApiClient(apiConfig));
|
||||||
|
// Refs to prevent infinite loops
|
||||||
|
const sendLeaderboardRef = useRef(null);
|
||||||
|
const hasInitRef = useRef(false); // Track if init has been called
|
||||||
|
const callbacksRef = useRef({ onGameLoaded, onLog, onSessionStatus, onError }); // Stable callback refs
|
||||||
|
// Update callback refs on each render
|
||||||
|
callbacksRef.current = { onGameLoaded, onLog, onSessionStatus, onError };
|
||||||
|
// Helper: Create structured error
|
||||||
|
const createError = useCallback((err) => {
|
||||||
|
const errorMsg = err?.message || err?.error || String(err);
|
||||||
|
const errorCode = err?.code; // Error code from API response
|
||||||
|
// Check error code first (more reliable than message parsing)
|
||||||
|
if (errorCode !== undefined) {
|
||||||
|
// Session not started
|
||||||
|
if (errorCode === SessionErrorCodes.SESSION_NOT_STARTED) {
|
||||||
|
return {
|
||||||
|
type: 'not_started',
|
||||||
|
code: errorCode,
|
||||||
|
message: errorMsg || 'Bài tập chưa bắt đầu',
|
||||||
|
details: err,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Session ended
|
||||||
|
if (errorCode === SessionErrorCodes.SESSION_ENDED) {
|
||||||
|
return {
|
||||||
|
type: 'session',
|
||||||
|
code: errorCode,
|
||||||
|
message: errorMsg || 'Bài tập đã kết thúc',
|
||||||
|
details: err,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Session completed
|
||||||
|
if (errorCode === SessionErrorCodes.SESSION_COMPLETED) {
|
||||||
|
return {
|
||||||
|
type: 'session',
|
||||||
|
code: errorCode,
|
||||||
|
message: errorMsg || 'Bạn đã hoàn thành bài tập này rồi',
|
||||||
|
details: err,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Session timeout
|
||||||
|
if (errorCode === SessionErrorCodes.SESSION_TIMEOUT) {
|
||||||
|
return {
|
||||||
|
type: 'timeout',
|
||||||
|
code: errorCode,
|
||||||
|
message: errorMsg || 'Đã hết thời gian làm bài',
|
||||||
|
details: err,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: Parse error message
|
||||||
|
// API errors
|
||||||
|
if (errorMsg.includes('API Error') || errorMsg.includes('Failed to fetch')) {
|
||||||
|
// Check if it contains session-related message
|
||||||
|
if (errorMsg.includes('chưa bắt đầu')) {
|
||||||
|
return {
|
||||||
|
type: 'not_started',
|
||||||
|
message: 'Bài tập chưa bắt đầu',
|
||||||
|
details: err,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'api',
|
||||||
|
message: errorMsg,
|
||||||
|
details: err,
|
||||||
|
canRetry: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Network errors
|
||||||
|
if (errorMsg.includes('network') || errorMsg.includes('ECONNREFUSED')) {
|
||||||
|
return {
|
||||||
|
type: 'network',
|
||||||
|
message: 'Lỗi kết nối mạng. Vui lòng kiểm tra internet.',
|
||||||
|
details: err,
|
||||||
|
canRetry: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Session errors (message-based fallback)
|
||||||
|
if (errorMsg.includes('chưa bắt đầu')) {
|
||||||
|
return {
|
||||||
|
type: 'not_started',
|
||||||
|
message: errorMsg || 'Bài tập chưa bắt đầu',
|
||||||
|
details: err,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('hết thời gian') || errorMsg.includes('đã kết thúc') ||
|
||||||
|
errorMsg.includes('đã hoàn thành') || errorMsg.includes('session')) {
|
||||||
|
return {
|
||||||
|
type: 'session',
|
||||||
|
message: errorMsg,
|
||||||
|
details: err,
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Timeout errors
|
||||||
|
if (errorMsg.includes('timeout') || errorMsg.includes('timed out')) {
|
||||||
|
return {
|
||||||
|
type: 'timeout',
|
||||||
|
message: 'Kết nối quá lâu. Vui lòng thử lại.',
|
||||||
|
details: err,
|
||||||
|
canRetry: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'unknown',
|
||||||
|
message: errorMsg || 'Đã xảy ra lỗi không xác định',
|
||||||
|
details: err,
|
||||||
|
canRetry: true
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
// Helper: Handle error with callback
|
||||||
|
const handleError = useCallback((err) => {
|
||||||
|
const gameError = createError(err);
|
||||||
|
setError(gameError);
|
||||||
|
setLoading(false);
|
||||||
|
if (onError) {
|
||||||
|
onError(gameError);
|
||||||
|
}
|
||||||
|
if (onLog) {
|
||||||
|
onLog(`[SDK ERR] ${gameError.type}: ${gameError.message}`, 'error');
|
||||||
|
}
|
||||||
|
}, [createError, onError, onLog]);
|
||||||
|
// Retry function
|
||||||
|
const retryInit = useCallback(() => {
|
||||||
|
if (callbacksRef.current.onLog)
|
||||||
|
callbacksRef.current.onLog('[SDK] Retrying initialization...', 'info');
|
||||||
|
hasInitRef.current = false; // Allow re-init
|
||||||
|
setError(null);
|
||||||
|
setLoading(true);
|
||||||
|
setGameUrl('');
|
||||||
|
setGameState(null);
|
||||||
|
}, []);
|
||||||
|
// Define Handlers
|
||||||
|
const handleAnswer = useCallback(async (data) => {
|
||||||
|
try {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK RECV] Answer Raw: ${JSON.stringify(data)}`, 'info');
|
||||||
|
const report = normalizeAnswerReport(data);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK] Processed Answer: ID ${report.question_id} | Choice: ${report.choice} | ${report.is_correct ? 'Correct' : 'Wrong'}`, 'info');
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API REQ] Submitting Answer ID ${report.question_id}...`, 'info');
|
||||||
|
const res = await apiClient.submitAnswer(assignmentId, studentId, report.question_id, report.choice, report.time_spent, report.is_timeout);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API RES] Submit Result: ${JSON.stringify(res)}`, 'success');
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('[GamePlayer] Submit error:', err);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API ERR] Submit Failed: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}, [apiClient, assignmentId, studentId, onLog]);
|
||||||
|
const handleFinalResult = useCallback(async (data) => {
|
||||||
|
try {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK RECV] Final Result Raw: ${JSON.stringify(data)}`, 'info');
|
||||||
|
if (onLog)
|
||||||
|
onLog('[SDK] Game Complete. Calling API...', 'info');
|
||||||
|
const res = await apiClient.completeSession(assignmentId, studentId);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API RES] Session Completed. Score: ${res.data?.finalScore}`, 'success');
|
||||||
|
// Auto-refresh leaderboard after completion to ensure user rank is included
|
||||||
|
if (onLog)
|
||||||
|
onLog('[SDK] Auto-refreshing leaderboard after completion...', 'info');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500)); // Small delay for backend to process
|
||||||
|
try {
|
||||||
|
const lbRes = await apiClient.getLeaderboard(assignmentId, studentId);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API RES] Post-completion Leaderboard: ${JSON.stringify(lbRes)}`, 'success');
|
||||||
|
if (lbRes.success && lbRes.data && sendLeaderboardRef.current) {
|
||||||
|
const leaderboardData = {
|
||||||
|
top_players: (lbRes.data.topPlayers || []).map((p) => ({
|
||||||
|
rank: p.rank,
|
||||||
|
name: p.name || p.studentName || p.userId,
|
||||||
|
score: p.score ?? p.finalScore ?? 0,
|
||||||
|
student_id: p.studentId || p.userId,
|
||||||
|
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||||
|
completed_at: p.completedAt
|
||||||
|
})),
|
||||||
|
user_rank: lbRes.data.userRank ? {
|
||||||
|
rank: lbRes.data.userRank.rank,
|
||||||
|
name: lbRes.data.userRank.name || lbRes.data.userRank.studentName,
|
||||||
|
score: lbRes.data.userRank.score ?? lbRes.data.userRank.finalScore ?? 0,
|
||||||
|
student_id: lbRes.data.userRank.studentId || lbRes.data.userRank.userId,
|
||||||
|
time_spent: lbRes.data.userRank.timeSpent ?? lbRes.data.userRank.time_spent ?? 0,
|
||||||
|
completed_at: lbRes.data.userRank.completedAt
|
||||||
|
} : null
|
||||||
|
};
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK SEND] Updated Leaderboard: ${JSON.stringify(leaderboardData)}`, 'info');
|
||||||
|
sendLeaderboardRef.current(leaderboardData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (lbErr) {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API ERR] Leaderboard refresh failed: ${lbErr.message}`, 'warning');
|
||||||
|
}
|
||||||
|
if (onComplete)
|
||||||
|
onComplete(res);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('[GamePlayer] Complete error:', err);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API ERR] Complete Failed: ${err.message}`, 'error');
|
||||||
|
if (onError)
|
||||||
|
onError(err);
|
||||||
|
}
|
||||||
|
}, [apiClient, assignmentId, studentId, onComplete, onError, onLog]);
|
||||||
|
// SDK Hook
|
||||||
|
const { isReady, sendGameData, sendLeaderboard } = useGameIframeSDK({
|
||||||
|
iframeRef,
|
||||||
|
iframeOrigin: '*',
|
||||||
|
debug,
|
||||||
|
onGameReady: () => {
|
||||||
|
if (onLog)
|
||||||
|
onLog('[SDK RECV] Iframe Ready', 'success');
|
||||||
|
},
|
||||||
|
onAnswerReport: handleAnswer,
|
||||||
|
onFinalResult: handleFinalResult,
|
||||||
|
onLeaderboardRequest: async (top) => {
|
||||||
|
try {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK RECV] Leaderboard Request Raw: top=${top}`, 'info');
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API REQ] Fetching Leaderboard (top ${top})...`, 'info');
|
||||||
|
const res = await apiClient.getLeaderboard(assignmentId, studentId);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API RES] Leaderboard Raw: ${JSON.stringify(res)}`, 'success');
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const leaderboardData = {
|
||||||
|
top_players: (res.data.topPlayers || []).map((p) => ({
|
||||||
|
rank: p.rank,
|
||||||
|
name: p.name || p.studentName || p.userId,
|
||||||
|
score: p.score ?? p.finalScore ?? 0,
|
||||||
|
student_id: p.studentId || p.userId,
|
||||||
|
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||||
|
completed_at: p.completedAt
|
||||||
|
})),
|
||||||
|
user_rank: res.data.userRank ? {
|
||||||
|
rank: res.data.userRank.rank,
|
||||||
|
name: res.data.userRank.name || res.data.userRank.studentName,
|
||||||
|
score: res.data.userRank.score ?? res.data.userRank.finalScore ?? 0,
|
||||||
|
student_id: res.data.userRank.studentId || res.data.userRank.userId,
|
||||||
|
time_spent: res.data.userRank.timeSpent ?? res.data.userRank.time_spent ?? 0,
|
||||||
|
completed_at: res.data.userRank.completedAt
|
||||||
|
} : null
|
||||||
|
};
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK SEND] Leaderboard Payload: ${JSON.stringify(leaderboardData)}`, 'info');
|
||||||
|
const sent = sendLeaderboard(leaderboardData);
|
||||||
|
if (onLog)
|
||||||
|
onLog(sent ? '[SDK] Leaderboard sent successfully' : '[SDK ERR] Failed to send leaderboard', sent ? 'success' : 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('[GamePlayer] Leaderboard error:', err);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API ERR] Leaderboard Failed: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Update ref after hook
|
||||||
|
sendLeaderboardRef.current = sendLeaderboard;
|
||||||
|
// 1. Fetch Game Data & Start Session
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
let loadingTimeoutId;
|
||||||
|
const initGame = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
if (onLog)
|
||||||
|
onLog('[SDK] Initializing Game...', 'info');
|
||||||
|
// Set loading timeout
|
||||||
|
loadingTimeoutId = setTimeout(() => {
|
||||||
|
if (mounted && loading) {
|
||||||
|
if (onLog)
|
||||||
|
onLog('[SDK WARN] Loading taking longer than expected...', 'warning');
|
||||||
|
}
|
||||||
|
}, loadingTimeout);
|
||||||
|
// getGameWithProgress đã tự động tạo/restore session trong backend
|
||||||
|
// thông qua getOrCreateSubmissionCache - không cần gọi startLiveSession riêng
|
||||||
|
const res = await apiClient.getGameWithProgress(assignmentId, studentId);
|
||||||
|
if (!res.success || !res.data) {
|
||||||
|
throw new Error(res.message || 'Failed to load game');
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
// Check session status TRƯỚC để quyết định có load game hay không
|
||||||
|
const session = res.data.session;
|
||||||
|
const progress = res.data.progress;
|
||||||
|
// Case 1: Already completed
|
||||||
|
if (res.data.isFinished || session?.completed) {
|
||||||
|
// CHỈ set error nếu CHƯA load game (lần đầu vào)
|
||||||
|
if (!hasLoadedGame) {
|
||||||
|
const errorObj = {
|
||||||
|
type: 'session',
|
||||||
|
message: 'Bạn đã hoàn thành bài tập này rồi',
|
||||||
|
details: { score: progress?.score, answered: progress?.answered, total: progress?.total },
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
setError(errorObj);
|
||||||
|
setLoading(false);
|
||||||
|
clearTimeout(loadingTimeoutId);
|
||||||
|
if (onError)
|
||||||
|
onError(errorObj);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK] ${errorObj.message}`, 'warning');
|
||||||
|
}
|
||||||
|
// Luôn gọi callback (cho cả 2 trường hợp)
|
||||||
|
if (onSessionStatus) {
|
||||||
|
onSessionStatus({
|
||||||
|
type: 'completed',
|
||||||
|
message: 'Bạn đã hoàn thành bài tập này',
|
||||||
|
data: {
|
||||||
|
answered: progress?.answered || 0,
|
||||||
|
total: progress?.total || 0,
|
||||||
|
score: progress?.score || 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Nếu CHƯA load game → STOP
|
||||||
|
// Nếu ĐÃ load game → tiếp tục (cho game hiển thị leaderboard)
|
||||||
|
if (!hasLoadedGame) {
|
||||||
|
return; // ❌ STOP - không load game
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Case 2: Session timeout
|
||||||
|
if (session && session.remainingTime !== null && session.remainingTime <= 0) {
|
||||||
|
// CHỈ set error nếu CHƯA load game
|
||||||
|
if (!hasLoadedGame) {
|
||||||
|
const errorObj = {
|
||||||
|
type: 'timeout',
|
||||||
|
message: 'Thời gian làm bài đã hết',
|
||||||
|
details: { answered: progress?.answered, total: progress?.total },
|
||||||
|
canRetry: false
|
||||||
|
};
|
||||||
|
setError(errorObj);
|
||||||
|
setLoading(false);
|
||||||
|
clearTimeout(loadingTimeoutId);
|
||||||
|
if (onError)
|
||||||
|
onError(errorObj);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK] ${errorObj.message}`, 'warning');
|
||||||
|
}
|
||||||
|
// Luôn gọi callback
|
||||||
|
if (onSessionStatus) {
|
||||||
|
onSessionStatus({
|
||||||
|
type: 'timeout',
|
||||||
|
message: 'Thời gian làm bài đã hết',
|
||||||
|
data: {
|
||||||
|
answered: progress?.answered || 0,
|
||||||
|
total: progress?.total || 0,
|
||||||
|
score: progress?.score || 0,
|
||||||
|
remainingTime: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Nếu CHƯA load game → STOP
|
||||||
|
if (!hasLoadedGame) {
|
||||||
|
return; // ❌ STOP - không load game
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Case 3: Valid session - LOAD game
|
||||||
|
setGameState(res.data);
|
||||||
|
setGameUrl(res.data.gameUrl);
|
||||||
|
setLoading(false);
|
||||||
|
setHasLoadedGame(true); // ✅ Đánh dấu đã load game
|
||||||
|
clearTimeout(loadingTimeoutId);
|
||||||
|
if (onGameLoaded)
|
||||||
|
onGameLoaded(res.data);
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[API RES] Game Loaded: ${res.data.title || res.data.gameType || assignmentId}`, 'success');
|
||||||
|
// Notify session status for valid sessions
|
||||||
|
if (session && onSessionStatus) {
|
||||||
|
// Resumed session (có câu đã trả lời)
|
||||||
|
if (progress && progress.answered > 0) {
|
||||||
|
onSessionStatus({
|
||||||
|
type: 'resumed',
|
||||||
|
message: `Tiếp tục làm bài (Đã làm ${progress.answered}/${progress.total} câu)`,
|
||||||
|
data: {
|
||||||
|
answered: progress.answered,
|
||||||
|
total: progress.total,
|
||||||
|
score: progress.score,
|
||||||
|
remainingTime: session.remainingTime,
|
||||||
|
startedAt: session.startedAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// New session
|
||||||
|
else {
|
||||||
|
onSessionStatus({
|
||||||
|
type: 'new',
|
||||||
|
message: 'Bắt đầu làm bài mới',
|
||||||
|
data: {
|
||||||
|
answered: 0,
|
||||||
|
total: progress?.total || 0,
|
||||||
|
score: 0,
|
||||||
|
remainingTime: session.remainingTime,
|
||||||
|
startedAt: session.startedAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Log session info
|
||||||
|
if (session && onLog) {
|
||||||
|
const sessionInfo = `Session started at ${session.startedAt}, remaining: ${session.remainingTime}s`;
|
||||||
|
onLog(`[SDK] ${sessionInfo}`, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('[GamePlayer] Init error:', err);
|
||||||
|
if (mounted) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (assignmentId && studentId && !hasInitRef.current) {
|
||||||
|
hasInitRef.current = true; // Prevent re-init
|
||||||
|
initGame();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
hasInitRef.current = false; // ✅ Reset for StrictMode remount
|
||||||
|
if (loadingTimeoutId)
|
||||||
|
clearTimeout(loadingTimeoutId);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [assignmentId, studentId]); // ✅ Chỉ re-init khi assignmentId hoặc studentId thay đổi
|
||||||
|
// 2. Auto Send Data when Ready
|
||||||
|
useEffect(() => {
|
||||||
|
if (isReady && gameState) {
|
||||||
|
// Get expiresAt from session (absolute timestamp for accurate sync)
|
||||||
|
const getEndTimeIso = () => {
|
||||||
|
const session = gameState.session;
|
||||||
|
if (!session)
|
||||||
|
return undefined;
|
||||||
|
// Ưu tiên dùng expiresAt
|
||||||
|
if (session.expiresAt) {
|
||||||
|
return session.expiresAt;
|
||||||
|
}
|
||||||
|
// Fallback: tính từ startedAt + duration
|
||||||
|
if (session.startedAt && session.duration) {
|
||||||
|
const startTimeMs = new Date(session.startedAt).getTime();
|
||||||
|
const expiresAtMs = startTimeMs + (session.duration * 1000);
|
||||||
|
return new Date(expiresAtMs).toISOString();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
const payload = createGamePayload({
|
||||||
|
gameId: gameState.id,
|
||||||
|
userId: studentId,
|
||||||
|
gameData: gameState.jsonData,
|
||||||
|
answeredQuestions: gameState.completed_question_ids,
|
||||||
|
extraData: extraGameData,
|
||||||
|
endTimeIso: getEndTimeIso() // ✅ Absolute timestamp
|
||||||
|
});
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[SDK SEND] Game Payload: ${JSON.stringify(payload)}`, 'info');
|
||||||
|
sendGameData(payload);
|
||||||
|
}
|
||||||
|
}, [isReady, gameState, studentId, sendGameData, extraGameData, onLog]);
|
||||||
|
// ===== RENDER =====
|
||||||
|
// 1. Error State
|
||||||
|
if (error) {
|
||||||
|
if (renderError) {
|
||||||
|
return _jsx(_Fragment, { children: renderError(error, retryInit) });
|
||||||
|
}
|
||||||
|
// Default error UI
|
||||||
|
return (_jsxs("div", { className: "game-player-error", style: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
padding: '2rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
...style
|
||||||
|
}, children: [_jsx("div", { style: {
|
||||||
|
fontSize: '3rem',
|
||||||
|
marginBottom: '1rem'
|
||||||
|
}, children: error.type === 'network' ? '🌐' :
|
||||||
|
error.type === 'timeout' ? '⏱️' :
|
||||||
|
error.type === 'session' ? '🔒' :
|
||||||
|
error.type === 'not_started' ? '📅' : '⚠️' }), _jsx("h2", { style: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
color: error.type === 'not_started' ? '#fd7e14' : '#dc3545'
|
||||||
|
}, children: error.type === 'network' ? 'Lỗi Kết Nối' :
|
||||||
|
error.type === 'timeout' ? 'Hết Giờ Làm Bài' :
|
||||||
|
error.type === 'session' ? 'Phiên Làm Bài Đã Kết Thúc' :
|
||||||
|
error.type === 'not_started' ? 'Bài Tập Chưa Bắt Đầu' :
|
||||||
|
'Đã Xảy Ra Lỗi' }), _jsx("p", { style: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: '#6c757d',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
maxWidth: '500px'
|
||||||
|
}, children: error.message }), error.canRetry && (_jsx("button", { onClick: retryInit, style: {
|
||||||
|
padding: '0.75rem 2rem',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#fff',
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background-color 0.2s'
|
||||||
|
}, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#0056b3', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = '#007bff', children: "\uD83D\uDD04 Th\u1EED L\u1EA1i" })), !error.canRetry && (_jsx("p", { style: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: '#6c757d',
|
||||||
|
marginTop: '1rem'
|
||||||
|
}, children: "Vui l\u00F2ng li\u00EAn h\u1EC7 gi\u00E1o vi\u00EAn ho\u1EB7c quay l\u1EA1i trang ch\u1EE7" }))] }));
|
||||||
|
}
|
||||||
|
// 2. Loading State
|
||||||
|
if (loading || !gameUrl) {
|
||||||
|
if (renderLoading) {
|
||||||
|
return _jsx(_Fragment, { children: renderLoading() });
|
||||||
|
}
|
||||||
|
// Default loading UI
|
||||||
|
return (_jsxs("div", { className: "game-player-loading", style: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
...style
|
||||||
|
}, children: [_jsx("div", { style: {
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
border: '5px solid #e9ecef',
|
||||||
|
borderTop: '5px solid #007bff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
marginBottom: '1rem'
|
||||||
|
} }), _jsx("p", { style: {
|
||||||
|
fontSize: '1.125rem',
|
||||||
|
color: '#6c757d',
|
||||||
|
fontWeight: '500'
|
||||||
|
}, children: "\u0110ang t\u1EA3i tr\u00F2 ch\u01A1i..." }), _jsx("style", { children: `
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
` })] }));
|
||||||
|
}
|
||||||
|
// 3. Game Iframe
|
||||||
|
return (_jsx("iframe", { ref: iframeRef, src: gameUrl, className: className, style: { width: '100%', height: '100%', border: 'none', ...style }, allowFullScreen: true }));
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=GamePlayer.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/kit/react/GamePlayer.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/kit/react/GamePlayer.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
112
G102-sequence/sdk/package/dist/esm/kit/react/GameTestPlayer.js
vendored
Normal file
112
G102-sequence/sdk/package/dist/esm/kit/react/GameTestPlayer.js
vendored
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { useRef, useEffect, useState } from 'react';
|
||||||
|
import { useGameIframeSDK } from '../../useGameIframeSDK';
|
||||||
|
/**
|
||||||
|
* GameTestPlayer - Component test đơn giản
|
||||||
|
*
|
||||||
|
* Chỉ load game data vào iframe, KHÔNG gọi API
|
||||||
|
* Dùng để test game iframe locally
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <GameTestPlayer
|
||||||
|
* gameUrl="http://localhost:3000/game"
|
||||||
|
* gameData={[
|
||||||
|
* { id: 1, question: "What is 2+2?", options: ["3","4","5"], answer: "4" },
|
||||||
|
* { id: 2, question: "Capital of France?", options: ["London","Paris","Berlin"], answer: "Paris" }
|
||||||
|
* ]}
|
||||||
|
* debug={true}
|
||||||
|
* onLog={(msg, type) => console.log(`[${type}] ${msg}`)}
|
||||||
|
* onAnswer={(data) => console.log('Answer:', data)}
|
||||||
|
* onComplete={(result) => console.log('Complete:', result)}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const GameTestPlayer = ({ gameUrl, gameData, userId = 'test_user', gameId = 'test_game', extraData, endTimeIso, className, style, debug = false, onAnswer, onComplete, onLog, onLeaderboardRequest, mockLeaderboard }) => {
|
||||||
|
const iframeRef = useRef(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
// SDK Hook
|
||||||
|
const { isReady, sendGameData, sendLeaderboard } = useGameIframeSDK({
|
||||||
|
iframeRef,
|
||||||
|
iframeOrigin: '*',
|
||||||
|
debug,
|
||||||
|
onGameReady: () => {
|
||||||
|
if (onLog)
|
||||||
|
onLog('[TEST] Iframe Ready', 'success');
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
onAnswerReport: (data) => {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[TEST] Answer: ${JSON.stringify(data)}`, 'info');
|
||||||
|
if (onAnswer)
|
||||||
|
onAnswer(data);
|
||||||
|
},
|
||||||
|
onFinalResult: (result) => {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[TEST] Complete: ${JSON.stringify(result)}`, 'success');
|
||||||
|
if (onComplete)
|
||||||
|
onComplete(result);
|
||||||
|
},
|
||||||
|
onLeaderboardRequest: (top) => {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[TEST] Leaderboard Request: top=${top}`, 'info');
|
||||||
|
if (onLeaderboardRequest)
|
||||||
|
onLeaderboardRequest(top);
|
||||||
|
// Auto send mock leaderboard if provided
|
||||||
|
if (mockLeaderboard) {
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[TEST] Sending mock leaderboard`, 'info');
|
||||||
|
sendLeaderboard(mockLeaderboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Auto send game data when ready
|
||||||
|
useEffect(() => {
|
||||||
|
if (isReady && gameData) {
|
||||||
|
const payload = {
|
||||||
|
game_id: String(gameId),
|
||||||
|
user_id: userId,
|
||||||
|
data: gameData,
|
||||||
|
completed_question_ids: [],
|
||||||
|
...(extraData || {})
|
||||||
|
};
|
||||||
|
if (endTimeIso) {
|
||||||
|
payload.end_time_iso = endTimeIso;
|
||||||
|
}
|
||||||
|
if (onLog)
|
||||||
|
onLog(`[TEST] Sending Game Data: ${gameData.length} items`, 'info');
|
||||||
|
sendGameData(payload);
|
||||||
|
}
|
||||||
|
}, [isReady, gameData, gameId, userId, extraData, endTimeIso, sendGameData, onLog]);
|
||||||
|
return (_jsxs("div", { style: { position: 'relative', width: '100%', height: '100%', ...style }, children: [isLoading && (_jsx("div", { style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||||
|
zIndex: 10
|
||||||
|
}, children: _jsxs("div", { style: { textAlign: 'center' }, children: [_jsx("div", { style: {
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
border: '4px solid #e9ecef',
|
||||||
|
borderTop: '4px solid #007bff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
margin: '0 auto 1rem'
|
||||||
|
} }), _jsx("p", { style: { color: '#6c757d' }, children: "Loading game..." }), _jsx("style", { children: `
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
` })] }) })), _jsx("iframe", { ref: iframeRef, src: gameUrl, className: className, style: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
border: 'none'
|
||||||
|
}, allowFullScreen: true })] }));
|
||||||
|
};
|
||||||
|
export default GameTestPlayer;
|
||||||
|
//# sourceMappingURL=GameTestPlayer.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/kit/react/GameTestPlayer.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/kit/react/GameTestPlayer.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"GameTestPlayer.js","sourceRoot":"","sources":["../../../../src/kit/react/GameTestPlayer.tsx"],"names":[],"mappings":"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"}
|
||||||
357
G102-sequence/sdk/package/dist/esm/loader/SenaGameSDK.js
vendored
Normal file
357
G102-sequence/sdk/package/dist/esm/loader/SenaGameSDK.js
vendored
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
/**
|
||||||
|
* SenaGame SDK Loader
|
||||||
|
*
|
||||||
|
* Ready-to-use interface for game developers.
|
||||||
|
* Handles SDK iframe creation, communication, and lifecycle.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```html
|
||||||
|
* <script src="sena-game-sdk.js"></script>
|
||||||
|
* <script>
|
||||||
|
* const game = new SenaGameSDK({
|
||||||
|
* iframePath: '/path/to/sdk-iframe/index.html',
|
||||||
|
* mode: 'live',
|
||||||
|
* gameCode: 'G001',
|
||||||
|
* onReady: (sdk) => {
|
||||||
|
* sdk.pushData({ items: [...] });
|
||||||
|
* },
|
||||||
|
* onAnswerResult: (result) => {
|
||||||
|
* console.log('Answer:', result);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* </script>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
// ========================================
|
||||||
|
// MESSAGE TYPES (matching SDK)
|
||||||
|
// ========================================
|
||||||
|
const MSG = {
|
||||||
|
// Outgoing (Game → SDK)
|
||||||
|
INIT: 'SDK_INIT',
|
||||||
|
PUSH_DATA: 'SDK_PUSH_DATA',
|
||||||
|
SUBMIT_ANSWER: 'SDK_CHECK_ANSWER',
|
||||||
|
COMPLETE_GAME: 'SDK_COMPLETE_GAME',
|
||||||
|
GET_STATUS: 'SDK_GET_STATUS',
|
||||||
|
// Incoming (SDK → Game)
|
||||||
|
READY: 'SDK_READY',
|
||||||
|
DATA_READY: 'SDK_DATA_READY',
|
||||||
|
ANSWER_RESULT: 'SDK_ANSWER_RESULT',
|
||||||
|
GAME_COMPLETE: 'SDK_GAME_COMPLETE',
|
||||||
|
SESSION_STARTED: 'SDK_SESSION_STARTED',
|
||||||
|
STATUS: 'SDK_STATUS',
|
||||||
|
ERROR: 'SDK_ERROR',
|
||||||
|
};
|
||||||
|
// ========================================
|
||||||
|
// DEFAULT CONFIG
|
||||||
|
// ========================================
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
iframePath: '../sdk-iframe/index.html',
|
||||||
|
mode: 'preview',
|
||||||
|
gameCode: '',
|
||||||
|
debug: false,
|
||||||
|
timeout: 10000,
|
||||||
|
iframeStyle: 'position:fixed;width:1px;height:1px;left:-9999px;border:none;',
|
||||||
|
};
|
||||||
|
// ========================================
|
||||||
|
// SENA GAME SDK CLASS
|
||||||
|
// ========================================
|
||||||
|
export class SenaGameSDK {
|
||||||
|
constructor(config) {
|
||||||
|
this.iframe = null;
|
||||||
|
this.isReady = false;
|
||||||
|
this.isDataReady = false;
|
||||||
|
this.pendingMessages = [];
|
||||||
|
this.initResolver = null;
|
||||||
|
this.timeoutId = null;
|
||||||
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||||
|
// --- CLEANUP OLD LISTENERS ---
|
||||||
|
// Tránh trường hợp init SDK nhiều lần bị trùng listener cũ
|
||||||
|
const oldSDK = window._sena_game_sdk_instance;
|
||||||
|
if (oldSDK && typeof oldSDK.destroy === 'function') {
|
||||||
|
oldSDK.destroy();
|
||||||
|
}
|
||||||
|
window._sena_game_sdk_instance = this;
|
||||||
|
// Create promise for ready state
|
||||||
|
this.initPromise = new Promise((resolve, reject) => {
|
||||||
|
this.initResolver = { resolve, reject };
|
||||||
|
});
|
||||||
|
// Bind methods
|
||||||
|
this._handleMessage = this._handleMessage.bind(this);
|
||||||
|
// Auto-initialize
|
||||||
|
this._init();
|
||||||
|
}
|
||||||
|
// ----------------------------------------
|
||||||
|
// PUBLIC API
|
||||||
|
// ----------------------------------------
|
||||||
|
/**
|
||||||
|
* Push game data to SDK
|
||||||
|
*/
|
||||||
|
pushData(data) {
|
||||||
|
// Safe extraction of the array
|
||||||
|
let itemsArray = [];
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
itemsArray = data;
|
||||||
|
}
|
||||||
|
else if (data && Array.isArray(data.items)) {
|
||||||
|
itemsArray = data.items;
|
||||||
|
}
|
||||||
|
else if (data && Array.isArray(data.data)) {
|
||||||
|
itemsArray = data.data;
|
||||||
|
}
|
||||||
|
else if (data && data.items && typeof data.items === 'object') {
|
||||||
|
itemsArray = [data.items];
|
||||||
|
}
|
||||||
|
else if (data && data.data && typeof data.data === 'object') {
|
||||||
|
itemsArray = [data.data];
|
||||||
|
}
|
||||||
|
// Transform to SDK iframe internal format
|
||||||
|
const payload = {
|
||||||
|
data: itemsArray,
|
||||||
|
completed_question_ids: (data && data.completed_question_ids) || []
|
||||||
|
};
|
||||||
|
this._send(MSG.PUSH_DATA, payload);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Submit an answer
|
||||||
|
*/
|
||||||
|
submitAnswer(answer) {
|
||||||
|
// Transform to SDK iframe format
|
||||||
|
const payload = {
|
||||||
|
question_id: answer.questionId,
|
||||||
|
choice: answer.selectedAnswer,
|
||||||
|
time_spent: answer.timeSpent ?? 0
|
||||||
|
};
|
||||||
|
this._send(MSG.SUBMIT_ANSWER, payload);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Complete the game
|
||||||
|
*/
|
||||||
|
completeGame() {
|
||||||
|
this._send(MSG.COMPLETE_GAME, {});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get current status
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
this._send(MSG.GET_STATUS, {});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wait for SDK to be ready
|
||||||
|
*/
|
||||||
|
async ready() {
|
||||||
|
if (this.isReady)
|
||||||
|
return this;
|
||||||
|
return this.initPromise;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if SDK is ready
|
||||||
|
*/
|
||||||
|
get sdkReady() {
|
||||||
|
return this.isReady;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if data is ready
|
||||||
|
*/
|
||||||
|
get dataReady() {
|
||||||
|
return this.isDataReady;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Destroy the SDK instance
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
window.removeEventListener('message', this._handleMessage);
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
if (this.iframe && this.iframe.parentNode) {
|
||||||
|
this.iframe.parentNode.removeChild(this.iframe);
|
||||||
|
}
|
||||||
|
window.removeEventListener('message', this._handleMessage);
|
||||||
|
this.isReady = false;
|
||||||
|
this._log('SDK destroyed');
|
||||||
|
}
|
||||||
|
// ----------------------------------------
|
||||||
|
// PRIVATE METHODS
|
||||||
|
// ----------------------------------------
|
||||||
|
_init() {
|
||||||
|
this._log('Initializing SenaGameSDK...');
|
||||||
|
// Setup message listener
|
||||||
|
window.addEventListener('message', this._handleMessage);
|
||||||
|
// Create iframe
|
||||||
|
this._createIframe();
|
||||||
|
// Setup timeout
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
if (!this.isReady) {
|
||||||
|
const error = new Error('SDK initialization timeout');
|
||||||
|
this._error(error);
|
||||||
|
if (this.initResolver) {
|
||||||
|
this.initResolver.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.config.timeout);
|
||||||
|
}
|
||||||
|
_createIframe() {
|
||||||
|
this.iframe = document.createElement('iframe');
|
||||||
|
this.iframe.id = 'sena-game-sdk-iframe';
|
||||||
|
this.iframe.src = this.config.iframePath;
|
||||||
|
this.iframe.style.cssText = this.config.iframeStyle || '';
|
||||||
|
this.iframe.onload = () => {
|
||||||
|
this._log('Iframe loaded, sending INIT...');
|
||||||
|
setTimeout(() => {
|
||||||
|
this._send(MSG.INIT, {
|
||||||
|
mode: this.config.mode,
|
||||||
|
game_code: this.config.gameCode,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
this.iframe.onerror = () => {
|
||||||
|
this._error(new Error('Failed to load SDK iframe'));
|
||||||
|
};
|
||||||
|
document.body.appendChild(this.iframe);
|
||||||
|
this._log(`Iframe created: ${this.config.iframePath}`);
|
||||||
|
}
|
||||||
|
_send(type, payload) {
|
||||||
|
if (!this.iframe || !this.iframe.contentWindow) {
|
||||||
|
this._log(`Queuing message: ${type}`, 'warn');
|
||||||
|
this.pendingMessages.push({ type, payload });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = { type, payload, timestamp: Date.now() };
|
||||||
|
this._log(`→ ${type}`, 'send');
|
||||||
|
this.iframe.contentWindow.postMessage(message, '*');
|
||||||
|
}
|
||||||
|
_handleMessage(event) {
|
||||||
|
const data = event.data;
|
||||||
|
if (!data || !data.type)
|
||||||
|
return;
|
||||||
|
// Only process SDK messages
|
||||||
|
if (!data.type.startsWith('SDK_'))
|
||||||
|
return;
|
||||||
|
this._log(`← ${data.type}`, 'recv');
|
||||||
|
switch (data.type) {
|
||||||
|
case MSG.READY:
|
||||||
|
this._onSDKReady();
|
||||||
|
break;
|
||||||
|
case MSG.DATA_READY:
|
||||||
|
this._onDataReady(data.payload);
|
||||||
|
break;
|
||||||
|
case MSG.ANSWER_RESULT:
|
||||||
|
this._onAnswerResult(data.payload);
|
||||||
|
break;
|
||||||
|
case MSG.GAME_COMPLETE:
|
||||||
|
this._onGameComplete(data.payload);
|
||||||
|
break;
|
||||||
|
case MSG.SESSION_STARTED:
|
||||||
|
this._onSessionStart(data.payload);
|
||||||
|
break;
|
||||||
|
case MSG.ERROR:
|
||||||
|
this._error(new Error(data.payload?.message || 'SDK Error'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onSDKReady() {
|
||||||
|
this.isReady = true;
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
this.timeoutId = null;
|
||||||
|
}
|
||||||
|
// Flush pending messages
|
||||||
|
while (this.pendingMessages.length > 0) {
|
||||||
|
const msg = this.pendingMessages.shift();
|
||||||
|
if (msg) {
|
||||||
|
this._send(msg.type, msg.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Callback
|
||||||
|
if (this.config.onReady) {
|
||||||
|
this.config.onReady(this);
|
||||||
|
}
|
||||||
|
// Resolve promise
|
||||||
|
if (this.initResolver) {
|
||||||
|
this.initResolver.resolve(this);
|
||||||
|
}
|
||||||
|
this._log('✅ SDK Ready!');
|
||||||
|
}
|
||||||
|
_onDataReady(payload) {
|
||||||
|
this.isDataReady = true;
|
||||||
|
// Normalize payload to camelCase
|
||||||
|
const normalized = {
|
||||||
|
items: payload.items || [],
|
||||||
|
totalQuestions: payload.total_questions || payload.totalQuestions || 0,
|
||||||
|
completedCount: payload.completed_count || payload.completedCount || 0,
|
||||||
|
resumeData: payload.resume_data || payload.resumeData || []
|
||||||
|
};
|
||||||
|
if (this.config.onDataReady) {
|
||||||
|
this.config.onDataReady(normalized);
|
||||||
|
}
|
||||||
|
this._log(`✅ Data Ready: ${normalized.items.length} items`);
|
||||||
|
}
|
||||||
|
_onAnswerResult(payload) {
|
||||||
|
const normalized = {
|
||||||
|
questionId: payload.question_id || payload.questionId,
|
||||||
|
isCorrect: payload.correct !== undefined ? payload.correct : payload.isCorrect,
|
||||||
|
correctAnswer: payload.correct_answer || payload.correctAnswer || '',
|
||||||
|
score: payload.score || 0,
|
||||||
|
currentScore: payload.current_score || payload.currentScore || 0,
|
||||||
|
totalAnswered: payload.total_answered || payload.totalAnswered || 0
|
||||||
|
};
|
||||||
|
if (this.config.onAnswerResult) {
|
||||||
|
this.config.onAnswerResult(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onGameComplete(payload) {
|
||||||
|
const normalized = {
|
||||||
|
success: payload.success !== undefined ? payload.success : true,
|
||||||
|
finalScore: payload.score !== undefined ? payload.score : (payload.finalScore || 0),
|
||||||
|
correctCount: payload.correct !== undefined ? payload.correct : (payload.correctCount || 0),
|
||||||
|
totalQuestions: payload.total !== undefined ? payload.total : (payload.totalQuestions || 0),
|
||||||
|
wrongCount: payload.wrong !== undefined ? payload.wrong : (payload.wrongCount || 0),
|
||||||
|
total: payload.total || 0
|
||||||
|
};
|
||||||
|
if (this.config.onGameComplete) {
|
||||||
|
this.config.onGameComplete(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onSessionStart(payload) {
|
||||||
|
const normalized = {
|
||||||
|
assignmentId: payload.assignment_id || payload.assignmentId,
|
||||||
|
userId: payload.student_id || payload.userId,
|
||||||
|
gameId: payload.game_code || payload.gameId,
|
||||||
|
startedAt: payload.started_at || payload.startedAt || new Date().toISOString()
|
||||||
|
};
|
||||||
|
if (this.config.onSessionStart) {
|
||||||
|
this.config.onSessionStart(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_error(error) {
|
||||||
|
this._log(`❌ Error: ${error.message}`, 'error');
|
||||||
|
if (this.config.onError) {
|
||||||
|
this.config.onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_log(message, type = 'info') {
|
||||||
|
if (!this.config.debug)
|
||||||
|
return;
|
||||||
|
const prefix = '[SenaGameSDK]';
|
||||||
|
const styles = {
|
||||||
|
info: 'color: #888',
|
||||||
|
send: 'color: #ff0',
|
||||||
|
recv: 'color: #0f0',
|
||||||
|
warn: 'color: #fa0',
|
||||||
|
error: 'color: #f00',
|
||||||
|
};
|
||||||
|
console.log(`%c${prefix} ${message}`, styles[type] || styles.info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** SDK version */
|
||||||
|
SenaGameSDK.VERSION = '1.0.0';
|
||||||
|
// ========================================
|
||||||
|
// EXPORT FOR BROWSER (UMD)
|
||||||
|
// ========================================
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.SenaGameSDK = SenaGameSDK;
|
||||||
|
}
|
||||||
|
export default SenaGameSDK;
|
||||||
|
//# sourceMappingURL=SenaGameSDK.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/loader/SenaGameSDK.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/loader/SenaGameSDK.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
G102-sequence/sdk/package/dist/esm/loader/index.js
vendored
Normal file
7
G102-sequence/sdk/package/dist/esm/loader/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* SenaGame SDK Loader
|
||||||
|
* Export all types and classes for game developers
|
||||||
|
*/
|
||||||
|
export { SenaGameSDK, } from './SenaGameSDK';
|
||||||
|
export { SenaGameSDK as default } from './SenaGameSDK';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/loader/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/loader/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/loader/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,WAAW,GASd,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,IAAI,OAAO,EAAE,MAAM,eAAe,CAAC"}
|
||||||
465
G102-sequence/sdk/package/dist/esm/sdk-iframe/SdkIframeCore.js
vendored
Normal file
465
G102-sequence/sdk/package/dist/esm/sdk-iframe/SdkIframeCore.js
vendored
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
/**
|
||||||
|
* SDK Iframe Core
|
||||||
|
* Chạy trong hidden iframe riêng biệt
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Receive data từ parent (preview mode)
|
||||||
|
* - Fetch data từ API (live mode)
|
||||||
|
* - Store answers securely
|
||||||
|
* - Verify answers locally (fast feedback)
|
||||||
|
* - Sync với server (background)
|
||||||
|
* - Report results
|
||||||
|
*/
|
||||||
|
import { checkAnswer, sanitizeForClient, GAME_CODES } from '../kit/GameDataHandler';
|
||||||
|
import { getMockData } from '../client/MockData';
|
||||||
|
import { SDK_MESSAGE_TYPES, createSdkMessage, isSdkMessage, } from './types';
|
||||||
|
// =============================================================================
|
||||||
|
// SDK IFRAME CORE
|
||||||
|
// =============================================================================
|
||||||
|
export class SdkIframeCore {
|
||||||
|
constructor(config = {}) {
|
||||||
|
this.boundMessageHandler = null;
|
||||||
|
this.mode = null;
|
||||||
|
this.gameCode = null;
|
||||||
|
this.assignmentId = null;
|
||||||
|
this.studentId = null;
|
||||||
|
this.apiBaseUrl = '';
|
||||||
|
this.authToken = '';
|
||||||
|
// Data storage
|
||||||
|
this.originalItems = new Map(); // Có đáp án
|
||||||
|
this.clientItems = new Map(); // Shuffled (cho Client)
|
||||||
|
this.sanitizedItems = [];
|
||||||
|
this.userAnswers = new Map();
|
||||||
|
this.completedQuestions = new Set();
|
||||||
|
this.isInitialized = false;
|
||||||
|
this.parentOrigin = null;
|
||||||
|
this.config = {
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
allowedOrigins: config.allowedOrigins ?? ['*'],
|
||||||
|
};
|
||||||
|
this.setupMessageListener();
|
||||||
|
this.log('info', 'SDK Iframe Core initialized');
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// MESSAGE HANDLING
|
||||||
|
// ==========================================================================
|
||||||
|
setupMessageListener() {
|
||||||
|
this.boundMessageHandler = this.handleMessage.bind(this);
|
||||||
|
window.addEventListener('message', this.boundMessageHandler);
|
||||||
|
}
|
||||||
|
handleMessage(event) {
|
||||||
|
// Validate origin
|
||||||
|
if (!this.isAllowedOrigin(event.origin)) {
|
||||||
|
this.log('warn', `Blocked message from unauthorized origin: ${event.origin}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = event.data;
|
||||||
|
if (!isSdkMessage(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.log('debug', `Received: ${data.type}`, data.payload);
|
||||||
|
// Store parent origin for responses
|
||||||
|
if (!this.parentOrigin) {
|
||||||
|
this.parentOrigin = event.origin;
|
||||||
|
}
|
||||||
|
switch (data.type) {
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_INIT:
|
||||||
|
this.handleInit(data.payload, data.request_id);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_PUSH_DATA:
|
||||||
|
this.handlePushData(data.payload, data.request_id);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_CHECK_ANSWER:
|
||||||
|
this.handleCheckAnswer(data.payload, data.request_id);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_GET_RESULT:
|
||||||
|
this.handleGetResult(data.request_id);
|
||||||
|
break;
|
||||||
|
case SDK_MESSAGE_TYPES.SDK_RETRY_SYNC:
|
||||||
|
this.handleRetrySync(data.payload, data.request_id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.log('warn', `Unknown message type: ${data.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isAllowedOrigin(origin) {
|
||||||
|
if (this.config.allowedOrigins.includes('*')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.config.allowedOrigins.includes(origin);
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// MESSAGE HANDLERS
|
||||||
|
// ==========================================================================
|
||||||
|
async handleInit(payload, requestId) {
|
||||||
|
try {
|
||||||
|
this.mode = payload.mode;
|
||||||
|
this.gameCode = payload.game_code;
|
||||||
|
this.assignmentId = payload.assignment_id ?? null;
|
||||||
|
this.studentId = payload.student_id ?? null;
|
||||||
|
this.apiBaseUrl = payload.api_base_url ?? '';
|
||||||
|
this.authToken = payload.auth_token ?? '';
|
||||||
|
// Validate game code
|
||||||
|
if (!GAME_CODES[this.gameCode]) {
|
||||||
|
throw new Error(`Invalid game code: ${this.gameCode}`);
|
||||||
|
}
|
||||||
|
this.isInitialized = true;
|
||||||
|
// Send ready
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_READY, {
|
||||||
|
mode: this.mode,
|
||||||
|
game_code: this.gameCode,
|
||||||
|
}, requestId);
|
||||||
|
this.log('info', `Initialized: mode=${this.mode}, game_code=${this.gameCode}`);
|
||||||
|
// Mode handling
|
||||||
|
if (this.mode === 'live' && this.assignmentId) {
|
||||||
|
await this.fetchLiveData();
|
||||||
|
}
|
||||||
|
else if (this.mode === 'dev') {
|
||||||
|
const mockData = getMockData(this.gameCode);
|
||||||
|
if (mockData) {
|
||||||
|
this.log('info', `[Dev] Loaded mock data for ${this.gameCode}`);
|
||||||
|
this.processData(mockData.data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.sendError('MOCK_NOT_FOUND', `No mock data found for ${this.gameCode}`, requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
this.sendError('INIT_ERROR', error.message, requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handlePushData(payload, requestId) {
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
this.sendError('NOT_INITIALIZED', 'SDK not initialized. Call SDK_INIT first.', requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.mode !== 'preview') {
|
||||||
|
this.sendError('INVALID_MODE', 'SDK_PUSH_DATA only allowed in preview mode', requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.processData(payload.data, payload.completed_question_ids);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
this.sendError('DATA_ERROR', error.message, requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async handleCheckAnswer(payload, requestId) {
|
||||||
|
if (!this.isInitialized || this.originalItems.size === 0) {
|
||||||
|
this.sendError('NOT_READY', 'No data loaded', requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { question_id, choice, time_spent = 0 } = payload;
|
||||||
|
// ===== RESOLVE CHOICE BY GAME CODE =====
|
||||||
|
// Each game code has its own resolution logic
|
||||||
|
const code = this.gameCode;
|
||||||
|
const clientItem = this.clientItems.get(question_id);
|
||||||
|
let answerValue = choice; // Default: use raw choice
|
||||||
|
switch (code) {
|
||||||
|
// ===== QUIZ VARIANTS (G001-G005): Client sends INDEX -> resolve to VALUE =====
|
||||||
|
case 'G001': // Quiz Text-Text
|
||||||
|
case 'G002': // Quiz Audio-Text
|
||||||
|
case 'G003': // Quiz Text-Audio
|
||||||
|
case 'G004': // Quiz Image-Text
|
||||||
|
case 'G005': // Quiz Text-Image
|
||||||
|
// Sanitizers store __shuffledOptions which contains resolved values (strings)
|
||||||
|
if (clientItem && typeof choice === 'number') {
|
||||||
|
const shuffled = clientItem.__shuffledOptions;
|
||||||
|
if (Array.isArray(shuffled) && choice >= 0 && choice < shuffled.length) {
|
||||||
|
answerValue = shuffled[choice];
|
||||||
|
this.log('debug', `[${code}] Resolved: index ${choice} -> "${answerValue}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// ===== SEQUENCE WORD VARIANTS (G110-G113) =====
|
||||||
|
case 'G110': // Sequence Word
|
||||||
|
case 'G111': // Sequence Word Audio
|
||||||
|
case 'G112': // Sequence Word Audio
|
||||||
|
case 'G113': // Sequence Word Audio
|
||||||
|
// ===== SEQUENCE SENTENCE VARIANTS (G120-G123) =====
|
||||||
|
case 'G120': // Sequence Sentence
|
||||||
|
case 'G121': // Sequence Sentence Audio
|
||||||
|
case 'G122': // Sequence Sentence Audio
|
||||||
|
case 'G123': // Sequence Sentence Audio
|
||||||
|
// Client gửi mảng chữ đã sắp xếp ["H","E","L","L","O"]
|
||||||
|
// Không cần resolve - so trực tiếp với answer
|
||||||
|
this.log('debug', `[${code}] Answer array: ${JSON.stringify(choice)}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.log('warn', `Unknown game code: ${code}, using raw choice`);
|
||||||
|
}
|
||||||
|
// Get original item
|
||||||
|
const originalItem = this.originalItems.get(question_id);
|
||||||
|
if (!originalItem) {
|
||||||
|
this.sendError('QUESTION_NOT_FOUND', `Question ${question_id} not found`, requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// =====================================================================
|
||||||
|
// LOCAL VERIFY (Preview / Dev): Verify locally, return immediately
|
||||||
|
// =====================================================================
|
||||||
|
if (this.mode === 'preview' || this.mode === 'dev') {
|
||||||
|
// Verify using RESOLVED VALUE (answerValue)
|
||||||
|
const result = checkAnswer(this.gameCode, originalItem, answerValue);
|
||||||
|
// Store answer
|
||||||
|
const storedAnswer = {
|
||||||
|
question_id,
|
||||||
|
choice,
|
||||||
|
answer_value: answerValue, // Store resolved value for retry
|
||||||
|
correct: result.isCorrect,
|
||||||
|
score: result.score,
|
||||||
|
synced: true, // Local (preview/dev) = always synced
|
||||||
|
time_spent,
|
||||||
|
};
|
||||||
|
this.userAnswers.set(question_id, storedAnswer);
|
||||||
|
// Send result immediately
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_ANSWER_RESULT, {
|
||||||
|
question_id,
|
||||||
|
correct: result.isCorrect,
|
||||||
|
score: result.score,
|
||||||
|
synced: true,
|
||||||
|
}, requestId);
|
||||||
|
this.log('info', `[Local] Answer checked locally (${this.mode}): ${question_id} = ${result.isCorrect}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// =====================================================================
|
||||||
|
// LIVE MODE: Submit to server, wait for response, then return result
|
||||||
|
// =====================================================================
|
||||||
|
try {
|
||||||
|
// Submit RESOLVED VALUE (answerValue) to server
|
||||||
|
const serverResult = await this.submitAnswerToServer(question_id, answerValue, time_spent);
|
||||||
|
// Store answer with server result
|
||||||
|
const storedAnswer = {
|
||||||
|
question_id,
|
||||||
|
choice,
|
||||||
|
answer_value: answerValue, // Store resolved value for retry
|
||||||
|
correct: serverResult.correct,
|
||||||
|
score: serverResult.score,
|
||||||
|
synced: true,
|
||||||
|
time_spent,
|
||||||
|
};
|
||||||
|
this.userAnswers.set(question_id, storedAnswer);
|
||||||
|
// Send result from server
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_ANSWER_RESULT, {
|
||||||
|
question_id,
|
||||||
|
correct: serverResult.correct,
|
||||||
|
score: serverResult.score,
|
||||||
|
synced: true,
|
||||||
|
}, requestId);
|
||||||
|
this.log('info', `[Live] Answer verified by server: ${question_id} = ${serverResult.correct}`);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Server error - send error to game
|
||||||
|
this.sendError('SUBMIT_ERROR', error.message || 'Failed to submit answer', requestId);
|
||||||
|
this.log('error', `[Live] Failed to submit answer: ${question_id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Submit answer to server and wait for result (Live mode)
|
||||||
|
*/
|
||||||
|
async submitAnswerToServer(questionId, choice, timeSpent) {
|
||||||
|
if (!this.apiBaseUrl || !this.assignmentId) {
|
||||||
|
throw new Error('Missing apiBaseUrl or assignmentId');
|
||||||
|
}
|
||||||
|
const url = `${this.apiBaseUrl}/submissions/live/answer`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (this.authToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
assignment_id: this.assignmentId,
|
||||||
|
student_id: this.studentId,
|
||||||
|
question_id: questionId,
|
||||||
|
selected_answer: choice,
|
||||||
|
time_spent: timeSpent,
|
||||||
|
is_timeout: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || `Server error: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
return {
|
||||||
|
correct: result.correct ?? false,
|
||||||
|
score: result.score ?? (result.correct ? 1 : 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
handleGetResult(requestId) {
|
||||||
|
const details = Array.from(this.userAnswers.values()).map(answer => ({
|
||||||
|
question_id: answer.question_id,
|
||||||
|
choice: answer.choice,
|
||||||
|
correct: answer.correct,
|
||||||
|
synced: answer.synced,
|
||||||
|
time_spent: answer.time_spent,
|
||||||
|
}));
|
||||||
|
const correct = details.filter(d => d.correct).length;
|
||||||
|
const total = this.originalItems.size;
|
||||||
|
const allSynced = details.every(d => d.synced);
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_FINAL_RESULT, {
|
||||||
|
score: total > 0 ? Math.round((correct / total) * 100) : 0,
|
||||||
|
total,
|
||||||
|
correct,
|
||||||
|
wrong: total - correct,
|
||||||
|
synced: allSynced,
|
||||||
|
details,
|
||||||
|
}, requestId);
|
||||||
|
}
|
||||||
|
async handleRetrySync(payload, requestId) {
|
||||||
|
const answer = this.userAnswers.get(payload.question_id);
|
||||||
|
if (!answer) {
|
||||||
|
this.sendError('ANSWER_NOT_FOUND', `Answer for ${payload.question_id} not found`, requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (answer.synced) {
|
||||||
|
// Already synced
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_SYNC_STATUS, {
|
||||||
|
question_id: payload.question_id,
|
||||||
|
synced: true,
|
||||||
|
}, requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Retry submit to server - use RESOLVED value, not raw choice
|
||||||
|
try {
|
||||||
|
const serverResult = await this.submitAnswerToServer(answer.question_id, answer.answer_value, // Use resolved value!
|
||||||
|
answer.time_spent);
|
||||||
|
// Update stored answer
|
||||||
|
answer.correct = serverResult.correct;
|
||||||
|
answer.score = serverResult.score;
|
||||||
|
answer.synced = true;
|
||||||
|
// Send success
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_SYNC_STATUS, {
|
||||||
|
question_id: payload.question_id,
|
||||||
|
synced: true,
|
||||||
|
server_result: serverResult,
|
||||||
|
}, requestId);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
this.sendError('RETRY_ERROR', error.message || 'Retry failed', requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// DATA PROCESSING
|
||||||
|
// ==========================================================================
|
||||||
|
processData(items, completedIds) {
|
||||||
|
// Store original items (with answers)
|
||||||
|
this.originalItems.clear();
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.id) {
|
||||||
|
this.originalItems.set(item.id, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Track completed questions
|
||||||
|
this.completedQuestions.clear();
|
||||||
|
if (completedIds) {
|
||||||
|
completedIds.forEach(q => this.completedQuestions.add(q.id));
|
||||||
|
}
|
||||||
|
// Sanitize for game (remove answers)
|
||||||
|
this.sanitizedItems = sanitizeForClient(this.gameCode, items);
|
||||||
|
// Store sanitized items for lookup (to map choice index -> value)
|
||||||
|
this.clientItems.clear();
|
||||||
|
this.sanitizedItems.forEach(item => {
|
||||||
|
if (item.id)
|
||||||
|
this.clientItems.set(item.id, item);
|
||||||
|
});
|
||||||
|
// Send data ready
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_DATA_READY, {
|
||||||
|
items: this.sanitizedItems,
|
||||||
|
total_questions: items.length,
|
||||||
|
completed_count: this.completedQuestions.size,
|
||||||
|
resume_data: completedIds,
|
||||||
|
});
|
||||||
|
this.log('info', `Data processed: ${items.length} items, ${this.completedQuestions.size} completed`);
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// API CALLS
|
||||||
|
// ==========================================================================
|
||||||
|
async fetchLiveData() {
|
||||||
|
if (!this.apiBaseUrl || !this.assignmentId) {
|
||||||
|
this.sendError('CONFIG_ERROR', 'Missing apiBaseUrl or assignmentId for live mode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = `${this.apiBaseUrl}/games/${this.assignmentId}`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (this.authToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
// Extract items from response
|
||||||
|
const items = data.data || data.items || data.questions || [];
|
||||||
|
const completedIds = data.completed_question_ids || [];
|
||||||
|
this.processData(items, completedIds);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
this.log('error', 'Failed to fetch live data', error);
|
||||||
|
this.sendError('FETCH_ERROR', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// HELPERS
|
||||||
|
// ==========================================================================
|
||||||
|
sendToParent(type, payload, requestId) {
|
||||||
|
const message = createSdkMessage(type, payload, requestId);
|
||||||
|
// NOTE: Khi chạy local file://, event.origin có thể là "null" (string) => postMessage sẽ throw nếu dùng làm targetOrigin
|
||||||
|
const origin = this.parentOrigin;
|
||||||
|
const targetOrigin = (!origin || origin === 'null') ? '*' : origin;
|
||||||
|
window.parent.postMessage(message, targetOrigin);
|
||||||
|
this.log('debug', `Sent: ${type}`, payload);
|
||||||
|
}
|
||||||
|
sendError(code, message, requestId) {
|
||||||
|
this.sendToParent(SDK_MESSAGE_TYPES.SDK_ERROR, {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
}, requestId);
|
||||||
|
this.log('error', `Error: ${code} - ${message}`);
|
||||||
|
}
|
||||||
|
log(level, message, data) {
|
||||||
|
if (!this.config.debug && level === 'debug')
|
||||||
|
return;
|
||||||
|
const prefix = '[SdkIframe]';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// CLEANUP
|
||||||
|
// ==========================================================================
|
||||||
|
destroy() {
|
||||||
|
if (this.boundMessageHandler) {
|
||||||
|
window.removeEventListener('message', this.boundMessageHandler);
|
||||||
|
this.boundMessageHandler = null;
|
||||||
|
}
|
||||||
|
this.originalItems.clear();
|
||||||
|
this.userAnswers.clear();
|
||||||
|
this.log('info', 'SDK Iframe destroyed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Auto-init khi script được load
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.SdkIframe = new SdkIframeCore({
|
||||||
|
debug: new URLSearchParams(window.location.search).get('debug') === 'true',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=SdkIframeCore.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/sdk-iframe/SdkIframeCore.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/sdk-iframe/SdkIframeCore.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
11
G102-sequence/sdk/package/dist/esm/sdk-iframe/index.js
vendored
Normal file
11
G102-sequence/sdk/package/dist/esm/sdk-iframe/index.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* SDK Iframe exports
|
||||||
|
*/
|
||||||
|
import { SdkIframeCore } from './SdkIframeCore';
|
||||||
|
export * from './types';
|
||||||
|
export { SdkIframeCore };
|
||||||
|
// Export to window for direct browser usage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.SdkIframeCore = SdkIframeCore;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/sdk-iframe/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/sdk-iframe/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sdk-iframe/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,4CAA4C;AAC5C,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;IAC/B,MAAc,CAAC,aAAa,GAAG,aAAa,CAAC;AAClD,CAAC"}
|
||||||
38
G102-sequence/sdk/package/dist/esm/sdk-iframe/types.js
vendored
Normal file
38
G102-sequence/sdk/package/dist/esm/sdk-iframe/types.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* SDK Iframe Message Types
|
||||||
|
* Shared types cho SDK Iframe và Game Bridge
|
||||||
|
*/
|
||||||
|
// =============================================================================
|
||||||
|
// MESSAGE TYPES
|
||||||
|
// =============================================================================
|
||||||
|
export const SDK_MESSAGE_TYPES = {
|
||||||
|
// Parent/Game → SDK Iframe
|
||||||
|
SDK_INIT: 'SDK_INIT',
|
||||||
|
SDK_PUSH_DATA: 'SDK_PUSH_DATA',
|
||||||
|
SDK_CHECK_ANSWER: 'SDK_CHECK_ANSWER',
|
||||||
|
SDK_GET_RESULT: 'SDK_GET_RESULT',
|
||||||
|
SDK_RETRY_SYNC: 'SDK_RETRY_SYNC',
|
||||||
|
// SDK Iframe → Parent/Game
|
||||||
|
SDK_READY: 'SDK_READY',
|
||||||
|
SDK_DATA_READY: 'SDK_DATA_READY',
|
||||||
|
SDK_ANSWER_RESULT: 'SDK_ANSWER_RESULT',
|
||||||
|
SDK_SYNC_STATUS: 'SDK_SYNC_STATUS',
|
||||||
|
SDK_SYNC_ERROR: 'SDK_SYNC_ERROR',
|
||||||
|
SDK_FINAL_RESULT: 'SDK_FINAL_RESULT',
|
||||||
|
SDK_ERROR: 'SDK_ERROR',
|
||||||
|
};
|
||||||
|
// =============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// =============================================================================
|
||||||
|
export function createSdkMessage(type, payload, requestId) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
payload,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
request_id: requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function isSdkMessage(data) {
|
||||||
|
return data && typeof data === 'object' && 'type' in data && Object.values(SDK_MESSAGE_TYPES).includes(data.type);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=types.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/sdk-iframe/types.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/sdk-iframe/types.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/sdk-iframe/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,gFAAgF;AAChF,gBAAgB;AAChB,gFAAgF;AAEhF,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC7B,2BAA2B;IAC3B,QAAQ,EAAE,UAAU;IACpB,aAAa,EAAE,eAAe;IAC9B,gBAAgB,EAAE,kBAAkB;IACpC,cAAc,EAAE,gBAAgB;IAChC,cAAc,EAAE,gBAAgB;IAEhC,2BAA2B;IAC3B,SAAS,EAAE,WAAW;IACtB,cAAc,EAAE,gBAAgB;IAChC,iBAAiB,EAAE,mBAAmB;IACtC,eAAe,EAAE,iBAAiB;IAClC,cAAc,EAAE,gBAAgB;IAChC,gBAAgB,EAAE,kBAAkB;IACpC,SAAS,EAAE,WAAW;CAChB,CAAC;AAyGX,gFAAgF;AAChF,mBAAmB;AACnB,gFAAgF;AAEhF,MAAM,UAAU,gBAAgB,CAAI,IAAoB,EAAE,OAAU,EAAE,SAAkB;IACpF,OAAO;QACH,IAAI;QACJ,OAAO;QACP,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,UAAU,EAAE,SAAS;KACxB,CAAC;AACN,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAS;IAClC,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtH,CAAC"}
|
||||||
23
G102-sequence/sdk/package/dist/esm/types.js
vendored
Normal file
23
G102-sequence/sdk/package/dist/esm/types.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Game Iframe SDK - Types Definition
|
||||||
|
*/
|
||||||
|
// =============================================================================
|
||||||
|
// MESSAGE TYPES
|
||||||
|
// =============================================================================
|
||||||
|
export const MESSAGE_TYPES = {
|
||||||
|
// Parent → Iframe
|
||||||
|
SERVER_PUSH_DATA: 'SERVER_PUSH_DATA',
|
||||||
|
SERVER_PUSH_LEADERBOARD: 'SERVER_PUSH_LEADERBOARD',
|
||||||
|
// Iframe → Parent
|
||||||
|
GAME_READY: 'GAME_READY',
|
||||||
|
ANSWER_REPORT: 'ANSWER_REPORT',
|
||||||
|
FINAL_RESULT: 'FINAL_RESULT',
|
||||||
|
GET_LEADERBOARD: 'GET_LEADERBOARD',
|
||||||
|
};
|
||||||
|
export const DEFAULT_CONFIG = {
|
||||||
|
iframeOrigin: '*',
|
||||||
|
readyDelay: 500,
|
||||||
|
autoSendOnReady: true,
|
||||||
|
debug: false,
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=types.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/types.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/types.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,gFAAgF;AAChF,gBAAgB;AAChB,gFAAgF;AAEhF,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,kBAAkB;IAClB,gBAAgB,EAAE,kBAAkB;IACpC,uBAAuB,EAAE,yBAAyB;IAElD,kBAAkB;IAClB,UAAU,EAAE,YAAY;IACxB,aAAa,EAAE,eAAe;IAC9B,YAAY,EAAE,cAAc;IAC5B,eAAe,EAAE,iBAAiB;CAC5B,CAAC;AAqFX,MAAM,CAAC,MAAM,cAAc,GAAkC;IACzD,YAAY,EAAE,GAAG;IACjB,UAAU,EAAE,GAAG;IACf,eAAe,EAAE,IAAI;IACrB,KAAK,EAAE,KAAK;CACf,CAAC"}
|
||||||
117
G102-sequence/sdk/package/dist/esm/useGameIframeSDK.js
vendored
Normal file
117
G102-sequence/sdk/package/dist/esm/useGameIframeSDK.js
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* Game Iframe SDK - React Hook
|
||||||
|
* Custom hook để sử dụng SDK trong React components
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* import { useGameIframeSDK } from 'game-iframe-sdk/react';
|
||||||
|
*
|
||||||
|
* function GamePlayer() {
|
||||||
|
* const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
*
|
||||||
|
* const {
|
||||||
|
* isReady,
|
||||||
|
* sendGameData,
|
||||||
|
* sendLeaderboard
|
||||||
|
* } = useGameIframeSDK({
|
||||||
|
* iframeRef,
|
||||||
|
* iframeOrigin: 'http://senaai.vn:1357',
|
||||||
|
* onGameReady: () => console.log('Game ready!'),
|
||||||
|
* onAnswerReport: (data) => submitToServer(data),
|
||||||
|
* onFinalResult: (data) => showResults(data),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* return <iframe ref={iframeRef} src={gameUrl} />;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { GameIframeSDK } from './GameIframeSDK';
|
||||||
|
export function useGameIframeSDK(options) {
|
||||||
|
const { iframeRef, iframeOrigin, readyDelay, autoSendOnReady, debug, onGameReady, onAnswerReport, onFinalResult, onLeaderboardRequest, onError, } = options;
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const sdkRef = useRef(null);
|
||||||
|
// Use refs for callbacks to avoid re-creating SDK when callbacks change
|
||||||
|
const callbacksRef = useRef({
|
||||||
|
onGameReady,
|
||||||
|
onAnswerReport,
|
||||||
|
onFinalResult,
|
||||||
|
onLeaderboardRequest,
|
||||||
|
onError,
|
||||||
|
});
|
||||||
|
// Update callback refs on each render (no effect re-run)
|
||||||
|
callbacksRef.current = {
|
||||||
|
onGameReady,
|
||||||
|
onAnswerReport,
|
||||||
|
onFinalResult,
|
||||||
|
onLeaderboardRequest,
|
||||||
|
onError,
|
||||||
|
};
|
||||||
|
// Initialize SDK - only depends on config, NOT callbacks
|
||||||
|
useEffect(() => {
|
||||||
|
const sdk = new GameIframeSDK({
|
||||||
|
iframeOrigin,
|
||||||
|
readyDelay,
|
||||||
|
autoSendOnReady,
|
||||||
|
debug,
|
||||||
|
});
|
||||||
|
sdkRef.current = sdk;
|
||||||
|
// Subscribe to events using refs (stable references)
|
||||||
|
const unsubscribers = [];
|
||||||
|
unsubscribers.push(sdk.on('gameReady', () => {
|
||||||
|
setIsReady(true);
|
||||||
|
callbacksRef.current.onGameReady?.();
|
||||||
|
}));
|
||||||
|
unsubscribers.push(sdk.on('answerReport', (data) => {
|
||||||
|
callbacksRef.current.onAnswerReport?.(data);
|
||||||
|
}));
|
||||||
|
unsubscribers.push(sdk.on('finalResult', (data) => {
|
||||||
|
callbacksRef.current.onFinalResult?.(data);
|
||||||
|
}));
|
||||||
|
unsubscribers.push(sdk.on('leaderboardRequest', (data) => {
|
||||||
|
callbacksRef.current.onLeaderboardRequest?.(data.top || 10);
|
||||||
|
}));
|
||||||
|
unsubscribers.push(sdk.on('error', (err) => {
|
||||||
|
callbacksRef.current.onError?.(err);
|
||||||
|
}));
|
||||||
|
return () => {
|
||||||
|
unsubscribers.forEach((unsub) => unsub());
|
||||||
|
sdk.destroy();
|
||||||
|
sdkRef.current = null;
|
||||||
|
};
|
||||||
|
}, [iframeOrigin, readyDelay, autoSendOnReady, debug]); // ✅ No callback deps
|
||||||
|
// Sync iframe ref with SDK
|
||||||
|
useEffect(() => {
|
||||||
|
if (sdkRef.current && iframeRef.current) {
|
||||||
|
sdkRef.current.setIframe(iframeRef.current);
|
||||||
|
}
|
||||||
|
}, [iframeRef.current]);
|
||||||
|
// Reset ready state when iframe src changes
|
||||||
|
useEffect(() => {
|
||||||
|
setIsReady(false);
|
||||||
|
}, [iframeRef.current?.src]);
|
||||||
|
// Memoized methods
|
||||||
|
const sendGameData = useCallback((data) => {
|
||||||
|
return sdkRef.current?.sendGameData(data) ?? false;
|
||||||
|
}, []);
|
||||||
|
const sendLeaderboard = useCallback((data) => {
|
||||||
|
return sdkRef.current?.sendLeaderboard(data) ?? false;
|
||||||
|
}, []);
|
||||||
|
const queueGameData = useCallback((data) => {
|
||||||
|
sdkRef.current?.queueGameData(data);
|
||||||
|
}, []);
|
||||||
|
const reloadIframe = useCallback(() => {
|
||||||
|
setIsReady(false);
|
||||||
|
return sdkRef.current?.reloadIframe() ?? false;
|
||||||
|
}, []);
|
||||||
|
return {
|
||||||
|
sdk: sdkRef.current,
|
||||||
|
isReady,
|
||||||
|
sendGameData,
|
||||||
|
sendLeaderboard,
|
||||||
|
queueGameData,
|
||||||
|
reloadIframe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export default useGameIframeSDK;
|
||||||
|
//# sourceMappingURL=useGameIframeSDK.js.map
|
||||||
1
G102-sequence/sdk/package/dist/esm/useGameIframeSDK.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/esm/useGameIframeSDK.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"useGameIframeSDK.js","sourceRoot":"","sources":["../../src/useGameIframeSDK.ts"],"names":[],"mappings":"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"}
|
||||||
108
G102-sequence/sdk/package/dist/game-bridge/GameBridge.d.ts
vendored
Normal file
108
G102-sequence/sdk/package/dist/game-bridge/GameBridge.d.ts
vendored
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Game Bridge
|
||||||
|
* Wrapper đơn giản cho game developers để giao tiếp với SDK Iframe
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```typescript
|
||||||
|
* import { GameBridge } from 'game-iframe-sdk/game-bridge';
|
||||||
|
*
|
||||||
|
* const bridge = new GameBridge({
|
||||||
|
* sdkIframeUrl: 'https://sdk.sena.tech/sdk-iframe.html',
|
||||||
|
* debug: true,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Init
|
||||||
|
* await bridge.init({
|
||||||
|
* mode: 'preview',
|
||||||
|
* game_code: 'G001',
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Listen for data
|
||||||
|
* bridge.on('dataReady', (data) => {
|
||||||
|
* renderGame(data.items);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Check answer
|
||||||
|
* bridge.checkAnswer('q1', userChoice).then(result => {
|
||||||
|
* showFeedback(result.correct);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import { SdkInitPayload, SdkPushDataPayload, SdkReadyPayload, SdkDataReadyPayload, SdkAnswerResultPayload, SdkSyncStatusPayload, SdkSyncErrorPayload, SdkFinalResultPayload, SdkErrorPayload } from '../sdk-iframe/types';
|
||||||
|
export interface GameBridgeConfig {
|
||||||
|
sdkIframeUrl: string;
|
||||||
|
debug?: boolean;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
export interface GameBridgeEvents {
|
||||||
|
ready: SdkReadyPayload;
|
||||||
|
dataReady: SdkDataReadyPayload;
|
||||||
|
answerResult: SdkAnswerResultPayload;
|
||||||
|
syncStatus: SdkSyncStatusPayload;
|
||||||
|
syncError: SdkSyncErrorPayload;
|
||||||
|
finalResult: SdkFinalResultPayload;
|
||||||
|
error: SdkErrorPayload;
|
||||||
|
}
|
||||||
|
type EventHandler<T> = (data: T) => void;
|
||||||
|
export declare class GameBridge {
|
||||||
|
private config;
|
||||||
|
private sdkIframe;
|
||||||
|
private sdkOrigin;
|
||||||
|
private isReady;
|
||||||
|
private handlers;
|
||||||
|
private pendingRequests;
|
||||||
|
private requestCounter;
|
||||||
|
constructor(config: GameBridgeConfig);
|
||||||
|
/**
|
||||||
|
* Create SDK Iframe and initialize
|
||||||
|
*/
|
||||||
|
init(payload: SdkInitPayload): Promise<SdkReadyPayload>;
|
||||||
|
/**
|
||||||
|
* Push data (preview mode)
|
||||||
|
*/
|
||||||
|
pushData(payload: SdkPushDataPayload): Promise<SdkDataReadyPayload>;
|
||||||
|
/**
|
||||||
|
* Check answer - returns local result immediately
|
||||||
|
* Also triggers server sync in background
|
||||||
|
*/
|
||||||
|
checkAnswer(questionId: string, choice: any, timeSpent?: number): Promise<SdkAnswerResultPayload>;
|
||||||
|
/**
|
||||||
|
* Get final result
|
||||||
|
*/
|
||||||
|
getFinalResult(): Promise<SdkFinalResultPayload>;
|
||||||
|
/**
|
||||||
|
* Retry sync for a question
|
||||||
|
*/
|
||||||
|
retrySync(questionId: string): Promise<SdkSyncStatusPayload>;
|
||||||
|
/**
|
||||||
|
* Subscribe to events
|
||||||
|
*/
|
||||||
|
on<K extends keyof GameBridgeEvents>(event: K, handler: EventHandler<GameBridgeEvents[K]>): () => void;
|
||||||
|
/**
|
||||||
|
* Unsubscribe from events
|
||||||
|
*/
|
||||||
|
off<K extends keyof GameBridgeEvents>(event: K, handler: EventHandler<GameBridgeEvents[K]>): void;
|
||||||
|
/**
|
||||||
|
* Check if SDK is ready
|
||||||
|
*/
|
||||||
|
isSdkReady(): boolean;
|
||||||
|
/**
|
||||||
|
* Destroy bridge and cleanup
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
private setupMessageListener;
|
||||||
|
private handleMessage;
|
||||||
|
private emit;
|
||||||
|
private sendRequest;
|
||||||
|
private log;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get or create GameBridge instance
|
||||||
|
*/
|
||||||
|
export declare function getGameBridge(config?: GameBridgeConfig): GameBridge;
|
||||||
|
/**
|
||||||
|
* Destroy GameBridge instance
|
||||||
|
*/
|
||||||
|
export declare function destroyGameBridge(): void;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=GameBridge.d.ts.map
|
||||||
1
G102-sequence/sdk/package/dist/game-bridge/GameBridge.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/game-bridge/GameBridge.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"GameBridge.d.ts","sourceRoot":"","sources":["../../src/game-bridge/GameBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAGH,cAAc,EACd,kBAAkB,EAElB,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EAGlB,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,EAAE,mBAAmB,CAAC;IAC/B,YAAY,EAAE,sBAAsB,CAAC;IACrC,UAAU,EAAE,oBAAoB,CAAC;IACjC,SAAS,EAAE,mBAAmB,CAAC;IAC/B,WAAW,EAAE,qBAAqB,CAAC;IACnC,KAAK,EAAE,eAAe,CAAC;CAC1B;AAED,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAMzC,qBAAa,UAAU;IACnB,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,OAAO,CAAS;IAGxB,OAAO,CAAC,QAAQ,CAAkE;IAGlF,OAAO,CAAC,eAAe,CAIR;IAEf,OAAO,CAAC,cAAc,CAAK;gBAEf,MAAM,EAAE,gBAAgB;IAsBpC;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAmB7D;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAQzE;;;OAGG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IASvG;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAItD;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAUlE;;OAEG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAQtG;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAQjG;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,OAAO,IAAI,IAAI;IAwBf,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,aAAa;IA4DrB,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,GAAG;CAiBd;AAQD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,UAAU,CAQnE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
|
||||||
298
G102-sequence/sdk/package/dist/game-bridge/GameBridge.js
vendored
Normal file
298
G102-sequence/sdk/package/dist/game-bridge/GameBridge.js
vendored
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Game Bridge
|
||||||
|
* Wrapper đơn giản cho game developers để giao tiếp với SDK Iframe
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```typescript
|
||||||
|
* import { GameBridge } from 'game-iframe-sdk/game-bridge';
|
||||||
|
*
|
||||||
|
* const bridge = new GameBridge({
|
||||||
|
* sdkIframeUrl: 'https://sdk.sena.tech/sdk-iframe.html',
|
||||||
|
* debug: true,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Init
|
||||||
|
* await bridge.init({
|
||||||
|
* mode: 'preview',
|
||||||
|
* game_code: 'G001',
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Listen for data
|
||||||
|
* bridge.on('dataReady', (data) => {
|
||||||
|
* renderGame(data.items);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Check answer
|
||||||
|
* bridge.checkAnswer('q1', userChoice).then(result => {
|
||||||
|
* showFeedback(result.correct);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.GameBridge = void 0;
|
||||||
|
exports.getGameBridge = getGameBridge;
|
||||||
|
exports.destroyGameBridge = destroyGameBridge;
|
||||||
|
const types_1 = require("../sdk-iframe/types");
|
||||||
|
// =============================================================================
|
||||||
|
// GAME BRIDGE
|
||||||
|
// =============================================================================
|
||||||
|
class GameBridge {
|
||||||
|
constructor(config) {
|
||||||
|
this.sdkIframe = null;
|
||||||
|
this.sdkOrigin = '';
|
||||||
|
this.isReady = false;
|
||||||
|
// Event handlers
|
||||||
|
this.handlers = new Map();
|
||||||
|
// Pending requests (for promise-based API)
|
||||||
|
this.pendingRequests = new Map();
|
||||||
|
this.requestCounter = 0;
|
||||||
|
this.config = {
|
||||||
|
sdkIframeUrl: config.sdkIframeUrl,
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
timeout: config.timeout ?? 10000,
|
||||||
|
};
|
||||||
|
// Extract origin from SDK URL
|
||||||
|
try {
|
||||||
|
const url = new URL(this.config.sdkIframeUrl);
|
||||||
|
this.sdkOrigin = url.origin;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
this.sdkOrigin = '*';
|
||||||
|
}
|
||||||
|
this.setupMessageListener();
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - Init
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Create SDK Iframe and initialize
|
||||||
|
*/
|
||||||
|
async init(payload) {
|
||||||
|
// Create hidden iframe
|
||||||
|
this.sdkIframe = document.createElement('iframe');
|
||||||
|
this.sdkIframe.src = this.config.sdkIframeUrl;
|
||||||
|
this.sdkIframe.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden;';
|
||||||
|
this.sdkIframe.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(this.sdkIframe);
|
||||||
|
this.log('info', 'SDK Iframe created');
|
||||||
|
// Wait for iframe to load
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.sdkIframe.onload = () => resolve();
|
||||||
|
});
|
||||||
|
// Send init
|
||||||
|
return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_INIT, payload);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Push data (preview mode)
|
||||||
|
*/
|
||||||
|
async pushData(payload) {
|
||||||
|
return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_PUSH_DATA, payload);
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - Game Actions
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Check answer - returns local result immediately
|
||||||
|
* Also triggers server sync in background
|
||||||
|
*/
|
||||||
|
async checkAnswer(questionId, choice, timeSpent) {
|
||||||
|
const payload = {
|
||||||
|
question_id: questionId,
|
||||||
|
choice,
|
||||||
|
time_spent: timeSpent,
|
||||||
|
};
|
||||||
|
return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_CHECK_ANSWER, payload);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get final result
|
||||||
|
*/
|
||||||
|
async getFinalResult() {
|
||||||
|
return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_GET_RESULT, {});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Retry sync for a question
|
||||||
|
*/
|
||||||
|
async retrySync(questionId) {
|
||||||
|
return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_RETRY_SYNC, {
|
||||||
|
question_id: questionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - Events
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Subscribe to events
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unsubscribe from events
|
||||||
|
*/
|
||||||
|
off(event, handler) {
|
||||||
|
this.handlers.get(event)?.delete(handler);
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// PUBLIC API - State
|
||||||
|
// ==========================================================================
|
||||||
|
/**
|
||||||
|
* Check if SDK is ready
|
||||||
|
*/
|
||||||
|
isSdkReady() {
|
||||||
|
return this.isReady;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Destroy bridge and cleanup
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
// Clear pending requests
|
||||||
|
this.pendingRequests.forEach((pending) => {
|
||||||
|
clearTimeout(pending.timeout);
|
||||||
|
pending.reject(new Error('Bridge destroyed'));
|
||||||
|
});
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
// Remove iframe
|
||||||
|
if (this.sdkIframe && this.sdkIframe.parentNode) {
|
||||||
|
this.sdkIframe.parentNode.removeChild(this.sdkIframe);
|
||||||
|
}
|
||||||
|
this.sdkIframe = null;
|
||||||
|
// Clear handlers
|
||||||
|
this.handlers.clear();
|
||||||
|
this.log('info', 'Bridge destroyed');
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// INTERNAL - Message Handling
|
||||||
|
// ==========================================================================
|
||||||
|
setupMessageListener() {
|
||||||
|
window.addEventListener('message', this.handleMessage.bind(this));
|
||||||
|
}
|
||||||
|
handleMessage(event) {
|
||||||
|
// Validate origin (if not *)
|
||||||
|
if (this.sdkOrigin !== '*' && event.origin !== this.sdkOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = event.data;
|
||||||
|
if (!(0, types_1.isSdkMessage)(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.log('debug', `Received: ${data.type}`, data.payload);
|
||||||
|
// Handle pending request response
|
||||||
|
if (data.request_id && this.pendingRequests.has(data.request_id)) {
|
||||||
|
const pending = this.pendingRequests.get(data.request_id);
|
||||||
|
clearTimeout(pending.timeout);
|
||||||
|
this.pendingRequests.delete(data.request_id);
|
||||||
|
if (data.type === types_1.SDK_MESSAGE_TYPES.SDK_ERROR) {
|
||||||
|
pending.reject(data.payload);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pending.resolve(data.payload);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Handle events
|
||||||
|
switch (data.type) {
|
||||||
|
case types_1.SDK_MESSAGE_TYPES.SDK_READY:
|
||||||
|
this.isReady = true;
|
||||||
|
this.emit('ready', data.payload);
|
||||||
|
break;
|
||||||
|
case types_1.SDK_MESSAGE_TYPES.SDK_DATA_READY:
|
||||||
|
this.emit('dataReady', data.payload);
|
||||||
|
break;
|
||||||
|
case types_1.SDK_MESSAGE_TYPES.SDK_ANSWER_RESULT:
|
||||||
|
this.emit('answerResult', data.payload);
|
||||||
|
break;
|
||||||
|
case types_1.SDK_MESSAGE_TYPES.SDK_SYNC_STATUS:
|
||||||
|
this.emit('syncStatus', data.payload);
|
||||||
|
break;
|
||||||
|
case types_1.SDK_MESSAGE_TYPES.SDK_SYNC_ERROR:
|
||||||
|
this.emit('syncError', data.payload);
|
||||||
|
break;
|
||||||
|
case types_1.SDK_MESSAGE_TYPES.SDK_FINAL_RESULT:
|
||||||
|
this.emit('finalResult', data.payload);
|
||||||
|
break;
|
||||||
|
case types_1.SDK_MESSAGE_TYPES.SDK_ERROR:
|
||||||
|
this.emit('error', data.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit(event, data) {
|
||||||
|
this.handlers.get(event)?.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler(data);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.log('error', `Error in ${event} handler`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ==========================================================================
|
||||||
|
// INTERNAL - Sending Messages
|
||||||
|
// ==========================================================================
|
||||||
|
sendRequest(type, payload) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.sdkIframe?.contentWindow) {
|
||||||
|
reject(new Error('SDK Iframe not ready'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const requestId = `req_${++this.requestCounter}_${Date.now()}`;
|
||||||
|
const message = (0, types_1.createSdkMessage)(type, payload, requestId);
|
||||||
|
// Setup timeout
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.pendingRequests.delete(requestId);
|
||||||
|
reject(new Error(`Request timeout: ${type}`));
|
||||||
|
}, this.config.timeout);
|
||||||
|
// Store pending request
|
||||||
|
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
||||||
|
// Send message
|
||||||
|
this.sdkIframe.contentWindow.postMessage(message, this.sdkOrigin);
|
||||||
|
this.log('debug', `Sent: ${type}`, payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log(level, message, data) {
|
||||||
|
if (!this.config.debug && level === 'debug')
|
||||||
|
return;
|
||||||
|
const prefix = '[GameBridge]';
|
||||||
|
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.GameBridge = GameBridge;
|
||||||
|
// =============================================================================
|
||||||
|
// FACTORY
|
||||||
|
// =============================================================================
|
||||||
|
let bridgeInstance = null;
|
||||||
|
/**
|
||||||
|
* Get or create GameBridge instance
|
||||||
|
*/
|
||||||
|
function getGameBridge(config) {
|
||||||
|
if (!bridgeInstance && config) {
|
||||||
|
bridgeInstance = new GameBridge(config);
|
||||||
|
}
|
||||||
|
if (!bridgeInstance) {
|
||||||
|
throw new Error('GameBridge not initialized. Call with config first.');
|
||||||
|
}
|
||||||
|
return bridgeInstance;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Destroy GameBridge instance
|
||||||
|
*/
|
||||||
|
function destroyGameBridge() {
|
||||||
|
bridgeInstance?.destroy();
|
||||||
|
bridgeInstance = null;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=GameBridge.js.map
|
||||||
1
G102-sequence/sdk/package/dist/game-bridge/GameBridge.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/game-bridge/GameBridge.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
G102-sequence/sdk/package/dist/game-bridge/index.d.ts
vendored
Normal file
7
G102-sequence/sdk/package/dist/game-bridge/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Game Bridge exports
|
||||||
|
* Dành cho game developers tích hợp vào game
|
||||||
|
*/
|
||||||
|
export { GameBridge, getGameBridge, destroyGameBridge, type GameBridgeConfig, type GameBridgeEvents, } from './GameBridge';
|
||||||
|
export { SDK_MESSAGE_TYPES, type SdkMessageType, type SdkInitPayload, type SdkPushDataPayload, type SdkCheckAnswerPayload, type SdkReadyPayload, type SdkDataReadyPayload, type SdkAnswerResultPayload, type SdkSyncStatusPayload, type SdkSyncErrorPayload, type SdkFinalResultPayload, type SdkErrorPayload, } from '../sdk-iframe/types';
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
G102-sequence/sdk/package/dist/game-bridge/index.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/game-bridge/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/game-bridge/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACxB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACH,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,GACvB,MAAM,qBAAqB,CAAC"}
|
||||||
15
G102-sequence/sdk/package/dist/game-bridge/index.js
vendored
Normal file
15
G102-sequence/sdk/package/dist/game-bridge/index.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Game Bridge exports
|
||||||
|
* Dành cho game developers tích hợp vào game
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.SDK_MESSAGE_TYPES = exports.destroyGameBridge = exports.getGameBridge = exports.GameBridge = void 0;
|
||||||
|
var GameBridge_1 = require("./GameBridge");
|
||||||
|
Object.defineProperty(exports, "GameBridge", { enumerable: true, get: function () { return GameBridge_1.GameBridge; } });
|
||||||
|
Object.defineProperty(exports, "getGameBridge", { enumerable: true, get: function () { return GameBridge_1.getGameBridge; } });
|
||||||
|
Object.defineProperty(exports, "destroyGameBridge", { enumerable: true, get: function () { return GameBridge_1.destroyGameBridge; } });
|
||||||
|
// Re-export types từ sdk-iframe
|
||||||
|
var types_1 = require("../sdk-iframe/types");
|
||||||
|
Object.defineProperty(exports, "SDK_MESSAGE_TYPES", { enumerable: true, get: function () { return types_1.SDK_MESSAGE_TYPES; } });
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/game-bridge/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/game-bridge/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/game-bridge/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,2CAMsB;AALlB,wGAAA,UAAU,OAAA;AACV,2GAAA,aAAa,OAAA;AACb,+GAAA,iBAAiB,OAAA;AAKrB,gCAAgC;AAChC,6CAa6B;AAZzB,0GAAA,iBAAiB,OAAA"}
|
||||||
69
G102-sequence/sdk/package/dist/index.d.ts
vendored
Normal file
69
G102-sequence/sdk/package/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Game Iframe SDK - Main Entry Point
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
* @module game-iframe-sdk
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - types.ts: Type definitions
|
||||||
|
* - mappers.ts: Data transformation/mapping
|
||||||
|
* - EventEmitter.ts: Simple typed event emitter
|
||||||
|
* - MessageHandler.ts: Handle incoming messages from iframe
|
||||||
|
* - MessageSender.ts: Send messages to iframe
|
||||||
|
* - GameIframeSDK.ts: Main SDK (composes above layers)
|
||||||
|
* - useGameIframeSDK.ts: React hook
|
||||||
|
*
|
||||||
|
* @example Browser/Vanilla JS
|
||||||
|
* ```typescript
|
||||||
|
* import { GameIframeSDK } from 'game-iframe-sdk';
|
||||||
|
*
|
||||||
|
* const sdk = new GameIframeSDK({
|
||||||
|
* iframeOrigin: 'http://senaai.vn:1357',
|
||||||
|
* debug: true
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* sdk.setIframe(document.getElementById('gameIframe'));
|
||||||
|
*
|
||||||
|
* sdk.on('gameReady', () => {
|
||||||
|
* sdk.sendGameData({ game_id: 'xxx', user_id: 'yyy', questions: [...] });
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example React
|
||||||
|
* ```tsx
|
||||||
|
* import { useGameIframeSDK } from 'game-iframe-sdk';
|
||||||
|
*
|
||||||
|
* function GamePlayer() {
|
||||||
|
* const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
*
|
||||||
|
* const { isReady, sendGameData } = useGameIframeSDK({
|
||||||
|
* iframeRef,
|
||||||
|
* iframeOrigin: 'http://senaai.vn:1357',
|
||||||
|
* onAnswerReport: (data) => submitToServer(data),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* return <iframe ref={iframeRef} src={gameUrl} />;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export { GameIframeSDK, createGameIframeSDK, getGameIframeSDK, destroyGameIframeSDK, } from './GameIframeSDK';
|
||||||
|
export { MessageHandler } from './MessageHandler';
|
||||||
|
export type { MessageHandlerConfig, MessageHandlerEvents } from './MessageHandler';
|
||||||
|
export { MessageSender } from './MessageSender';
|
||||||
|
export type { MessageSenderConfig, SendResult } from './MessageSender';
|
||||||
|
export { EventEmitter } from './EventEmitter';
|
||||||
|
export { useGameIframeSDK } from './useGameIframeSDK';
|
||||||
|
export type { UseGameIframeSDKOptions, UseGameIframeSDKReturn } from './useGameIframeSDK';
|
||||||
|
export { MESSAGE_TYPES, type MessageType, type PushDataPayload, type CompletedQuestionInfo, type AnswerReportData, type FinalResultData, type LeaderboardPlayer, type LeaderboardData, type GameIframeSDKConfig, DEFAULT_CONFIG, type SDKEvents, type SDKEventName, type SDKEventHandler, } from './types';
|
||||||
|
import * as Kit from './kit';
|
||||||
|
export { Kit };
|
||||||
|
import * as Client from './client';
|
||||||
|
export { Client };
|
||||||
|
export { GameClientSDK, getGameClientSDK, destroyGameClientSDK, type ClientMode, type ClientSDKConfig, type URLParams, type GameDataPayload, type AnswerResult as ClientAnswerResult, type FinalResult as ClientFinalResult, type ClientSDKEvents, } from './client';
|
||||||
|
import * as SdkIframe from './sdk-iframe';
|
||||||
|
export { SdkIframe };
|
||||||
|
export { SdkIframeCore, SDK_MESSAGE_TYPES, type SdkMessageType, type SdkMessage, type SdkInitPayload, type SdkPushDataPayload, type SdkCheckAnswerPayload, type SdkReadyPayload, type SdkDataReadyPayload, type SdkAnswerResultPayload, type SdkSyncStatusPayload, type SdkSyncErrorPayload, type SdkFinalResultPayload, type SdkErrorPayload, } from './sdk-iframe';
|
||||||
|
import * as GameBridgeModule from './game-bridge';
|
||||||
|
export { GameBridgeModule };
|
||||||
|
export { GameBridge, getGameBridge, destroyGameBridge, type GameBridgeConfig, type GameBridgeEvents, } from './game-bridge';
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
G102-sequence/sdk/package/dist/index.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAMH,OAAO,EACH,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,GACvB,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAEnF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAS9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAM1F,OAAO,EAEH,aAAa,EACb,KAAK,WAAW,EAGhB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAG1B,KAAK,gBAAgB,EACrB,KAAK,eAAe,EAGpB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAGpB,KAAK,mBAAmB,EACxB,cAAc,EAGd,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,MAAM,SAAS,CAAC;AAMjB,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,GAAG,EAAE,CAAC;AAMf,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EACH,aAAa,EACb,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,YAAY,IAAI,kBAAkB,EACvC,KAAK,WAAW,IAAI,iBAAiB,EACrC,KAAK,eAAe,GACvB,MAAM,UAAU,CAAC;AAMlB,OAAO,KAAK,SAAS,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EACH,aAAa,EACb,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,GACvB,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,gBAAgB,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,OAAO,EACH,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACxB,MAAM,eAAe,CAAC"}
|
||||||
147
G102-sequence/sdk/package/dist/index.js
vendored
Normal file
147
G102-sequence/sdk/package/dist/index.js
vendored
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Game Iframe SDK - Main Entry Point
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
* @module game-iframe-sdk
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - types.ts: Type definitions
|
||||||
|
* - mappers.ts: Data transformation/mapping
|
||||||
|
* - EventEmitter.ts: Simple typed event emitter
|
||||||
|
* - MessageHandler.ts: Handle incoming messages from iframe
|
||||||
|
* - MessageSender.ts: Send messages to iframe
|
||||||
|
* - GameIframeSDK.ts: Main SDK (composes above layers)
|
||||||
|
* - useGameIframeSDK.ts: React hook
|
||||||
|
*
|
||||||
|
* @example Browser/Vanilla JS
|
||||||
|
* ```typescript
|
||||||
|
* import { GameIframeSDK } from 'game-iframe-sdk';
|
||||||
|
*
|
||||||
|
* const sdk = new GameIframeSDK({
|
||||||
|
* iframeOrigin: 'http://senaai.vn:1357',
|
||||||
|
* debug: true
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* sdk.setIframe(document.getElementById('gameIframe'));
|
||||||
|
*
|
||||||
|
* sdk.on('gameReady', () => {
|
||||||
|
* sdk.sendGameData({ game_id: 'xxx', user_id: 'yyy', questions: [...] });
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example React
|
||||||
|
* ```tsx
|
||||||
|
* import { useGameIframeSDK } from 'game-iframe-sdk';
|
||||||
|
*
|
||||||
|
* function GamePlayer() {
|
||||||
|
* const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
*
|
||||||
|
* const { isReady, sendGameData } = useGameIframeSDK({
|
||||||
|
* iframeRef,
|
||||||
|
* iframeOrigin: 'http://senaai.vn:1357',
|
||||||
|
* onAnswerReport: (data) => submitToServer(data),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* return <iframe ref={iframeRef} src={gameUrl} />;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.destroyGameBridge = exports.getGameBridge = exports.GameBridge = exports.GameBridgeModule = exports.SDK_MESSAGE_TYPES = exports.SdkIframeCore = exports.SdkIframe = exports.destroyGameClientSDK = exports.getGameClientSDK = exports.GameClientSDK = exports.Client = exports.Kit = exports.DEFAULT_CONFIG = exports.MESSAGE_TYPES = exports.useGameIframeSDK = exports.EventEmitter = exports.MessageSender = exports.MessageHandler = exports.destroyGameIframeSDK = exports.getGameIframeSDK = exports.createGameIframeSDK = exports.GameIframeSDK = void 0;
|
||||||
|
// =============================================================================
|
||||||
|
// CORE SDK
|
||||||
|
// =============================================================================
|
||||||
|
var GameIframeSDK_1 = require("./GameIframeSDK");
|
||||||
|
Object.defineProperty(exports, "GameIframeSDK", { enumerable: true, get: function () { return GameIframeSDK_1.GameIframeSDK; } });
|
||||||
|
Object.defineProperty(exports, "createGameIframeSDK", { enumerable: true, get: function () { return GameIframeSDK_1.createGameIframeSDK; } });
|
||||||
|
Object.defineProperty(exports, "getGameIframeSDK", { enumerable: true, get: function () { return GameIframeSDK_1.getGameIframeSDK; } });
|
||||||
|
Object.defineProperty(exports, "destroyGameIframeSDK", { enumerable: true, get: function () { return GameIframeSDK_1.destroyGameIframeSDK; } });
|
||||||
|
// =============================================================================
|
||||||
|
// LAYERS (for advanced usage)
|
||||||
|
// =============================================================================
|
||||||
|
var MessageHandler_1 = require("./MessageHandler");
|
||||||
|
Object.defineProperty(exports, "MessageHandler", { enumerable: true, get: function () { return MessageHandler_1.MessageHandler; } });
|
||||||
|
var MessageSender_1 = require("./MessageSender");
|
||||||
|
Object.defineProperty(exports, "MessageSender", { enumerable: true, get: function () { return MessageSender_1.MessageSender; } });
|
||||||
|
var EventEmitter_1 = require("./EventEmitter");
|
||||||
|
Object.defineProperty(exports, "EventEmitter", { enumerable: true, get: function () { return EventEmitter_1.EventEmitter; } });
|
||||||
|
// Mappers removed - mapped data should be handled by the application
|
||||||
|
// =============================================================================
|
||||||
|
// =============================================================================
|
||||||
|
// REACT HOOK
|
||||||
|
// =============================================================================
|
||||||
|
var useGameIframeSDK_1 = require("./useGameIframeSDK");
|
||||||
|
Object.defineProperty(exports, "useGameIframeSDK", { enumerable: true, get: function () { return useGameIframeSDK_1.useGameIframeSDK; } });
|
||||||
|
// =============================================================================
|
||||||
|
// TYPES
|
||||||
|
// =============================================================================
|
||||||
|
var types_1 = require("./types");
|
||||||
|
// Message Types
|
||||||
|
Object.defineProperty(exports, "MESSAGE_TYPES", { enumerable: true, get: function () { return types_1.MESSAGE_TYPES; } });
|
||||||
|
Object.defineProperty(exports, "DEFAULT_CONFIG", { enumerable: true, get: function () { return types_1.DEFAULT_CONFIG; } });
|
||||||
|
// =============================================================================
|
||||||
|
// KIT (Helper Mappers, API, Components)
|
||||||
|
// =============================================================================
|
||||||
|
const Kit = __importStar(require("./kit"));
|
||||||
|
exports.Kit = Kit;
|
||||||
|
// =============================================================================
|
||||||
|
// CLIENT SDK (for Game Iframe - used by game developers)
|
||||||
|
// =============================================================================
|
||||||
|
const Client = __importStar(require("./client"));
|
||||||
|
exports.Client = Client;
|
||||||
|
var client_1 = require("./client");
|
||||||
|
Object.defineProperty(exports, "GameClientSDK", { enumerable: true, get: function () { return client_1.GameClientSDK; } });
|
||||||
|
Object.defineProperty(exports, "getGameClientSDK", { enumerable: true, get: function () { return client_1.getGameClientSDK; } });
|
||||||
|
Object.defineProperty(exports, "destroyGameClientSDK", { enumerable: true, get: function () { return client_1.destroyGameClientSDK; } });
|
||||||
|
// =============================================================================
|
||||||
|
// SDK IFRAME (Hidden iframe - runs on separate domain for security)
|
||||||
|
// =============================================================================
|
||||||
|
const SdkIframe = __importStar(require("./sdk-iframe"));
|
||||||
|
exports.SdkIframe = SdkIframe;
|
||||||
|
var sdk_iframe_1 = require("./sdk-iframe");
|
||||||
|
Object.defineProperty(exports, "SdkIframeCore", { enumerable: true, get: function () { return sdk_iframe_1.SdkIframeCore; } });
|
||||||
|
Object.defineProperty(exports, "SDK_MESSAGE_TYPES", { enumerable: true, get: function () { return sdk_iframe_1.SDK_MESSAGE_TYPES; } });
|
||||||
|
// =============================================================================
|
||||||
|
// GAME BRIDGE (For game developers - communicates with SDK Iframe)
|
||||||
|
// =============================================================================
|
||||||
|
const GameBridgeModule = __importStar(require("./game-bridge"));
|
||||||
|
exports.GameBridgeModule = GameBridgeModule;
|
||||||
|
var game_bridge_1 = require("./game-bridge");
|
||||||
|
Object.defineProperty(exports, "GameBridge", { enumerable: true, get: function () { return game_bridge_1.GameBridge; } });
|
||||||
|
Object.defineProperty(exports, "getGameBridge", { enumerable: true, get: function () { return game_bridge_1.getGameBridge; } });
|
||||||
|
Object.defineProperty(exports, "destroyGameBridge", { enumerable: true, get: function () { return game_bridge_1.destroyGameBridge; } });
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
G102-sequence/sdk/package/dist/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,gFAAgF;AAChF,WAAW;AACX,gFAAgF;AAEhF,iDAKyB;AAJrB,8GAAA,aAAa,OAAA;AACb,oHAAA,mBAAmB,OAAA;AACnB,iHAAA,gBAAgB,OAAA;AAChB,qHAAA,oBAAoB,OAAA;AAGxB,gFAAgF;AAChF,8BAA8B;AAC9B,gFAAgF;AAEhF,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAGvB,iDAAgD;AAAvC,8GAAA,aAAa,OAAA;AAGtB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,qEAAqE;AACrE,gFAAgF;AAEhF,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AAGzB,gFAAgF;AAChF,QAAQ;AACR,gFAAgF;AAEhF,iCAyBiB;AAxBb,gBAAgB;AAChB,sGAAA,aAAa,OAAA;AAiBb,uGAAA,cAAc,OAAA;AAQlB,gFAAgF;AAChF,wCAAwC;AACxC,gFAAgF;AAEhF,2CAA6B;AACpB,kBAAG;AAEZ,gFAAgF;AAChF,yDAAyD;AACzD,gFAAgF;AAEhF,iDAAmC;AAC1B,wBAAM;AAEf,mCAWkB;AAVd,uGAAA,aAAa,OAAA;AACb,0GAAA,gBAAgB,OAAA;AAChB,8GAAA,oBAAoB,OAAA;AAUxB,gFAAgF;AAChF,oEAAoE;AACpE,gFAAgF;AAEhF,wDAA0C;AACjC,8BAAS;AAElB,2CAesB;AAdlB,2GAAA,aAAa,OAAA;AACb,+GAAA,iBAAiB,OAAA;AAerB,gFAAgF;AAChF,mEAAmE;AACnE,gFAAgF;AAEhF,gEAAkD;AACzC,4CAAgB;AAEzB,6CAMuB;AALnB,yGAAA,UAAU,OAAA;AACV,4GAAA,aAAa,OAAA;AACb,gHAAA,iBAAiB,OAAA"}
|
||||||
113
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts
vendored
Normal file
113
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* GameDataHandler - Data Sanitizer & Scorer
|
||||||
|
*
|
||||||
|
* Game Codes:
|
||||||
|
* - G001: Quiz text-text
|
||||||
|
* - G002: Quiz audio-text (audio question, text answer)
|
||||||
|
* - G003: Quiz text-audio (text question, audio answer)
|
||||||
|
* - G004: Quiz image-text (image question, text answer)
|
||||||
|
* - G005: Quiz text-image (text question, image answer)
|
||||||
|
*
|
||||||
|
* - G110: Sequence Word - no audio
|
||||||
|
* - G111: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
* - G112: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
* - G113: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
*
|
||||||
|
* - G120: Sequence Sentence - no audio
|
||||||
|
* - G121: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
* - G122: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
* - G123: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
*/
|
||||||
|
export type GameCode = 'G001' | 'G002' | 'G003' | 'G004' | 'G005' | 'G110' | 'G111' | 'G112' | 'G113' | 'G120' | 'G121' | 'G122' | 'G123';
|
||||||
|
export declare const GAME_CODES: {
|
||||||
|
readonly G001: {
|
||||||
|
readonly name: "Quiz Text-Text";
|
||||||
|
readonly category: "quiz";
|
||||||
|
readonly hasAudio: false;
|
||||||
|
readonly hasImage: false;
|
||||||
|
};
|
||||||
|
readonly G002: {
|
||||||
|
readonly name: "Quiz Audio-Text";
|
||||||
|
readonly category: "quiz";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
readonly hasImage: false;
|
||||||
|
};
|
||||||
|
readonly G003: {
|
||||||
|
readonly name: "Quiz Text-Audio";
|
||||||
|
readonly category: "quiz";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
readonly hasImage: false;
|
||||||
|
};
|
||||||
|
readonly G004: {
|
||||||
|
readonly name: "Quiz Image-Text";
|
||||||
|
readonly category: "quiz";
|
||||||
|
readonly hasAudio: false;
|
||||||
|
readonly hasImage: true;
|
||||||
|
};
|
||||||
|
readonly G005: {
|
||||||
|
readonly name: "Quiz Text-Image";
|
||||||
|
readonly category: "quiz";
|
||||||
|
readonly hasAudio: false;
|
||||||
|
readonly hasImage: true;
|
||||||
|
};
|
||||||
|
readonly G110: {
|
||||||
|
readonly name: "Sequence Word";
|
||||||
|
readonly category: "sequence_word";
|
||||||
|
readonly hasAudio: false;
|
||||||
|
};
|
||||||
|
readonly G111: {
|
||||||
|
readonly name: "Sequence Word Audio";
|
||||||
|
readonly category: "sequence_word";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
};
|
||||||
|
readonly G112: {
|
||||||
|
readonly name: "Sequence Word Audio";
|
||||||
|
readonly category: "sequence_word";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
};
|
||||||
|
readonly G113: {
|
||||||
|
readonly name: "Sequence Word Audio";
|
||||||
|
readonly category: "sequence_word";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
};
|
||||||
|
readonly G120: {
|
||||||
|
readonly name: "Sequence Sentence";
|
||||||
|
readonly category: "sequence_sentence";
|
||||||
|
readonly hasAudio: false;
|
||||||
|
};
|
||||||
|
readonly G121: {
|
||||||
|
readonly name: "Sequence Sentence Audio";
|
||||||
|
readonly category: "sequence_sentence";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
};
|
||||||
|
readonly G122: {
|
||||||
|
readonly name: "Sequence Sentence Audio";
|
||||||
|
readonly category: "sequence_sentence";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
};
|
||||||
|
readonly G123: {
|
||||||
|
readonly name: "Sequence Sentence Audio";
|
||||||
|
readonly category: "sequence_sentence";
|
||||||
|
readonly hasAudio: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export declare function getGameCategory(code: GameCode): string;
|
||||||
|
/**
|
||||||
|
* Sanitize game data before sending to iframe
|
||||||
|
* CRITICAL: Never send answers/correct data to client
|
||||||
|
*/
|
||||||
|
export declare function sanitizeForClient(code: GameCode, items: any[]): any[];
|
||||||
|
export interface AnswerCheckResult {
|
||||||
|
isCorrect: boolean;
|
||||||
|
score: number;
|
||||||
|
feedback?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if player's answer is correct
|
||||||
|
*
|
||||||
|
* @param code - Game code (G001, G110, etc.)
|
||||||
|
* @param originalItem - Original item from server (has answer field!)
|
||||||
|
* @param playerAnswer - Player's answer (text for quiz, array for sequence)
|
||||||
|
*/
|
||||||
|
export declare function checkAnswer(code: GameCode, originalItem: any, playerAnswer: any): AnswerCheckResult;
|
||||||
|
//# sourceMappingURL=GameDataHandler.d.ts.map
|
||||||
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"GameDataHandler.d.ts","sourceRoot":"","sources":["../../src/kit/GameDataHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,MAAM,MAAM,QAAQ,GAEd,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAE1C,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAEjC,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAGxC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmBb,CAAC;AAEX,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEtD;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,CAkDrE;AAwSD,MAAM,WAAW,iBAAiB;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,GAAG,iBAAiB,CA4BnG"}
|
||||||
450
G102-sequence/sdk/package/dist/kit/GameDataHandler.js
vendored
Normal file
450
G102-sequence/sdk/package/dist/kit/GameDataHandler.js
vendored
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* GameDataHandler - Data Sanitizer & Scorer
|
||||||
|
*
|
||||||
|
* Game Codes:
|
||||||
|
* - G001: Quiz text-text
|
||||||
|
* - G002: Quiz audio-text (audio question, text answer)
|
||||||
|
* - G003: Quiz text-audio (text question, audio answer)
|
||||||
|
* - G004: Quiz image-text (image question, text answer)
|
||||||
|
* - G005: Quiz text-image (text question, image answer)
|
||||||
|
*
|
||||||
|
* - G110: Sequence Word - no audio
|
||||||
|
* - G111: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
* - G112: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
* - G113: Sequence Word - có audio, missing_letter_count từ item
|
||||||
|
*
|
||||||
|
* - G120: Sequence Sentence - no audio
|
||||||
|
* - G121: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
* - G122: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
* - G123: Sequence Sentence - có audio, missing_letter_count từ item
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.GAME_CODES = void 0;
|
||||||
|
exports.getGameCategory = getGameCategory;
|
||||||
|
exports.sanitizeForClient = sanitizeForClient;
|
||||||
|
exports.checkAnswer = checkAnswer;
|
||||||
|
// Game code metadata
|
||||||
|
exports.GAME_CODES = {
|
||||||
|
// Quiz
|
||||||
|
G001: { name: 'Quiz Text-Text', category: 'quiz', hasAudio: false, hasImage: false },
|
||||||
|
G002: { name: 'Quiz Audio-Text', category: 'quiz', hasAudio: true, hasImage: false },
|
||||||
|
G003: { name: 'Quiz Text-Audio', category: 'quiz', hasAudio: true, hasImage: false },
|
||||||
|
G004: { name: 'Quiz Image-Text', category: 'quiz', hasAudio: false, hasImage: true },
|
||||||
|
G005: { name: 'Quiz Text-Image', category: 'quiz', hasAudio: false, hasImage: true },
|
||||||
|
// Sequence Word
|
||||||
|
G110: { name: 'Sequence Word', category: 'sequence_word', hasAudio: false },
|
||||||
|
G111: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||||
|
G112: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||||
|
G113: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||||
|
// Sequence Sentence
|
||||||
|
G120: { name: 'Sequence Sentence', category: 'sequence_sentence', hasAudio: false },
|
||||||
|
G121: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||||
|
G122: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||||
|
G123: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||||
|
};
|
||||||
|
function getGameCategory(code) {
|
||||||
|
return exports.GAME_CODES[code]?.category || 'unknown';
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// SANITIZE DATA FOR CLIENT (REMOVE ANSWERS)
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* Sanitize game data before sending to iframe
|
||||||
|
* CRITICAL: Never send answers/correct data to client
|
||||||
|
*/
|
||||||
|
function sanitizeForClient(code, items) {
|
||||||
|
if (!Array.isArray(items))
|
||||||
|
return [];
|
||||||
|
switch (code) {
|
||||||
|
// ===== QUIZ VARIANTS =====
|
||||||
|
case 'G001': // Quiz text-text
|
||||||
|
return sanitizeQuizTextText(items);
|
||||||
|
case 'G002': // Quiz audio-text
|
||||||
|
return sanitizeQuizAudioText(items);
|
||||||
|
case 'G003': // Quiz text-audio
|
||||||
|
return sanitizeQuizTextAudio(items);
|
||||||
|
case 'G004': // Quiz image-text
|
||||||
|
return sanitizeQuizImageText(items);
|
||||||
|
case 'G005': // Quiz text-image
|
||||||
|
return sanitizeQuizTextImage(items);
|
||||||
|
// ===== SEQUENCE WORD VARIANTS =====
|
||||||
|
case 'G110': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
case 'G111': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
case 'G112': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
case 'G113': // Sequence word
|
||||||
|
return sanitizeSequenceWord(items);
|
||||||
|
// ===== SEQUENCE SENTENCE VARIANTS =====
|
||||||
|
case 'G120': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
case 'G121': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
case 'G122': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
case 'G123': // Sequence sentence
|
||||||
|
return sanitizeSequenceSentence(items);
|
||||||
|
default:
|
||||||
|
console.warn(`[GameDataHandler] Unknown game code: ${code}, returning raw data`);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// QUIZ SANITIZERS
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* G001: Quiz Text-Text
|
||||||
|
* Client receives: id, question, options (shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizTextText(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
// Normalize options to {text: string}
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { text: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
return { text: String(o.text ?? '') };
|
||||||
|
}
|
||||||
|
return { text: String(o ?? '') };
|
||||||
|
});
|
||||||
|
// Shuffle to hide answer position
|
||||||
|
shuffleArray(options);
|
||||||
|
// Save shuffled text order for SDK to resolve index
|
||||||
|
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.question,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledTexts, // SDK internal
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G002: Quiz Audio-Text
|
||||||
|
* Client receives: id, question (audio URL), options (shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizAudioText(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { text: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
return { text: String(o.text ?? '') };
|
||||||
|
}
|
||||||
|
return { text: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.audio || item.audio_url,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledTexts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G003: Quiz Text-Audio
|
||||||
|
* Client receives: id, question (text), options (audio URLs shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizTextAudio(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { audio: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
const audioUrl = o.audio || o.audio_url || '';
|
||||||
|
return { audio: String(audioUrl) };
|
||||||
|
}
|
||||||
|
return { audio: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledAudios = options.map((o) => String(o.audio ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.question,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledAudios,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G004: Quiz Image-Text
|
||||||
|
* Client receives: id, image_url, question (hint), options (shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizImageText(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { text: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
return { text: String(o.text ?? '') };
|
||||||
|
}
|
||||||
|
return { text: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
image_url: item.image_url,
|
||||||
|
question: item.question,
|
||||||
|
options: options,
|
||||||
|
__shuffledOptions: shuffledTexts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* G005: Quiz Text-Image
|
||||||
|
* Client receives: id, question (text), options (image URLs shuffled)
|
||||||
|
* Client does NOT receive: answer
|
||||||
|
*/
|
||||||
|
function sanitizeQuizTextImage(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const options = (item.options || []).map((o) => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { image_url: o };
|
||||||
|
}
|
||||||
|
if (o && typeof o === 'object') {
|
||||||
|
const imageUrl = o.image_url || o.image || '';
|
||||||
|
return { image_url: String(imageUrl) };
|
||||||
|
}
|
||||||
|
return { image_url: String(o ?? '') };
|
||||||
|
});
|
||||||
|
shuffleArray(options);
|
||||||
|
const shuffledUrls = options.map((o) => String(o.image_url ?? ''));
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: item.question,
|
||||||
|
options: options, // Each option has {image_url: ...}
|
||||||
|
__shuffledOptions: shuffledUrls,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// SEQUENCE WORD SANITIZERS
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* Sequence Word (G110-G113)
|
||||||
|
* Client receives: id, question (array with blanks), options (missing letters shuffled), audio_url (optional)
|
||||||
|
* Client does NOT receive: word, parts, answer, missing_letter_count
|
||||||
|
*
|
||||||
|
* Logic:
|
||||||
|
* 1. Read missing_letter_count from item (count of letters to blank out)
|
||||||
|
* 2. Randomly select positions to blank
|
||||||
|
* 3. question: array with blanks at selected positions
|
||||||
|
* 4. options: extracted missing letters (shuffled)
|
||||||
|
*/
|
||||||
|
function sanitizeSequenceWord(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const parts = item.answer || item.parts || [];
|
||||||
|
const missingCount = item.missing_letter_count || 0;
|
||||||
|
if (missingCount === 0 || parts.length === 0) {
|
||||||
|
// No missing - all visible
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: [...parts],
|
||||||
|
options: [],
|
||||||
|
...(item.audio_url && { audio_url: item.audio_url })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Randomly select which positions to blank
|
||||||
|
const allIndices = Array.from({ length: parts.length }, (_, i) => i);
|
||||||
|
const blankIndices = new Set();
|
||||||
|
const count = Math.min(missingCount, parts.length);
|
||||||
|
while (blankIndices.size < count) {
|
||||||
|
const randomIdx = Math.floor(Math.random() * allIndices.length);
|
||||||
|
const actualIdx = allIndices[randomIdx];
|
||||||
|
blankIndices.add(actualIdx);
|
||||||
|
allIndices.splice(randomIdx, 1);
|
||||||
|
}
|
||||||
|
// Build question array with blanks at random positions
|
||||||
|
const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p));
|
||||||
|
// Extract missing letters and shuffle
|
||||||
|
const missingLetters = Array.from(blankIndices).map(i => String(parts[i]));
|
||||||
|
shuffleArray(missingLetters);
|
||||||
|
const result = {
|
||||||
|
id: item.id,
|
||||||
|
question, // e.g. ["H", "", "L", "", "O"]
|
||||||
|
options: missingLetters, // e.g. ["L", "E"] - shuffled
|
||||||
|
__shuffledOptions: [...missingLetters] // SDK internal: to resolve indices
|
||||||
|
};
|
||||||
|
if (item.audio_url) {
|
||||||
|
result.audio_url = item.audio_url;
|
||||||
|
}
|
||||||
|
// CRITICAL: Do NOT send word, parts, answer, missing_letter_count
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// SEQUENCE SENTENCE SANITIZERS
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* Sequence Sentence (G120-G123)
|
||||||
|
* Client receives: id, question (array with blanks), options (missing words shuffled), audio_url (optional)
|
||||||
|
* Client does NOT receive: sentence, parts, answer, missing_letter_count
|
||||||
|
*
|
||||||
|
* Logic: Same as Sequence Word
|
||||||
|
* 1. Read missing_letter_count from item
|
||||||
|
* 2. Randomly select positions to blank
|
||||||
|
* 3. question: array with blanks
|
||||||
|
* 4. options: extracted missing words (shuffled)
|
||||||
|
*/
|
||||||
|
function sanitizeSequenceSentence(items) {
|
||||||
|
return items.map(item => {
|
||||||
|
const parts = item.answer || item.parts || [];
|
||||||
|
const missingCount = item.missing_letter_count || 0;
|
||||||
|
if (missingCount === 0 || parts.length === 0) {
|
||||||
|
// No missing - all visible
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
question: [...parts],
|
||||||
|
options: [],
|
||||||
|
...(item.audio_url && { audio_url: item.audio_url })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Randomly select which positions to blank
|
||||||
|
const allIndices = Array.from({ length: parts.length }, (_, i) => i);
|
||||||
|
const blankIndices = new Set();
|
||||||
|
const count = Math.min(missingCount, parts.length);
|
||||||
|
while (blankIndices.size < count) {
|
||||||
|
const randomIdx = Math.floor(Math.random() * allIndices.length);
|
||||||
|
const actualIdx = allIndices[randomIdx];
|
||||||
|
blankIndices.add(actualIdx);
|
||||||
|
allIndices.splice(randomIdx, 1);
|
||||||
|
}
|
||||||
|
// Build question array with blanks at random positions
|
||||||
|
const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p));
|
||||||
|
// Extract missing words and shuffle
|
||||||
|
const missingWords = Array.from(blankIndices).map(i => String(parts[i]));
|
||||||
|
shuffleArray(missingWords);
|
||||||
|
const result = {
|
||||||
|
id: item.id,
|
||||||
|
question, // e.g. ["I", "", "reading", ""]
|
||||||
|
options: missingWords, // e.g. ["love", "books"] - shuffled
|
||||||
|
__shuffledOptions: [...missingWords] // SDK internal
|
||||||
|
};
|
||||||
|
if (item.audio_url) {
|
||||||
|
result.audio_url = item.audio_url;
|
||||||
|
}
|
||||||
|
// CRITICAL: Do NOT send sentence, parts, answer, missing_letter_count
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if player's answer is correct
|
||||||
|
*
|
||||||
|
* @param code - Game code (G001, G110, etc.)
|
||||||
|
* @param originalItem - Original item from server (has answer field!)
|
||||||
|
* @param playerAnswer - Player's answer (text for quiz, array for sequence)
|
||||||
|
*/
|
||||||
|
function checkAnswer(code, originalItem, playerAnswer) {
|
||||||
|
switch (code) {
|
||||||
|
// ===== QUIZ VARIANTS (G001-G005) =====
|
||||||
|
case 'G001': // Quiz Text-Text
|
||||||
|
case 'G002': // Quiz Audio-Text
|
||||||
|
case 'G003': // Quiz Text-Audio
|
||||||
|
case 'G004': // Quiz Image-Text
|
||||||
|
case 'G005': // Quiz Text-Image
|
||||||
|
return checkQuizAnswer(originalItem, playerAnswer);
|
||||||
|
// ===== SEQUENCE WORD VARIANTS (G110-G113) =====
|
||||||
|
case 'G110': // Sequence Word
|
||||||
|
case 'G111': // Sequence Word Audio
|
||||||
|
case 'G112': // Sequence Word Audio
|
||||||
|
case 'G113': // Sequence Word Audio
|
||||||
|
return checkSequenceAnswer(originalItem, playerAnswer);
|
||||||
|
// ===== SEQUENCE SENTENCE VARIANTS (G120-G123) =====
|
||||||
|
case 'G120': // Sequence Sentence
|
||||||
|
case 'G121': // Sequence Sentence Audio
|
||||||
|
case 'G122': // Sequence Sentence Audio
|
||||||
|
case 'G123': // Sequence Sentence Audio
|
||||||
|
return checkSequenceAnswer(originalItem, playerAnswer);
|
||||||
|
default:
|
||||||
|
console.warn(`[GameDataHandler] Unknown game code for scoring: ${code}`);
|
||||||
|
return { isCorrect: false, score: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Quiz Scoring
|
||||||
|
function checkQuizAnswer(item, answerChoice) {
|
||||||
|
const correctAnswer = String(item.answer || '');
|
||||||
|
if (!correctAnswer) {
|
||||||
|
return { isCorrect: false, score: 0, feedback: 'No correct answer defined' };
|
||||||
|
}
|
||||||
|
let playerAnswerText;
|
||||||
|
if (typeof answerChoice === 'number') {
|
||||||
|
// Index: resolve from original options
|
||||||
|
if (Array.isArray(item.options)) {
|
||||||
|
const v = item.options[answerChoice];
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
playerAnswerText = v;
|
||||||
|
}
|
||||||
|
else if (v && typeof v === 'object' && 'text' in v) {
|
||||||
|
playerAnswerText = String(v.text ?? '');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
playerAnswerText = String(v ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { isCorrect: false, score: 0, feedback: 'Invalid question options' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Direct text answer
|
||||||
|
if (answerChoice && typeof answerChoice === 'object' && 'text' in answerChoice) {
|
||||||
|
playerAnswerText = String(answerChoice.text ?? '');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
playerAnswerText = String(answerChoice ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isCorrect = playerAnswerText.toLowerCase().trim() === correctAnswer.toLowerCase().trim();
|
||||||
|
return {
|
||||||
|
isCorrect,
|
||||||
|
score: isCorrect ? 1 : 0,
|
||||||
|
feedback: isCorrect
|
||||||
|
? `✅ Correct! "${playerAnswerText}" matches answer "${correctAnswer}"`
|
||||||
|
: `❌ Wrong. You chose "${playerAnswerText}" but correct answer is "${correctAnswer}"`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Sequence Scoring
|
||||||
|
function checkSequenceAnswer(item, answer) {
|
||||||
|
const correctOrder = item.answer || item.parts;
|
||||||
|
if (!Array.isArray(answer) || !Array.isArray(correctOrder)) {
|
||||||
|
return { isCorrect: false, score: 0 };
|
||||||
|
}
|
||||||
|
const isCorrect = arraysEqual(answer, correctOrder);
|
||||||
|
return {
|
||||||
|
isCorrect,
|
||||||
|
score: isCorrect ? 1 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// UTILITIES
|
||||||
|
// =============================================================================
|
||||||
|
function shuffleArray(array) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function arraysEqual(a, b) {
|
||||||
|
if (a.length !== b.length)
|
||||||
|
return false;
|
||||||
|
return a.every((val, idx) => {
|
||||||
|
if (typeof val === 'string' && typeof b[idx] === 'string') {
|
||||||
|
return val.toLowerCase().trim() === b[idx].toLowerCase().trim();
|
||||||
|
}
|
||||||
|
return val === b[idx];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=GameDataHandler.js.map
|
||||||
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
19
G102-sequence/sdk/package/dist/kit/api.d.ts
vendored
Normal file
19
G102-sequence/sdk/package/dist/kit/api.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Game API Client Kit
|
||||||
|
* Standardized API client for communicating with Game Backend
|
||||||
|
*/
|
||||||
|
export interface GameApiConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
getHeaders?: () => Record<string, string>;
|
||||||
|
}
|
||||||
|
export declare class GameApiClient {
|
||||||
|
private config;
|
||||||
|
constructor(config: GameApiConfig);
|
||||||
|
private request;
|
||||||
|
getGameWithProgress(assignmentId: number | string, studentId: string, refresh?: boolean): Promise<any>;
|
||||||
|
startLiveSession(assignmentId: number | string, studentId: string, refresh?: boolean): Promise<any>;
|
||||||
|
submitAnswer(assignmentId: number | string, studentId: string, questionId: string, answer: any, timeSpent?: number, isTimeout?: boolean): Promise<any>;
|
||||||
|
completeSession(assignmentId: number | string, studentId: string): Promise<any>;
|
||||||
|
getLeaderboard(assignmentId: number | string, studentId: string): Promise<any>;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=api.d.ts.map
|
||||||
1
G102-sequence/sdk/package/dist/kit/api.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/api.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/kit/api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,aAAa;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED,qBAAa,aAAa;IACV,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;YAE3B,OAAO;IA6Cf,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,UAAQ;IAIrF,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,UAAQ;IAOlF,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,SAAI,EAAE,SAAS,UAAQ;IAWhI,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;IAOhE,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;CAGxE"}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user