diff --git a/commands/fun/trivia.js b/commands/fun/trivia.js index 9a72d90..43b810b 100644 --- a/commands/fun/trivia.js +++ b/commands/fun/trivia.js @@ -10,205 +10,130 @@ const ongoingTrivia = new Set(); // Track users with ongoing trivia let lastApiCall = 0; -module.exports = { - data: new SlashCommandBuilder() - .setName("trivia") - .setDescription("Play a trivia game") - .addStringOption((option) => - option - .setName("category") - .setDescription("Choose a trivia category") - .setRequired(true) - .addChoices( - { name: "General Knowledge", value: "9" }, - { name: "Video Games", value: "15" }, - { name: "Anime & Manga", value: "31" }, - { name: "Computers", value: "18" }, - { name: "Board Games", value: "16" }, - { name: "Comics", value: "29" }, - { name: "Cartoons & Animations", value: "32" }, - { name: "Film", value: "11" }, - { name: "Science & Nature", value: "17" }, - { name: "Animals", value: "27" }, - { name: "Music", value: "12" }, - { name: "History", value: "23" }, - { name: "Geography", value: "22" }, - { name: "Mythology", value: "20" } - ) - ), +const CATEGORY_MAP = { + 15: "Video Games", + 31: "Anime & Manga", + 18: "Computers", + 16: "Board Games", + 29: "Comics", + 32: "Cartoons & Animations", + 11: "Film", + 9: "General Knowledge", + 17: "Science & Nature", + 27: "Animals", + 12: "Music", + 23: "History", + 22: "Geography", + 20: "Mythology", +}; - async execute(interaction, client) { - const userId = interaction.user.id; - const username = interaction.user.username; - const guild = interaction.guild; - const timeLimit = 30000; // Time limit for answering in milliseconds +const fetchTriviaQuestion = async (categoryId, categoryName) => { + try { + let triviaQuestion = await TriviaQuestion.findOne({ + last_served: { $lt: new Date(Date.now() - QUESTION_EXPIRY) }, + category: categoryName, + }).sort({ last_served: 1 }); - // Check if the user already has an ongoing trivia game - if (ongoingTrivia.has(userId)) { - return interaction.reply({ - content: - "You already have an ongoing trivia game. Please finish it before starting a new one.", - ephemeral: true, // Only visible to the user + if (!triviaQuestion || Date.now() - lastApiCall >= API_INTERVAL) { + const response = await axios.get( + `https://opentdb.com/api.php?amount=1&category=${categoryId}` + ); + triviaQuestion = response.data.results[0]; + lastApiCall = Date.now(); + + await TriviaQuestion.create({ + question: decode(triviaQuestion.question), + correct_answer: decode(triviaQuestion.correct_answer), + incorrect_answers: triviaQuestion.incorrect_answers.map(decode), + category: categoryName, + last_served: null, + }); + + triviaQuestion = await TriviaQuestion.findOne({ + question: decode(triviaQuestion.question), + category: categoryName, }); } - // Add the user to the set of active trivia players - ongoingTrivia.add(userId); + if (triviaQuestion) { + triviaQuestion.last_served = new Date(); + await triviaQuestion.save(); + } - try { - const categoryId = interaction.options.getString("category"); - const categoryName = (() => { - switch (categoryId) { - case "15": - return "Video Games"; - case "31": - return "Anime & Manga"; - case "18": - return "Computers"; - case "16": - return "Board Games"; - case "29": - return "Comics"; - case "32": - return "Cartoons & Animations"; - case "11": - return "Film"; - case "9": - return "General Knowledge"; - case "17": - return "Science & Nature"; - case "27": - return "Animals"; - case "12": - return "Music"; - case "23": - return "History"; - case "22": - return "Geography"; - case "20": - return "Mythology"; - default: - return "Video Games"; - } - })() - .replace(/[^a-zA-Z0-9 &]/g, "") - .trim(); // Remove special characters and trim the category name for MongoDB query purposes in the Emebed title + return triviaQuestion; + } catch (error) { + console.error("Error fetching or saving trivia question:", error); + throw new Error("Error fetching trivia question"); + } +}; - // Fetch a trivia question from the cache or the API - let triviaQuestion = await TriviaQuestion.findOne({ - last_served: { $lt: new Date(Date.now() - QUESTION_EXPIRY) }, // Fetch questions not served recently - category: categoryName, // Filter by category - }).sort({ last_served: 1 }); +const createTriviaEmbed = ( + categoryName, + question, + answerMap, + guild, + timeLimit +) => { + return new EmbedBuilder() + .setColor("#0099ff") + .setTitle(`${categoryName} Trivia Question`) + .setDescription(question) + .addFields( + Object.entries(answerMap).map(([number, answer]) => ({ + name: `Option ${number}`, + value: answer, + inline: true, + })) + ) + .setTimestamp() + .setFooter({ + text: `${guild.name} | Answer within ${timeLimit / 1000} seconds`, + iconURL: guild.iconURL(), + }); +}; - if (!triviaQuestion || Date.now() - lastApiCall >= API_INTERVAL) { - // Fetch a new trivia question from OTDB - const response = await axios.get( - `https://opentdb.com/api.php?amount=1&category=${categoryId}` - ); +const handleAnswerCollection = async ( + interaction, + triviaQuestion, + answerMap, + correctAnswer, + allAnswers, + timeLimit, + userId, + username +) => { + try { + const answerFilter = (response) => { + const userInput = response.content.trim(); + const userAnswerNumber = parseInt(userInput, 10); + const userAnswerText = + allAnswers.includes(userInput) || + (answerMap[userAnswerNumber] && + answerMap[userAnswerNumber] === correctAnswer); - triviaQuestion = response.data.results[0]; - lastApiCall = Date.now(); + return ( + response.author.id === userId && + (userAnswerText || (userAnswerNumber >= 1 && userAnswerNumber <= 4)) + ); + }; - // Save the new trivia question to MongoDB - await TriviaQuestion.create({ - question: decode(triviaQuestion.question), - correct_answer: decode(triviaQuestion.correct_answer), - incorrect_answers: triviaQuestion.incorrect_answers.map(decode), - category: categoryName, // Include the category - last_served: null, // Initially not served - }); + const answerCollector = interaction.channel.createMessageCollector({ + filter: answerFilter, + max: 1, + time: timeLimit, + }); - // Fetch the newly created question - triviaQuestion = await TriviaQuestion.findOne({ - question: decode(triviaQuestion.question), - category: categoryName, // Filter by category - }); - } - - if (triviaQuestion) { - triviaQuestion.last_served = new Date(); - await triviaQuestion.save(); - } - - const question = decode(triviaQuestion.question); - const correctAnswer = decode(triviaQuestion.correct_answer); - const incorrectAnswers = triviaQuestion.incorrect_answers.map(decode); - let allAnswers = [...incorrectAnswers, correctAnswer]; - - // Declare answerMap before any conditions - let answerMap = {}; - - // Handle True/False questions specifically - if (triviaQuestion.type === "boolean") { - // Always keep "True" as option 1 and "False" as option 2 - answerMap = { 1: "True", 2: "False" }; - } else { - // Shuffle answers for other types of questions - allAnswers = allAnswers.sort(() => Math.random() - 0.5); - - // Assign the map without redeclaring - answerMap = allAnswers.reduce((map, answer, index) => { - map[index + 1] = answer; - return map; - }, {}); - } - - // Create an embed with the trivia question and numbered options - const triviaEmbed = new EmbedBuilder() - .setColor("#0099ff") - .setTitle(`${categoryName} Trivia Question`) - .setDescription(question) - .addFields( - Object.entries(answerMap).map(([number, answer]) => ({ - name: `Option ${number}`, - value: answer, - inline: true, - })) - ) - .setTimestamp() - .setFooter({ - text: `${guild.name} | Answer within ${timeLimit / 1000} seconds`, - iconURL: guild.iconURL(), - }); - - await interaction.reply({ - embeds: [triviaEmbed], - }); - - // Create a message collector specific to the user - const answerFilter = (response) => { - const userInput = response.content.trim(); - const userAnswerNumber = parseInt(userInput, 10); - const userAnswerText = - allAnswers.includes(userInput) || - (answerMap[userAnswerNumber] && - answerMap[userAnswerNumber] === correctAnswer); - - // Check if the input is a number within valid range or a text that matches one of the options - return ( - response.author.id === userId && - (userAnswerText || (userAnswerNumber >= 1 && userAnswerNumber <= 4)) - ); - }; - - const answerCollector = interaction.channel.createMessageCollector({ - filter: answerFilter, - max: 1, - time: timeLimit, - }); - - answerCollector.on("collect", async (message) => { + answerCollector.on("collect", async (message) => { + try { const userInput = message.content.trim(); const userAnswerNumber = parseInt(userInput, 10); const userAnswer = answerMap[userAnswerNumber] || userInput; - let resultMessage = "Incorrect! Better luck next time."; + let resultMessage = + userAnswer === correctAnswer + ? "Correct!" + : "Incorrect! Better luck next time."; - if (userAnswer === correctAnswer) { - resultMessage = "Correct!"; - } - - // Update leaderboard let userScore = await Leaderboard.findOne({ userId }); if (!userScore) { userScore = new Leaderboard({ @@ -229,31 +154,122 @@ module.exports = { `${resultMessage} <@${userId}> You've answered ${userScore.correctAnswers} questions correctly out of ${userScore.gamesPlayed} games.` ); - // Remove user from the ongoing trivia set once the game is finished ongoingTrivia.delete(userId); - }); + } catch (error) { + console.error("Error processing collected answer:", error); + await interaction.followUp({ + content: "There was an error processing your answer.", + ephemeral: true, + }); + ongoingTrivia.delete(userId); + } + }); - answerCollector.on("end", (collected, reason) => { - if (reason === "time") { - interaction.followUp( - `<@${userId}> Time's up! You didn't answer in time.` - ); + answerCollector.on("end", (collected, reason) => { + if (reason === "time") { + interaction.followUp( + `<@${userId}> Time's up! You didn't answer in time.` + ); + ongoingTrivia.delete(userId); + } + }); + } catch (error) { + console.error("Error handling answer collection:", error); + await interaction.followUp({ + content: "There was an error handling your response.", + ephemeral: true, + }); + ongoingTrivia.delete(userId); + } +}; - // Remove user from the ongoing trivia set when the game times out - ongoingTrivia.delete(userId); - } +module.exports = { + data: new SlashCommandBuilder() + .setName("trivia") + .setDescription("Play a trivia game") + .addStringOption((option) => + option + .setName("category") + .setDescription("Choose a trivia category") + .setRequired(true) + .addChoices( + ...Object.entries(CATEGORY_MAP).map(([value, name]) => ({ + name, + value, + })) + ) + ), + + async execute(interaction, client) { + const userId = interaction.user.id; + const username = interaction.user.username; + const guild = interaction.guild; + const timeLimit = 30000; // Time limit for answering in milliseconds + + if (ongoingTrivia.has(userId)) { + return interaction.reply({ + content: + "You already have an ongoing trivia game. Please finish it before starting a new one.", + ephemeral: true, }); + } + + ongoingTrivia.add(userId); + + try { + const categoryId = interaction.options.getString("category"); + const categoryName = CATEGORY_MAP[categoryId] || "Video Games"; + + const triviaQuestion = await fetchTriviaQuestion( + categoryId, + categoryName + ); + if (!triviaQuestion) throw new Error("Failed to fetch trivia question"); + + const question = decode(triviaQuestion.question); + const correctAnswer = decode(triviaQuestion.correct_answer); + const incorrectAnswers = triviaQuestion.incorrect_answers.map(decode); + let allAnswers = [...incorrectAnswers, correctAnswer]; + + let answerMap = {}; + + if (triviaQuestion.type === "boolean") { + answerMap = { 1: "True", 2: "False" }; + } else { + allAnswers = allAnswers.sort(() => Math.random() - 0.5); + answerMap = allAnswers.reduce((map, answer, index) => { + map[index + 1] = answer; + return map; + }, {}); + } + + const triviaEmbed = createTriviaEmbed( + categoryName, + question, + answerMap, + guild, + timeLimit + ); + + await interaction.reply({ embeds: [triviaEmbed] }); + + await handleAnswerCollection( + interaction, + triviaQuestion, + answerMap, + correctAnswer, + allAnswers, + timeLimit, + userId, + username + ); } catch (error) { - console.error("Error fetching trivia question:", error); - - // Inform the user about the error and let them retry + console.error("Error executing trivia command:", error); await interaction.reply({ content: "Trivia API hit the rate limit. Please try again in 5 seconds.", ephemeral: true, }); - - // Remove the user from the ongoing trivia set in case of an error ongoingTrivia.delete(userId); } },