You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
8.3KB

  1. # -*- coding: utf-8 -*-
  2. import discord
  3. import asyncio
  4. import requests
  5. import datetime
  6. from html import unescape
  7. from random import shuffle
  8. from discord.ext.commands import group
  9. """
  10. Notes for myself.
  11. Game logic
  12. START
  13. WAIT FOR USERS WITH JOIN AND LEAVE
  14. START GAME FUNCTION
  15. LOOP:
  16. PARSE QUESTIONS
  17. ADD REACTION
  18. CORRECT ANSWER SCREEN
  19. ADD SCORES
  20. END
  21. END SCORES AND WINNER SCREEN"""
  22. class Trivia:
  23. """
  24. Trivia is based off the lovely https://opentdb.com made by PixelTail Games.
  25. This cog requires the bot account to be in the Roxbot Emoji Server to work.
  26. """
  27. def __init__(self, bot_client):
  28. # Get emoji objects here for the reactions. Basically to speedup the reactions for the game.
  29. # For some reason this is quicker than bot.get_emojis
  30. self.bot = bot_client
  31. a_emoji = discord.utils.get(self.bot.emojis, id=419572828854026252)
  32. b_emoji = discord.utils.get(self.bot.emojis, id=419572828925329429)
  33. c_emoji = discord.utils.get(self.bot.emojis, id=419572829231775755)
  34. d_emoji = discord.utils.get(self.bot.emojis, id=419572828954820620)
  35. self.emojis = [a_emoji, b_emoji, c_emoji, d_emoji]
  36. self.games = {}
  37. # Game Functions
  38. def get_questions(self, amount=10):
  39. r = requests.get("https://opentdb.com/api.php?amount={}".format(amount))
  40. return r.json()
  41. def parse_question(self, question):
  42. output = "Category: {}\nDifficulty: {}\nQuestion: {}\n".format(question["category"], question["difficulty"],
  43. unescape(question["question"]))
  44. if question["type"] == "boolean":
  45. # List of possible answers
  46. choices = ["True", "False"]
  47. correct = question["correct_answer"]
  48. # Get index of correct answer
  49. correct = choices.index(correct)
  50. # Create output
  51. answers = "{} {}\n{} {}".format(str(self.emojis[0]), choices[0], str(self.emojis[1]), choices[1])
  52. else:
  53. # Get possible answers and shuffle them in a list
  54. incorrect = question["incorrect_answers"]
  55. correct = question["correct_answer"]
  56. choices = [incorrect[0], incorrect[1], incorrect[2], correct]
  57. for answer in choices:
  58. choices[choices.index(answer)] = unescape(answer)
  59. shuffle(choices)
  60. # Then get the index of the correct answer
  61. correct = choices.index(correct)
  62. # Create output
  63. answers = "{} {}\n{} {}\n{} {}\n{} {}".format(str(self.emojis[0]), choices[0], str(self.emojis[1]), choices[1], str(self.emojis[2]), choices[2], str(self.emojis[3]), choices[3])
  64. return output, answers, correct
  65. async def add_question_reactions(self, message, question):
  66. if question["type"] == "boolean":
  67. amount = 2
  68. else:
  69. amount = 4
  70. for x in range(amount):
  71. await message.add_reaction(self.emojis[x])
  72. async def game(self, ctx, channel, questions):
  73. # For loop all the questions for the game, Maybe I should move the game dictionary here instead.
  74. for question in questions:
  75. # Parse question dictionary into something usable
  76. output, answers, correct = self.parse_question(question)
  77. self.games[channel.id]["correct_answer"] = correct
  78. # Send a message, add the emoji reactions, then edit in the question to avoid issues with answering before reactions are done.
  79. message = await ctx.send(output)
  80. await self.add_question_reactions(message, question)
  81. await message.edit(content=output+answers)
  82. self.games[channel.id]["current_question"] = message
  83. # Wait for answers
  84. await asyncio.sleep(10)
  85. self.games[channel.id]["current_question"] = None
  86. await message.clear_reactions()
  87. # Display Correct answer and calculate and display scores.
  88. index = self.games[channel.id]["correct_answer"]
  89. await ctx.send("Correct Answer is {} '{}'".format(self.emojis[index], question["correct_answer"]))
  90. correct_out = ""
  91. for user, time in self.games[channel.id]["correct_users"].items():
  92. seconds = (time - message.edited_at).total_seconds()
  93. correct_out += "{} answered correctly in {}s\n".format(discord.utils.get(ctx.guild.members, id=user), seconds)
  94. if not correct_out:
  95. await ctx.send("No one got anything right.")
  96. else:
  97. await ctx.send(correct_out)
  98. # Scores
  99. # Display that
  100. # Final checks for next question
  101. self.games[channel.id]["correct_users"] = {}
  102. self.games[channel.id]["players_answered"] = []
  103. # make sure to check that there is still players playing after a question
  104. # Game Naturally Ends
  105. # Some stuff here displaying score
  106. self.games.pop(channel.id)
  107. await ctx.send("GAME END")
  108. # Discord Events
  109. async def on_reaction_add(self, reaction, user):
  110. """Logic for answering a question"""
  111. # TODO: Debug this.
  112. time = datetime.datetime.now()
  113. if user == self.bot.user: # reaction.me isnt working idk why
  114. return
  115. channel = reaction.message.channel
  116. message = reaction.message
  117. if channel.id in self.games:
  118. if user.id in self.games[channel.id]["players"] and message.id == self.games[channel.id]["current_question"].id:
  119. if reaction.emoji in self.emojis and user.id not in self.games[channel.id]["players_answered"]:
  120. self.games[channel.id]["players_answered"].append(user.id)
  121. if reaction.emoji == self.emojis[self.games[channel.id]["correct_answer"]]:
  122. self.games[channel.id]["correct_users"][user.id] = time
  123. return # Maybe add something removing reactions if they are not allowed.
  124. else:
  125. return await message.remove_reaction(reaction, user)
  126. else:
  127. return await message.remove_reaction(reaction, user)
  128. else:
  129. return
  130. # Commands
  131. @group(aliases=["tr"])
  132. async def trivia(self, ctx):
  133. pass
  134. @trivia.command()
  135. async def start(self, ctx, amount = "medium"):
  136. channel = ctx.channel
  137. player = ctx.author
  138. # Check if a game is already running and if so exit.
  139. if channel.id in self.games:
  140. # Game active in this channel already
  141. await ctx.send("A game is already being run in this channel.", delete_after=2)
  142. await asyncio.sleep(2)
  143. return await ctx.message.delete()
  144. # Setup variables and wait for all players to join.
  145. # Length of game
  146. length = {"short": 5, "medium": 10, "long": 15}
  147. if amount not in length:
  148. amount = "medium"
  149. # Game Dictionaries
  150. game = {
  151. "players": {player.id: 0},
  152. "active": 0,
  153. "length": length[amount],
  154. "current_question": None,
  155. "players_answered": [],
  156. "correct_users": {},
  157. "correct_answer": ""
  158. }
  159. self.games[channel.id] = game
  160. # Waiting for players
  161. await ctx.send("Game Successfully created. Starting in 20 seconds...")
  162. #await asyncio.sleep(20)
  163. # Get questions
  164. questions = self.get_questions(length[amount])
  165. # Checks if there is any players to play the game still
  166. if not self.games[channel.id]["players"]:
  167. self.games.pop(channel.id)
  168. return await ctx.send("Abandoning game due to lack of players.")
  169. # Starts game
  170. self.games[channel.id]["active"] = 1
  171. await ctx.send("GAME START")
  172. await self.game(ctx, channel, questions["results"])
  173. @trivia.command()
  174. async def join(self, ctx):
  175. channel = ctx.channel
  176. # Checks if game is in this channel. Then if one isn't active, then if the player has already joined.
  177. if channel.id in self.games:
  178. if not self.games[channel.id]["active"]:
  179. player = ctx.author
  180. if player.id not in self.games[channel.id]["players"]:
  181. self.games[channel.id]["players"][player.id] = 0
  182. return await ctx.send("Player {} joined the game".format(player.mention))
  183. # Failures
  184. else:
  185. await ctx.send("You have already joined the game. If you want to leave, do `{}trivia leave`".format(self.bot.command_prefix), delete_after=2)
  186. await asyncio.sleep(2)
  187. return await ctx.message.delete()
  188. else:
  189. await ctx.send("Game is already in progress.", delete_after=2)
  190. await asyncio.sleep(2)
  191. return await ctx.message.delete()
  192. else:
  193. await ctx.send("Game isn't being played here.", delete_after=2)
  194. await asyncio.sleep(2)
  195. return await ctx.message.delete()
  196. @trivia.command()
  197. async def leave(self, ctx):
  198. channel = ctx.channel
  199. player = ctx.author
  200. # CAN LEAVE: Game is started or has been activated
  201. # CANT LEAVE: Game is not active or not in the game
  202. if channel.id in self.games:
  203. if player.id in self.games[channel.id]["players"]:
  204. self.games[channel.id]["players"].pop(player.id)
  205. await ctx.send("{} has left the game.".format(player.mention))
  206. return await ctx.message.delete()
  207. else:
  208. await ctx.send("You are not in this game", delete_after=2)
  209. await asyncio.sleep(2)
  210. return await ctx.message.delete()
  211. else:
  212. await ctx.send("Game isn't being played here.", delete_after=2)
  213. await asyncio.sleep(2)
  214. return await ctx.message.delete()
  215. def setup(Bot):
  216. Bot.add_cog(Trivia(Bot))