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

This commit is contained in:
lubukhu
2026-01-24 13:35:11 +07:00
parent 6c3e93636e
commit 65fd0158a3
145 changed files with 10262 additions and 0 deletions

View 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'}]
}
]
});
```