We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[Trivia] Errors in game sessions need better handling
Currently any exception inside trivia session's task just stops it without any logging and feedback to session's players.
Example issues that can get hidden because of this issue: #3583, #3584

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `redbot/cogs/trivia/session.py`
Content:
```
1 """Module to manage trivia sessions."""
2 import asyncio
3 import time
4 import random
5 from collections import Counter
6 import discord
7 from redbot.core import bank
8 from redbot.core.i18n import Translator
9 from redbot.core.utils.chat_formatting import box, bold, humanize_list, humanize_number
10 from redbot.core.utils.common_filters import normalize_smartquotes
11 from .log import LOG
12 
13 __all__ = ["TriviaSession"]
14 
15 T_ = Translator("TriviaSession", __file__)
16 
17 
18 _ = lambda s: s
19 _REVEAL_MESSAGES = (
20     _("I know this one! {answer}!"),
21     _("Easy: {answer}."),
22     _("Oh really? It's {answer} of course."),
23 )
24 _FAIL_MESSAGES = (
25     _("To the next one I guess..."),
26     _("Moving on..."),
27     _("I'm sure you'll know the answer of the next one."),
28     _("\N{PENSIVE FACE} Next one."),
29 )
30 _ = T_
31 
32 
33 class TriviaSession:
34     """Class to run a session of trivia with the user.
35 
36     To run the trivia session immediately, use `TriviaSession.start` instead of
37     instantiating directly.
38 
39     Attributes
40     ----------
41     ctx : `commands.Context`
42         Context object from which this session will be run.
43         This object assumes the session was started in `ctx.channel`
44         by `ctx.author`.
45     question_list : `dict`
46         A list of tuples mapping questions (`str`) to answers (`list` of
47         `str`).
48     settings : `dict`
49         Settings for the trivia session, with values for the following:
50          - ``max_score`` (`int`)
51          - ``delay`` (`float`)
52          - ``timeout`` (`float`)
53          - ``reveal_answer`` (`bool`)
54          - ``bot_plays`` (`bool`)
55          - ``allow_override`` (`bool`)
56          - ``payout_multiplier`` (`float`)
57     scores : `collections.Counter`
58         A counter with the players as keys, and their scores as values. The
59         players are of type `discord.Member`.
60     count : `int`
61         The number of questions which have been asked.
62 
63     """
64 
65     def __init__(self, ctx, question_list: dict, settings: dict):
66         self.ctx = ctx
67         list_ = list(question_list.items())
68         random.shuffle(list_)
69         self.question_list = list_
70         self.settings = settings
71         self.scores = Counter()
72         self.count = 0
73         self._last_response = time.time()
74         self._task = None
75 
76     @classmethod
77     def start(cls, ctx, question_list, settings):
78         """Create and start a trivia session.
79 
80         This allows the session to manage the running and cancellation of its
81         own tasks.
82 
83         Parameters
84         ----------
85         ctx : `commands.Context`
86             Same as `TriviaSession.ctx`
87         question_list : `dict`
88             Same as `TriviaSession.question_list`
89         settings : `dict`
90             Same as `TriviaSession.settings`
91 
92         Returns
93         -------
94         TriviaSession
95             The new trivia session being run.
96 
97         """
98         session = cls(ctx, question_list, settings)
99         loop = ctx.bot.loop
100         session._task = loop.create_task(session.run())
101         return session
102 
103     async def run(self):
104         """Run the trivia session.
105 
106         In order for the trivia session to be stopped correctly, this should
107         only be called internally by `TriviaSession.start`.
108         """
109         await self._send_startup_msg()
110         max_score = self.settings["max_score"]
111         delay = self.settings["delay"]
112         timeout = self.settings["timeout"]
113         for question, answers in self._iter_questions():
114             async with self.ctx.typing():
115                 await asyncio.sleep(3)
116             self.count += 1
117             msg = bold(_("Question number {num}!").format(num=self.count)) + "\n\n" + question
118             await self.ctx.send(msg)
119             continue_ = await self.wait_for_answer(answers, delay, timeout)
120             if continue_ is False:
121                 break
122             if any(score >= max_score for score in self.scores.values()):
123                 await self.end_game()
124                 break
125         else:
126             await self.ctx.send(_("There are no more questions!"))
127             await self.end_game()
128 
129     async def _send_startup_msg(self):
130         list_names = []
131         for idx, tup in enumerate(self.settings["lists"].items()):
132             name, author = tup
133             if author:
134                 title = _("{trivia_list} (by {author})").format(trivia_list=name, author=author)
135             else:
136                 title = name
137             list_names.append(title)
138         await self.ctx.send(
139             _("Starting Trivia: {list_names}").format(list_names=humanize_list(list_names))
140         )
141 
142     def _iter_questions(self):
143         """Iterate over questions and answers for this session.
144 
145         Yields
146         ------
147         `tuple`
148             A tuple containing the question (`str`) and the answers (`tuple` of
149             `str`).
150 
151         """
152         for question, answers in self.question_list:
153             answers = _parse_answers(answers)
154             yield question, answers
155 
156     async def wait_for_answer(self, answers, delay: float, timeout: float):
157         """Wait for a correct answer, and then respond.
158 
159         Scores are also updated in this method.
160 
161         Returns False if waiting was cancelled; this is usually due to the
162         session being forcibly stopped.
163 
164         Parameters
165         ----------
166         answers : `iterable` of `str`
167             A list of valid answers to the current question.
168         delay : float
169             How long users have to respond (in seconds).
170         timeout : float
171             How long before the session ends due to no responses (in seconds).
172 
173         Returns
174         -------
175         bool
176             :code:`True` if the session wasn't interrupted.
177 
178         """
179         try:
180             message = await self.ctx.bot.wait_for(
181                 "message", check=self.check_answer(answers), timeout=delay
182             )
183         except asyncio.TimeoutError:
184             if time.time() - self._last_response >= timeout:
185                 await self.ctx.send(_("Guys...? Well, I guess I'll stop then."))
186                 self.stop()
187                 return False
188             if self.settings["reveal_answer"]:
189                 reply = T_(random.choice(_REVEAL_MESSAGES)).format(answer=answers[0])
190             else:
191                 reply = T_(random.choice(_FAIL_MESSAGES))
192             if self.settings["bot_plays"]:
193                 reply += _(" **+1** for me!")
194                 self.scores[self.ctx.guild.me] += 1
195             await self.ctx.send(reply)
196         else:
197             self.scores[message.author] += 1
198             reply = _("You got it {user}! **+1** to you!").format(user=message.author.display_name)
199             await self.ctx.send(reply)
200         return True
201 
202     def check_answer(self, answers):
203         """Get a predicate to check for correct answers.
204 
205         The returned predicate takes a message as its only parameter,
206         and returns ``True`` if the message contains any of the
207         given answers.
208 
209         Parameters
210         ----------
211         answers : `iterable` of `str`
212             The answers which the predicate must check for.
213 
214         Returns
215         -------
216         function
217             The message predicate.
218 
219         """
220         answers = tuple(s.lower() for s in answers)
221 
222         def _pred(message: discord.Message):
223             early_exit = message.channel != self.ctx.channel or message.author == self.ctx.guild.me
224             if early_exit:
225                 return False
226 
227             self._last_response = time.time()
228             guess = message.content.lower()
229             guess = normalize_smartquotes(guess)
230             for answer in answers:
231                 if " " in answer and answer in guess:
232                     # Exact matching, issue #331
233                     return True
234                 elif any(word == answer for word in guess.split(" ")):
235                     return True
236             return False
237 
238         return _pred
239 
240     async def end_game(self):
241         """End the trivia session and display scrores."""
242         if self.scores:
243             await self.send_table()
244         multiplier = self.settings["payout_multiplier"]
245         if multiplier > 0:
246             await self.pay_winner(multiplier)
247         self.stop()
248 
249     async def send_table(self):
250         """Send a table of scores to the session's channel."""
251         table = "+ Results: \n\n"
252         for user, score in self.scores.most_common():
253             table += "+ {}\t{}\n".format(user, score)
254         await self.ctx.send(box(table, lang="diff"))
255 
256     def stop(self):
257         """Stop the trivia session, without showing scores."""
258         self.ctx.bot.dispatch("trivia_end", self)
259 
260     def force_stop(self):
261         """Cancel whichever tasks this session is running."""
262         self._task.cancel()
263         channel = self.ctx.channel
264         LOG.debug("Force stopping trivia session; #%s in %s", channel, channel.guild.id)
265 
266     async def pay_winner(self, multiplier: float):
267         """Pay the winner of this trivia session.
268 
269         The winner is only payed if there are at least 3 human contestants.
270 
271         Parameters
272         ----------
273         multiplier : float
274             The coefficient of the winner's score, used to determine the amount
275             paid.
276 
277         """
278         (winner, score) = next((tup for tup in self.scores.most_common(1)), (None, None))
279         me_ = self.ctx.guild.me
280         if winner is not None and winner != me_ and score > 0:
281             contestants = list(self.scores.keys())
282             if me_ in contestants:
283                 contestants.remove(me_)
284             if len(contestants) >= 3:
285                 amount = int(multiplier * score)
286                 if amount > 0:
287                     LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
288                     await bank.deposit_credits(winner, int(multiplier * score))
289                     await self.ctx.send(
290                         _(
291                             "Congratulations, {user}, you have received {num} {currency}"
292                             " for coming first."
293                         ).format(
294                             user=winner.display_name,
295                             num=humanize_number(amount),
296                             currency=await bank.get_currency_name(self.ctx.guild),
297                         )
298                     )
299 
300 
301 def _parse_answers(answers):
302     """Parse the raw answers to readable strings.
303 
304     The reason this exists is because of YAML's ambiguous syntax. For example,
305     if the answer to a question in YAML is ``yes``, YAML will load it as the
306     boolean value ``True``, which is not necessarily the desired answer. This
307     function aims to undo that for bools, and possibly for numbers in the
308     future too.
309 
310     Parameters
311     ----------
312     answers : `iterable` of `str`
313         The raw answers loaded from YAML.
314 
315     Returns
316     -------
317     `tuple` of `str`
318         The answers in readable/ guessable strings.
319 
320     """
321     ret = []
322     for answer in answers:
323         if isinstance(answer, bool):
324             if answer is True:
325                 ret.extend(["True", "Yes", "On"])
326             else:
327                 ret.extend(["False", "No", "Off"])
328         else:
329             ret.append(str(answer))
330     # Uniquify list
331     seen = set()
332     return tuple(x for x in ret if not (x in seen or seen.add(x)))
333
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/redbot/cogs/trivia/session.py b/redbot/cogs/trivia/session.py
--- a/redbot/cogs/trivia/session.py
+++ b/redbot/cogs/trivia/session.py
@@ -98,8 +98,26 @@
         session = cls(ctx, question_list, settings)
         loop = ctx.bot.loop
         session._task = loop.create_task(session.run())
+        session._task.add_done_callback(session._error_handler)
         return session
 
+    def _error_handler(self, fut):
+        """Catches errors in the session task."""
+        try:
+            fut.result()
+        except asyncio.CancelledError:
+            pass
+        except Exception as exc:
+            LOG.error("A trivia session has encountered an error.\n", exc_info=exc)
+            asyncio.create_task(
+                self.ctx.send(
+                    _(
+                        "An unexpected error occurred in the trivia session.\nCheck your console or logs for details."
+                    )
+                )
+            )
+            self.stop()
+
     async def run(self):
         """Run the trivia session.

{"golden_diff": "diff --git a/redbot/cogs/trivia/session.py b/redbot/cogs/trivia/session.py\n--- a/redbot/cogs/trivia/session.py\n+++ b/redbot/cogs/trivia/session.py\n@@ -98,8 +98,26 @@\n         session = cls(ctx, question_list, settings)\n         loop = ctx.bot.loop\n         session._task = loop.create_task(session.run())\n+        session._task.add_done_callback(session._error_handler)\n         return session\n \n+    def _error_handler(self, fut):\n+        \"\"\"Catches errors in the session task.\"\"\"\n+        try:\n+            fut.result()\n+        except asyncio.CancelledError:\n+            pass\n+        except Exception as exc:\n+            LOG.error(\"A trivia session has encountered an error.\\n\", exc_info=exc)\n+            asyncio.create_task(\n+                self.ctx.send(\n+                    _(\n+                        \"An unexpected error occurred in the trivia session.\\nCheck your console or logs for details.\"\n+                    )\n+                )\n+            )\n+            self.stop()\n+\n     async def run(self):\n         \"\"\"Run the trivia session.\n", "issue": "[Trivia] Errors in game sessions need better handling\nCurrently any exception inside trivia session's task just stops it without any logging and feedback to session's players.\r\nExample issues that can get hidden because of this issue: #3583, #3584\n", "before_files": [{"content": "\"\"\"Module to manage trivia sessions.\"\"\"\nimport asyncio\nimport time\nimport random\nfrom collections import Counter\nimport discord\nfrom redbot.core import bank\nfrom redbot.core.i18n import Translator\nfrom redbot.core.utils.chat_formatting import box, bold, humanize_list, humanize_number\nfrom redbot.core.utils.common_filters import normalize_smartquotes\nfrom .log import LOG\n\n__all__ = [\"TriviaSession\"]\n\nT_ = Translator(\"TriviaSession\", __file__)\n\n\n_ = lambda s: s\n_REVEAL_MESSAGES = (\n    _(\"I know this one! {answer}!\"),\n    _(\"Easy: {answer}.\"),\n    _(\"Oh really? It's {answer} of course.\"),\n)\n_FAIL_MESSAGES = (\n    _(\"To the next one I guess...\"),\n    _(\"Moving on...\"),\n    _(\"I'm sure you'll know the answer of the next one.\"),\n    _(\"\\N{PENSIVE FACE} Next one.\"),\n)\n_ = T_\n\n\nclass TriviaSession:\n    \"\"\"Class to run a session of trivia with the user.\n\n    To run the trivia session immediately, use `TriviaSession.start` instead of\n    instantiating directly.\n\n    Attributes\n    ----------\n    ctx : `commands.Context`\n        Context object from which this session will be run.\n        This object assumes the session was started in `ctx.channel`\n        by `ctx.author`.\n    question_list : `dict`\n        A list of tuples mapping questions (`str`) to answers (`list` of\n        `str`).\n    settings : `dict`\n        Settings for the trivia session, with values for the following:\n         - ``max_score`` (`int`)\n         - ``delay`` (`float`)\n         - ``timeout`` (`float`)\n         - ``reveal_answer`` (`bool`)\n         - ``bot_plays`` (`bool`)\n         - ``allow_override`` (`bool`)\n         - ``payout_multiplier`` (`float`)\n    scores : `collections.Counter`\n        A counter with the players as keys, and their scores as values. The\n        players are of type `discord.Member`.\n    count : `int`\n        The number of questions which have been asked.\n\n    \"\"\"\n\n    def __init__(self, ctx, question_list: dict, settings: dict):\n        self.ctx = ctx\n        list_ = list(question_list.items())\n        random.shuffle(list_)\n        self.question_list = list_\n        self.settings = settings\n        self.scores = Counter()\n        self.count = 0\n        self._last_response = time.time()\n        self._task = None\n\n    @classmethod\n    def start(cls, ctx, question_list, settings):\n        \"\"\"Create and start a trivia session.\n\n        This allows the session to manage the running and cancellation of its\n        own tasks.\n\n        Parameters\n        ----------\n        ctx : `commands.Context`\n            Same as `TriviaSession.ctx`\n        question_list : `dict`\n            Same as `TriviaSession.question_list`\n        settings : `dict`\n            Same as `TriviaSession.settings`\n\n        Returns\n        -------\n        TriviaSession\n            The new trivia session being run.\n\n        \"\"\"\n        session = cls(ctx, question_list, settings)\n        loop = ctx.bot.loop\n        session._task = loop.create_task(session.run())\n        return session\n\n    async def run(self):\n        \"\"\"Run the trivia session.\n\n        In order for the trivia session to be stopped correctly, this should\n        only be called internally by `TriviaSession.start`.\n        \"\"\"\n        await self._send_startup_msg()\n        max_score = self.settings[\"max_score\"]\n        delay = self.settings[\"delay\"]\n        timeout = self.settings[\"timeout\"]\n        for question, answers in self._iter_questions():\n            async with self.ctx.typing():\n                await asyncio.sleep(3)\n            self.count += 1\n            msg = bold(_(\"Question number {num}!\").format(num=self.count)) + \"\\n\\n\" + question\n            await self.ctx.send(msg)\n            continue_ = await self.wait_for_answer(answers, delay, timeout)\n            if continue_ is False:\n                break\n            if any(score >= max_score for score in self.scores.values()):\n                await self.end_game()\n                break\n        else:\n            await self.ctx.send(_(\"There are no more questions!\"))\n            await self.end_game()\n\n    async def _send_startup_msg(self):\n        list_names = []\n        for idx, tup in enumerate(self.settings[\"lists\"].items()):\n            name, author = tup\n            if author:\n                title = _(\"{trivia_list} (by {author})\").format(trivia_list=name, author=author)\n            else:\n                title = name\n            list_names.append(title)\n        await self.ctx.send(\n            _(\"Starting Trivia: {list_names}\").format(list_names=humanize_list(list_names))\n        )\n\n    def _iter_questions(self):\n        \"\"\"Iterate over questions and answers for this session.\n\n        Yields\n        ------\n        `tuple`\n            A tuple containing the question (`str`) and the answers (`tuple` of\n            `str`).\n\n        \"\"\"\n        for question, answers in self.question_list:\n            answers = _parse_answers(answers)\n            yield question, answers\n\n    async def wait_for_answer(self, answers, delay: float, timeout: float):\n        \"\"\"Wait for a correct answer, and then respond.\n\n        Scores are also updated in this method.\n\n        Returns False if waiting was cancelled; this is usually due to the\n        session being forcibly stopped.\n\n        Parameters\n        ----------\n        answers : `iterable` of `str`\n            A list of valid answers to the current question.\n        delay : float\n            How long users have to respond (in seconds).\n        timeout : float\n            How long before the session ends due to no responses (in seconds).\n\n        Returns\n        -------\n        bool\n            :code:`True` if the session wasn't interrupted.\n\n        \"\"\"\n        try:\n            message = await self.ctx.bot.wait_for(\n                \"message\", check=self.check_answer(answers), timeout=delay\n            )\n        except asyncio.TimeoutError:\n            if time.time() - self._last_response >= timeout:\n                await self.ctx.send(_(\"Guys...? Well, I guess I'll stop then.\"))\n                self.stop()\n                return False\n            if self.settings[\"reveal_answer\"]:\n                reply = T_(random.choice(_REVEAL_MESSAGES)).format(answer=answers[0])\n            else:\n                reply = T_(random.choice(_FAIL_MESSAGES))\n            if self.settings[\"bot_plays\"]:\n                reply += _(\" **+1** for me!\")\n                self.scores[self.ctx.guild.me] += 1\n            await self.ctx.send(reply)\n        else:\n            self.scores[message.author] += 1\n            reply = _(\"You got it {user}! **+1** to you!\").format(user=message.author.display_name)\n            await self.ctx.send(reply)\n        return True\n\n    def check_answer(self, answers):\n        \"\"\"Get a predicate to check for correct answers.\n\n        The returned predicate takes a message as its only parameter,\n        and returns ``True`` if the message contains any of the\n        given answers.\n\n        Parameters\n        ----------\n        answers : `iterable` of `str`\n            The answers which the predicate must check for.\n\n        Returns\n        -------\n        function\n            The message predicate.\n\n        \"\"\"\n        answers = tuple(s.lower() for s in answers)\n\n        def _pred(message: discord.Message):\n            early_exit = message.channel != self.ctx.channel or message.author == self.ctx.guild.me\n            if early_exit:\n                return False\n\n            self._last_response = time.time()\n            guess = message.content.lower()\n            guess = normalize_smartquotes(guess)\n            for answer in answers:\n                if \" \" in answer and answer in guess:\n                    # Exact matching, issue #331\n                    return True\n                elif any(word == answer for word in guess.split(\" \")):\n                    return True\n            return False\n\n        return _pred\n\n    async def end_game(self):\n        \"\"\"End the trivia session and display scrores.\"\"\"\n        if self.scores:\n            await self.send_table()\n        multiplier = self.settings[\"payout_multiplier\"]\n        if multiplier > 0:\n            await self.pay_winner(multiplier)\n        self.stop()\n\n    async def send_table(self):\n        \"\"\"Send a table of scores to the session's channel.\"\"\"\n        table = \"+ Results: \\n\\n\"\n        for user, score in self.scores.most_common():\n            table += \"+ {}\\t{}\\n\".format(user, score)\n        await self.ctx.send(box(table, lang=\"diff\"))\n\n    def stop(self):\n        \"\"\"Stop the trivia session, without showing scores.\"\"\"\n        self.ctx.bot.dispatch(\"trivia_end\", self)\n\n    def force_stop(self):\n        \"\"\"Cancel whichever tasks this session is running.\"\"\"\n        self._task.cancel()\n        channel = self.ctx.channel\n        LOG.debug(\"Force stopping trivia session; #%s in %s\", channel, channel.guild.id)\n\n    async def pay_winner(self, multiplier: float):\n        \"\"\"Pay the winner of this trivia session.\n\n        The winner is only payed if there are at least 3 human contestants.\n\n        Parameters\n        ----------\n        multiplier : float\n            The coefficient of the winner's score, used to determine the amount\n            paid.\n\n        \"\"\"\n        (winner, score) = next((tup for tup in self.scores.most_common(1)), (None, None))\n        me_ = self.ctx.guild.me\n        if winner is not None and winner != me_ and score > 0:\n            contestants = list(self.scores.keys())\n            if me_ in contestants:\n                contestants.remove(me_)\n            if len(contestants) >= 3:\n                amount = int(multiplier * score)\n                if amount > 0:\n                    LOG.debug(\"Paying trivia winner: %d credits --> %s\", amount, str(winner))\n                    await bank.deposit_credits(winner, int(multiplier * score))\n                    await self.ctx.send(\n                        _(\n                            \"Congratulations, {user}, you have received {num} {currency}\"\n                            \" for coming first.\"\n                        ).format(\n                            user=winner.display_name,\n                            num=humanize_number(amount),\n                            currency=await bank.get_currency_name(self.ctx.guild),\n                        )\n                    )\n\n\ndef _parse_answers(answers):\n    \"\"\"Parse the raw answers to readable strings.\n\n    The reason this exists is because of YAML's ambiguous syntax. For example,\n    if the answer to a question in YAML is ``yes``, YAML will load it as the\n    boolean value ``True``, which is not necessarily the desired answer. This\n    function aims to undo that for bools, and possibly for numbers in the\n    future too.\n\n    Parameters\n    ----------\n    answers : `iterable` of `str`\n        The raw answers loaded from YAML.\n\n    Returns\n    -------\n    `tuple` of `str`\n        The answers in readable/ guessable strings.\n\n    \"\"\"\n    ret = []\n    for answer in answers:\n        if isinstance(answer, bool):\n            if answer is True:\n                ret.extend([\"True\", \"Yes\", \"On\"])\n            else:\n                ret.extend([\"False\", \"No\", \"Off\"])\n        else:\n            ret.append(str(answer))\n    # Uniquify list\n    seen = set()\n    return tuple(x for x in ret if not (x in seen or seen.add(x)))\n", "path": "redbot/cogs/trivia/session.py"}], "after_files": [{"content": "\"\"\"Module to manage trivia sessions.\"\"\"\nimport asyncio\nimport time\nimport random\nfrom collections import Counter\nimport discord\nfrom redbot.core import bank\nfrom redbot.core.i18n import Translator\nfrom redbot.core.utils.chat_formatting import box, bold, humanize_list, humanize_number\nfrom redbot.core.utils.common_filters import normalize_smartquotes\nfrom .log import LOG\n\n__all__ = [\"TriviaSession\"]\n\nT_ = Translator(\"TriviaSession\", __file__)\n\n\n_ = lambda s: s\n_REVEAL_MESSAGES = (\n    _(\"I know this one! {answer}!\"),\n    _(\"Easy: {answer}.\"),\n    _(\"Oh really? It's {answer} of course.\"),\n)\n_FAIL_MESSAGES = (\n    _(\"To the next one I guess...\"),\n    _(\"Moving on...\"),\n    _(\"I'm sure you'll know the answer of the next one.\"),\n    _(\"\\N{PENSIVE FACE} Next one.\"),\n)\n_ = T_\n\n\nclass TriviaSession:\n    \"\"\"Class to run a session of trivia with the user.\n\n    To run the trivia session immediately, use `TriviaSession.start` instead of\n    instantiating directly.\n\n    Attributes\n    ----------\n    ctx : `commands.Context`\n        Context object from which this session will be run.\n        This object assumes the session was started in `ctx.channel`\n        by `ctx.author`.\n    question_list : `dict`\n        A list of tuples mapping questions (`str`) to answers (`list` of\n        `str`).\n    settings : `dict`\n        Settings for the trivia session, with values for the following:\n         - ``max_score`` (`int`)\n         - ``delay`` (`float`)\n         - ``timeout`` (`float`)\n         - ``reveal_answer`` (`bool`)\n         - ``bot_plays`` (`bool`)\n         - ``allow_override`` (`bool`)\n         - ``payout_multiplier`` (`float`)\n    scores : `collections.Counter`\n        A counter with the players as keys, and their scores as values. The\n        players are of type `discord.Member`.\n    count : `int`\n        The number of questions which have been asked.\n\n    \"\"\"\n\n    def __init__(self, ctx, question_list: dict, settings: dict):\n        self.ctx = ctx\n        list_ = list(question_list.items())\n        random.shuffle(list_)\n        self.question_list = list_\n        self.settings = settings\n        self.scores = Counter()\n        self.count = 0\n        self._last_response = time.time()\n        self._task = None\n\n    @classmethod\n    def start(cls, ctx, question_list, settings):\n        \"\"\"Create and start a trivia session.\n\n        This allows the session to manage the running and cancellation of its\n        own tasks.\n\n        Parameters\n        ----------\n        ctx : `commands.Context`\n            Same as `TriviaSession.ctx`\n        question_list : `dict`\n            Same as `TriviaSession.question_list`\n        settings : `dict`\n            Same as `TriviaSession.settings`\n\n        Returns\n        -------\n        TriviaSession\n            The new trivia session being run.\n\n        \"\"\"\n        session = cls(ctx, question_list, settings)\n        loop = ctx.bot.loop\n        session._task = loop.create_task(session.run())\n        session._task.add_done_callback(session._error_handler)\n        return session\n\n    def _error_handler(self, fut):\n        \"\"\"Catches errors in the session task.\"\"\"\n        try:\n            fut.result()\n        except asyncio.CancelledError:\n            pass\n        except Exception as exc:\n            LOG.error(\"A trivia session has encountered an error.\\n\", exc_info=exc)\n            asyncio.create_task(\n                self.ctx.send(\n                    _(\n                        \"An unexpected error occurred in the trivia session.\\nCheck your console or logs for details.\"\n                    )\n                )\n            )\n            self.stop()\n\n    async def run(self):\n        \"\"\"Run the trivia session.\n\n        In order for the trivia session to be stopped correctly, this should\n        only be called internally by `TriviaSession.start`.\n        \"\"\"\n        await self._send_startup_msg()\n        max_score = self.settings[\"max_score\"]\n        delay = self.settings[\"delay\"]\n        timeout = self.settings[\"timeout\"]\n        for question, answers in self._iter_questions():\n            async with self.ctx.typing():\n                await asyncio.sleep(3)\n            self.count += 1\n            msg = bold(_(\"Question number {num}!\").format(num=self.count)) + \"\\n\\n\" + question\n            await self.ctx.send(msg)\n            continue_ = await self.wait_for_answer(answers, delay, timeout)\n            if continue_ is False:\n                break\n            if any(score >= max_score for score in self.scores.values()):\n                await self.end_game()\n                break\n        else:\n            await self.ctx.send(_(\"There are no more questions!\"))\n            await self.end_game()\n\n    async def _send_startup_msg(self):\n        list_names = []\n        for idx, tup in enumerate(self.settings[\"lists\"].items()):\n            name, author = tup\n            if author:\n                title = _(\"{trivia_list} (by {author})\").format(trivia_list=name, author=author)\n            else:\n                title = name\n            list_names.append(title)\n        await self.ctx.send(\n            _(\"Starting Trivia: {list_names}\").format(list_names=humanize_list(list_names))\n        )\n\n    def _iter_questions(self):\n        \"\"\"Iterate over questions and answers for this session.\n\n        Yields\n        ------\n        `tuple`\n            A tuple containing the question (`str`) and the answers (`tuple` of\n            `str`).\n\n        \"\"\"\n        for question, answers in self.question_list:\n            answers = _parse_answers(answers)\n            yield question, answers\n\n    async def wait_for_answer(self, answers, delay: float, timeout: float):\n        \"\"\"Wait for a correct answer, and then respond.\n\n        Scores are also updated in this method.\n\n        Returns False if waiting was cancelled; this is usually due to the\n        session being forcibly stopped.\n\n        Parameters\n        ----------\n        answers : `iterable` of `str`\n            A list of valid answers to the current question.\n        delay : float\n            How long users have to respond (in seconds).\n        timeout : float\n            How long before the session ends due to no responses (in seconds).\n\n        Returns\n        -------\n        bool\n            :code:`True` if the session wasn't interrupted.\n\n        \"\"\"\n        try:\n            message = await self.ctx.bot.wait_for(\n                \"message\", check=self.check_answer(answers), timeout=delay\n            )\n        except asyncio.TimeoutError:\n            if time.time() - self._last_response >= timeout:\n                await self.ctx.send(_(\"Guys...? Well, I guess I'll stop then.\"))\n                self.stop()\n                return False\n            if self.settings[\"reveal_answer\"]:\n                reply = T_(random.choice(_REVEAL_MESSAGES)).format(answer=answers[0])\n            else:\n                reply = T_(random.choice(_FAIL_MESSAGES))\n            if self.settings[\"bot_plays\"]:\n                reply += _(\" **+1** for me!\")\n                self.scores[self.ctx.guild.me] += 1\n            await self.ctx.send(reply)\n        else:\n            self.scores[message.author] += 1\n            reply = _(\"You got it {user}! **+1** to you!\").format(user=message.author.display_name)\n            await self.ctx.send(reply)\n        return True\n\n    def check_answer(self, answers):\n        \"\"\"Get a predicate to check for correct answers.\n\n        The returned predicate takes a message as its only parameter,\n        and returns ``True`` if the message contains any of the\n        given answers.\n\n        Parameters\n        ----------\n        answers : `iterable` of `str`\n            The answers which the predicate must check for.\n\n        Returns\n        -------\n        function\n            The message predicate.\n\n        \"\"\"\n        answers = tuple(s.lower() for s in answers)\n\n        def _pred(message: discord.Message):\n            early_exit = message.channel != self.ctx.channel or message.author == self.ctx.guild.me\n            if early_exit:\n                return False\n\n            self._last_response = time.time()\n            guess = message.content.lower()\n            guess = normalize_smartquotes(guess)\n            for answer in answers:\n                if \" \" in answer and answer in guess:\n                    # Exact matching, issue #331\n                    return True\n                elif any(word == answer for word in guess.split(\" \")):\n                    return True\n            return False\n\n        return _pred\n\n    async def end_game(self):\n        \"\"\"End the trivia session and display scrores.\"\"\"\n        if self.scores:\n            await self.send_table()\n        multiplier = self.settings[\"payout_multiplier\"]\n        if multiplier > 0:\n            await self.pay_winner(multiplier)\n        self.stop()\n\n    async def send_table(self):\n        \"\"\"Send a table of scores to the session's channel.\"\"\"\n        table = \"+ Results: \\n\\n\"\n        for user, score in self.scores.most_common():\n            table += \"+ {}\\t{}\\n\".format(user, score)\n        await self.ctx.send(box(table, lang=\"diff\"))\n\n    def stop(self):\n        \"\"\"Stop the trivia session, without showing scores.\"\"\"\n        self.ctx.bot.dispatch(\"trivia_end\", self)\n\n    def force_stop(self):\n        \"\"\"Cancel whichever tasks this session is running.\"\"\"\n        self._task.cancel()\n        channel = self.ctx.channel\n        LOG.debug(\"Force stopping trivia session; #%s in %s\", channel, channel.guild.id)\n\n    async def pay_winner(self, multiplier: float):\n        \"\"\"Pay the winner of this trivia session.\n\n        The winner is only payed if there are at least 3 human contestants.\n\n        Parameters\n        ----------\n        multiplier : float\n            The coefficient of the winner's score, used to determine the amount\n            paid.\n\n        \"\"\"\n        (winner, score) = next((tup for tup in self.scores.most_common(1)), (None, None))\n        me_ = self.ctx.guild.me\n        if winner is not None and winner != me_ and score > 0:\n            contestants = list(self.scores.keys())\n            if me_ in contestants:\n                contestants.remove(me_)\n            if len(contestants) >= 3:\n                amount = int(multiplier * score)\n                if amount > 0:\n                    LOG.debug(\"Paying trivia winner: %d credits --> %s\", amount, str(winner))\n                    await bank.deposit_credits(winner, int(multiplier * score))\n                    await self.ctx.send(\n                        _(\n                            \"Congratulations, {user}, you have received {num} {currency}\"\n                            \" for coming first.\"\n                        ).format(\n                            user=winner.display_name,\n                            num=humanize_number(amount),\n                            currency=await bank.get_currency_name(self.ctx.guild),\n                        )\n                    )\n\n\ndef _parse_answers(answers):\n    \"\"\"Parse the raw answers to readable strings.\n\n    The reason this exists is because of YAML's ambiguous syntax. For example,\n    if the answer to a question in YAML is ``yes``, YAML will load it as the\n    boolean value ``True``, which is not necessarily the desired answer. This\n    function aims to undo that for bools, and possibly for numbers in the\n    future too.\n\n    Parameters\n    ----------\n    answers : `iterable` of `str`\n        The raw answers loaded from YAML.\n\n    Returns\n    -------\n    `tuple` of `str`\n        The answers in readable/ guessable strings.\n\n    \"\"\"\n    ret = []\n    for answer in answers:\n        if isinstance(answer, bool):\n            if answer is True:\n                ret.extend([\"True\", \"Yes\", \"On\"])\n            else:\n                ret.extend([\"False\", \"No\", \"Off\"])\n        else:\n            ret.append(str(answer))\n    # Uniquify list\n    seen = set()\n    return tuple(x for x in ret if not (x in seen or seen.add(x)))\n", "path": "redbot/cogs/trivia/session.py"}]}