| ID | +Type Code | +EN Title | +VI Title | +Category | +Thumb | +Actions | +
|---|---|---|---|---|---|---|
| Đang tải dữ liệu... | +||||||
diff --git a/controllers/gameTypeController.js b/controllers/gameTypeController.js index 14aa43d..aafa3f8 100644 --- a/controllers/gameTypeController.js +++ b/controllers/gameTypeController.js @@ -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(); diff --git a/models/GameType.js b/models/GameType.js index 7710a90..8415e70 100644 --- a/models/GameType.js +++ b/models/GameType.js @@ -30,6 +30,10 @@ const GameType = sequelize.define('game_types', { type: DataTypes.TEXT, comment: 'Mô tả loại game (tiếng Việt)' }, + category : { + type: DataTypes.STRING(100), + comment: 'Danh mục loại game: quiz, puzzle, adventure, etc.' + }, type: { type: DataTypes.STRING(100), allowNull: false, diff --git a/public/game-config.html b/public/game-config.html index ca4e8e3..ecd01a7 100644 --- a/public/game-config.html +++ b/public/game-config.html @@ -436,6 +436,10 @@ const html = `
Quản lý các loại trò chơi giáo dục
+| ID | +Type Code | +EN Title | +VI Title | +Category | +Thumb | +Actions | +
|---|---|---|---|---|---|---|
| Đang tải dữ liệu... | +||||||