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.

296 lines
9.7KB

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