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,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

View 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"}

View 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

View 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"}

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"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"}

View 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

View 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"}

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../src/MessageSender.ts"],"names":[],"mappings":"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"}

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"GameClientSDK.d.ts","sourceRoot":"","sources":["../../src/client/GameClientSDK.ts"],"names":[],"mappings":"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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

View 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"}

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../../src/MessageHandler.ts"],"names":[],"mappings":"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"}

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../../src/MessageSender.ts"],"names":[],"mappings":"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"}

View 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

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/kit/api.ts"],"names":[],"mappings":"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"}

View 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

View 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"}

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"mappers.js","sourceRoot":"","sources":["../../../src/kit/mappers.ts"],"names":[],"mappings":"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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"GameTestPlayer.js","sourceRoot":"","sources":["../../../../src/kit/react/GameTestPlayer.tsx"],"names":[],"mappings":"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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

View 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"}

View 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

View 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"}

View 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

View File

@@ -0,0 +1 @@
{"version":3,"file":"useGameIframeSDK.js","sourceRoot":"","sources":["../../src/useGameIframeSDK.ts"],"names":[],"mappings":"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"}

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View 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

View 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"}

View 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

View 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
View 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

View 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"}

View 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

View 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"}

View 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

File diff suppressed because one or more lines are too long

View 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

View 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"}

View File

@@ -0,0 +1,86 @@
"use strict";
/**
* Game API Client Kit
* Standardized API client for communicating with Game Backend
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameApiClient = void 0;
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}`);
}
}
exports.GameApiClient = GameApiClient;
//# sourceMappingURL=api.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/kit/api.ts"],"names":[],"mappings":"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"}

Some files were not shown because too many files have changed in this diff Show More