This commit is contained in:
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'}]
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user