507 lines
10 KiB
Markdown
507 lines
10 KiB
Markdown
# 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'}]
|
||
}
|
||
]
|
||
});
|
||
```
|