update new defined
All checks were successful
Deploy to Production / deploy (push) Successful in 19s
All checks were successful
Deploy to Production / deploy (push) Successful in 19s
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user