This commit is contained in:
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"}
|
||||
Reference in New Issue
Block a user