update new defined
All checks were successful
Deploy to Production / deploy (push) Successful in 19s

This commit is contained in:
silverpro89
2026-01-29 14:04:04 +07:00
parent 41cfb533d5
commit 0958e7ddf1
6 changed files with 1006 additions and 4 deletions

View File

@@ -206,8 +206,8 @@ class GameTypeController {
await gameType.update(updateData);
// Invalidate cache
await cacheUtils.del(`game_type:${id}`);
await cacheUtils.del(`game_type:type:${gameType.type}`);
await cacheUtils.delete(`game_type:${id}`);
await cacheUtils.delete(`game_type:type:${gameType.type}`);
await cacheUtils.deletePattern('game_types:*');
res.json({
@@ -241,8 +241,8 @@ class GameTypeController {
await gameType.destroy();
// Invalidate cache
await cacheUtils.del(`game_type:${id}`);
await cacheUtils.del(`game_type:type:${typeCode}`);
await cacheUtils.delete(`game_type:${id}`);
await cacheUtils.delete(`game_type:type:${typeCode}`);
await cacheUtils.deletePattern('game_types:*');
res.json({
@@ -340,6 +340,411 @@ class GameTypeController {
next(error);
}
}
/**
* Get developer guide for game code
* GET /api/game-types/guide?gameCode=G2510S1T30
*/
async getGuide(req, res, next) {
try {
const { gameCode } = req.query;
if (!gameCode) {
return res.status(400).json({
success: false,
message: 'gameCode is required',
example: '/api/game-types/guide?gameCode=G2510S1T30'
});
}
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
const match = gameCode.match(regex);
if (!match) {
return res.status(400).json({
success: false,
message: 'Invalid gameCode format!',
format: 'Gxxxx or GxxxxSxTxx',
examples: [
'G1400 - Quiz Text to Text (4 options)',
'G1510 - Quiz Image to Text (5 options)',
'G2410S1T30 - Sort 4 items, shuffled, 30s timer',
'G3500 - Memory Card 5 pairs'
]
});
}
const category = match[1];
const count = match[2];
const qIdx = match[3];
const oIdx = match[4];
const shuffle = match[5] !== undefined ? match[5] : '1';
const time = match[6] !== undefined ? match[6] : '0';
const types = { '0': 'Text', '1': 'Image', '2': 'Audio' };
let gameName = '', instruction = '', displayMode = '';
switch(category) {
case '1':
gameName = "Quiz (Trắc nghiệm)";
instruction = `Người dùng chọn 1 đáp án đúng trong ${count} options`;
displayMode = `Question: ${types[qIdx]} → Options: ${types[oIdx]}`;
break;
case '2':
if (qIdx === '0' && oIdx === '0') {
gameName = "Sort Word (Sắp xếp từ)";
instruction = `Sắp xếp ${count} từ/ký tự thành chuỗi hoàn chỉnh`;
} else {
gameName = "Sequences (Sắp xếp chuỗi)";
instruction = `Sắp xếp ${count} items theo đúng thứ tự`;
}
displayMode = `Hint: ${types[qIdx]} → Items: ${types[oIdx]}`;
break;
case '3':
if (qIdx === oIdx) {
gameName = "Memory Card (Trí nhớ)";
instruction = `Lật và ghép ${count} cặp thẻ giống nhau`;
} else {
gameName = "Matching (Nối cặp)";
instruction = `Nối ${count} items từ 2 nhóm với nhau`;
}
displayMode = `Group A: ${types[qIdx]} ↔ Group B: ${types[oIdx]}`;
break;
case '4':
gameName = "Lesson Card (Học từ vựng)";
instruction = `Hiển thị ${count} thẻ học với context, audio, image, và meaning`;
displayMode = `Context: ${types[qIdx]} | Meaning: ${types[oIdx]} (Always with Audio & Image)`;
break;
case '5':
if (qIdx === '0' && oIdx === '0') {
gameName = "Sort Sentence (Sắp xếp câu)";
instruction = `Sắp xếp ${count} câu theo đúng thứ tự`;
} else {
gameName = "Sentence Sequences (Sắp xếp câu theo trình tự)";
instruction = `Sắp xếp ${count} câu/items theo đúng thứ tự`;
}
displayMode = `Hint: ${types[qIdx]} → Items: ${types[oIdx]}`;
break;
}
// Generate guide text
let guide = '';
guide += `║SENA SDK - DEVELOPER GUIDE║\n`;
guide += `📊 GAME ANALYSIS\n`;
guide += `Game Type : ${gameName}\n`;
guide += `Objective : ${instruction}\n`;
guide += `Display Mode : ${displayMode}\n`;
guide += `Items Count : ${count}\n`;
guide += `Shuffle : ${shuffle === '1' ? 'YES (call sdk.start() to shuffle)' : 'NO (S0)'}\n`;
guide += `Time Limit : ${time === '0' ? 'Unlimited' : time + ' seconds'}\n\n`;
// Generate sample data based on game type
const sampleData = GameTypeController.prototype.generateSampleData(category, count, qIdx, oIdx, shuffle, time, gameCode);
res.json({
success: true,
data: {
gameCode,
analysis: {
category: parseInt(category),
gameName,
instruction,
displayMode,
itemsCount: parseInt(count),
shuffle: shuffle === '1',
timeLimit: parseInt(time),
questionType: types[qIdx],
optionType: types[oIdx]
},
guide,
sampleData
}
});
} catch (error) {
next(error);
}
}
/**
* Generate sample data based on game type
*/
generateSampleData(category, count, qIdx, oIdx, shuffle, time, gameCode) {
const types = { '0': 'text', '1': 'image', '2': 'audio' };
const questionType = types[qIdx];
const optionType = types[oIdx];
let sample = {
gameCode: gameCode,
config: {
time: {
value: parseInt(time),
description: time === '0' ? 'Không giới hạn thời gian' : `Giới hạn thời gian trả lời ${time} giây`,
options: [
{ value: 0, label: 'Không giới hạn thời gian' },
{ value: 30, label: 'Giới hạn 30 giây' },
{ value: 60, label: 'Giới hạn 60 giây' },
{ value: 90, label: 'Giới hạn 90 giây' }
]
},
shuffle: {
value: shuffle === '1',
description: shuffle === '1' ? 'Có xáo trộn các đáp án' : 'Không xáo trộn các đáp án',
options: [
{ value: 0, label: 'Không xáo trộn các đáp án' },
{ value: 1, label: 'Có xáo trộn các đáp án' }
]
}
},
question: {},
options: [],
answer: ''
};
// Generate question
if (category === '1') {
// Quiz
if (questionType === 'text') {
sample.question = { text: 'What is the capital of France?' };
} else if (questionType === 'image') {
sample.question = { image: 'https://example.com/question.jpg' };
} else {
sample.question = { audio: 'https://example.com/question.mp3' };
}
// Generate options
const answers = ['Paris', 'London', 'Berlin', 'Madrid', 'Rome', 'Amsterdam', 'Vienna', 'Brussels', 'Oslo'];
for (let i = 0; i < parseInt(count); i++) {
if (optionType === 'text') {
sample.options.push({ text: answers[i], isCorrect: i === 0 });
} else if (optionType === 'image') {
sample.options.push({
text: answers[i],
image: `https://example.com/option${i+1}.jpg`,
isCorrect: i === 0
});
} else {
sample.options.push({
text: answers[i],
audio: `https://example.com/option${i+1}.mp3`,
isCorrect: i === 0
});
}
}
sample.answer = answers[0]; // Always return text for correct answer
} else if (category === '2') {
// Sort/Sequences - for letters/characters with blanks
// G2xyz: x=number of blanks, y=question type (0=text,1=image,2=audio), z=always text (ignore)
const numBlanks = parseInt(count); // Number of blanks and options
// Question based on type (qIdx)
if (questionType === 'text') {
sample.question = 'Sắp xếp các chữ cái để tạo thành tên con vật';
} else if (questionType === 'image') {
sample.question = 'https://example.com/question_hint.jpg';
} else {
sample.question = 'https://example.com/question_hint.mp3';
}
// Full answer array
const fullWords = [
['C', 'H', 'I', 'C', 'K', 'E', 'N'],
['T', 'I', 'G', 'E', 'R'],
['L', 'I', 'O', 'N'],
['E', 'A', 'G', 'L', 'E'],
['D', 'O', 'L', 'P', 'H', 'I', 'N'],
['M', 'O', 'N', 'K', 'E', 'Y'],
['E', 'L', 'E', 'P', 'H', 'A', 'N', 'T']
];
const selectedWord = fullWords[0]; // Use CHICKEN as default
// Create hint with exactly 'count' number of blanks
// Distribute blanks evenly across the word
const blankPositions = [];
if (numBlanks >= selectedWord.length) {
// If blanks >= word length, make all positions blank
for (let i = 0; i < selectedWord.length; i++) {
blankPositions.push(i);
}
} else {
// Distribute blanks evenly
const step = Math.floor(selectedWord.length / numBlanks);
for (let i = 0; i < numBlanks; i++) {
blankPositions.push(Math.min(i * step + Math.floor(step / 2), selectedWord.length - 1));
}
}
const hintValue = selectedWord.map((letter, idx) => {
return blankPositions.includes(idx) ? '_' : letter;
});
// Hint is a separate object, not inside question (always text)
sample.hint = {
type: 'display',
value: hintValue
};
// Options are the letters that need to be filled in blanks (always text)
sample.options = blankPositions.map(pos => selectedWord[pos]);
sample.answer = selectedWord;
} else if (category === '3') {
// Instruction/hint is text
sample.question = questionType === optionType ?
{ instruction: 'Flip and match pairs of cards' } :
{ instruction: 'Match items from two groups' };
// Memory/Matching
for (let i = 0; i < parseInt(count); i++) {
if (questionType === optionType) {
// Memory Card - same type pairs (need 2 cards per pair)
if (optionType === 'text') {
const numbers = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
sample.options.push({ text: numbers[i], pairId: i });
sample.options.push({ text: `${i+1}`, pairId: i });
} else if (optionType === 'image') {
sample.options.push({
image: `https://example.com/card${i+1}a.jpg`,
pairId: i
});
sample.options.push({
image: `https://example.com/card${i+1}b.jpg`,
pairId: i
});
} else {
sample.options.push({
audio: `https://example.com/card${i+1}a.mp3`,
pairId: i
});
sample.options.push({
audio: `https://example.com/card${i+1}b.mp3`,
pairId: i
});
}
} else {
// Matching - different types
const left = questionType === 'text' ?
{ text: `Item ${i+1}` } :
{ image: `https://example.com/left${i+1}.jpg` };
const right = optionType === 'text' ?
{ text: `Match ${i+1}` } :
{ image: `https://example.com/right${i+1}.jpg` };
sample.options.push({
left,
right,
pairId: i
});
}
}
// Answer is array of pairIds in order: [0,0,1,1] for 2 pairs
sample.answer = sample.options.map(o => o.pairId);
} else if (category === '4') {
// Lesson Card - Always return all 4 fields (context, audio, image, meaning)
sample.question = { instruction: 'Learn vocabulary with context, audio, image, and meaning' };
sample.options = [];
const contexts = [
'The cat is sleeping on the sofa',
'I drink water every day',
'She loves to read books',
'They play football in the park',
'We eat breakfast at 7 AM',
'He drives a red car',
'The sun rises in the east',
'Birds fly in the sky',
'Children study at school'
];
const meanings = [
'Con mèo đang ngủ trên ghế sofa',
'Tôi uống nước mỗi ngày',
'Cô ấy thích đọc sách',
'Họ chơi bóng đá ở công viên',
'Chúng tôi ăn sáng lúc 7 giờ',
'Anh ấy lái một chiếc xe màu đỏ',
'Mặt trời mọc ở phía đông',
'Chim bay trên bầu trời',
'Trẻ em học ở trường'
];
for (let i = 0; i < parseInt(count); i++) {
let card = {
context: contexts[i],
audio: `https://example.com/audio${i+1}.mp3`,
image: `https://example.com/image${i+1}.jpg`,
meaning: meanings[i]
};
sample.options.push(card);
}
sample.answer = 'User completes all lesson cards';
} else if (category === '5') {
// Sort/Sequences for words (similar to G2 but with words instead of letters)
// G5xyz: x=number of blanks, y=question type (0=text,1=image,2=audio), z=always text (ignore)
const numBlanks = parseInt(count); // Number of blanks and options
// Question based on type (qIdx)
if (questionType === 'text') {
sample.question = 'Sắp xếp các từ để tạo thành câu hoàn chỉnh';
} else if (questionType === 'image') {
sample.question = 'https://example.com/sentence_question.jpg';
} else {
sample.question = 'https://example.com/sentence_question.mp3';
}
// Full answer array (words instead of letters)
const sentences = [
['I', 'wake', 'up', 'early', 'in', 'the', 'morning'],
['She', 'goes', 'to', 'school', 'by', 'bus'],
['They', 'are', 'playing', 'in', 'the', 'park'],
['We', 'will', 'visit', 'the', 'museum', 'tomorrow'],
['He', 'has', 'finished', 'his', 'homework'],
['The', 'weather', 'is', 'nice', 'today'],
['My', 'family', 'likes', 'to', 'travel']
];
const selectedSentence = sentences[0]; // Use first sentence as default
// Create hint with exactly 'count' number of blanks
// Distribute blanks evenly across the sentence
const blankPositions = [];
if (numBlanks >= selectedSentence.length) {
// If blanks >= sentence length, make all positions blank
for (let i = 0; i < selectedSentence.length; i++) {
blankPositions.push(i);
}
} else {
// Distribute blanks evenly
const step = Math.floor(selectedSentence.length / numBlanks);
for (let i = 0; i < numBlanks; i++) {
blankPositions.push(Math.min(i * step + Math.floor(step / 2), selectedSentence.length - 1));
}
}
const hintValue = selectedSentence.map((word, idx) => {
return blankPositions.includes(idx) ? '_' : word;
});
// Hint is a separate object, not inside question (always text words)
sample.hint = {
type: 'display',
value: hintValue
};
// Options are the words that need to be filled in blanks (always text)
sample.options = blankPositions.map(pos => selectedSentence[pos]);
sample.answer = selectedSentence;
}
return sample;
}
}
module.exports = new GameTypeController();