diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7a6e260 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Create an issue to report a bug +title: '' +labels: bug +assignees: bobloy + +--- + +**Describe the bug** + + +**To Reproduce** + +1. Load cog '...' +2. Run command '....' +3. See error + +**Expected behavior** + + +**Screenshots or Error Messages** + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..42f5500 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature Request]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + diff --git a/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md b/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md new file mode 100644 index 0000000..25bcc81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-audiotrivia-list.md @@ -0,0 +1,26 @@ +--- +name: New AudioTrivia List +about: Submit a new AudioTrivia list to be added +title: "[AudioTrivia Submission]" +labels: 'cog: audiotrivia' +assignees: bobloy + +--- + +**What is this trivia list?** + + +**Number of Questions** + + +**Original Content?** + + +- [ ] Yes +- [ ] No + + +**Did I test the list?** + +- [ ] Yes +- [ ] No diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..dd944c8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,62 @@ +'cog: announcedaily': + - announcedaily/* +'cog: audiotrivia': + - audiotrivia/* +'cog: ccrole': + - ccrole/* +'cog: chatter': + - chatter/* +'cog: conquest': + - conquest/* +'cog: dad': + - dad/* +'cog: exclusiverole': + - exclusiverole/* +'cog: fifo': + - fifo/* +'cog: firstmessage': + - firstmessage/* +'cog: flag': + - flag/* +'cog: forcemention': + - forcemention/* +'cog: hangman': + - hangman +'cog: infochannel': + - infochannel/* +'cog: isitdown': + - isitdown/* +'cog: launchlib': + - launchlib/* +'cog: leaver': + - leaver/* +'cog: lovecalculator': + - lovecalculator/* +'cog: lseen': + - lseen/* +'cog: nudity': + - nudity/* +'cog: planttycoon': + - planttycoon/* +'cog: qrinvite': + - qrinvite/* +'cog: reactrestrict': + - reactrestrict/* +'cog: recyclingplant': + - recyclingplant/* +'cog: rpsls': + - rpsls/* +'cog: sayurl': + - sayurl/* +'cog: scp': + - scp/* +'cog: stealemoji': + - stealemoji/* +'cog: timerole': + - timerole/* +'cog: tts': + - tts/* +'cog: unicode': + - unicode/* +'cog: werewolf': + - werewolf \ No newline at end of file diff --git a/.github/workflows/black_check.yml b/.github/workflows/black_check.yml new file mode 100644 index 0000000..5350f98 --- /dev/null +++ b/.github/workflows/black_check.yml @@ -0,0 +1,20 @@ +# GitHub Action that uses Black to reformat the Python code in an incoming pull request. +# If all Python code in the pull request is compliant with Black then this Action does nothing. +# Othewrwise, Black is run and its changes are committed back to the incoming pull request. +# https://github.com/cclauss/autoblack + +name: black +on: [pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: '3.8' + - name: Install Black + run: pip install --upgrade --no-cache-dir black + - name: Run black --check . + run: black --check --diff -l 99 . \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..65e6640 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,19 @@ +# This workflow will triage pull requests and apply a label based on the +# paths that are modified in the pull request. +# +# To use this workflow, you will need to set up a .github/labeler.yml +# file with configuration. For more information, see: +# https://github.com/actions/labeler + +name: Labeler +on: [pull_request] + +jobs: + label: + + runs-on: ubuntu-latest + + steps: + - uses: actions/labeler@2.2.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/audiotrivia/audiosession.py b/audiotrivia/audiosession.py index 780d4b9..1f3297b 100644 --- a/audiotrivia/audiosession.py +++ b/audiotrivia/audiosession.py @@ -1,21 +1,25 @@ """Module to manage audio trivia sessions.""" import asyncio +import logging -import lavalink from redbot.cogs.trivia import TriviaSession +from redbot.cogs.trivia.session import _parse_answers +from redbot.core.utils.chat_formatting import bold + +log = logging.getLogger("red.fox_v3.audiotrivia.audiosession") class AudioSession(TriviaSession): """Class to run a session of audio trivia""" - def __init__(self, ctx, question_list: dict, settings: dict, player: lavalink.Player): + def __init__(self, ctx, question_list: dict, settings: dict, audio=None): super().__init__(ctx, question_list, settings) - self.player = player + self.audio = audio @classmethod - def start(cls, ctx, question_list, settings, player: lavalink.Player = None): - session = cls(ctx, question_list, settings, player) + def start(cls, ctx, question_list, settings, audio=None): + session = cls(ctx, question_list, settings, audio) loop = ctx.bot.loop session._task = loop.create_task(session.run()) return session @@ -23,52 +27,95 @@ class AudioSession(TriviaSession): async def run(self): """Run the audio trivia session. - In order for the trivia session to be stopped correctly, this should - only be called internally by `TriviaSession.start`. - """ + In order for the trivia session to be stopped correctly, this should + only be called internally by `TriviaSession.start`. + """ await self._send_startup_msg() max_score = self.settings["max_score"] delay = self.settings["delay"] + audio_delay = self.settings["audio_delay"] timeout = self.settings["timeout"] - for question, answers in self._iter_questions(): + if self.audio is not None: + import lavalink + + player = lavalink.get_player(self.ctx.guild.id) + player.store("channel", self.ctx.channel.id) # What's this for? I dunno + await self.audio.set_player_settings(self.ctx) + else: + lavalink = None + player = False + + for question, answers, audio_url in self._iter_questions(): async with self.ctx.typing(): await asyncio.sleep(3) self.count += 1 - await self.player.stop() - - msg = "**Question number {}!**\n\nName this audio!".format(self.count) - await self.ctx.send(msg) - # print("Audio question: {}".format(question)) - - # await self.ctx.invoke(self.audio.play(ctx=self.ctx, query=question)) - # ctx_copy = copy(self.ctx) + msg = bold(f"Question number {self.count}!") + f"\n\n{question}" + if player: + await player.stop() + if audio_url: + if not player: + log.debug("Got an audio question in a non-audio trivia session") + continue - # await self.ctx.invoke(self.player.play, query=question) - query = question.strip("<>") - tracks = await self.player.get_tracks(query) - seconds = tracks[0].length / 1000 + load_result = await player.load_tracks(audio_url) + if ( + load_result.has_error + or load_result.load_type != lavalink.enums.LoadType.TRACK_LOADED + ): + await self.ctx.maybe_send_embed( + "Audio Track has an error, skipping. See logs for details" + ) + log.info(f"Track has error: {load_result.exception_message}") + continue + tracks = load_result.tracks + track = tracks[0] + seconds = track.length / 1000 + track.uri = "" # Hide the info from `now` + if self.settings["repeat"] and seconds < audio_delay: + # Append it until it's longer than the delay + tot_length = seconds + 0 + while tot_length < audio_delay: + player.add(self.ctx.author, track) + tot_length += seconds + else: + player.add(self.ctx.author, track) - if self.settings["repeat"] and seconds < delay: - tot_length = seconds + 0 - while tot_length < delay: - self.player.add(self.ctx.author, tracks[0]) - tot_length += seconds - else: - self.player.add(self.ctx.author, tracks[0]) - - if not self.player.current: - await self.player.play() + if not player.current: + await player.play() + await self.ctx.maybe_send_embed(msg) + log.debug(f"Audio question: {question}") - continue_ = await self.wait_for_answer(answers, delay, timeout) + continue_ = await self.wait_for_answer( + answers, audio_delay if audio_url else delay, timeout + ) if continue_ is False: break if any(score >= max_score for score in self.scores.values()): await self.end_game() break else: - await self.ctx.send("There are no more questions!") + await self.ctx.maybe_send_embed("There are no more questions!") await self.end_game() async def end_game(self): await super().end_game() - await self.player.disconnect() + if self.audio is not None: + await self.ctx.invoke(self.audio.command_disconnect) + + def _iter_questions(self): + """Iterate over questions and answers for this session. + + Yields + ------ + `tuple` + A tuple containing the question (`str`) and the answers (`tuple` of + `str`). + + """ + for question, q_data in self.question_list: + answers = _parse_answers(q_data["answers"]) + _audio = q_data["audio"] + if _audio: + yield _audio, answers, question.strip("<>") + else: + yield question, answers, _audio diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py index c0c88fd..9617f32 100644 --- a/audiotrivia/audiotrivia.py +++ b/audiotrivia/audiotrivia.py @@ -1,21 +1,22 @@ import datetime +import logging import pathlib -from typing import List +from typing import List, Optional +import discord import lavalink import yaml from redbot.cogs.audio import Audio -from redbot.cogs.trivia import LOG -from redbot.cogs.trivia.trivia import InvalidListError, Trivia +from redbot.cogs.trivia.trivia import InvalidListError, Trivia, get_core_lists from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.data_manager import cog_data_path -from redbot.core.utils.chat_formatting import box +from redbot.core.utils.chat_formatting import bold, box from .audiosession import AudioSession -# from redbot.cogs.audio.utils import userlimit +log = logging.getLogger("red.fox_v3.audiotrivia") class AudioTrivia(Trivia): @@ -27,12 +28,11 @@ class AudioTrivia(Trivia): def __init__(self, bot: Red): super().__init__() self.bot = bot - self.audio = None self.audioconf = Config.get_conf( self, identifier=651171001051118411410511810597, force_registration=True ) - self.audioconf.register_guild(delay=30.0, repeat=True) + self.audioconf.register_guild(audio_delay=30.0, repeat=True) @commands.group() @commands.guild_only() @@ -43,122 +43,112 @@ class AudioTrivia(Trivia): settings_dict = await audioset.all() msg = box( "**Audio settings**\n" - "Answer time limit: {delay} seconds\n" + "Answer time limit: {audio_delay} seconds\n" "Repeat Short Audio: {repeat}" "".format(**settings_dict), lang="py", ) await ctx.send(msg) - @atriviaset.command(name="delay") - async def atriviaset_delay(self, ctx: commands.Context, seconds: float): + @atriviaset.command(name="timelimit") + async def atriviaset_timelimit(self, ctx: commands.Context, seconds: float): """Set the maximum seconds permitted to answer a question.""" if seconds < 4.0: await ctx.send("Must be at least 4 seconds.") return settings = self.audioconf.guild(ctx.guild) - await settings.delay.set(seconds) - await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds)) + await settings.audo_delay.set(seconds) + await ctx.maybe_send_embed(f"Done. Maximum seconds to answer set to {seconds}.") @atriviaset.command(name="repeat") async def atriviaset_repeat(self, ctx: commands.Context, true_or_false: bool): """Set whether or not short audio will be repeated""" settings = self.audioconf.guild(ctx.guild) await settings.repeat.set(true_or_false) - await ctx.send("Done. Repeating short audio is now set to {}.".format(true_or_false)) + await ctx.maybe_send_embed(f"Done. Repeating short audio is now set to {true_or_false}.") @commands.group(invoke_without_command=True) @commands.guild_only() async def audiotrivia(self, ctx: commands.Context, *categories: str): - """Start trivia session on the specified category. + """Start trivia session on the specified category or categories. + Includes Audio categories. You may list multiple categories, in which case the trivia will involve questions from all of them. """ if not categories and ctx.invoked_subcommand is None: await ctx.send_help() return - - if self.audio is None: - self.audio: Audio = self.bot.get_cog("Audio") - - if self.audio is None: - await ctx.send("Audio is not loaded. Load it and try again") - return - categories = [c.lower() for c in categories] session = self._get_trivia_session(ctx.channel) if session is not None: - await ctx.send("There is already an ongoing trivia session in this channel.") - return - status = await self.audio.config.status() - notify = await self.audio.config.guild(ctx.guild).notify() - - if status: - await ctx.send( - f"It is recommended to disable audio status with `{ctx.prefix}audioset status`" + await ctx.maybe_send_embed( + "There is already an ongoing trivia session in this channel." ) - - if notify: - await ctx.send( - f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`" - ) - - if not self.audio._player_check(ctx): - try: - if not ctx.author.voice.channel.permissions_for( - ctx.me - ).connect or self.audio.is_vc_full(ctx.author.voice.channel): - return await ctx.send("I don't have permission to connect to your channel.") - await lavalink.connect(ctx.author.voice.channel) - lavaplayer = lavalink.get_player(ctx.guild.id) - lavaplayer.store("connect", datetime.datetime.utcnow()) - except AttributeError: - return await ctx.send("Connect to a voice channel first.") - - lavaplayer = lavalink.get_player(ctx.guild.id) - lavaplayer.store("channel", ctx.channel.id) # What's this for? I dunno - - await self.audio.set_player_settings(ctx) - - if not ctx.author.voice or ctx.author.voice.channel != lavaplayer.channel: - return await ctx.send( - "You must be in the voice channel to use the audiotrivia command." - ) - + return trivia_dict = {} authors = [] + any_audio = False for category in reversed(categories): # We reverse the categories so that the first list's config takes # priority over the others. try: dict_ = self.get_audio_list(category) except FileNotFoundError: - await ctx.send( - "Invalid category `{0}`. See `{1}audiotrivia list`" + await ctx.maybe_send_embed( + f"Invalid category `{category}`. See `{ctx.prefix}audiotrivia list`" " for a list of trivia categories." - "".format(category, ctx.prefix) ) except InvalidListError: - await ctx.send( + await ctx.maybe_send_embed( "There was an error parsing the trivia list for" - " the `{}` category. It may be formatted" - " incorrectly.".format(category) + f" the `{category}` category. It may be formatted" + " incorrectly." ) else: - trivia_dict.update(dict_) - authors.append(trivia_dict.pop("AUTHOR", None)) + is_audio = dict_.pop("AUDIO", False) + authors.append(dict_.pop("AUTHOR", None)) + trivia_dict.update( + {_q: {"audio": is_audio, "answers": _a} for _q, _a in dict_.items()} + ) + any_audio = any_audio or is_audio continue return if not trivia_dict: - await ctx.send( + await ctx.maybe_send_embed( "The trivia list was parsed successfully, however it appears to be empty!" ) return + if not any_audio: + audio = None + else: + audio: Optional["Audio"] = self.bot.get_cog("Audio") + if audio is None: + await ctx.send("Audio lists were parsed but Audio is not loaded!") + return + status = await audio.config.status() + notify = await audio.config.guild(ctx.guild).notify() + + if status: + await ctx.maybe_send_embed( + f"It is recommended to disable audio status with `{ctx.prefix}audioset status`" + ) + + if notify: + await ctx.maybe_send_embed( + f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`" + ) + + failed = await ctx.invoke(audio.command_summon) + if failed: + return + lavaplayer = lavalink.get_player(ctx.guild.id) + lavaplayer.store("channel", ctx.channel.id) # What's this for? I dunno + settings = await self.config.guild(ctx.guild).all() audiosettings = await self.audioconf.guild(ctx.guild).all() - config = trivia_dict.pop("CONFIG", None) + config = trivia_dict.pop("CONFIG", {"answer": None})["answer"] if config and settings["allow_override"]: settings.update(config) settings["lists"] = dict(zip(categories, reversed(authors))) @@ -166,22 +156,33 @@ class AudioTrivia(Trivia): # Delay in audiosettings overwrites delay in settings combined_settings = {**settings, **audiosettings} session = AudioSession.start( - ctx=ctx, question_list=trivia_dict, settings=combined_settings, player=lavaplayer, + ctx, + trivia_dict, + combined_settings, + audio, ) self.trivia_sessions.append(session) - LOG.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id) + log.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id) @audiotrivia.command(name="list") @commands.guild_only() async def audiotrivia_list(self, ctx: commands.Context): - """List available trivia categories.""" - lists = set(p.stem for p in self._audio_lists()) - - msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists)))) - if len(msg) > 1000: - await ctx.author.send(msg) - return - await ctx.send(msg) + """List available trivia including audio categories.""" + lists = set(p.stem for p in self._all_audio_lists()) + if await ctx.embed_requested(): + await ctx.send( + embed=discord.Embed( + title="Available trivia lists", + colour=await ctx.embed_colour(), + description=", ".join(sorted(lists)), + ) + ) + else: + msg = box(bold("Available trivia lists") + "\n\n" + ", ".join(sorted(lists))) + if len(msg) > 1000: + await ctx.author.send(msg) + else: + await ctx.send(msg) def get_audio_list(self, category: str) -> dict: """Get the audiotrivia list corresponding to the given category. @@ -198,7 +199,7 @@ class AudioTrivia(Trivia): """ try: - path = next(p for p in self._audio_lists() if p.stem == category) + path = next(p for p in self._all_audio_lists() if p.stem == category) except StopIteration: raise FileNotFoundError("Could not find the `{}` category.".format(category)) @@ -210,13 +211,15 @@ class AudioTrivia(Trivia): else: return dict_ - def _audio_lists(self) -> List[pathlib.Path]: + def _all_audio_lists(self) -> List[pathlib.Path]: + # Custom trivia lists uploaded with audiotrivia. Not necessarily audio lists personal_lists = [p.resolve() for p in cog_data_path(self).glob("*.yaml")] - return personal_lists + get_core_lists() + # Add to that custom lists uploaded with trivia and core lists + return personal_lists + get_core_audio_lists() + self._all_lists() -def get_core_lists() -> List[pathlib.Path]: +def get_core_audio_lists() -> List[pathlib.Path]: """Return a list of paths for all trivia lists packaged with the bot.""" core_lists_path = pathlib.Path(__file__).parent.resolve() / "data/lists" return list(core_lists_path.glob("*.yaml")) diff --git a/audiotrivia/data/lists/anime.yaml b/audiotrivia/data/lists/audioanime.yaml similarity index 99% rename from audiotrivia/data/lists/anime.yaml rename to audiotrivia/data/lists/audioanime.yaml index 7a27a0e..8aa518d 100644 --- a/audiotrivia/data/lists/anime.yaml +++ b/audiotrivia/data/lists/audioanime.yaml @@ -1,4 +1,5 @@ AUTHOR: Plab +AUDIO: "[Audio] Identify this Anime!" https://www.youtube.com/watch?v=2uq34TeWEdQ: - 'Hagane no Renkinjutsushi (2009)' - '(2009) الخيميائي المعدني الكامل' diff --git a/audiotrivia/data/lists/nhlgoalhorns.yaml b/audiotrivia/data/lists/audionhlgoalhorns.yaml similarity index 97% rename from audiotrivia/data/lists/nhlgoalhorns.yaml rename to audiotrivia/data/lists/audionhlgoalhorns.yaml index 689f478..9e86313 100644 --- a/audiotrivia/data/lists/nhlgoalhorns.yaml +++ b/audiotrivia/data/lists/audionhlgoalhorns.yaml @@ -1,4 +1,5 @@ AUTHOR: Lazar +AUDIO: "[Audio] Identify this NHL Team by their goal horn" https://youtu.be/6OejNXrGkK0: - Anaheim Ducks - Anaheim diff --git a/audiotrivia/data/lists/audiovideogames.yaml b/audiotrivia/data/lists/audiovideogames.yaml new file mode 100644 index 0000000..d7f594c --- /dev/null +++ b/audiotrivia/data/lists/audiovideogames.yaml @@ -0,0 +1,1763 @@ +AUTHOR: Bobloy +AUDIO: "[Audio] Identify this video game" +https://www.youtube.com/watch?v=GBPbJyxqHV0: +- Super Mario 64 +https://www.youtube.com/watch?v=0jXTBAGv9ZQ: +- Halo +https://www.youtube.com/watch?v=ZksNhHyEhE0: +- Sonic Generations +https://www.youtube.com/watch?v=4qJ-xEZhGms: +- The Legend of Zelda A Link to the Past +- A Link to the Past +https://www.youtube.com/watch?v=0J5gN1DwNDE: +- The Elder Scrolls 4 Oblivion +- Elder Scrolls 4 +- Oblivion +https://www.youtube.com/watch?v=VTsD2FjmLsw: +- Mass Effect 2 +https://www.youtube.com/watch?v=yYTY9EkFSRU: +- Mortal Kombat Deception +https://www.youtube.com/watch?v=uhscMsBhNhw: +- Super Mario Bros +https://www.youtube.com/watch?v=Jk4P10nsq4c: +- Kingdom Hearts +https://www.youtube.com/watch?v=qDnaIfiH37w: +- Super Smash Bros for Wii U +- Super Smash Bros Wii U +https://www.youtube.com/watch?v=-8wo0KBQ3oI: +- Doom +https://www.youtube.com/watch?v=jLJLyneZGKc: +- Super Street Fighter 2 +https://www.youtube.com/watch?v=T5ASJvTgpJo: +- Call Of Duty Black Ops +https://www.youtube.com/watch?v=Gb33Qnbw520: +- Super Mario Land +https://www.youtube.com/watch?v=omvdFcr0z08: +- Mortal Kombat +https://www.youtube.com/watch?v=W4VTq0sa9yg: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=7Lj1aw4J8ZA: +- Kirby's Dream Land +- Kirbys Dream Land +https://www.youtube.com/watch?v=O5FsjHRIVrc: +- Overwatch +https://www.youtube.com/watch?v=kSA4U4jNq4E: +- Hitman +https://www.youtube.com/watch?v=kzvZE4BY0hY: +- Fallout 4 +https://www.youtube.com/watch?v=5vYsVk23oxA: +- Super Metroid +https://www.youtube.com/watch?v=TVEyGntyOZQ: +- Sonic 2 +https://www.youtube.com/watch?v=EWbfixMWg4g: +- Super Smash Bros +https://www.youtube.com/watch?v=Kxp1qIYy2jQ: +- Super Smash Bros Melee +https://www.youtube.com/watch?v=zeKE0NHUtUw: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=eWSU8YOa3jU: +- Super Smash Bros 4 +https://www.youtube.com/watch?v=oC83pBZ0Z2I: +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=b8JbXCe3NJk: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=O9FeFkMyBIY: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=xtJcoZ9E9ZQ: +- WOW +- World of Warcraft +https://www.youtube.com/watch?v=oDo3wlDretk: +- Street Fighter 2 +https://www.youtube.com/watch?v=l2qD1h9mGF0: +- Sonic Unleashed +https://www.youtube.com/watch?v=Oa2TCaE4MGk: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=WJRoRt155mA: +- Mega Man 2 +https://www.youtube.com/watch?v=N6noiPFB3io: +- Minecraft +https://www.youtube.com/watch?v=XcHDVwL-1W8: +- Call Of Duty WWII +- Call of Duty WW2 +https://www.youtube.com/watch?v=Cgh4CFzKCps: +- Cuphead +https://www.youtube.com/watch?v=mEgE5z6Sl7Y: +- Assassin's Creed 2 +https://www.youtube.com/watch?v=KmmNYTg-wSA: +- Donkey Kong +https://www.youtube.com/watch?v=dVPF-lboK14: +- God of War 3 +https://www.youtube.com/watch?v=vf2yKCCRE50: +- New Super Mario Bros Wii +https://www.youtube.com/watch?v=BqPgeWf8vNM: +- Minecraft +https://www.youtube.com/watch?v=23hJeaLotEw: +- The Legend of Zelda Skyward Sword +- Skyward Sword +https://www.youtube.com/watch?v=hMa4hZQbrms: +- Undertale +https://www.youtube.com/watch?v=LWVjNNKDYz8: +- Ryse Son of Rome +https://www.youtube.com/watch?v=EAwWPadFsOA: +- Mortal Kombat +https://www.youtube.com/watch?v=YEZhF_98cIc: +- Uncharted 4 +https://www.youtube.com/watch?v=LRKfd5EKBtI: +- Destiny +https://www.youtube.com/watch?v=TgjOBqD_ljk: +- Super Mario Galaxy +https://www.youtube.com/watch?v=cPWBG6_jn4Y: +- The Legend of Zelda Breath of the Wild +- Breath of the Wild +- Zelda Botw +https://www.youtube.com/watch?v=M-U3sVX2G3w: +- Metroid +https://www.youtube.com/watch?v=b62RgDBmly8: +- Fire Emblem +https://www.youtube.com/watch?v=d2tdSiQAF20: +- Sonic the Hedgehog 2006 +- Sonic the Hedgehog 06 +- Sonic 06 +- Sonic 2006 +https://www.youtube.com/watch?v=qI-Takf76RY: +- Mortal Kombat +https://www.youtube.com/watch?v=GFDvcR7Ozbg: +- Kingdom hearts Birth By Sleep +https://www.youtube.com/watch?v=F-QCjTBR0Pg: +- Halo 2 Anniversary +https://www.youtube.com/watch?v=c0SuIMUoShI: +- Super Mario Bros +https://www.youtube.com/watch?v=LnSp9rgfel8: +- Borderlands +https://www.youtube.com/watch?v=cpXXfCzSmJQ: +- Red Dead Redemption +https://www.youtube.com/watch?v=CMfx3MT8ge0: +- The Elder Scrolls 5 Skyrim +- Elder Scrolls 5 +- Skyrim +https://www.youtube.com/watch?v=LtmZJwbLjb0: +- Super Mario 3D World +https://www.youtube.com/watch?v=HGXo7LExG6o: +- Tetris +https://www.youtube.com/watch?v=aUS36INQoUw: +- Fire Emblem Shadow Dragon and the Blade of Light +- Shadow Dragon and the Blade of Light +https://www.youtube.com/watch?v=s7RRgF5Ve_E: +- Undertale +https://www.youtube.com/watch?v=MuVAaU63K7k: +- Sonic Heroes +https://www.youtube.com/watch?v=7lq5rr0RTeU: +- The Legend of Zelda Twilight Princess +- Twilight Princess +https://www.youtube.com/watch?v=S62IDJBdGWA: +- Mortal Kombat X +- Mortal Komba 10 +https://www.youtube.com/watch?v=_50vhftK66I: +- Super Mario World +https://www.youtube.com/watch?v=cZjSrW4p7r8: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=e-nXonpnFHI: +- Monster Hunter Frontier G6 +https://www.youtube.com/watch?v=_dWMv2hfck0: +- Halo 2 +https://www.youtube.com/watch?v=SV0IF4XVBlE: +- Battlefield 1 +https://www.youtube.com/watch?v=rqIRZHPuiqE: +- Middle Earth Shadow of War +- Shadow of War +https://www.youtube.com/watch?v=qQGh4dy1cCA: +- Killzone 3 +https://www.youtube.com/watch?v=e9r5hx47kxM: +- Super Mario Odyssey +https://www.youtube.com/watch?v=SXKrsJZWqK0: +- Batman Arkham City +https://www.youtube.com/watch?v=Wbk8WeQU0rc: +- Far Cry +https://www.youtube.com/watch?v=4i8qAZOu5-g: +- Crash Bandicoot +https://www.youtube.com/watch?v=qEpaZR2Dvlg: +- Super Mario World +https://www.youtube.com/watch?v=-uEc8_dcYUc: +- Battlefield 4 +https://www.youtube.com/watch?v=jqE8M2ZnFL8: +- Grand Theft Auto 4 +https://www.youtube.com/watch?v=_kTBbTSjZpI: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=QG77HTdreh0: +- Kid Icarus Uprising +https://www.youtube.com/watch?v=Z6NaZrPQGfY: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=GkQUDi0pMAE: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=o78T9-I4OGA: +- The Legend of Zelda The Wind Waker +- Zelda Wind Waker +- Zelda The Wind Waker +https://www.youtube.com/watch?v=dLDWYSQkUbE: +- Halo Combat Evolved +https://www.youtube.com/watch?v=mp9mzmq5Oas: +- Tokyo Mirage Sessions FE +https://www.youtube.com/watch?v=T5_vKsROSU0: +- Halo 2 +https://www.youtube.com/watch?v=MC0hV3dea9g: +- Sonic R +https://www.youtube.com/watch?v=dHkyqHlQ_is: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=VfGeeZwv4Os: +- Diablo III +- Diablo 3 +https://www.youtube.com/watch?v=E3tkgU0pQmQ: +- Super Mario Sunshine +https://www.youtube.com/watch?v=53aDI5K49F4: +- Resident Evil 2 +https://www.youtube.com/watch?v=f43swtBDkuo: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=cn144yWiZF4: +- Silent Hill 2 +https://www.youtube.com/watch?v=JSR5rROsvL0: +- Super Mario 3D Land +https://www.youtube.com/watch?v=CXxEUEK2_cs: +- Forza Motorsport 6 +https://www.youtube.com/watch?v=r8sB8N44eBE: +- Sonic Adventure (Theme of Knuckles) +- Sonic Adventure +https://www.youtube.com/watch?v=jrB6DLpIDaA: +- Rocket League +https://www.youtube.com/watch?v=6vY7S6iwwlQ: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=ndaJphlTPY8: +- Gears of War +https://www.youtube.com/watch?v=j9JWZ0mlX94: +- Luigi's Mansion +- Luigis Mansion +https://www.youtube.com/watch?v=zh4r_sXHyjc: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=CvERHiTfx9w: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=1TxEdJaZkY4: +- Dead by Daylight +https://www.youtube.com/watch?v=rxkiOLs06Z8: +- Injustice 2 +https://www.youtube.com/watch?v=bG0xho_yoaU: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=xsyPHkeyz6o: +- Super Mario Galaxy 2 +https://www.youtube.com/watch?v=MdQvcgBmexw: +- Battlefront 2 +https://www.youtube.com/watch?v=6lR4nexN7RE: +- DOTA 2 +https://www.youtube.com/watch?v=m0NhFUw8mpI: +- Super Smash Bros Wii U +https://www.youtube.com/watch?v=E9s1ltPGQOo: +- Mii Channel +https://www.youtube.com/watch?v=QSBco8kVuZM: +- Fallout 3 +https://www.youtube.com/watch?v=Tz5Ld2STm2M: +- Ghost Recon +https://www.youtube.com/watch?v=oY9m2sHQwLs: +- Sonic R +https://www.youtube.com/watch?v=NF8kT_6giu8: +- Halo 2 +https://www.youtube.com/watch?v=EK3q3Jb3TCQ: +- The Elder Scrolls 5 Skyrim +- Elder Scrolls 5 +- Skyrim +https://www.youtube.com/watch?v=h4fOIVlEOIY: +- Assassin’s Creed Origins +- Assasins Creed Origins +https://www.youtube.com/watch?v=mziw3FQkZYg: +- Metroid Prime +https://www.youtube.com/watch?v=CXdNLpOrwFQ: +- Pikmin 3 +https://www.youtube.com/watch?v=RZFhVs6OGAc: +- Wii Fit +https://www.youtube.com/watch?v=UW-7Em3BYiA: +- Star Fox 64 +https://www.youtube.com/watch?v=9k-XrIGobsA: +- Super Mario 3D World +https://www.youtube.com/watch?v=kohY_vaMp0c: +- Bayonetta +https://www.youtube.com/watch?v=4roOuDyLkNo: +- Fallout 3 +https://www.youtube.com/watch?v=4wzRjZh3EkM: +- DOTA 2 +https://www.youtube.com/watch?v=OiwLQAA_V8E: +- Fire Emblem Fates +https://www.youtube.com/watch?v=UigzN-4JR14: +- Kingdom Hearts +https://www.youtube.com/watch?v=bIoPaeigMJw: +- Halo 2 +https://www.youtube.com/watch?v=JIE94pn4iJs: +- Killer Instinct +https://www.youtube.com/watch?v=Y9Jt52Q6GD4: +- Deadpool +https://www.youtube.com/watch?v=uNBzfe3TAEs: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=PZVNi6L7g54: +- Mario Party 8 +https://www.youtube.com/watch?v=0uom8gJxe_8: +- Overwatch +https://www.youtube.com/watch?v=OCH6nQYflwY: +- Kirby's Dream Land +- Kirbys Dream Land +https://www.youtube.com/watch?v=PDURJZp3Sv4: +- World of Warcraft +- WoW +https://www.youtube.com/watch?v=3DYqiG1VmMQ: +- Diablo 2 +https://www.youtube.com/watch?v=xWSiztHV0us: +- Mortal Kombat Armageddon +https://www.youtube.com/watch?v=D6fkkVMsrAA: +- Super Smash Bros 3DS +- Mr Game and Watch +https://www.youtube.com/watch?v=tz82xbLvK_k: +- Undertale +https://www.youtube.com/watch?v=EV6E13xODyA: +- Bayonetta +https://www.youtube.com/watch?v=bq_jS6o3OoY: +- Super Mario 64 +https://www.youtube.com/watch?v=M3CSWLFL5u8: +- The Legend of Zelda The Wind Waker +- Zelda Wind Waker +- Zelda The Wind Waker +https://www.youtube.com/watch?v=PnzjqTvHJto: +- Destiny +https://www.youtube.com/watch?v=JntQ1X46qgg: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=M9Ki3br3Qec: +- Deadrising +https://www.youtube.com/watch?v=bwaFADxmoeo: +- Splatoon +https://www.youtube.com/watch?v=Tc0fDGVQtJk: +- Metal Gear Solid +https://www.youtube.com/watch?v=ZvwQNlNIcr8: +- Forza Horizon 3 +https://www.youtube.com/watch?v=B7eRmpqtL40: +- Call of Duty Ghosts +https://www.youtube.com/watch?v=qURei6svd90: +- Doom II +- Doom 2 +https://www.youtube.com/watch?v=U1sYDWzhEog: +- Mario Party 8 +https://www.youtube.com/watch?v=aKKFjNVZkHU: +- Marvel vs Capcom 3 +https://www.youtube.com/watch?v=PDM2qukzKwg: +- Team Fortress 2 +https://www.youtube.com/watch?v=0nlJuwO0GDs: +- League of Legends +https://www.youtube.com/watch?v=UuL8nnyzf14: +- Fire Emblem The Sacred Stones +https://www.youtube.com/watch?v=ARFeJ3z6wes: +- Mario Party DS +https://www.youtube.com/watch?v=pNB4Cy4mftI: +- Sonic and the Black Knight +https://www.youtube.com/watch?v=GWba_XNUxtA: +- Quantum Break +https://www.youtube.com/watch?v=7XjrTV6M84c: +- Dead Rising 3 +https://www.youtube.com/watch?v=LjYWmShOoA4: +- Overwatch +https://www.youtube.com/watch?v=1pga6OLyrAg: +- Borderlands +https://www.youtube.com/watch?v=1OEyASR5C5U: +- Uncharted +https://www.youtube.com/watch?v=K-pJ1xMF7QE: +- League of Legends +https://www.youtube.com/watch?v=9q_MEnn5w0I: +- Minecraft +https://www.youtube.com/watch?v=5WduvI0CZ4c: +- Mega Man +https://www.youtube.com/watch?v=mB7veHQPF6M: +- Halo 2 +https://www.youtube.com/watch?v=mtxlFKP0OgY: +- Call of Duty Modern Warfare 2 +- Modern Warfare 2 +https://www.youtube.com/watch?v=2Jmty_NiaXc: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=6YQm2M-v4l0: +- Metal Gear +https://www.youtube.com/watch?v=r9spthA_nAA: +- Earthbound +https://www.youtube.com/watch?v=M_hUepNd52Q: +- Fallout New Vegas +https://www.youtube.com/watch?v=_X2A7qLDuLQ: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=8SM_39H-ztc: +- Kingdom Hearts +https://www.youtube.com/watch?v=Vj3dgToY_Fg: +- Ratchet and clank +https://www.youtube.com/watch?v=jgn3GSiYtEc: +- Middle earth Shadow of Mordor +- Shadow of Mordor +https://www.youtube.com/watch?v=sZ1f6BEkn_g: +- Paper Mario The Thousand-Year Door +- Paper Mario The Thousand Year Door +https://www.youtube.com/watch?v=T7AkuzZOSWM: +- DOTA 2 +https://www.youtube.com/watch?v=ixR9kOAR3ZY: +- Dragon Quest IX Sentinels of the Starry Skies +- Dragon Quest 9 +https://www.youtube.com/watch?v=WiQ1O5of0u4: +- Street Fighter II +- Street Fighter 2 +https://www.youtube.com/watch?v=0hEYvdMoF2g: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=mK4mP7grUWY: +- Final Fantasy IX +- Final Fantasy 9 +https://www.youtube.com/watch?v=YzfEjZV46wM: +- Forza Motorsport +https://www.youtube.com/watch?v=vX7G4Qq6Li0: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=95jDylAOsBQ: +- Assassins Creed +https://www.youtube.com/watch?v=EjazC45Qkww: +- Castlevania II Simon's Quest +- Castlevania 2 +https://www.youtube.com/watch?v=w09rc1VfWRQ: +- Forza Motorsport 4 +https://www.youtube.com/watch?v=Nc9w4QoSdfk: +- Tomb Raider +https://www.youtube.com/watch?v=VD9XmYnml_M: +- GTA V +- GTA 5 +- Grand Theft Auto 5 +https://www.youtube.com/watch?v=D5Z95cH7iDA: +- Ghost Recon Island Thunder +https://www.youtube.com/watch?v=o9Pu92St5O0: +- Metal Gear Solid 4 +https://www.youtube.com/watch?v=Bm2U7fqtSak: +- Ninja Gaiden +https://www.youtube.com/watch?v=NfCwWu8z8zE: +- Resident Evil +https://www.youtube.com/watch?v=zWHMj6ccSwA: +- Nintendo Land +https://www.youtube.com/watch?v=L8Vbg-1kPvg: +- Far Cry 5 +https://www.youtube.com/watch?v=hOju2ThS--M: +- Sonic the Hedgehog 2006 +- Sonic the Hedgehog 06 +- Sonic 06 +- Sonic 2006 +https://www.youtube.com/watch?v=EYW7-hNXZlM: +- Sonic CD +https://www.youtube.com/watch?v=ijEOqab3StE: +- Shadow the Hedgehog +https://www.youtube.com/watch?v=nJD-Ufi1jGk: +- The Elder Scrolls 3 Morrowind +- Elder Scrolls 3 +- Morrowind +https://www.youtube.com/watch?v=Iy4iQvJo24U: +- Crysis 2 +https://www.youtube.com/watch?v=o_ayLF9vdls: +- Dragon Age Origins +https://www.youtube.com/watch?v=LFcH84oNU6s: +- Skies of Arcadia +https://www.youtube.com/watch?v=9wMjq58Fjvo: +- Castle Crashers +https://www.youtube.com/watch?v=WA2WjP6sgrc: +- Donkey Kong Country Tropical Freeze +- Donkey Kong Tropical Freeze +https://www.youtube.com/watch?v=_Uzlm2MaCWw: +- MegaMan Maverick Hunter X +https://www.youtube.com/watch?v=Vm7aNxzWTJk: +- Prince of Persia +https://www.youtube.com/watch?v=viM0-3PXef0: +- The Witcher 3 +- Witcher 3 +https://www.youtube.com/watch?v=TO7UI0WIqVw: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=Y6ljFaKRTrI: +- Portal +https://www.youtube.com/watch?v=8yj-25MOgOM: +- Star Fox Zero +https://www.youtube.com/watch?v=W7rhEKTX-sE: +- Shovel Knight +https://www.youtube.com/watch?v=dTZ8uhJ5hIE: +- Kirby's Epic Yarn Music +- Kirbys Epic Yarn Music +https://www.youtube.com/watch?v=GQZLEegUK74: +- Goldeneye 007 +https://www.youtube.com/watch?v=zz8m1oEkW5k: +- Tetris Blitz +https://www.youtube.com/watch?v=ya3yxTbkh5s: +- Okami +https://www.youtube.com/watch?v=OYc_Vosp9_Y: +- The Legend of Zelda A Link Between Worlds +- Zelda A Link Between Worlds +https://www.youtube.com/watch?v=6LB7LZZGpkw: +- Silent Hill 2 +https://www.youtube.com/watch?v=TLYimAlnxEk: +- Mario Kart 8 +https://www.youtube.com/watch?v=-_Lwf6uaYOs: +- Kid Icarus Uprising +https://www.youtube.com/watch?v=H1etAFiAPYQ: +- Pikmin 3 +https://www.youtube.com/watch?v=jrbN2y5r-vU: +- Starcraft +https://www.youtube.com/watch?v=3o_RwQysgA8: +- The Last of Us +https://www.youtube.com/watch?v=75OlLWtV8gc: +- Pokémon Diamond/Pearl/Platinum +- Pokémon Diamond +- Pokemon Diamond +- Pokémon Pearl +- Pokemon Pearl +- Pokémon Platinum +- Pokemon Platinum +https://www.youtube.com/watch?v=sWTqNGUFF0g: +- Gunman Clive +https://www.youtube.com/watch?v=D4ragdexomw: +- Apollo Justice Ace Attorney +- Ace Attorney Apollo Justice +https://www.youtube.com/watch?v=poxDJHoYxCc: +- Gravity Rush +- Gravity Daze +https://www.youtube.com/watch?v=kwmOSMttZtM: +- Luigi's Mansion Dark Moon +- Luigis Mansion Dark Moon +https://www.youtube.com/watch?v=Ib19evMJJVI: +- PAYDAY The Heist +https://www.youtube.com/watch?v=7jVPwHqXz8A: +- Metroid Fusion +https://www.youtube.com/watch?v=utOa8ZMKwSA: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=2OOYZcVIp8w: +- Super Mario Galaxy +https://www.youtube.com/watch?v=u4jlk2x3Om0: +- Banjo-Kazooie Nuts & Bolts +- Banjo-Kazooie Nuts and Bolts +- Banjo Kazooie Nuts and Bolts +https://www.youtube.com/watch?v=mbsm2YXfAzo: +- Mortal Kombat Deception +https://www.youtube.com/watch?v=WB1I3PV7Ml8: +- Crash Bandicoot +https://www.youtube.com/watch?v=gy0T8nmw5GQ: +- Hitman 2 Silent Assassin +- Hitman 2 +https://www.youtube.com/watch?v=WuUVKOwUEA0: +- Super Mario Bros 3 +https://www.youtube.com/watch?v=6hgK3XJWSOk: +- Startropics +https://www.youtube.com/watch?v=_5iRzoSLWMM: +- The Legend of Zelda A Link to the Past +- A Link to the Past +https://www.youtube.com/watch?v=bV80prS5hBk: +- Silent Hill +https://www.youtube.com/watch?v=RDeJMAsrZMU: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=QEmbOL3AAEs: +- Final Fantasy VI +- Final Fantasy 6 +https://www.youtube.com/watch?v=CoOTDI2KZco: +- Mega Man 3 +https://www.youtube.com/watch?v=Xeloqt4Wkcw: +- Streets Of Rage 2 +https://www.youtube.com/watch?v=wOQ5YyAorrw: +- Pokémon X & Y +- Pokemon X +- Pokemon Y +https://www.youtube.com/watch?v=CU1goWmsTRg: +- Duck Hunt +https://www.youtube.com/watch?v=oJqlpwN9mUM: +- Mario Party 2 +https://www.youtube.com/watch?v=ye78aoUtqOY: +- Pokémon GO +- Pokemon GO +https://www.youtube.com/watch?v=hf_D3pK6LVI: +- Phoenix Write Ace Attorney +https://www.youtube.com/watch?v=7qvudKHKozc: +- Shining Force II +- Shining Force 2 +https://www.youtube.com/watch?v=xPJcOybiCKM: +- Super Mario 64 +https://www.youtube.com/watch?v=IHxFTQYAqfc: +- Halo 3 +https://www.youtube.com/watch?v=0t9TlXAnDDM: +- Call of Juarez Gunslinger +https://www.youtube.com/watch?v=LuZy6A9luhU: +- Far Cry 3 +https://www.youtube.com/watch?v=HcvX3Md4uvM: +- Rainbow Six Vegas 2 +https://www.youtube.com/watch?v=RQYh342H5ec: +- Nintendo 3DS Nintendo Video +- Nintendo Video +https://www.youtube.com/watch?v=EMlQiS372Y0: +- Wii sports resort +https://www.youtube.com/watch?v=OtXHRSRQZvU: +- DK Rap +- Super Smash Bros Melee +- Donkey Kong 64 +https://www.youtube.com/watch?v=fmHEwxYdwcA: +- Mario & Luigi Bowsers Inside Story +- Bowsers Inside Story +- Mario and Luigi Bowsers Inside Story +https://www.youtube.com/watch?v=9cXiGjjLZAI: +- Pikmin 3 +https://www.youtube.com/watch?v=Q47iqNfsA80: +- The Legend of Zelda A Link Between Worlds +- A Link Between Worlds +https://www.youtube.com/watch?v=uQjcAvGTD7M: +- Professor Layton vs. Phoenix Wright Ace Attorney +- Professor Layton vs Phoenix Wright +https://www.youtube.com/watch?v=tBTb8H6vJe8: +- Donkey Kong 64 +https://www.youtube.com/watch?v=qQFseWcceDs: +- Pilotwings +https://www.youtube.com/watch?v=OVZ7EaqplsI: +- eShop Wii U +- eShop +https://www.youtube.com/watch?v=2hVsp2ncHoA: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=kU36OmpCK_c: +- The Legend of Zelda Skyward Sword +- Skyward Sword +https://www.youtube.com/watch?v=7TCI6rdokD4: +- Street Fighter II +- Street Fighter 2 +https://www.youtube.com/watch?v=-unUysuRmWo: +- Sonic Adventure 2 +https://www.youtube.com/watch?v=QoIE04AqDgI: +- Kirby's Epic Yarn +- Kirbys Epic Yarn +https://www.youtube.com/watch?v=lSaDibas_Sw: +- Call of Duty Black Ops 3 +- Black Ops 3 +https://www.youtube.com/watch?v=HTUq3Ik1GHM: +- Kingdom Hearts II +- Kingdom Hearts 2 +https://www.youtube.com/watch?v=6ylyRyoUyH8: +- One Piece Pirate Warriors +https://www.youtube.com/watch?v=FJd6TMOZuJQ: +- Pac-Man +- Pac Man +https://www.youtube.com/watch?v=mBwmfDh7qtA: +- Dig Dug +https://www.youtube.com/watch?v=XjPF3AwVPM4: +- Finaly Fantasy +https://www.youtube.com/watch?v=KnoXA_EAnjo: +- Sonic Heroes +https://www.youtube.com/watch?v=XHpXEfa1kzk: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=mld2IxYXgt0: +- Super Robot Wars K +- SRW K +https://www.youtube.com/watch?v=Xi31NAktYDw: +- BioShock 2 +https://www.youtube.com/watch?v=YqyhezUoDj4: +- Call of Duty World at War +- World at War +https://www.youtube.com/watch?v=hOB4q01VCVg: +- Battlefield 1943 +https://www.youtube.com/watch?v=_cZoUYndFHo: +- Cuphead +https://www.youtube.com/watch?v=ojS8lCeUgsg: +- Deadpool +https://www.youtube.com/watch?v=2HTs2n0q-RA: +- Sonic Riders Zero Gravity +https://www.youtube.com/watch?v=yykgQClYlCk: +- TitanFall +https://www.youtube.com/watch?v=I6XMQYkPczI: +- Bravely Default +https://www.youtube.com/watch?v=q_TxCygjNIM: +- Deadrising +https://www.youtube.com/watch?v=2iLl_uusujo: +- STAR WARS The Old Republic +https://www.youtube.com/watch?v=KDyTjQmIWAw: +- Check Mii Out Channel Contest Menu Wii +- Check Mii Out +https://www.youtube.com/watch?v=VhjwXsDJQ2Y: +- Wii Sports +https://www.youtube.com/watch?v=fxMW5__vaDQ: +- Super Mario 64 +https://www.youtube.com/watch?v=4DJzm9cFGkg: +- Puyopuyo Tetris +- Puyo puyo Tetris +https://www.youtube.com/watch?v=G1MfL1oGN0E: +- Call of Duty Modern Warfare 3 +- Modern Warfare 3 +https://www.youtube.com/watch?v=bRLzfoySFWc: +- Assassin's Creed 4 Black Flag +- Assasins Creed 4 +- Assasins Creed Black Flag +https://www.youtube.com/watch?v=30lnA-haJiM: +- XBOX 360 Avatar Editor +- Avatar Editor +https://www.youtube.com/watch?v=czTksCF6X8Y: +- Spider-Man 2 The Game +- Spider Man 2 +- Spider-Man 2 +https://www.youtube.com/watch?v=_FAYOrpr-Fk: +- Wii Sports +https://www.youtube.com/watch?v=UbqoImQZ-EY: +- Touhou Embodiment of Scarlet Devil +- Touhou EoSD +- Embodiment of Scarlet Devil +https://www.youtube.com/watch?v=qIHqpjVxTtY: +- Super Stardust Delta +https://www.youtube.com/watch?v=VU7qTVkjMWU: +- World of Warcraft Wrath of the Lich King +- Wrath of the Lich King +- WoW WotLK +https://www.youtube.com/watch?v=YB2hQicc13Q: +- Super Smash Bros Melee +https://www.youtube.com/watch?v=9NBu_N9paNM: +- Metro Last Light +https://www.youtube.com/watch?v=aJOovInlk-w: +- Assassin's Creed 4 Black Flag +- Assasins Creed 4 +- Assasins Creed Black Flag +https://www.youtube.com/watch?v=6OYeyGi67UI: +- Call of Duty Ghosts +https://www.youtube.com/watch?v=IuLMkfG_Xu0: +- World of Warcraft +- WoW +https://www.youtube.com/watch?v=gR03h48G9rw: +- Destiny +https://www.youtube.com/watch?v=e87BLzzcwqQ: +- Halo Combat Evolved +https://www.youtube.com/watch?v=0_FquzflYYU: +- Luigis Mansion +- Luigi's Mansion +https://www.youtube.com/watch?v=I3XPus2T58o: +- The Legend of Zelda Twilight Princess +- Twilight Princess +https://www.youtube.com/watch?v=pVFYljt05hg: +- Metal Gear Solid 3 +https://www.youtube.com/watch?v=XHXVFBKZ9i8: +- Rhythm Heaven +https://www.youtube.com/watch?v=9hUT8VsLIG4: +- Pac-Man Championship Edition 2 +- Pac Man Championship Edition 2 +- Pac-Man CE 2 +- Pac Man CE 2 +https://www.youtube.com/watch?v=OziIZxI-Am4: +- Super Mario Maker +https://www.youtube.com/watch?v=frWaTmdN1Yk: +- Saga Frontier 2 +https://www.youtube.com/watch?v=r3m8_TDltwg: +- The Simpsons Hit & Run +- The Simpsons Hit and Run +https://www.youtube.com/watch?v=6kZ5TiSl2d8: +- Metal Gear Solid 2 +https://www.youtube.com/watch?v=CvL6fzXOJyY: +- Middle Earth Shadow of War +- Shadow of War +https://www.youtube.com/watch?v=zsxwbxS6LRM: +- Pocky & Rocky +- Pocky and Rocky +https://www.youtube.com/watch?v=YEo8vfvU6t8: +- Mario Golf World Tour +https://www.youtube.com/watch?v=q38jsbJ0afg: +- Marvel Ultimate Alliance 2 +https://www.youtube.com/watch?v=jbppvtGktvA: +- Super Street Fighter IV +- Super Street Fighter 4 +https://www.youtube.com/watch?v=kEdRtzNAh0g: +- Mortal Kombat +https://www.youtube.com/watch?v=SiLykPb_14g: +- Snipperclips +https://www.youtube.com/watch?v=2LhOV_ygYlY: +- Ghostbusters The Videogame +- Ghostbusters +https://www.youtube.com/watch?v=TUA9WwTV10M: +- Back to the Future the Game +- Back to the Future +https://www.youtube.com/watch?v=krISRDj6_2w: +- DSi Shop Theme +- DSi Shop +https://www.youtube.com/watch?v=R5DJENl__ZE: +- The Elder Scrolls 3 Morrowind +- Elder Scrolls 3 +- Morrowind +https://www.youtube.com/watch?v=2MDXwJ7Skik: +- Mortal Kombat 9 +https://www.youtube.com/watch?v=NKkXYxFh9M0: +- Killzone 3 +https://www.youtube.com/watch?v=5rfpGcIWe7E: +- BioShock 2 +https://www.youtube.com/watch?v=euJNO3xzprQ: +- Call of Duty Black Ops 3 +- Black Ops 3 +https://www.youtube.com/watch?v=L8EfpcF9rRc: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=LaM9YRUsDXs: +- Dark Souls +https://www.youtube.com/watch?v=nghTrcPBp3s: +- Yoshi's Story +- Yoshis Story +https://www.youtube.com/watch?v=l7vpszYDFEo: +- Destiny 2 +https://www.youtube.com/watch?v=ukMWZymYV-E: +- Borderlands +https://www.youtube.com/watch?v=ybhjox7LhmY: +- Mario Kart Wii +https://www.youtube.com/watch?v=9vevdlA1mFE: +- Pocky & Rocky 2 +- Pocky and Rocky 2 +https://www.youtube.com/watch?v=sZZgenIEL9o: +- Call of Duty Black Ops 4 +- Black Ops 4 +https://www.youtube.com/watch?v=OTF5gn5S0zs: +- Super Mario Bros Super Smash Bros Brawl +- Super Mario Bros +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=OiWIpzor_Vk: +- Far Cry 5 +https://www.youtube.com/watch?v=h-xih93YIwk: +- Super Mario Bros 3 +https://www.youtube.com/watch?v=aHW0Vn-wRzs: +- Team Sonic Racing +https://www.youtube.com/watch?v=DehK_Y0TUbE: +- Angry Birds +https://www.youtube.com/watch?v=eVKj3u8JUm0: +- Super Mario 64 +https://www.youtube.com/watch?v=vCF_mG9T6-Y: +- Super Smash Bros Ultimate +- Super Smash Bros Melee +https://www.youtube.com/watch?v=o1rXwfuffwk: +- Battle Stadium D.O.N. +- Battle Stadium DON +https://www.youtube.com/watch?v=y_qHuDjE3CQ: +- Undertale +https://www.youtube.com/watch?v=kNK3gZnd3Qc: +- LEGO Marvel Super Heroes +https://www.youtube.com/watch?v=a6t_uyg_pF8: +- Final Fantasy VI +- Final Fantasy 6 +https://www.youtube.com/watch?v=Vc1wzDWFvf8: +- Super Smash Bros Melee +https://www.youtube.com/watch?v=4QR21UOXUTk: +- J Stars Victory Vs +https://www.youtube.com/watch?v=8avMLHvLwRQ: +- Wii Shop Channel +- Wii Shop +https://www.youtube.com/watch?v=d5c4KOopwLs: +- Wii Sports +https://www.youtube.com/watch?v=K5PNe0a04_I: +- Fire Emblem Echoes Shadows of Valentia +- Fire Emblem Echoes +- Shadows of Valentia +https://www.youtube.com/watch?v=8GxAi9b0ZvM: +- Super Mario Odyssey +https://www.youtube.com/watch?v=uTcOC04eEro: +- Yu-Gi-Oh! Duel Links +- Yu-Gi-Oh Duel Links +- YuGiOh Duel Links +- Yu Gi Oh Duel Links +https://www.youtube.com/watch?v=nTSXf2YVNUs: +- Final Fantasy III +- Final Fantasy 3 +https://www.youtube.com/watch?v=96uAHbo2f0o: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=AD0SP68Z2D0: +- Tom Clancys The Division +- The Division +https://www.youtube.com/watch?v=XmK8vhe6_yI: +- Destiny +https://www.youtube.com/watch?v=OM_qluyUGtY: +- Halo 2 +https://www.youtube.com/watch?v=KCQpsPFOEyU: +- Tom Clancys Rainbow Six Siege +- Rainbow Six Siege +- R6 Siege +https://www.youtube.com/watch?v=Rvi6c8toWJM: +- Counter-Strike Global Offensive +- CS GO +- CSGO +- Counter Strike GO +- Counter Strike Global Offensive +https://www.youtube.com/watch?v=WGkBan-hrR0: +- Ninja Gaiden 2 +https://www.youtube.com/watch?v=NPKG8HOyH50: +- Call of Duty Black Ops 4 +- Black Ops 4 +https://www.youtube.com/watch?v=Pva3UHPUHns: +- Luigis Mansion +https://www.youtube.com/watch?v=Gv__pDOd9R4: +- Donkey Kong 64 +https://www.youtube.com/watch?v=IJxIR35cWFg: +- Mario Party 8 +https://www.youtube.com/watch?v=6bWqgqcaQVs: +- Batman Arkham City +https://www.youtube.com/watch?v=FL6BRBzv8Ec: +- Anthem +https://www.youtube.com/watch?v=FG9uTzsqLNc: +- Counter-Strike Global Offensive +- CS GO +- CSGO +- Counter Strike GO +- Counter Strike Global Offensive +https://www.youtube.com/watch?v=-s9hGP8EPC4: +- Crackdown +https://www.youtube.com/watch?v=p7ew-8C3G_M: +- Killzone +https://www.youtube.com/watch?v=HyoZspjzaT8: +- Dragonball Z Budokai Tenkaichi 2 +https://www.youtube.com/watch?v=KveGWSYDxKY: +- Minecraft +https://www.youtube.com/watch?v=rXGdYmigdzw: +- Conker's Bad Fur Day +- Conkers Bad Fur Day +https://www.youtube.com/watch?v=bxJaHn4OcOk: +- Nintendo 3DS Camera Music +- 3DS Camera +https://www.youtube.com/watch?v=YZ3XjVVNagU: +- Undertale +https://www.youtube.com/watch?v=Rdeuye6BfmA: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=rVzkd6B7LCA: +- Halo 3 +https://www.youtube.com/watch?v=IhQ45MHHZGA: +- Anthem +https://www.youtube.com/watch?v=rYpqRmsA94Y: +- Spider-Man PS4 +- Spider Man +https://www.youtube.com/watch?v=dFkuCBF-bf0: +- Sonic Colors +https://www.youtube.com/watch?v=jPfcnzPixBI: +- One Piece World Seeker +https://www.youtube.com/watch?v=KamjTqi4VjI: +- Kirby Air Ride +https://www.youtube.com/watch?v=7kCXNPZscFU: +- Dance Dance Revolution Mario Mix +- DDR Mario Mix +https://www.youtube.com/watch?v=CaXapWQeeeg: +- Spongebob Battle for Bikini Bottom +https://www.youtube.com/watch?v=cSiyDP_B5vY: +- The Witcher 3 Wild Hunt +- Witcher 3 +- Witcher Wild Hunt +https://www.youtube.com/watch?v=bGTWOWeFFw8: +- Marvel vs Capcom 3 +https://www.youtube.com/watch?v=Du0r5FOKbis: +- Bayonetta +https://www.youtube.com/watch?v=07mbmJHeBhg: +- Monopoly +https://www.youtube.com/watch?v=tPbnW2HmuIY: +- Super Mario Odyssey +https://www.youtube.com/watch?v=X6SVxsouuvE: +- Call of Duty Black Ops +- Black Ops +https://www.youtube.com/watch?v=zxgJTtumaV8: +- Tetris (Game Boy) - 10. Fanfare Victory (Varation) +https://www.youtube.com/watch?v=cOlCCc-_2sg: +- Dark Castle - Kirby's Dream Land 2 +https://www.youtube.com/watch?v=UDJtsfR51To: +- Xenoblade Chronicles OST - Gaur Plain +https://www.youtube.com/watch?v=tg-yC0xcI9s: +- Triggernometry - Red Dead Redemption Soundtrack +https://www.youtube.com/watch?v=rNNQBAj10n4: +- Marvel vs Capcom 3 +https://www.youtube.com/watch?v=bwhHD92bQUk: +- Sonic CD +https://www.youtube.com/watch?v=ZlTnLsjg59s: +- GTA V +- GTA 5 +- Grand Theft Auto 5 +https://www.youtube.com/watch?v=i-x22mnsUJ8: +- Gears 5 +- Gears of War 5 +https://www.youtube.com/watch?v=mOw3pZQctJ0: +- Call of Duty Advanced Warfare +https://www.youtube.com/watch?v=cnosICv2Mjg: +- Hotel Mario +https://www.youtube.com/watch?v=MMwM6hLEyeY: +- Sonic Mania +https://www.youtube.com/watch?v=XsliycURi0Y: +- Crackdown +https://www.youtube.com/watch?v=a51CkkWec1Q: +- Half-Life 2 +https://www.youtube.com/watch?v=iKBVXX8Vh7g: +- Call of Duty WWII +- Call of Duty WW2 +https://www.youtube.com/watch?v=qEvxbJROGuQ: +- Kirby Super Star +https://www.youtube.com/watch?v=_J4R6WsSKVs: +- Cuphead +https://www.youtube.com/watch?v=vcEGFAaSpzI: +- Final Fantasy VII +- Final Fantasy 7 +https://www.youtube.com/watch?v=cOWRNLaCMJg: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=XF2fA1epvLE: +- Super Mario World +https://www.youtube.com/watch?v=-tv0ZXF7uA4: +- GTA V +- GTA 5 +- Grand Theft Auto 5 +https://www.youtube.com/watch?v=3JncraxIMXY: +- Battlefield V +- Battlefield 5 +https://www.youtube.com/watch?v=fYnw_xoZCsg: +- Mario Teaches Typing 2 +https://www.youtube.com/watch?v=FQ73YtVm3Js: +- Spider-Man 2 The Game +- Spider Man 2 +- Spider-Man 2 +https://www.youtube.com/watch?v=nC5FK864qTk: +- Mortal Kombat Armageddon +https://www.youtube.com/watch?v=SnqPJv0UqKo: +- The Elder Scrolls 5 Skyrim +- Elder Scrolls 5 +- Skyrim +https://www.youtube.com/watch?v=pTE54izXTqc: +- Arkanoid R 2000 +- Arkanoid 2000 +- Arkanoid Returns +https://www.youtube.com/watch?v=FezNgPThD3M: +- Undertale +https://www.youtube.com/watch?v=DCX6c07rcJw: +- The World Ends With You +https://www.youtube.com/watch?v=hyQ6bn1Dno4: +- Sonic & All-Stars Racing Transformed +- Sonic and All Stars Racing Transformed +- Sonic and All-Stars Racing Transformed +- Sonic and Sega All-Stars Racing Transformed +- Sonic and Sega All Stars Racing Transformed +https://www.youtube.com/watch?v=okksreh2eQ4: +- Disney Universe +https://www.youtube.com/watch?v=3L97-DxwgRU: +- Billy The Wizard +https://www.youtube.com/watch?v=kY1F5QO5VOo: +- LittleBigPlanet +- Little Big Planet +https://www.youtube.com/watch?v=QEEPfzrDpoc: +- Bust a Groove +https://www.youtube.com/watch?v=JkO05j0zzZQ: +- LA Noire +https://www.youtube.com/watch?v=TNosg0EXQlU: +- Tom Clancys Rainbow Six Siege +- Rainbow Six Siege +- R6 Siege +https://www.youtube.com/watch?v=h38zN_vF_dQ: +- Fallout +https://www.youtube.com/watch?v=P9GX6Fyxr90: +- Snipperclips +https://www.youtube.com/watch?v=aKTJNO3HLEE: +- Xenoblade Chronicles +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=0gs56c9qGKA: +- Marvel Ultimate Alliance +https://www.youtube.com/watch?v=SzDmvY2pRJI: +- Yoshi's New Island +- Yoshis New Island +https://www.youtube.com/watch?v=IVH6Gl7W0hI: +- Super Mario Galaxy +https://www.youtube.com/watch?v=tZHcVMqXGhQ: +- Fire Emblem Echoes Shadows of Valentia +- Fire Emblem Echoes +- Shadows of Valentia +https://www.youtube.com/watch?v=Ltay2kMtCeM: +- Fortune Street +https://www.youtube.com/watch?v=mi3SE5j5XrU: +- Wizard101 +- Wizard 101 +https://www.youtube.com/watch?v=_nfld1C7RuE: +- Sonic and the Secret Rings +https://www.youtube.com/watch?v=nJB0OvU_vkg: +- Animal Crossing New Leaf +https://www.youtube.com/watch?v=xi0M9SIaLb4: +- Xenoblade Chronicles +https://www.youtube.com/watch?v=qw2lEHk5kDI: +- Gears of War 5 +- Gears 5 +https://www.youtube.com/watch?v=zlUWnUzpzSA: +- Halo 5 Guardians +https://www.youtube.com/watch?v=iUZyJHDasqA: +- Dark Souls II +- Dark Souls 2 +https://www.youtube.com/watch?v=jO3BZ9b1YsE: +- Nintendogs + Cats +- Nintendogs and Cats +https://www.youtube.com/watch?v=wDgQdr8ZkTw: +- Undertale +https://www.youtube.com/watch?v=O2MrJvaSjDE: +- Paper Mario Sticker Star +https://www.youtube.com/watch?v=umSJ--aGeYo: +- Portal +https://www.youtube.com/watch?v=JJOJdWubO-o: +- Plants vs Zombies Battle For Neighborville +- PvZ Battle for Neighborville +- Plants vs Zombies +https://www.youtube.com/watch?v=hUiAmrtxits: +- The Legend of Zelda The Wind Waker +- Zelda Wind Waker +- Zelda The Wind Waker +https://www.youtube.com/watch?v=1h9bU-y49-A: +- Hyper Street Fighter II The Anniversary Edition +- Hyper Street Fighter 2 +https://www.youtube.com/watch?v=sPWnlR5PEdQ: +- Call of Duty Advanced Warfare +https://www.youtube.com/watch?v=KF32DRg9opA: +- DuckTales +https://www.youtube.com/watch?v=rWatUe5yOP8: +- Sonic Heroes +https://www.youtube.com/watch?v=OTyghyyX34I: +- Paperboy 2 +https://www.youtube.com/watch?v=bGIP3IUgOg8: +- Super Mario Odyssey +https://www.youtube.com/watch?v=M8gJxIWSxGE: +- Battletoads +https://www.youtube.com/watch?v=btgi3TPL3AE: +- Castlevania +https://www.youtube.com/watch?v=rct233J9lsE: +- Dead Rising 2 Off The Record +- Dead Rising 2 +https://www.youtube.com/watch?v=Bz-QyMQDg_8: +- Life is Strange +https://www.youtube.com/watch?v=l2ETBzOAsV0: +- Sonic Forces +https://www.youtube.com/watch?v=6xzRa1EtOaw: +- Mario Strikers Charged +https://www.youtube.com/watch?v=6QpG48UEcI4: +- Fruit Ninja +https://www.youtube.com/watch?v=4n6WP9qHyRM: +- World of Warcraft +https://www.youtube.com/watch?v=R0cia14N9no: +- League of Legends +https://www.youtube.com/watch?v=iiNyQD5Yq3E: +- Star Wars Republic Commando +https://www.youtube.com/watch?v=nJ3o3kQZWm8: +- Parappa the Rapper +https://www.youtube.com/watch?v=2aV-ej4gwJU: +- Destiny 2 +https://www.youtube.com/watch?v=eCwzdl2q6Ho: +- King of Fighters XIII +- King of Fighters 13 +https://www.youtube.com/watch?v=PLDyWLbuptQ: +- Undertale +https://www.youtube.com/watch?v=3EdeEKDxhPE: +- Halo 3 +https://www.youtube.com/watch?v=em_oGE5Ht2Y: +- Resident Evil +https://www.youtube.com/watch?v=bQYuuUN7pSY: +- Mario Sports Mix +https://www.youtube.com/watch?v=cHJLdF2WNxI: +- World of Warcraft +https://www.youtube.com/watch?v=EXAb0Df3Rpc: +- Clannad +https://www.youtube.com/watch?v=LeeD7lWh2RE: +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=nJa7hJEfdww: +- Witcher 3 +https://www.youtube.com/watch?v=-cg5QXLS4nc: +- Cuphead +https://www.youtube.com/watch?v=8eDRM-YzpIs: +- Mario Sports Mix +https://www.youtube.com/watch?v=sunu9ShZ4xs: +- Mortal Kombat Armageddon +https://www.youtube.com/watch?v=n25nqibaIDg: +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=8KT7jcB72fQ: +- Donkey Kong 64 +https://www.youtube.com/watch?v=QCQ3DpI5hHk: +- Plants Vs Zombies +https://www.youtube.com/watch?v=Ze3G5EksIQw: +- The Stanley Parable +- Stanley Parable +https://www.youtube.com/watch?v=9pPQnhhUqtU: +- Total Distortion +https://www.youtube.com/watch?v=WSig9qC9tWE: +- Pokemon Sword and Shield +- Pokemon Sword +- Pokemon Shield +https://www.youtube.com/watch?v=5_E_y1AWAfc: +- Undertale +https://www.youtube.com/watch?v=TsfRhKE5iP8: +- Tomb Raider +https://www.youtube.com/watch?v=n5mo2zPBl3k: +- Middle Earth Shadow of Modor +- Shadow of Modor +https://www.youtube.com/watch?v=Z2g-xpGOusI: +- Tom Clancys The Division +- The Division +https://www.youtube.com/watch?v=S5DFhaprlRM: +- Super Smash Bros +https://www.youtube.com/watch?v=mtfsZv_wr98: +- Tekken +https://www.youtube.com/watch?v=Y8V_MvH8y8U: +- Fallout New Vegas +https://www.youtube.com/watch?v=4eGCH6tAycE: +- Xenoblade 2 +https://www.youtube.com/watch?v=HwJgHF0xDbU: +- Street Fighter V +- Street Fighter 5 +https://www.youtube.com/watch?v=taNV00DWLq8: +- ARMS +https://www.youtube.com/watch?v=9XzAPa4ji7M: +- Punch-Out!! +- Punch Out +- Punch-Out +https://www.youtube.com/watch?v=el_r5y_AcrE: +- Super Mario Sunshine +https://www.youtube.com/watch?v=7dRVuXYmB1M: +- Deltarune +https://www.youtube.com/watch?v=E_KMghGlDY8: +- Nintendo 3DS Music StreetPass Mii Plaza +- Nintendo 3DS +- StreetPass +- Mii Plaza +https://www.youtube.com/watch?v=QJbYjj5cNqQ: +- Wolfenstein The New Order +https://www.youtube.com/watch?v=qHpKdriwcOg: +- Kingdom Hearts +https://www.youtube.com/watch?v=jxMzCya8pQM: +- Payday 2 +https://www.youtube.com/watch?v=LQdUAMrcEnw: +- Farcry 5 +https://www.youtube.com/watch?v=r5Q4FOSPSx0: +- The Witcher 2 Assasins of Kings +- The Witcher 2 +https://www.youtube.com/watch?v=hU3K2a4uXxs: +- Sonic Unleashed +https://www.youtube.com/watch?v=2UKw9aheG3Y: +- Mafia 2 +https://www.youtube.com/watch?v=YTy9v9a7Tmo: +- Undertale +https://www.youtube.com/watch?v=rU_im8hx2TI: +- Earthbound +https://www.youtube.com/watch?v=sIL2fuxlMkc: +- Super Mario Galaxy +https://www.youtube.com/watch?v=-KBLgp5_smc: +- Mortal Kombat 9 +https://www.youtube.com/watch?v=Nsps0I58yUM: +- Dark Souls +https://www.youtube.com/watch?v=RjLWGyx4zoA: +- Persona 5 +https://www.youtube.com/watch?v=XXNVnq1r3yY: +- Castlevania Harmony of Despair +https://www.youtube.com/watch?v=6qIjHtI3kGU: +- Far Cry 5 +https://www.youtube.com/watch?v=3FRJU4DGZSc: +- Red Dead Redemption 2 +https://www.youtube.com/watch?v=zKYmW19Sk8s: +- Sekiro +https://www.youtube.com/watch?v=NYoP3_p2-N8: +- Super Mario 3D Land +https://www.youtube.com/watch?v=XnV_wHQ01dU: +- Cuphead +https://www.youtube.com/watch?v=MXAHCZXaoy8: +- Pokémon Red/Blue/Yellow +- Pokémon Red +- Pokemon Red +- Pokémon Blue +- Pokemon Blue +- Pokémon Yellow +- Pokemon Yellow +https://www.youtube.com/watch?v=56xw3mryadY: +- Mega Man 11 +https://www.youtube.com/watch?v=1K1rV9kFs6I: +- Sonic 3 +https://www.youtube.com/watch?v=7RiHmQcHa84: +- Shin Megami Tensei III Nocturne +- Shin Megami Tensei 3 Nocturne +- Shin Megami Tensei 3 +https://www.youtube.com/watch?v=aPGwniiEfIk: +- Escape from Mars Starring Taz +- Taz in Escape from Mars +- Escape from Mars +https://www.youtube.com/watch?v=kZnS_HxSdvo: +- Nintendo 3DS Find Mii +- Find Mii +https://www.youtube.com/watch?v=JEj_8VkTHVw: +- Mario + Rabbids Kingdom Battle +- Mario and Rabbids Kingdom Battle +https://www.youtube.com/watch?v=n8yKpcyU0fg: +- Hot Wheels Beat That +https://www.youtube.com/watch?v=cIKpAjB5B00: +- Super Hero Squad Online +https://www.youtube.com/watch?v=dYvvKyYIc5s: +- Touhou Embodiment of Scarlet Devil +- Touhou EoSD +- Embodiment of Scarlet Devil +https://www.youtube.com/watch?v=0BkHICB4eJs: +- Half Life +https://www.youtube.com/watch?v=dqwjST3UYcc: +- Mass Effect +https://www.youtube.com/watch?v=WQJxqXo8_cY: +- Tom Clancys Rainbow Six Siege +- Rainbow Six Siege +- R6 Siege +https://www.youtube.com/watch?v=4xbXFcadgy0: +- For Honor +https://www.youtube.com/watch?v=MJrZKCZ2pqo: +- Mario Kart Wii +https://www.youtube.com/watch?v=GgbXr4rN4D4: +- Banjo-Kazooie +- Banjo Kazooi +https://www.youtube.com/watch?v=oL5fbozc3kU: +- Yoshi's Island +- Yoshis Island +https://www.youtube.com/watch?v=DMDYpRaUvIQ: +- Sonic R +https://www.youtube.com/watch?v=oOMy4AZTPLc: +- Metropolis Street Racer +https://www.youtube.com/watch?v=Zf2qOWmKiz0: +- Deltarune +https://www.youtube.com/watch?v=6jFaoLrLzd4: +- Persona 3 +https://www.youtube.com/watch?v=UKYXOobaceQ: +- Sonic 3D Blast +https://www.youtube.com/watch?v=R9awS7E0jDc: +- Donkey Konga +https://www.youtube.com/watch?v=-VhWTfHZBOI: +- Contrast +https://www.youtube.com/watch?v=6QaH6tOXHek: +- Miitomo +https://www.youtube.com/watch?v=-GK41pLUuP0: +- New Super Mario Bros +https://www.youtube.com/watch?v=xjUHMsAZti4: +- Earthbound +https://www.youtube.com/watch?v=y4y13vnGXec: +- Psycho Soldier +https://www.youtube.com/watch?v=sEhg1qAmWMg: +- Codename Kids Next Door Operation V.I.D.E.O.G.A.M.E. +- Codename KND Operation VIDEOGAME +- Codename Kids Next Door Operation VIDEOGAME +- Codename Kids Next Door +https://www.youtube.com/watch?v=TAQVE2e3u8o: +- Power Rangers +https://www.youtube.com/watch?v=NRcsLULM2ns: +- Happy Wheels +https://www.youtube.com/watch?v=pU_zIsgnb3w: +- Hitman Blood Money +https://www.youtube.com/watch?v=ZXX8f0JcSG4: +- Pokémon Black & White +- Pokemon Black and White +- Pokemon Black +- Pokemon White +https://www.youtube.com/watch?v=AWkOMVUJ7gs: +- Fire Emblem Fates Condemnation +- Fire Emblem Fates +https://www.youtube.com/watch?v=QyPR77rg1to: +- Undertale +https://www.youtube.com/watch?v=sxJ-LSBfrQ4: +- Splatoon 2 +https://www.youtube.com/watch?v=RZVyHH-voR8: +- Dark Souls +https://www.youtube.com/watch?v=nWfRO1yxaXs: +- Wario world +- Warioworld +https://www.youtube.com/watch?v=bMF_PRSXbXk: +- Half-Life Opposing Force +https://www.youtube.com/watch?v=XVpC7Tg-hH4: +- Dreams +https://www.youtube.com/watch?v=EkLvDaWNez8: +- New Super Mario Bros +https://www.youtube.com/watch?v=7LvsWVE9-es: +- Pokémon X & Y +- Pokemon X +- Pokemon Y +https://www.youtube.com/watch?v=iAWXT2HJFTI: +- The Legend of Zelda Breath of the Wild +- Breath of the Wild +- Zelda Botw +https://www.youtube.com/watch?v=JZw9wWmcXH4: +- Super Smash Bros Brawl +https://www.youtube.com/watch?v=XJsy5jJ7Dp0: +- Trauma Center Under the Knife 2 +- Trauma Center 2 +- Trauma Center Under the Knife +https://www.youtube.com/watch?v=gd22rCkvJoA: +- Assassin's Creed 4 Black Flag +- Assasins Creed 4 +- Assasins Creed Black Flag +https://www.youtube.com/watch?v=ECE7ZeDsuPk: +- Destiny 2 +https://www.youtube.com/watch?v=v0nlnUDKT1s: +- Fallout 4 +https://www.youtube.com/watch?v=zgLgSQPN0Gw: +- Super Mario Party +https://www.youtube.com/watch?v=Ib3FTf3Frbo: +- Fire Emblem Three Houses +- Super Smash Bros Ultimate +https://www.youtube.com/watch?v=LG0-UMOljBU: +- Pokémon Gold/Silver/Crystal +- Pokemon Gold +- Pokemon Silver +- Pokemon Crystal +https://www.youtube.com/watch?v=0cuhPPuTA3s: +- Saints Row The Third +https://www.youtube.com/watch?v=1scgMvQoLVg: +- Tomodachi Life +https://www.youtube.com/watch?v=D8ShsHeYkeI: +- Dark Souls 2 +https://www.youtube.com/watch?v=BG-0WXsZR-A: +- Marvel vs Capcom 2 +https://www.youtube.com/watch?v=NPjLgDF1hUg: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=upPtP_qtQHM: +- Super Paper Mario +https://www.youtube.com/watch?v=5xk8-dDqu50: +- Luigi's Mansion +- Luigis Mansion +https://www.youtube.com/watch?v=zop1cFVz1So: +- Killzone +https://www.youtube.com/watch?v=WLLE_PvJ5mY: +- Metroid Prime +https://www.youtube.com/watch?v=oMed0AQt6g4: +- Mass Effect +https://www.youtube.com/watch?v=S7NuuyVIYO8: +- Hitman +https://www.youtube.com/watch?v=ENWDVA0Xz88: +- Crackdown +https://www.youtube.com/watch?v=dQNAVqW1shA: +- Banjo-Kazooie +- Banjo Kazooie +https://www.youtube.com/watch?v=tnzziAe9tYA: +- Super Mario Galaxy 2 +https://www.youtube.com/watch?v=Ma7mdIHy-kA: +- Sonic Forces +https://www.youtube.com/watch?v=avyasO9uqfo: +- Donkey Kong Country +https://www.youtube.com/watch?v=i6v6w0mbPvU: +- A Hat in Time Music +https://www.youtube.com/watch?v=Df1nqnxRe7g: +- Left 4 Dead 2 +https://www.youtube.com/watch?v=QunjkirEY_g: +- Fire Emblem Fates +https://www.youtube.com/watch?v=-9bcB4_adYo: +- Crysis +https://www.youtube.com/watch?v=QMEqfXYjyvM: +- Deadspace +https://www.youtube.com/watch?v=a75SxyVBvKU: +- Half-Life 2 +- Half Life 2 +https://www.youtube.com/watch?v=5h1t94LxGd0: +- The Witcher 2 Assasins of Kings +- The Witcher 2 +https://www.youtube.com/watch?v=Eoo5oifOsUs: +- DayZ +https://www.youtube.com/watch?v=24ijaaCSrYM: +- For Honor +https://www.youtube.com/watch?v=sGjdjdyt5s4: +- Kirby Planet Robobot +https://www.youtube.com/watch?v=oA6kuCUmBpo: +- Captain Toad Treasure Tracker +https://www.youtube.com/watch?v=x9dJrEvHk3o: +- Mario & Sonic at the London 2012 Olympic Games +- Mario and Sonic at the London 2012 Olympic Games +https://www.youtube.com/watch?v=DheEtF1G_Rs: +- Splatoon 2 +https://www.youtube.com/watch?v=TcufdwbnF0k: +- Rhythm Heaven Fever +https://www.youtube.com/watch?v=4GChwuX81o4: +- Grabbed by the Ghoulies +https://www.youtube.com/watch?v=PhWge4ro354: +- Call of Duty Black Ops 2 +- Black Ops 2 +https://www.youtube.com/watch?v=bzbWrGr_of0: +- The Legend of Zelda Majora's Mask +- Zelda Majora's Mask +- Zelda Majoras Mask +https://www.youtube.com/watch?v=2RKnfOAI1A0: +- 3D Dot Game Heroes +https://www.youtube.com/watch?v=L2f5v1w_v8g: +- Conker Live & Reloaded +- Conker Live and Reloaded +https://www.youtube.com/watch?v=nVRQJ2i59XE: +- Wii Sports +https://www.youtube.com/watch?v=JDqJa1RC3q8: +- Kid Icarus Uprising +https://www.youtube.com/watch?v=Jq6oRi6UTXY: +- Sonic the Hedgehog 3 +https://www.youtube.com/watch?v=JzzJwrIN6Mc: +- Mega Man 3 +https://www.youtube.com/watch?v=s1AkaZuRaM4: +- Sonic Dash +https://www.youtube.com/watch?v=7S7WEkopK8k: +- Dance Dance Revolution Mario Mix +- Dance Dance Revolution Mario +https://www.youtube.com/watch?v=KVwdKcwWe2k: +- Yooka-Laylee Music +- Yooka Laylee +https://www.youtube.com/watch?v=ZdBO_V-guJ4: +- Call of Duty Modern Warfare +- Modern Warfare +https://www.youtube.com/watch?v=NQIeAbFT4Kc: +- League of Legends +https://www.youtube.com/watch?v=cnAoE7tcrBI: +- Max Payne +https://www.youtube.com/watch?v=pYf0C4Yh8pE: +- Apex Legends +https://www.youtube.com/watch?v=0W-trrXXcS8: +- Far Cry New Dawn +https://www.youtube.com/watch?v=jv249KSsjcM: +- Red Dead Redemption 2 +https://www.youtube.com/watch?v=z5JhMEP0GSU: +- Mario Party The Top 100 +- Mario Party Top 100 +https://www.youtube.com/watch?v=3nxFYoGNJKc: +- Banjo-Kazooie +- Banjo Kazooie +https://www.youtube.com/watch?v=6vICKfFo6bU: +- Wii Fit +https://www.youtube.com/watch?v=EJCq_hnFZyk: +- Fire Emblem Three Houses +https://www.youtube.com/watch?v=bXM7CvKCSx0: +- GTA San Andreas +- Grand Theft Auto San Andreas +https://www.youtube.com/watch?v=5YjGmTI_fPU: +- Bayonetta +https://www.youtube.com/watch?v=5wApmRF5gyM: +- Super Mario Bros 3 +https://www.youtube.com/watch?v=kRMTqfxkCsc: +- Mortal Kombat Deadly Alliance +https://www.youtube.com/watch?v=Ljqe4Nj7nBA: +- The Legend of Zelda Ocarina of Time +- Zelda Ocarina of Time +https://www.youtube.com/watch?v=RB4nFoA63rs: +- Gravity Rush +https://www.youtube.com/watch?v=UsaoAl5EIH8: +- Punch-Out!! +- Punch Out +- Punch-Out +https://www.youtube.com/watch?v=rMudHClToL0: +- Donkey Kong Country Tropical Freeze +- Donkey Kong Tropical Freeze +https://www.youtube.com/watch?v=iT8T-dBwztg: +- FlingSmash +https://www.youtube.com/watch?v=dcp5gPf7mhI: +- Jet Set Radio Future +https://www.youtube.com/watch?v=sC0cvwnG0Ik: +- Crazy Bus +https://www.youtube.com/watch?v=KJfMS5XzmIc: +- Animal Crossing New Horizons +https://www.youtube.com/watch?v=HAYPkVeWjXM: +- Halo 5 Guardians +https://www.youtube.com/watch?v=ozLqx3EboU4: +- Shantae Half-Genie Hero +- Shantae Half Genie Hero +- 'Shantae 1/2 Genie Hero' +https://www.youtube.com/watch?v=e2Gyaqf7EoU: +- Persona 3 +https://www.youtube.com/watch?v=KC5kHf58GMI: +- The Legend of Zelda Skyward Sword +- Skyward Sword +https://www.youtube.com/watch?v=XHzlfCpmqsw: +- Super Mario Kart +https://www.youtube.com/watch?v=6l1HUDuzyrk: +- Sonic Lost World +https://www.youtube.com/watch?v=512pcIZOjm0: +- Xenoblade Chronicles +https://www.youtube.com/watch?v=7ePFxuu9lik: +- The Walking Dead Game +- The Walking Dead +https://www.youtube.com/watch?v=rgq0wF_ByO8: +- Contrast +https://www.youtube.com/watch?v=O7eGxoFLOI4: +- Mario Kart Double Dash!! +- Mario Kart Double Dash +https://www.youtube.com/watch?v=6ppBSY92rzg: +- EarthBound +https://www.youtube.com/watch?v=mmiWjG0N21w: +- Kingdom Hearts +https://www.youtube.com/watch?v=kNlVo5udnPs: +- Team Fortress 2 +https://www.youtube.com/watch?v=-t_C-CgJ-3A: +- Pokemon Sun & Moon +- Pokemon Sun +- Pokemon Moon +https://www.youtube.com/watch?v=7G_aaak-tDE: +- The Legend of Zelda Majora's Mask +- Zelda Majora's Mask +- Zelda Majoras Mask +https://www.youtube.com/watch?v=rGFic2e-uTE: +- Kirby's Epic Yarn +- Kirbys Epic Yarn +https://www.youtube.com/watch?v=1Cfin7GJUGI: +- Mario vs Donkey Kong 2 March of the Minis +- Mario vs Donkey Kong 2 +https://www.youtube.com/watch?v=gtKxgf_lETg: +- Rhythm Heaven Fever +https://www.youtube.com/watch?v=xnmxhE9usbI: +- Animal Crossing +https://www.youtube.com/watch?v=kwQ6_rwvMp8: +- ARMS +https://www.youtube.com/watch?v=2odP8HQuPgs: +- Nintendogs +https://www.youtube.com/watch?v=9ICz9BArKiM: +- Splatoon 2 +https://www.youtube.com/watch?v=IeosKeE1psE: +- Super Street Fighter IV +- Super Street Fighter 4 +https://www.youtube.com/watch?v=P9lLD_hgbU4: +- Bayonetta +https://www.youtube.com/watch?v=QTRsbrFWomM: +- Fire Emblem Fates +https://www.youtube.com/watch?v=zwOnn-ObtNo: +- Chibi-Robo! +- Chibi-Robo +- Chibi Robo +https://www.youtube.com/watch?v=5FYneJvqtMc: +- Lego Dimensions +https://www.youtube.com/watch?v=jkk6f817nNU: +- Xenoblade Chronicles +https://www.youtube.com/watch?v=N8OHSXvneOE: +- Celeste +https://www.youtube.com/watch?v=ryAv7ESrJtQ: +- Half Life 2 +https://www.youtube.com/watch?v=vkailb3xcTI: +- Super Mario RPG +https://www.youtube.com/watch?v=MOkWNuGfCD0: +- Pokémon X & Y +- Pokemon X +- Pokemon Y +https://www.youtube.com/watch?v=JUYAX3z2JYc: +- Call of Duty Black Ops 2 +- Black Ops 2 +https://www.youtube.com/watch?v=kfo7hmy-qso: +- Bomberman Hero +https://www.youtube.com/watch?v=myxM9Q72hxE: +- NES Remix Pack +https://www.youtube.com/watch?v=PbYoMpwHq5c: +- Wii Play +https://www.youtube.com/watch?v=-NuIPxOmpmc: +- Star Fox +https://www.youtube.com/watch?v=Ga9wY3CzEJQ: +- Gex Enter the Gecko +- Gex +https://www.youtube.com/watch?v=mpqct1zMqTE: +- Killer Instinct S1 +- Killer Instinct +https://www.youtube.com/watch?v=g3jCAyPai2Y: +- Yakuza +https://www.youtube.com/watch?v=4GfXKM-2nak: +- Paper Mario The Origami King +https://www.youtube.com/watch?v=6rW52ajkrZ0: +- Street Fighter V +- Street Fighter 5 +https://www.youtube.com/watch?v=zLs96hgloOA: +- Sekiro +https://www.youtube.com/watch?v=qG_lzlLH3UU: +- Titanfall +https://www.youtube.com/watch?v=U8ilLSEjlAQ: +- Destiny 2 +https://www.youtube.com/watch?v=QFaZC_R3WoE: +- Resident Evil 3 +https://www.youtube.com/watch?v=1fmhHRNzZLo: +- Gears Of War +https://www.youtube.com/watch?v=Oc44myRhNRM: +- Pokemon Omega Ruby/Alpha Sapphire +- Pokemon Omega Ruby +- Pokemon Alpha Sapphire +https://www.youtube.com/watch?v=vNVz6N-bNZs: +- Sonic Adventure +https://www.youtube.com/watch?v=WcabIuZOWYY: +- DragonBall Xenoverse 2 +https://www.youtube.com/watch?v=GmpB0btoC6M: +- Yoshi's Cookie +- Yoshis Cookie +https://www.youtube.com/watch?v=KC-B3Q0cRrY: +- Streets Of Rage 2 +https://www.youtube.com/watch?v=DekKId4f-Yk: +- Super Mario 3D World +https://www.youtube.com/watch?v=xorWgC8vf3I: +- Fallout 3 +https://www.youtube.com/watch?v=X9Q4Zg1tYno: +- Super Smash Bros Wii U +https://www.youtube.com/watch?v=llOvd2yGEio: +- Team Fortress 2 +https://www.youtube.com/watch?v=RR_ifniLcW4: +- Donkey Kong Country 2 Diddy's Kong Quest +- Donkey Kong Country 2 +- Diddys Kong Quest +https://www.youtube.com/watch?v=NoBRdekWKxI: +- The Legend of Zelda Twilight Princess +- Twilight Princess +https://www.youtube.com/watch?v=rpocm85Hauc: +- Crash Tag Team Racing +https://www.youtube.com/watch?v=-4FvqXfQYbk: +- Dragon Ball FighterZ +https://www.youtube.com/watch?v=xgcyH9I1NBE: +- Sonic Adventure +https://www.youtube.com/watch?v=gESYyh_tsGA: +- Halo Infinite +https://www.youtube.com/watch?v=fXWCotTeNgk: +- Donkey Kong 64 +https://www.youtube.com/watch?v=tWBKtLiYe5w: +- Persona 4 Dancing All Night +- Persona 4 +https://www.youtube.com/watch?v=pQ-bjZD1EnI: +- Sonic Heroes +https://www.youtube.com/watch?v=4S9qb1jZGlA: +- Ghost of Tsushima +https://www.youtube.com/watch?v=v1M8VVDFxgo: +- Celeste +https://www.youtube.com/watch?v=ROBc_MVW-Lg: +- Fall Guys Ultimate Knockout +- Fall Guys +https://www.youtube.com/watch?v=r5EjvtyLJ1g: +- Sonic the Hedgehog CD +- Sonic CD +https://www.youtube.com/watch?v=JRPXRHS4XNQ: +- Battletoads +https://www.youtube.com/watch?v=hHKmORyO8WA: +- Mario Super Sluggers +https://www.youtube.com/watch?v=oeEiOtndLB0: +- Crusader Kings 2 +https://www.youtube.com/watch?v=TKvjEQXKeec: +- Earthbound +https://www.youtube.com/watch?v=m65ns26m9IE: +- A Hat in Time +https://www.youtube.com/watch?v=feM7-QL9x38: +- Bomberman Max 2 +https://www.youtube.com/watch?v=EGeoG0KQa68: +- Mappy Arcade +- Mappy +https://www.youtube.com/watch?v=ySYwqTKRtgQ: +- Mega Man 2 +https://www.youtube.com/watch?v=VCCZe8S5miY: +- Pac-Man World +- Pac Man World +https://www.youtube.com/watch?v=g-aNgman8Wo: +- Street Fighter 5 +https://www.youtube.com/watch?v=U1_PStq_tTM: +- Watch Dogs +https://www.youtube.com/watch?v=vLljFmx7yF8: +- Counter-Strike Global Offensive +- CS GO +- CSGO +- Counter Strike GO +- Counter Strike Global Offensive +https://www.youtube.com/watch?v=gsZJg5mmsHA: +- The Angry Video Game Nerd Adventures +- Angry Video Game Nerd Adventures +https://www.youtube.com/watch?v=n3c2xbYWKY0: +- Double Dragon +https://www.youtube.com/watch?v=JNEKqu6Pe48: +- Pac Man Party +- Pac-Man Party +https://www.youtube.com/watch?v=sEFIZh_Zscc: +- Bioshock +https://www.youtube.com/watch?v=fmU9MXi9Uz0: +- Under Night In-Birth +- Under Night In Birth +https://www.youtube.com/watch?v=EzDLLILqvLU: +- Asura's Wrath +- Asuras Wrath diff --git a/audiotrivia/data/lists/games.yaml b/audiotrivia/data/lists/non_funcitonal_lists/games.yaml similarity index 99% rename from audiotrivia/data/lists/games.yaml rename to audiotrivia/data/lists/non_funcitonal_lists/games.yaml index c3a9078..da1dcb6 100644 --- a/audiotrivia/data/lists/games.yaml +++ b/audiotrivia/data/lists/non_funcitonal_lists/games.yaml @@ -1,13 +1,14 @@ AUTHOR: Plab -https://www.youtube.com/watch?v=--bWm9hhoZo: +NEEDS: New links for all songs. +https://www.youtube.com/watch?v=f9O2Rjn1azc: - Transistor -https://www.youtube.com/watch?v=-4nCbgayZNE: +https://www.youtube.com/watch?v=PgUhYFkVdSY: - Dark Cloud 2 - Dark Cloud II -https://www.youtube.com/watch?v=-64NlME4lJU: +https://www.youtube.com/watch?v=1T1RZttyMwU: - Mega Man 7 - Mega Man VII -https://www.youtube.com/watch?v=-AesqnudNuw: +https://www.youtube.com/watch?v=AdDbbzuq1vY: - Mega Man 9 - Mega Man IX https://www.youtube.com/watch?v=-BmGDtP2t7M: diff --git a/chatter/chat.py b/chatter/chat.py index ad8e37b..ef75bb8 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -434,7 +434,7 @@ class Chatter(Cog): else: await ctx.maybe_send_embed("Error occurred :(") - @commands.Cog.listener() + @Cog.listener() async def on_message_without_command(self, message: discord.Message): """ Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py diff --git a/conquest/conquest.py b/conquest/conquest.py index fb8b280..fdf5e96 100644 --- a/conquest/conquest.py +++ b/conquest/conquest.py @@ -159,7 +159,12 @@ class Conquest(commands.Cog): self.data_path / self.current_map / f"current.{self.ext}", x, y, zoom ) - await ctx.send(file=discord.File(fp=zoomed_path, filename=f"current_zoomed.{self.ext}",)) + await ctx.send( + file=discord.File( + fp=zoomed_path, + filename=f"current_zoomed.{self.ext}", + ) + ) async def _create_zoomed_map(self, map_path, x, y, zoom, **kwargs): current_map = Image.open(map_path) diff --git a/dad/dad.py b/dad/dad.py index edf98c5..0be7fce 100644 --- a/dad/dad.py +++ b/dad/dad.py @@ -85,6 +85,8 @@ class Dad(Cog): @commands.Cog.listener() async def on_message_without_command(self, message: discord.Message): + if message.author.bot: + return guild: discord.Guild = getattr(message, "guild", None) if guild is None: return diff --git a/fifo/datetime_cron_converters.py b/fifo/datetime_cron_converters.py index d59ef37..9e01cc8 100644 --- a/fifo/datetime_cron_converters.py +++ b/fifo/datetime_cron_converters.py @@ -1,9 +1,10 @@ -from datetime import datetime +from datetime import datetime, tzinfo from typing import TYPE_CHECKING from apscheduler.triggers.cron import CronTrigger from dateutil import parser from discord.ext.commands import BadArgument, Converter +from pytz import timezone from fifo.timezones import assemble_timezones @@ -12,6 +13,18 @@ if TYPE_CHECKING: CronConverter = str else: + class TimezoneConverter(Converter): + async def convert(self, ctx, argument) -> tzinfo: + tzinfos = assemble_timezones() + if argument.upper() in tzinfos: + return tzinfos[argument.upper()] + + timez = timezone(argument) + + if timez is not None: + return timez + raise BadArgument() + class DatetimeConverter(Converter): async def convert(self, ctx, argument) -> datetime: dt = parser.parse(argument, fuzzy=True, tzinfos=assemble_timezones()) diff --git a/fifo/fifo.py b/fifo/fifo.py index e84e342..acd01ac 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, tzinfo from typing import Optional, Union import discord @@ -10,8 +10,9 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import TimedeltaConverter +from redbot.core.utils.chat_formatting import pagify -from .datetime_cron_converters import CronConverter, DatetimeConverter +from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter from .task import Task schedule_log = logging.getLogger("red.fox_v3.fifo.scheduler") @@ -57,6 +58,8 @@ class FIFO(commands.Cog): self.scheduler = None self.jobstore = None + self.tz_cog = None + async def red_delete_data_for_user(self, **kwargs): """Nothing to delete""" return @@ -131,6 +134,24 @@ class FIFO(commands.Cog): async def _remove_job(self, task: Task): return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) + async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]: + if self.tz_cog is None: + self.tz_cog = self.bot.get_cog("Timezone") + if self.tz_cog is None: + self.tz_cog = False # only try once to get the timezone cog + + if not self.tz_cog: + return None + try: + usertime = await self.tz_cog.config.user(user).usertime() + except AttributeError: + return None + + if usertime: + return await TimezoneConverter().convert(None, usertime) + else: + return None + @checks.is_owner() @commands.guild_only() @commands.command() @@ -139,7 +160,7 @@ class FIFO(commands.Cog): self.scheduler.remove_all_jobs() await self.config.guild(ctx.guild).tasks.clear() await self.config.jobs.clear() - await self.config.jobs_index.clear() + # await self.config.jobs_index.clear() await ctx.tick() @checks.is_owner() # Will be reduced when I figure out permissions later @@ -306,7 +327,11 @@ class FIFO(commands.Cog): out += f"{task_name}: {task_data}\n" if out: - await ctx.maybe_send_embed(out) + if len(out) > 2000: + for page in pagify(out): + await ctx.maybe_send_embed(page) + else: + await ctx.maybe_send_embed(out) else: await ctx.maybe_send_embed("No tasks to list") @@ -406,7 +431,7 @@ class FIFO(commands.Cog): job: Job = await self._process_task(task) delta_from_now: timedelta = job.next_run_time - datetime.now(job.next_run_time.tzinfo) await ctx.maybe_send_embed( - f"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n" + f"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n\n" f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" ) @@ -427,7 +452,9 @@ class FIFO(commands.Cog): ) return - result = await task.add_trigger("date", datetime_str) + maybe_tz = await self._get_tz(ctx.author) + + result = await task.add_trigger("date", datetime_str, maybe_tz) if not result: await ctx.maybe_send_embed( "Failed to add a date trigger to this task, see console for logs" @@ -444,7 +471,12 @@ class FIFO(commands.Cog): @fifo_trigger.command(name="cron") async def fifo_trigger_cron( - self, ctx: commands.Context, task_name: str, *, cron_str: CronConverter + self, + ctx: commands.Context, + task_name: str, + optional_tz: Optional[TimezoneConverter] = None, + *, + cron_str: CronConverter, ): """ Add a cron "time of day" trigger to the specified task @@ -460,7 +492,10 @@ class FIFO(commands.Cog): ) return - result = await task.add_trigger("cron", cron_str) + if optional_tz is None: + optional_tz = await self._get_tz(ctx.author) # might still be None + + result = await task.add_trigger("cron", cron_str, optional_tz) if not result: await ctx.maybe_send_embed( "Failed to add a cron trigger to this task, see console for logs" diff --git a/fifo/info.json b/fifo/info.json index dda63ce..eb2a576 100644 --- a/fifo/info.json +++ b/fifo/info.json @@ -3,14 +3,14 @@ "Bobloy" ], "min_bot_version": "3.4.0", - "description": "[ALPHA] Schedule commands to be run at certain times or intervals", + "description": "[BETA] Schedule commands to be run at certain times or intervals", "hidden": false, "install_msg": "Thank you for installing FIFO.\nGet started with `[p]load fifo`, then `[p]help FIFO`", - "short": "[ALPHA] Schedule commands to be run at certain times or intervals", + "short": "[BETA] Schedule commands to be run at certain times or intervals", "end_user_data_statement": "This cog does not store any End User Data", "requirements": [ "apscheduler", - "python-dateutil" + "pytz" ], "tags": [ "bobloy", @@ -24,6 +24,7 @@ "date", "datetime", "time", - "calendar" + "calendar", + "timezone" ] } \ No newline at end of file diff --git a/fifo/task.py b/fifo/task.py index 83158d8..f7dc45a 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -9,6 +9,7 @@ from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger from discord.utils import time_snowflake +from pytz import timezone from redbot.core import Config, commands from redbot.core.bot import Red @@ -25,10 +26,10 @@ def get_trigger(data): return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds) if data["type"] == "date": - return DateTrigger(data["time_data"]) + return DateTrigger(data["time_data"], timezone=data["tzinfo"]) if data["type"] == "cron": - return CronTrigger.from_crontab(data["time_data"]) + return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"]) return False @@ -70,6 +71,7 @@ class Task: default_trigger = { "type": "", "time_data": None, # Used for Interval and Date Triggers + "tzinfo": None, } def __init__( @@ -99,7 +101,13 @@ class Task: if t["type"] == "date": # Convert into datetime dt: datetime = t["time_data"] - triggers.append({"type": t["type"], "time_data": dt.isoformat()}) + triggers.append( + { + "type": t["type"], + "time_data": dt.isoformat(), + "tzinfo": getattr(t["tzinfo"], "zone", None), + } + ) # triggers.append( # { # "type": t["type"], @@ -117,9 +125,18 @@ class Task: continue if t["type"] == "cron": - triggers.append(t) # already a string, nothing to do - + if t["tzinfo"] is None: + triggers.append(t) # already a string, nothing to do + else: + triggers.append( + { + "type": t["type"], + "time_data": t["time_data"], + "tzinfo": getattr(t["tzinfo"], "zone", None), + } + ) continue + raise NotImplemented return triggers @@ -128,18 +145,27 @@ class Task: if not self.data or not self.data.get("triggers", None): return - for n, t in enumerate(self.data["triggers"]): + for t in self.data["triggers"]: + # Backwards compatibility + if "tzinfo" not in t: + t["tzinfo"] = None + + # First decode timezone if there is one + if t["tzinfo"] is not None: + t["tzinfo"] = timezone(t["tzinfo"]) + if t["type"] == "interval": # Convert into timedelta - self.data["triggers"][n]["time_data"] = timedelta(**t["time_data"]) + t["time_data"] = timedelta(**t["time_data"]) continue if t["type"] == "date": # Convert into datetime # self.data["triggers"][n]["time_data"] = datetime(**t["time_data"]) - self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"]) + t["time_data"] = datetime.fromisoformat(t["time_data"]) continue if t["type"] == "cron": continue # already a string + raise NotImplemented # async def load_from_data(self, data: Dict): @@ -300,8 +326,16 @@ class Task: self.data["command_str"] = command_str return True - async def add_trigger(self, param, parsed_time: Union[timedelta, datetime, str]): - trigger_data = {"type": param, "time_data": parsed_time} + async def add_trigger( + self, param, parsed_time: Union[timedelta, datetime, str], timezone=None + ): + # TODO: Save timezone separately for cron and date triggers + trigger_data = self.default_trigger.copy() + trigger_data["type"] = param + trigger_data["time_data"] = parsed_time + if timezone is not None: + trigger_data["tzinfo"] = timezone + if not get_trigger(trigger_data): return False diff --git a/fifo/timezones.py b/fifo/timezones.py index 5a322a4..54d7c3e 100644 --- a/fifo/timezones.py +++ b/fifo/timezones.py @@ -4,7 +4,8 @@ Timezone information for the dateutil parser All credit to https://github.com/prefrontal/dateutil-parser-timezones """ -from dateutil.tz import gettz +# from dateutil.tz import gettz +from pytz import timezone def assemble_timezones(): @@ -14,182 +15,216 @@ def assemble_timezones(): """ timezones = {} - timezones['ACDT'] = gettz('Australia/Darwin') # Australian Central Daylight Savings Time (UTC+10:30) - timezones['ACST'] = gettz('Australia/Darwin') # Australian Central Standard Time (UTC+09:30) - timezones['ACT'] = gettz('Brazil/Acre') # Acre Time (UTC−05) - timezones['ADT'] = gettz('America/Halifax') # Atlantic Daylight Time (UTC−03) - timezones['AEDT'] = gettz('Australia/Sydney') # Australian Eastern Daylight Savings Time (UTC+11) - timezones['AEST'] = gettz('Australia/Sydney') # Australian Eastern Standard Time (UTC+10) - timezones['AFT'] = gettz('Asia/Kabul') # Afghanistan Time (UTC+04:30) - timezones['AKDT'] = gettz('America/Juneau') # Alaska Daylight Time (UTC−08) - timezones['AKST'] = gettz('America/Juneau') # Alaska Standard Time (UTC−09) - timezones['AMST'] = gettz('America/Manaus') # Amazon Summer Time (Brazil)[1] (UTC−03) - timezones['AMT'] = gettz('America/Manaus') # Amazon Time (Brazil)[2] (UTC−04) - timezones['ART'] = gettz('America/Cordoba') # Argentina Time (UTC−03) - timezones['AST'] = gettz('Asia/Riyadh') # Arabia Standard Time (UTC+03) - timezones['AWST'] = gettz('Australia/Perth') # Australian Western Standard Time (UTC+08) - timezones['AZOST'] = gettz('Atlantic/Azores') # Azores Summer Time (UTC±00) - timezones['AZOT'] = gettz('Atlantic/Azores') # Azores Standard Time (UTC−01) - timezones['AZT'] = gettz('Asia/Baku') # Azerbaijan Time (UTC+04) - timezones['BDT'] = gettz('Asia/Brunei') # Brunei Time (UTC+08) - timezones['BIOT'] = gettz('Etc/GMT+6') # British Indian Ocean Time (UTC+06) - timezones['BIT'] = gettz('Pacific/Funafuti') # Baker Island Time (UTC−12) - timezones['BOT'] = gettz('America/La_Paz') # Bolivia Time (UTC−04) - timezones['BRST'] = gettz('America/Sao_Paulo') # Brasilia Summer Time (UTC−02) - timezones['BRT'] = gettz('America/Sao_Paulo') # Brasilia Time (UTC−03) - timezones['BST'] = gettz('Asia/Dhaka') # Bangladesh Standard Time (UTC+06) - timezones['BTT'] = gettz('Asia/Thimphu') # Bhutan Time (UTC+06) - timezones['CAT'] = gettz('Africa/Harare') # Central Africa Time (UTC+02) - timezones['CCT'] = gettz('Indian/Cocos') # Cocos Islands Time (UTC+06:30) - timezones['CDT'] = gettz('America/Chicago') # Central Daylight Time (North America) (UTC−05) - timezones['CEST'] = gettz('Europe/Berlin') # Central European Summer Time (Cf. HAEC) (UTC+02) - timezones['CET'] = gettz('Europe/Berlin') # Central European Time (UTC+01) - timezones['CHADT'] = gettz('Pacific/Chatham') # Chatham Daylight Time (UTC+13:45) - timezones['CHAST'] = gettz('Pacific/Chatham') # Chatham Standard Time (UTC+12:45) - timezones['CHOST'] = gettz('Asia/Choibalsan') # Choibalsan Summer Time (UTC+09) - timezones['CHOT'] = gettz('Asia/Choibalsan') # Choibalsan Standard Time (UTC+08) - timezones['CHST'] = gettz('Pacific/Guam') # Chamorro Standard Time (UTC+10) - timezones['CHUT'] = gettz('Pacific/Chuuk') # Chuuk Time (UTC+10) - timezones['CIST'] = gettz('Etc/GMT-8') # Clipperton Island Standard Time (UTC−08) - timezones['CIT'] = gettz('Asia/Makassar') # Central Indonesia Time (UTC+08) - timezones['CKT'] = gettz('Pacific/Rarotonga') # Cook Island Time (UTC−10) - timezones['CLST'] = gettz('America/Santiago') # Chile Summer Time (UTC−03) - timezones['CLT'] = gettz('America/Santiago') # Chile Standard Time (UTC−04) - timezones['COST'] = gettz('America/Bogota') # Colombia Summer Time (UTC−04) - timezones['COT'] = gettz('America/Bogota') # Colombia Time (UTC−05) - timezones['CST'] = gettz('America/Chicago') # Central Standard Time (North America) (UTC−06) - timezones['CT'] = gettz('Asia/Chongqing') # China time (UTC+08) - timezones['CVT'] = gettz('Atlantic/Cape_Verde') # Cape Verde Time (UTC−01) - timezones['CXT'] = gettz('Indian/Christmas') # Christmas Island Time (UTC+07) - timezones['DAVT'] = gettz('Antarctica/Davis') # Davis Time (UTC+07) - timezones['DDUT'] = gettz('Antarctica/DumontDUrville') # Dumont d'Urville Time (UTC+10) - timezones['DFT'] = gettz('Europe/Berlin') # AIX equivalent of Central European Time (UTC+01) - timezones['EASST'] = gettz('Chile/EasterIsland') # Easter Island Summer Time (UTC−05) - timezones['EAST'] = gettz('Chile/EasterIsland') # Easter Island Standard Time (UTC−06) - timezones['EAT'] = gettz('Africa/Mogadishu') # East Africa Time (UTC+03) - timezones['ECT'] = gettz('America/Guayaquil') # Ecuador Time (UTC−05) - timezones['EDT'] = gettz('America/New_York') # Eastern Daylight Time (North America) (UTC−04) - timezones['EEST'] = gettz('Europe/Bucharest') # Eastern European Summer Time (UTC+03) - timezones['EET'] = gettz('Europe/Bucharest') # Eastern European Time (UTC+02) - timezones['EGST'] = gettz('America/Scoresbysund') # Eastern Greenland Summer Time (UTC±00) - timezones['EGT'] = gettz('America/Scoresbysund') # Eastern Greenland Time (UTC−01) - timezones['EIT'] = gettz('Asia/Jayapura') # Eastern Indonesian Time (UTC+09) - timezones['EST'] = gettz('America/New_York') # Eastern Standard Time (North America) (UTC−05) - timezones['FET'] = gettz('Europe/Minsk') # Further-eastern European Time (UTC+03) - timezones['FJT'] = gettz('Pacific/Fiji') # Fiji Time (UTC+12) - timezones['FKST'] = gettz('Atlantic/Stanley') # Falkland Islands Summer Time (UTC−03) - timezones['FKT'] = gettz('Atlantic/Stanley') # Falkland Islands Time (UTC−04) - timezones['FNT'] = gettz('Brazil/DeNoronha') # Fernando de Noronha Time (UTC−02) - timezones['GALT'] = gettz('Pacific/Galapagos') # Galapagos Time (UTC−06) - timezones['GAMT'] = gettz('Pacific/Gambier') # Gambier Islands (UTC−09) - timezones['GET'] = gettz('Asia/Tbilisi') # Georgia Standard Time (UTC+04) - timezones['GFT'] = gettz('America/Cayenne') # French Guiana Time (UTC−03) - timezones['GILT'] = gettz('Pacific/Tarawa') # Gilbert Island Time (UTC+12) - timezones['GIT'] = gettz('Pacific/Gambier') # Gambier Island Time (UTC−09) - timezones['GMT'] = gettz('GMT') # Greenwich Mean Time (UTC±00) - timezones['GST'] = gettz('Asia/Muscat') # Gulf Standard Time (UTC+04) - timezones['GYT'] = gettz('America/Guyana') # Guyana Time (UTC−04) - timezones['HADT'] = gettz('Pacific/Honolulu') # Hawaii-Aleutian Daylight Time (UTC−09) - timezones['HAEC'] = gettz('Europe/Paris') # Heure Avancée d'Europe Centrale (CEST) (UTC+02) - timezones['HAST'] = gettz('Pacific/Honolulu') # Hawaii-Aleutian Standard Time (UTC−10) - timezones['HKT'] = gettz('Asia/Hong_Kong') # Hong Kong Time (UTC+08) - timezones['HMT'] = gettz('Indian/Kerguelen') # Heard and McDonald Islands Time (UTC+05) - timezones['HOVST'] = gettz('Asia/Hovd') # Khovd Summer Time (UTC+08) - timezones['HOVT'] = gettz('Asia/Hovd') # Khovd Standard Time (UTC+07) - timezones['ICT'] = gettz('Asia/Ho_Chi_Minh') # Indochina Time (UTC+07) - timezones['IDT'] = gettz('Asia/Jerusalem') # Israel Daylight Time (UTC+03) - timezones['IOT'] = gettz('Etc/GMT+3') # Indian Ocean Time (UTC+03) - timezones['IRDT'] = gettz('Asia/Tehran') # Iran Daylight Time (UTC+04:30) - timezones['IRKT'] = gettz('Asia/Irkutsk') # Irkutsk Time (UTC+08) - timezones['IRST'] = gettz('Asia/Tehran') # Iran Standard Time (UTC+03:30) - timezones['IST'] = gettz('Asia/Kolkata') # Indian Standard Time (UTC+05:30) - timezones['JST'] = gettz('Asia/Tokyo') # Japan Standard Time (UTC+09) - timezones['KGT'] = gettz('Asia/Bishkek') # Kyrgyzstan time (UTC+06) - timezones['KOST'] = gettz('Pacific/Kosrae') # Kosrae Time (UTC+11) - timezones['KRAT'] = gettz('Asia/Krasnoyarsk') # Krasnoyarsk Time (UTC+07) - timezones['KST'] = gettz('Asia/Seoul') # Korea Standard Time (UTC+09) - timezones['LHST'] = gettz('Australia/Lord_Howe') # Lord Howe Standard Time (UTC+10:30) - timezones['LINT'] = gettz('Pacific/Kiritimati') # Line Islands Time (UTC+14) - timezones['MAGT'] = gettz('Asia/Magadan') # Magadan Time (UTC+12) - timezones['MART'] = gettz('Pacific/Marquesas') # Marquesas Islands Time (UTC−09:30) - timezones['MAWT'] = gettz('Antarctica/Mawson') # Mawson Station Time (UTC+05) - timezones['MDT'] = gettz('America/Denver') # Mountain Daylight Time (North America) (UTC−06) - timezones['MEST'] = gettz('Europe/Paris') # Middle European Summer Time Same zone as CEST (UTC+02) - timezones['MET'] = gettz('Europe/Berlin') # Middle European Time Same zone as CET (UTC+01) - timezones['MHT'] = gettz('Pacific/Kwajalein') # Marshall Islands (UTC+12) - timezones['MIST'] = gettz('Antarctica/Macquarie') # Macquarie Island Station Time (UTC+11) - timezones['MIT'] = gettz('Pacific/Marquesas') # Marquesas Islands Time (UTC−09:30) - timezones['MMT'] = gettz('Asia/Rangoon') # Myanmar Standard Time (UTC+06:30) - timezones['MSK'] = gettz('Europe/Moscow') # Moscow Time (UTC+03) - timezones['MST'] = gettz('America/Denver') # Mountain Standard Time (North America) (UTC−07) - timezones['MUT'] = gettz('Indian/Mauritius') # Mauritius Time (UTC+04) - timezones['MVT'] = gettz('Indian/Maldives') # Maldives Time (UTC+05) - timezones['MYT'] = gettz('Asia/Kuching') # Malaysia Time (UTC+08) - timezones['NCT'] = gettz('Pacific/Noumea') # New Caledonia Time (UTC+11) - timezones['NDT'] = gettz('Canada/Newfoundland') # Newfoundland Daylight Time (UTC−02:30) - timezones['NFT'] = gettz('Pacific/Norfolk') # Norfolk Time (UTC+11) - timezones['NPT'] = gettz('Asia/Kathmandu') # Nepal Time (UTC+05:45) - timezones['NST'] = gettz('Canada/Newfoundland') # Newfoundland Standard Time (UTC−03:30) - timezones['NT'] = gettz('Canada/Newfoundland') # Newfoundland Time (UTC−03:30) - timezones['NUT'] = gettz('Pacific/Niue') # Niue Time (UTC−11) - timezones['NZDT'] = gettz('Pacific/Auckland') # New Zealand Daylight Time (UTC+13) - timezones['NZST'] = gettz('Pacific/Auckland') # New Zealand Standard Time (UTC+12) - timezones['OMST'] = gettz('Asia/Omsk') # Omsk Time (UTC+06) - timezones['ORAT'] = gettz('Asia/Oral') # Oral Time (UTC+05) - timezones['PDT'] = gettz('America/Los_Angeles') # Pacific Daylight Time (North America) (UTC−07) - timezones['PET'] = gettz('America/Lima') # Peru Time (UTC−05) - timezones['PETT'] = gettz('Asia/Kamchatka') # Kamchatka Time (UTC+12) - timezones['PGT'] = gettz('Pacific/Port_Moresby') # Papua New Guinea Time (UTC+10) - timezones['PHOT'] = gettz('Pacific/Enderbury') # Phoenix Island Time (UTC+13) - timezones['PKT'] = gettz('Asia/Karachi') # Pakistan Standard Time (UTC+05) - timezones['PMDT'] = gettz('America/Miquelon') # Saint Pierre and Miquelon Daylight time (UTC−02) - timezones['PMST'] = gettz('America/Miquelon') # Saint Pierre and Miquelon Standard Time (UTC−03) - timezones['PONT'] = gettz('Pacific/Pohnpei') # Pohnpei Standard Time (UTC+11) - timezones['PST'] = gettz('America/Los_Angeles') # Pacific Standard Time (North America) (UTC−08) - timezones['PYST'] = gettz('America/Asuncion') # Paraguay Summer Time (South America)[7] (UTC−03) - timezones['PYT'] = gettz('America/Asuncion') # Paraguay Time (South America)[8] (UTC−04) - timezones['RET'] = gettz('Indian/Reunion') # Réunion Time (UTC+04) - timezones['ROTT'] = gettz('Antarctica/Rothera') # Rothera Research Station Time (UTC−03) - timezones['SAKT'] = gettz('Asia/Vladivostok') # Sakhalin Island time (UTC+11) - timezones['SAMT'] = gettz('Europe/Samara') # Samara Time (UTC+04) - timezones['SAST'] = gettz('Africa/Johannesburg') # South African Standard Time (UTC+02) - timezones['SBT'] = gettz('Pacific/Guadalcanal') # Solomon Islands Time (UTC+11) - timezones['SCT'] = gettz('Indian/Mahe') # Seychelles Time (UTC+04) - timezones['SGT'] = gettz('Asia/Singapore') # Singapore Time (UTC+08) - timezones['SLST'] = gettz('Asia/Colombo') # Sri Lanka Standard Time (UTC+05:30) - timezones['SRET'] = gettz('Asia/Srednekolymsk') # Srednekolymsk Time (UTC+11) - timezones['SRT'] = gettz('America/Paramaribo') # Suriname Time (UTC−03) - timezones['SST'] = gettz('Asia/Singapore') # Singapore Standard Time (UTC+08) - timezones['SYOT'] = gettz('Antarctica/Syowa') # Showa Station Time (UTC+03) - timezones['TAHT'] = gettz('Pacific/Tahiti') # Tahiti Time (UTC−10) - timezones['TFT'] = gettz('Indian/Kerguelen') # Indian/Kerguelen (UTC+05) - timezones['THA'] = gettz('Asia/Bangkok') # Thailand Standard Time (UTC+07) - timezones['TJT'] = gettz('Asia/Dushanbe') # Tajikistan Time (UTC+05) - timezones['TKT'] = gettz('Pacific/Fakaofo') # Tokelau Time (UTC+13) - timezones['TLT'] = gettz('Asia/Dili') # Timor Leste Time (UTC+09) - timezones['TMT'] = gettz('Asia/Ashgabat') # Turkmenistan Time (UTC+05) - timezones['TOT'] = gettz('Pacific/Tongatapu') # Tonga Time (UTC+13) - timezones['TVT'] = gettz('Pacific/Funafuti') # Tuvalu Time (UTC+12) - timezones['ULAST'] = gettz('Asia/Ulan_Bator') # Ulaanbaatar Summer Time (UTC+09) - timezones['ULAT'] = gettz('Asia/Ulan_Bator') # Ulaanbaatar Standard Time (UTC+08) - timezones['USZ1'] = gettz('Europe/Kaliningrad') # Kaliningrad Time (UTC+02) - timezones['UTC'] = gettz('UTC') # Coordinated Universal Time (UTC±00) - timezones['UYST'] = gettz('America/Montevideo') # Uruguay Summer Time (UTC−02) - timezones['UYT'] = gettz('America/Montevideo') # Uruguay Standard Time (UTC−03) - timezones['UZT'] = gettz('Asia/Tashkent') # Uzbekistan Time (UTC+05) - timezones['VET'] = gettz('America/Caracas') # Venezuelan Standard Time (UTC−04) - timezones['VLAT'] = gettz('Asia/Vladivostok') # Vladivostok Time (UTC+10) - timezones['VOLT'] = gettz('Europe/Volgograd') # Volgograd Time (UTC+04) - timezones['VOST'] = gettz('Antarctica/Vostok') # Vostok Station Time (UTC+06) - timezones['VUT'] = gettz('Pacific/Efate') # Vanuatu Time (UTC+11) - timezones['WAKT'] = gettz('Pacific/Wake') # Wake Island Time (UTC+12) - timezones['WAST'] = gettz('Africa/Lagos') # West Africa Summer Time (UTC+02) - timezones['WAT'] = gettz('Africa/Lagos') # West Africa Time (UTC+01) - timezones['WEST'] = gettz('Europe/London') # Western European Summer Time (UTC+01) - timezones['WET'] = gettz('Europe/London') # Western European Time (UTC±00) - timezones['WIT'] = gettz('Asia/Jakarta') # Western Indonesian Time (UTC+07) - timezones['WST'] = gettz('Australia/Perth') # Western Standard Time (UTC+08) - timezones['YAKT'] = gettz('Asia/Yakutsk') # Yakutsk Time (UTC+09) - timezones['YEKT'] = gettz('Asia/Yekaterinburg') # Yekaterinburg Time (UTC+05) + timezones["ACDT"] = timezone( + "Australia/Darwin" + ) # Australian Central Daylight Savings Time (UTC+10:30) + timezones["ACST"] = timezone( + "Australia/Darwin" + ) # Australian Central Standard Time (UTC+09:30) + timezones["ACT"] = timezone("Brazil/Acre") # Acre Time (UTC−05) + timezones["ADT"] = timezone("America/Halifax") # Atlantic Daylight Time (UTC−03) + timezones["AEDT"] = timezone( + "Australia/Sydney" + ) # Australian Eastern Daylight Savings Time (UTC+11) + timezones["AEST"] = timezone("Australia/Sydney") # Australian Eastern Standard Time (UTC+10) + timezones["AFT"] = timezone("Asia/Kabul") # Afghanistan Time (UTC+04:30) + timezones["AKDT"] = timezone("America/Juneau") # Alaska Daylight Time (UTC−08) + timezones["AKST"] = timezone("America/Juneau") # Alaska Standard Time (UTC−09) + timezones["AMST"] = timezone("America/Manaus") # Amazon Summer Time (Brazil)[1] (UTC−03) + timezones["AMT"] = timezone("America/Manaus") # Amazon Time (Brazil)[2] (UTC−04) + timezones["ART"] = timezone("America/Cordoba") # Argentina Time (UTC−03) + timezones["AST"] = timezone("Asia/Riyadh") # Arabia Standard Time (UTC+03) + timezones["AWST"] = timezone("Australia/Perth") # Australian Western Standard Time (UTC+08) + timezones["AZOST"] = timezone("Atlantic/Azores") # Azores Summer Time (UTC±00) + timezones["AZOT"] = timezone("Atlantic/Azores") # Azores Standard Time (UTC−01) + timezones["AZT"] = timezone("Asia/Baku") # Azerbaijan Time (UTC+04) + timezones["BDT"] = timezone("Asia/Brunei") # Brunei Time (UTC+08) + timezones["BIOT"] = timezone("Etc/GMT+6") # British Indian Ocean Time (UTC+06) + timezones["BIT"] = timezone("Pacific/Funafuti") # Baker Island Time (UTC−12) + timezones["BOT"] = timezone("America/La_Paz") # Bolivia Time (UTC−04) + timezones["BRST"] = timezone("America/Sao_Paulo") # Brasilia Summer Time (UTC−02) + timezones["BRT"] = timezone("America/Sao_Paulo") # Brasilia Time (UTC−03) + timezones["BST"] = timezone("Asia/Dhaka") # Bangladesh Standard Time (UTC+06) + timezones["BTT"] = timezone("Asia/Thimphu") # Bhutan Time (UTC+06) + timezones["CAT"] = timezone("Africa/Harare") # Central Africa Time (UTC+02) + timezones["CCT"] = timezone("Indian/Cocos") # Cocos Islands Time (UTC+06:30) + timezones["CDT"] = timezone( + "America/Chicago" + ) # Central Daylight Time (North America) (UTC−05) + timezones["CEST"] = timezone( + "Europe/Berlin" + ) # Central European Summer Time (Cf. HAEC) (UTC+02) + timezones["CET"] = timezone("Europe/Berlin") # Central European Time (UTC+01) + timezones["CHADT"] = timezone("Pacific/Chatham") # Chatham Daylight Time (UTC+13:45) + timezones["CHAST"] = timezone("Pacific/Chatham") # Chatham Standard Time (UTC+12:45) + timezones["CHOST"] = timezone("Asia/Choibalsan") # Choibalsan Summer Time (UTC+09) + timezones["CHOT"] = timezone("Asia/Choibalsan") # Choibalsan Standard Time (UTC+08) + timezones["CHST"] = timezone("Pacific/Guam") # Chamorro Standard Time (UTC+10) + timezones["CHUT"] = timezone("Pacific/Chuuk") # Chuuk Time (UTC+10) + timezones["CIST"] = timezone("Etc/GMT-8") # Clipperton Island Standard Time (UTC−08) + timezones["CIT"] = timezone("Asia/Makassar") # Central Indonesia Time (UTC+08) + timezones["CKT"] = timezone("Pacific/Rarotonga") # Cook Island Time (UTC−10) + timezones["CLST"] = timezone("America/Santiago") # Chile Summer Time (UTC−03) + timezones["CLT"] = timezone("America/Santiago") # Chile Standard Time (UTC−04) + timezones["COST"] = timezone("America/Bogota") # Colombia Summer Time (UTC−04) + timezones["COT"] = timezone("America/Bogota") # Colombia Time (UTC−05) + timezones["CST"] = timezone( + "America/Chicago" + ) # Central Standard Time (North America) (UTC−06) + timezones["CT"] = timezone("Asia/Chongqing") # China time (UTC+08) + timezones["CVT"] = timezone("Atlantic/Cape_Verde") # Cape Verde Time (UTC−01) + timezones["CXT"] = timezone("Indian/Christmas") # Christmas Island Time (UTC+07) + timezones["DAVT"] = timezone("Antarctica/Davis") # Davis Time (UTC+07) + timezones["DDUT"] = timezone("Antarctica/DumontDUrville") # Dumont d'Urville Time (UTC+10) + timezones["DFT"] = timezone( + "Europe/Berlin" + ) # AIX equivalent of Central European Time (UTC+01) + timezones["EASST"] = timezone("Chile/EasterIsland") # Easter Island Summer Time (UTC−05) + timezones["EAST"] = timezone("Chile/EasterIsland") # Easter Island Standard Time (UTC−06) + timezones["EAT"] = timezone("Africa/Mogadishu") # East Africa Time (UTC+03) + timezones["ECT"] = timezone("America/Guayaquil") # Ecuador Time (UTC−05) + timezones["EDT"] = timezone( + "America/New_York" + ) # Eastern Daylight Time (North America) (UTC−04) + timezones["EEST"] = timezone("Europe/Bucharest") # Eastern European Summer Time (UTC+03) + timezones["EET"] = timezone("Europe/Bucharest") # Eastern European Time (UTC+02) + timezones["EGST"] = timezone("America/Scoresbysund") # Eastern Greenland Summer Time (UTC±00) + timezones["EGT"] = timezone("America/Scoresbysund") # Eastern Greenland Time (UTC−01) + timezones["EIT"] = timezone("Asia/Jayapura") # Eastern Indonesian Time (UTC+09) + timezones["EST"] = timezone( + "America/New_York" + ) # Eastern Standard Time (North America) (UTC−05) + timezones["FET"] = timezone("Europe/Minsk") # Further-eastern European Time (UTC+03) + timezones["FJT"] = timezone("Pacific/Fiji") # Fiji Time (UTC+12) + timezones["FKST"] = timezone("Atlantic/Stanley") # Falkland Islands Summer Time (UTC−03) + timezones["FKT"] = timezone("Atlantic/Stanley") # Falkland Islands Time (UTC−04) + timezones["FNT"] = timezone("Brazil/DeNoronha") # Fernando de Noronha Time (UTC−02) + timezones["GALT"] = timezone("Pacific/Galapagos") # Galapagos Time (UTC−06) + timezones["GAMT"] = timezone("Pacific/Gambier") # Gambier Islands (UTC−09) + timezones["GET"] = timezone("Asia/Tbilisi") # Georgia Standard Time (UTC+04) + timezones["GFT"] = timezone("America/Cayenne") # French Guiana Time (UTC−03) + timezones["GILT"] = timezone("Pacific/Tarawa") # Gilbert Island Time (UTC+12) + timezones["GIT"] = timezone("Pacific/Gambier") # Gambier Island Time (UTC−09) + timezones["GMT"] = timezone("GMT") # Greenwich Mean Time (UTC±00) + timezones["GST"] = timezone("Asia/Muscat") # Gulf Standard Time (UTC+04) + timezones["GYT"] = timezone("America/Guyana") # Guyana Time (UTC−04) + timezones["HADT"] = timezone("Pacific/Honolulu") # Hawaii-Aleutian Daylight Time (UTC−09) + timezones["HAEC"] = timezone("Europe/Paris") # Heure Avancée d'Europe Centrale (CEST) (UTC+02) + timezones["HAST"] = timezone("Pacific/Honolulu") # Hawaii-Aleutian Standard Time (UTC−10) + timezones["HKT"] = timezone("Asia/Hong_Kong") # Hong Kong Time (UTC+08) + timezones["HMT"] = timezone("Indian/Kerguelen") # Heard and McDonald Islands Time (UTC+05) + timezones["HOVST"] = timezone("Asia/Hovd") # Khovd Summer Time (UTC+08) + timezones["HOVT"] = timezone("Asia/Hovd") # Khovd Standard Time (UTC+07) + timezones["ICT"] = timezone("Asia/Ho_Chi_Minh") # Indochina Time (UTC+07) + timezones["IDT"] = timezone("Asia/Jerusalem") # Israel Daylight Time (UTC+03) + timezones["IOT"] = timezone("Etc/GMT+3") # Indian Ocean Time (UTC+03) + timezones["IRDT"] = timezone("Asia/Tehran") # Iran Daylight Time (UTC+04:30) + timezones["IRKT"] = timezone("Asia/Irkutsk") # Irkutsk Time (UTC+08) + timezones["IRST"] = timezone("Asia/Tehran") # Iran Standard Time (UTC+03:30) + timezones["IST"] = timezone("Asia/Kolkata") # Indian Standard Time (UTC+05:30) + timezones["JST"] = timezone("Asia/Tokyo") # Japan Standard Time (UTC+09) + timezones["KGT"] = timezone("Asia/Bishkek") # Kyrgyzstan time (UTC+06) + timezones["KOST"] = timezone("Pacific/Kosrae") # Kosrae Time (UTC+11) + timezones["KRAT"] = timezone("Asia/Krasnoyarsk") # Krasnoyarsk Time (UTC+07) + timezones["KST"] = timezone("Asia/Seoul") # Korea Standard Time (UTC+09) + timezones["LHST"] = timezone("Australia/Lord_Howe") # Lord Howe Standard Time (UTC+10:30) + timezones["LINT"] = timezone("Pacific/Kiritimati") # Line Islands Time (UTC+14) + timezones["MAGT"] = timezone("Asia/Magadan") # Magadan Time (UTC+12) + timezones["MART"] = timezone("Pacific/Marquesas") # Marquesas Islands Time (UTC−09:30) + timezones["MAWT"] = timezone("Antarctica/Mawson") # Mawson Station Time (UTC+05) + timezones["MDT"] = timezone( + "America/Denver" + ) # Mountain Daylight Time (North America) (UTC−06) + timezones["MEST"] = timezone( + "Europe/Paris" + ) # Middle European Summer Time Same zone as CEST (UTC+02) + timezones["MET"] = timezone("Europe/Berlin") # Middle European Time Same zone as CET (UTC+01) + timezones["MHT"] = timezone("Pacific/Kwajalein") # Marshall Islands (UTC+12) + timezones["MIST"] = timezone("Antarctica/Macquarie") # Macquarie Island Station Time (UTC+11) + timezones["MIT"] = timezone("Pacific/Marquesas") # Marquesas Islands Time (UTC−09:30) + timezones["MMT"] = timezone("Asia/Rangoon") # Myanmar Standard Time (UTC+06:30) + timezones["MSK"] = timezone("Europe/Moscow") # Moscow Time (UTC+03) + timezones["MST"] = timezone( + "America/Denver" + ) # Mountain Standard Time (North America) (UTC−07) + timezones["MUT"] = timezone("Indian/Mauritius") # Mauritius Time (UTC+04) + timezones["MVT"] = timezone("Indian/Maldives") # Maldives Time (UTC+05) + timezones["MYT"] = timezone("Asia/Kuching") # Malaysia Time (UTC+08) + timezones["NCT"] = timezone("Pacific/Noumea") # New Caledonia Time (UTC+11) + timezones["NDT"] = timezone("Canada/Newfoundland") # Newfoundland Daylight Time (UTC−02:30) + timezones["NFT"] = timezone("Pacific/Norfolk") # Norfolk Time (UTC+11) + timezones["NPT"] = timezone("Asia/Kathmandu") # Nepal Time (UTC+05:45) + timezones["NST"] = timezone("Canada/Newfoundland") # Newfoundland Standard Time (UTC−03:30) + timezones["NT"] = timezone("Canada/Newfoundland") # Newfoundland Time (UTC−03:30) + timezones["NUT"] = timezone("Pacific/Niue") # Niue Time (UTC−11) + timezones["NZDT"] = timezone("Pacific/Auckland") # New Zealand Daylight Time (UTC+13) + timezones["NZST"] = timezone("Pacific/Auckland") # New Zealand Standard Time (UTC+12) + timezones["OMST"] = timezone("Asia/Omsk") # Omsk Time (UTC+06) + timezones["ORAT"] = timezone("Asia/Oral") # Oral Time (UTC+05) + timezones["PDT"] = timezone( + "America/Los_Angeles" + ) # Pacific Daylight Time (North America) (UTC−07) + timezones["PET"] = timezone("America/Lima") # Peru Time (UTC−05) + timezones["PETT"] = timezone("Asia/Kamchatka") # Kamchatka Time (UTC+12) + timezones["PGT"] = timezone("Pacific/Port_Moresby") # Papua New Guinea Time (UTC+10) + timezones["PHOT"] = timezone("Pacific/Enderbury") # Phoenix Island Time (UTC+13) + timezones["PKT"] = timezone("Asia/Karachi") # Pakistan Standard Time (UTC+05) + timezones["PMDT"] = timezone( + "America/Miquelon" + ) # Saint Pierre and Miquelon Daylight time (UTC−02) + timezones["PMST"] = timezone( + "America/Miquelon" + ) # Saint Pierre and Miquelon Standard Time (UTC−03) + timezones["PONT"] = timezone("Pacific/Pohnpei") # Pohnpei Standard Time (UTC+11) + timezones["PST"] = timezone( + "America/Los_Angeles" + ) # Pacific Standard Time (North America) (UTC−08) + timezones["PYST"] = timezone( + "America/Asuncion" + ) # Paraguay Summer Time (South America)[7] (UTC−03) + timezones["PYT"] = timezone("America/Asuncion") # Paraguay Time (South America)[8] (UTC−04) + timezones["RET"] = timezone("Indian/Reunion") # Réunion Time (UTC+04) + timezones["ROTT"] = timezone("Antarctica/Rothera") # Rothera Research Station Time (UTC−03) + timezones["SAKT"] = timezone("Asia/Vladivostok") # Sakhalin Island time (UTC+11) + timezones["SAMT"] = timezone("Europe/Samara") # Samara Time (UTC+04) + timezones["SAST"] = timezone("Africa/Johannesburg") # South African Standard Time (UTC+02) + timezones["SBT"] = timezone("Pacific/Guadalcanal") # Solomon Islands Time (UTC+11) + timezones["SCT"] = timezone("Indian/Mahe") # Seychelles Time (UTC+04) + timezones["SGT"] = timezone("Asia/Singapore") # Singapore Time (UTC+08) + timezones["SLST"] = timezone("Asia/Colombo") # Sri Lanka Standard Time (UTC+05:30) + timezones["SRET"] = timezone("Asia/Srednekolymsk") # Srednekolymsk Time (UTC+11) + timezones["SRT"] = timezone("America/Paramaribo") # Suriname Time (UTC−03) + timezones["SST"] = timezone("Asia/Singapore") # Singapore Standard Time (UTC+08) + timezones["SYOT"] = timezone("Antarctica/Syowa") # Showa Station Time (UTC+03) + timezones["TAHT"] = timezone("Pacific/Tahiti") # Tahiti Time (UTC−10) + timezones["TFT"] = timezone("Indian/Kerguelen") # Indian/Kerguelen (UTC+05) + timezones["THA"] = timezone("Asia/Bangkok") # Thailand Standard Time (UTC+07) + timezones["TJT"] = timezone("Asia/Dushanbe") # Tajikistan Time (UTC+05) + timezones["TKT"] = timezone("Pacific/Fakaofo") # Tokelau Time (UTC+13) + timezones["TLT"] = timezone("Asia/Dili") # Timor Leste Time (UTC+09) + timezones["TMT"] = timezone("Asia/Ashgabat") # Turkmenistan Time (UTC+05) + timezones["TOT"] = timezone("Pacific/Tongatapu") # Tonga Time (UTC+13) + timezones["TVT"] = timezone("Pacific/Funafuti") # Tuvalu Time (UTC+12) + timezones["ULAST"] = timezone("Asia/Ulan_Bator") # Ulaanbaatar Summer Time (UTC+09) + timezones["ULAT"] = timezone("Asia/Ulan_Bator") # Ulaanbaatar Standard Time (UTC+08) + timezones["USZ1"] = timezone("Europe/Kaliningrad") # Kaliningrad Time (UTC+02) + timezones["UTC"] = timezone("UTC") # Coordinated Universal Time (UTC±00) + timezones["UYST"] = timezone("America/Montevideo") # Uruguay Summer Time (UTC−02) + timezones["UYT"] = timezone("America/Montevideo") # Uruguay Standard Time (UTC−03) + timezones["UZT"] = timezone("Asia/Tashkent") # Uzbekistan Time (UTC+05) + timezones["VET"] = timezone("America/Caracas") # Venezuelan Standard Time (UTC−04) + timezones["VLAT"] = timezone("Asia/Vladivostok") # Vladivostok Time (UTC+10) + timezones["VOLT"] = timezone("Europe/Volgograd") # Volgograd Time (UTC+04) + timezones["VOST"] = timezone("Antarctica/Vostok") # Vostok Station Time (UTC+06) + timezones["VUT"] = timezone("Pacific/Efate") # Vanuatu Time (UTC+11) + timezones["WAKT"] = timezone("Pacific/Wake") # Wake Island Time (UTC+12) + timezones["WAST"] = timezone("Africa/Lagos") # West Africa Summer Time (UTC+02) + timezones["WAT"] = timezone("Africa/Lagos") # West Africa Time (UTC+01) + timezones["WEST"] = timezone("Europe/London") # Western European Summer Time (UTC+01) + timezones["WET"] = timezone("Europe/London") # Western European Time (UTC±00) + timezones["WIT"] = timezone("Asia/Jakarta") # Western Indonesian Time (UTC+07) + timezones["WST"] = timezone("Australia/Perth") # Western Standard Time (UTC+08) + timezones["YAKT"] = timezone("Asia/Yakutsk") # Yakutsk Time (UTC+09) + timezones["YEKT"] = timezone("Asia/Yekaterinburg") # Yekaterinburg Time (UTC+05) - return timezones \ No newline at end of file + return timezones diff --git a/flag/flag.py b/flag/flag.py index 035ae1d..6216f65 100644 --- a/flag/flag.py +++ b/flag/flag.py @@ -54,7 +54,7 @@ class Flag(Cog): async def flagset(self, ctx: commands.Context): """ My custom cog - + Extra information goes here """ if ctx.invoked_subcommand is None: diff --git a/forcemention/forcemention.py b/forcemention/forcemention.py index dd8948c..3df68e5 100644 --- a/forcemention/forcemention.py +++ b/forcemention/forcemention.py @@ -30,8 +30,8 @@ class ForceMention(Cog): @commands.command() async def forcemention(self, ctx: commands.Context, role: str, *, message=""): """ - Mentions that role, regardless if it's unmentionable - """ + Mentions that role, regardless if it's unmentionable + """ role_obj = get(ctx.guild.roles, name=role) if role_obj is None: await ctx.maybe_send_embed("Couldn't find role named {}".format(role)) diff --git a/hangman/__init__.py b/hangman/__init__.py index dbc62e7..35012c4 100644 --- a/hangman/__init__.py +++ b/hangman/__init__.py @@ -6,4 +6,3 @@ def setup(bot): n = Hangman(bot) data_manager.bundled_data_path(n) bot.add_cog(n) - bot.add_listener(n.on_react, "on_reaction_add") diff --git a/hangman/hangman.py b/hangman/hangman.py index c7d005d..2b6ab07 100644 --- a/hangman/hangman.py +++ b/hangman/hangman.py @@ -50,27 +50,27 @@ class Hangman(Cog): theface = await self.config.guild(guild).theface() self.hanglist[guild] = ( """> - \_________ + \\_________ |/ | | | | | - |\___ + |\\___ """, """> - \_________ + \\_________ |/ | | | | | | - |\___ + |\\___ H""", """> - \_________ + \\_________ |/ | | """ + theface @@ -79,10 +79,10 @@ class Hangman(Cog): | | | - |\___ + |\\___ HA""", """> - \________ + \\________ |/ | | """ + theface @@ -91,10 +91,10 @@ class Hangman(Cog): | | | | - |\___ + |\\___ HAN""", """> - \_________ + \\_________ |/ | | """ + theface @@ -103,43 +103,43 @@ class Hangman(Cog): | | | | - |\___ + |\\___ HANG""", """> - \_________ + \\_________ |/ | | """ + theface + """ - | /|\ + | /|\\ | | | | - |\___ + |\\___ HANGM""", """> - \________ + \\________ |/ | | """ + theface + """ - | /|\ + | /|\\ | | | / | - |\___ + |\\___ HANGMA""", """> - \________ + \\________ |/ | | """ + theface + """ - | /|\ + | /|\\ | | - | / \ + | / \\ | - |\___ + |\\___ HANGMAN""", ) @@ -255,7 +255,7 @@ class Hangman(Cog): elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": out_str += "__" + i + "__ " else: - out_str += "**\_** " + out_str += "**\\_** " self.winbool[guild] = False return out_str @@ -286,10 +286,10 @@ class Hangman(Cog): await self._reprintgame(message) - @commands.Cog.listener() + @commands.Cog.listener("on_reaction_add") async def on_react(self, reaction, user: Union[discord.User, discord.Member]): - """ Thanks to flapjack reactpoll for guidelines - https://github.com/flapjax/FlapJack-Cogs/blob/master/reactpoll/reactpoll.py""" + """Thanks to flapjack reactpoll for guidelines + https://github.com/flapjax/FlapJack-Cogs/blob/master/reactpoll/reactpoll.py""" guild: discord.Guild = getattr(user, "guild", None) if guild is None: return diff --git a/lovecalculator/lovecalculator.py b/lovecalculator/lovecalculator.py index 95e9f97..20504bd 100644 --- a/lovecalculator/lovecalculator.py +++ b/lovecalculator/lovecalculator.py @@ -33,7 +33,7 @@ class LoveCalculator(Cog): x.replace(" ", "+"), y.replace(" ", "+") ) async with aiohttp.ClientSession(headers={"Connection": "keep-alive"}) as session: - async with session.get(url) as response: + async with session.get(url, ssl=False) as response: assert response.status == 200 resp = await response.text() @@ -60,14 +60,11 @@ class LoveCalculator(Cog): else: emoji = "💔" title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}" - except: + except (TypeError, ValueError): title = "Dr. Love has left a note for you." em = discord.Embed( - title=title, - description=result_text, - color=discord.Color.red(), - url=f"https://www.lovecalculator.com/{result_image}", + title=title, description=result_text, color=discord.Color.red(), url=url ) - + em.set_image(url=f"https://www.lovecalculator.com/{result_image}") await ctx.send(embed=em) diff --git a/lseen/lseen.py b/lseen/lseen.py index a451f57..3348b65 100644 --- a/lseen/lseen.py +++ b/lseen/lseen.py @@ -75,9 +75,7 @@ class LastSeen(Cog): else: last_seen = await self.config.member(member).seen() if last_seen is None: - await ctx.maybe_send_embed( - embed=discord.Embed(description="I've never seen this user") - ) + await ctx.maybe_send_embed("I've never seen this user") return last_seen = self.get_date_time(last_seen) diff --git a/nudity/nudity.py b/nudity/nudity.py index 0d46ca9..4233460 100644 --- a/nudity/nudity.py +++ b/nudity/nudity.py @@ -85,7 +85,9 @@ class Nudity(commands.Cog): if r["unsafe"] > 0.7: await nsfw_channel.send( "NSFW Image from {}".format(message.channel.mention), - file=discord.File(image,), + file=discord.File( + image, + ), ) @commands.Cog.listener() diff --git a/planttycoon/planttycoon.py b/planttycoon/planttycoon.py index 61e5e06..665fc9a 100644 --- a/planttycoon/planttycoon.py +++ b/planttycoon/planttycoon.py @@ -360,7 +360,9 @@ class PlantTycoon(commands.Cog): ``{0}prune``: Prune your plant.\n""" em = discord.Embed( - title=title, description=description.format(prefix), color=discord.Color.green(), + title=title, + description=description.format(prefix), + color=discord.Color.green(), ) em.set_thumbnail(url="https://image.prntscr.com/image/AW7GuFIBSeyEgkR2W3SeiQ.png") em.set_footer( @@ -525,7 +527,8 @@ class PlantTycoon(commands.Cog): if t: em = discord.Embed( - title="Plant statistics of {}".format(plant["name"]), color=discord.Color.green(), + title="Plant statistics of {}".format(plant["name"]), + color=discord.Color.green(), ) em.set_thumbnail(url=plant["image"]) em.add_field(name="**Name**", value=plant["name"]) @@ -583,7 +586,8 @@ class PlantTycoon(commands.Cog): author = ctx.author if product is None: em = discord.Embed( - title="All gardening supplies that you can buy:", color=discord.Color.green(), + title="All gardening supplies that you can buy:", + color=discord.Color.green(), ) for pd in self.products: em.add_field( @@ -616,8 +620,11 @@ class PlantTycoon(commands.Cog): await gardener.save_gardener() message = "You bought {}.".format(product.lower()) else: - message = "You don't have enough Thneeds. You have {}, but need {}.".format( - gardener.points, self.products[product.lower()]["cost"] * amount, + message = ( + "You don't have enough Thneeds. You have {}, but need {}.".format( + gardener.points, + self.products[product.lower()]["cost"] * amount, + ) ) else: message = "I don't have this product." diff --git a/rpsls/rpsls.py b/rpsls/rpsls.py index b831d1c..ed2e1dc 100644 --- a/rpsls/rpsls.py +++ b/rpsls/rpsls.py @@ -28,8 +28,8 @@ class RPSLS(Cog): @commands.command() async def rpsls(self, ctx: commands.Context, choice: str): """ - Play Rock Paper Scissors Lizard Spock by Sam Kass in Discord! - + Play Rock Paper Scissors Lizard Spock by Sam Kass in Discord! + Rules: Scissors cuts Paper Paper covers Rock diff --git a/timerole/timerole.py b/timerole/timerole.py index 3815dcd..7484267 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -157,7 +157,7 @@ class Timerole(Cog): await ctx.maybe_send_embed(out) async def timerole_update(self): - for guild in self.bot.guilds: + async for guild in AsyncIter(self.bot.guilds): addlist = [] removelist = [] @@ -200,7 +200,7 @@ class Timerole(Cog): async def announce_roles(self, title, role_list, channel, guild, to_add: True): results = "" - for member, role_id in role_list: + async for member, role_id in AsyncIter(role_list): role = discord.utils.get(guild.roles, id=role_id) try: if to_add: @@ -219,7 +219,7 @@ class Timerole(Cog): log.info(results) async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict): - for role_id in check_roles: + async for role_id in AsyncIter(check_roles): # Check for required role if "required" in role_dict[str(role_id)]: if not set(role_dict[str(role_id)]["required"]) & set(has_roles): @@ -242,6 +242,3 @@ class Timerole(Cog): while self is self.bot.get_cog("Timerole"): await self.timerole_update() await sleep_till_next_hour() - - - diff --git a/tts/tts.py b/tts/tts.py index 1291777..235d585 100644 --- a/tts/tts.py +++ b/tts/tts.py @@ -30,8 +30,8 @@ class TTS(Cog): @commands.command(aliases=["t2s", "text2"]) async def tts(self, ctx: commands.Context, *, text: str): """ - Send Text to speech messages as an mp3 - """ + Send Text to speech messages as an mp3 + """ mp3_fp = io.BytesIO() tts = gTTS(text, lang="en") tts.write_to_fp(mp3_fp) diff --git a/werewolf/builder.py b/werewolf/builder.py index 2ed34a2..f57a669 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -1,5 +1,7 @@ import bisect +import logging from collections import defaultdict +from operator import attrgetter from random import choice import discord @@ -8,77 +10,55 @@ import discord # Import all roles here from redbot.core import commands -from .roles.seer import Seer -from .roles.vanillawerewolf import VanillaWerewolf -from .roles.villager import Villager -from redbot.core.utils.menus import menu, prev_page, next_page, close_menu +# from .roles.seer import Seer +# from .roles.vanillawerewolf import VanillaWerewolf +# from .roles.villager import Villager -# All roles in this list for iterating +from werewolf import roles +from redbot.core.utils.menus import menu, prev_page, next_page, close_menu -ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) +from werewolf.constants import ROLE_CATEGORY_DESCRIPTIONS +from werewolf.role import Role -ALIGNMENT_COLORS = [0x008000, 0xff0000, 0xc0c0c0] -TOWN_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 1] -WW_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 2] -OTHER_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment not in [0, 1]] +log = logging.getLogger("red.fox_v3.werewolf.builder") -ROLE_PAGES = [] -PAGE_GROUPS = [0] +# All roles in this list for iterating -ROLE_CATEGORIES = { - 1: "Random", 2: "Investigative", 3: "Protective", 4: "Government", - 5: "Killing", 6: "Power (Special night action)", - 11: "Random", 12: "Deception", 15: "Killing", 16: "Support", - 21: "Benign", 22: "Evil", 23: "Killing"} +ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)} +ROLE_LIST = sorted( + [cls for cls in ROLE_DICT.values()], + key=attrgetter("alignment"), +) -CATEGORY_COUNT = [] +log.debug(f"{ROLE_DICT=}") +# Town, Werewolf, Neutral +ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0] -def role_embed(idx, role, color): - embed = discord.Embed(title="**{}** - {}".format(idx, str(role.__name__)), description=role.game_start_message, - color=color) - embed.add_field(name='Alignment', value=['Town', 'Werewolf', 'Neutral'][role.alignment - 1], inline=True) - embed.add_field(name='Multiples Allowed', value=str(not role.unique), inline=True) - embed.add_field(name='Role Type', value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True) - embed.add_field(name='Random Option', value=str(role.rand_choice), inline=True) +ROLE_PAGES = [] - return embed +def role_embed(idx, role: Role, color): + embed = discord.Embed( + title=f"**{idx}** - {role.__name__}", + description=role.game_start_message, + color=color, + ) + if role.icon_url is not None: + embed.set_thumbnail(url=role.icon_url) + + embed.add_field( + name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=False + ) + embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=False) + embed.add_field( + name="Role Types", + value=", ".join(ROLE_CATEGORY_DESCRIPTIONS[x] for x in role.category), + inline=False, + ) + embed.add_field(name="Random Option", value=str(role.rand_choice), inline=False) -def setup(): - # Roles - last_alignment = ROLE_LIST[0].alignment - for idx, role in enumerate(ROLE_LIST): - if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - last_alignment = role.alignment - - ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])) - - # Random Town Roles - if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - for k, v in ROLE_CATEGORIES.items(): - if 0 < k <= 6: - ROLE_PAGES.append(discord.Embed(title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000)) - CATEGORY_COUNT.append(k) - - # Random WW Roles - if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - for k, v in ROLE_CATEGORIES.items(): - if 10 < k <= 16: - ROLE_PAGES.append( - discord.Embed(title="RANDOM:Werewolf Role", description="Werewolf {}".format(v), color=0xff0000)) - CATEGORY_COUNT.append(k) - # Random Neutral Roles - if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - for k, v in ROLE_CATEGORIES.items(): - if 20 < k <= 26: - ROLE_PAGES.append( - discord.Embed(title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xc0c0c0)) - CATEGORY_COUNT.append(k) + return embed """ @@ -147,15 +127,15 @@ async def parse_code(code, game): return decode -async def encode(roles, rand_roles): +async def encode(role_list, rand_roles): """Convert role list to code""" out_code = "" - digit_sort = sorted(role for role in roles if role < 10) + digit_sort = sorted(role for role in role_list if role < 10) for role in digit_sort: out_code += str(role) - digit_sort = sorted(role for role in roles if 10 <= role < 100) + digit_sort = sorted(role for role in role_list if 10 <= role < 100) if digit_sort: out_code += "-" for role in digit_sort: @@ -187,49 +167,20 @@ async def encode(roles, rand_roles): return out_code -async def next_group(ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) - if perms.manage_messages: # Can manage messages, so remove react - try: - await message.remove_reaction(emoji, ctx.author) - except discord.NotFound: - pass - page = bisect.bisect_right(PAGE_GROUPS, page) - - if page == len(PAGE_GROUPS): - page = PAGE_GROUPS[0] - else: - page = PAGE_GROUPS[page] - - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) - - -async def prev_group(ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) - if perms.manage_messages: # Can manage messages, so remove react - try: - await message.remove_reaction(emoji, ctx.author) - except discord.NotFound: - pass - page = PAGE_GROUPS[bisect.bisect_left(PAGE_GROUPS, page) - 1] - - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) - - def role_from_alignment(alignment): - return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) - for idx, role in enumerate(ROLE_LIST) if alignment == role.alignment] + return [ + role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) + for idx, role in enumerate(ROLE_LIST) + if alignment == role.alignment + ] def role_from_category(category): - return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) - for idx, role in enumerate(ROLE_LIST) if category in role.category] + return [ + role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) + for idx, role in enumerate(ROLE_LIST) + if category in role.category + ] def role_from_id(idx): @@ -242,8 +193,11 @@ def role_from_id(idx): def role_from_name(name: str): - return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) - for idx, role in enumerate(ROLE_LIST) if name in role.__name__] + return [ + role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) + for idx, role in enumerate(ROLE_LIST) + if name in role.__name__ + ] def say_role_list(code_list, rand_roles): @@ -255,34 +209,87 @@ def say_role_list(code_list, rand_roles): for role in rand_roles: if 0 < role <= 6: - role_dict["Town {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Town {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 if 10 < role <= 16: - role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Werewolf {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 if 20 < role <= 26: - role_dict["Neutral {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Neutral {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 for k, v in role_dict.items(): - embed.add_field(name=k, value="Count: {}".format(v), inline=True) + embed.add_field(name=k, value=f"Count: {v}", inline=True) return embed class GameBuilder: - def __init__(self): self.code = [] self.rand_roles = [] - setup() + self.page_groups = [0] + self.category_count = [] + + self.setup() + + def setup(self): + # Roles + last_alignment = ROLE_LIST[0].alignment + for idx, role in enumerate(ROLE_LIST): + if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + last_alignment = role.alignment + + ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])) + + # Random Town Roles + if len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): + if 0 < k <= 9: + ROLE_PAGES.append( + discord.Embed( + title="RANDOM:Town Role", + description=f"Town {v}", + color=ALIGNMENT_COLORS[0], + ) + ) + self.category_count.append(k) + + # Random WW Roles + if len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): + if 10 < k <= 19: + ROLE_PAGES.append( + discord.Embed( + title="RANDOM:Werewolf Role", + description=f"Werewolf {v}", + color=ALIGNMENT_COLORS[1], + ) + ) + self.category_count.append(k) + # Random Neutral Roles + if len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): + if 20 < k <= 29: + ROLE_PAGES.append( + discord.Embed( + title=f"RANDOM:Neutral Role", + description=f"Neutral {v}", + color=ALIGNMENT_COLORS[2], + ) + ) + self.category_count.append(k) async def build_game(self, ctx: commands.Context): new_controls = { - '⏪': prev_group, + "⏪": self.prev_group, "⬅": prev_page, - '☑': self.select_page, + "☑": self.select_page, "➡": next_page, - '⏩': next_group, - '📇': self.list_roles, - "❌": close_menu + "⏩": self.next_group, + "📇": self.list_roles, + "❌": close_menu, } await ctx.send("Browse through roles and add the ones you want using the check mark") @@ -292,10 +299,17 @@ class GameBuilder: out = await encode(self.code, self.rand_roles) return out - async def list_roles(self, ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) + async def list_roles( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): + perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: await message.remove_reaction(emoji, ctx.author) @@ -304,13 +318,19 @@ class GameBuilder: await ctx.send(embed=say_role_list(self.code, self.rand_roles)) - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) - - async def select_page(self, ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) + + async def select_page( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): + perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: await message.remove_reaction(emoji, ctx.author) @@ -318,9 +338,53 @@ class GameBuilder: pass if page >= len(ROLE_LIST): - self.rand_roles.append(CATEGORY_COUNT[page - len(ROLE_LIST)]) + self.rand_roles.append(self.category_count[page - len(ROLE_LIST)]) else: self.code.append(page) - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) + + async def next_group( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): + perms = message.channel.permissions_for(ctx.me) + if perms.manage_messages: # Can manage messages, so remove react + try: + await message.remove_reaction(emoji, ctx.author) + except discord.NotFound: + pass + page = bisect.bisect_right(self.page_groups, page) + + if page == len(self.page_groups): + page = self.page_groups[0] + else: + page = self.page_groups[page] + + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) + + async def prev_group( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): + perms = message.channel.permissions_for(ctx.me) + if perms.manage_messages: # Can manage messages, so remove react + try: + await message.remove_reaction(emoji, ctx.author) + except discord.NotFound: + pass + page = self.page_groups[bisect.bisect_left(self.page_groups, page) - 1] + + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) diff --git a/werewolf/constants.py b/werewolf/constants.py new file mode 100644 index 0000000..bb77421 --- /dev/null +++ b/werewolf/constants.py @@ -0,0 +1,91 @@ +""" +Role Constants + + Role Alignment guide as follows: + Town: 1 + Werewolf: 2 + Neutral: 3 + + Additional alignments may be added when warring factions are added + (Rival werewolves, cultists, vampires) + + Role Category enrollment guide as follows (See Role.category): + Town: + 1: Random, 2: Investigative, 3: Protective, 4: Government, + 5: Killing, 6: Power (Special night action) + + Werewolf: + 11: Random, 12: Deception, 15: Killing, 16: Support + + Neutral: + 21: Benign, 22: Evil, 23: Killing + + + Example category: + category = [1, 5, 6] Could be Veteran + category = [1, 5] Could be Bodyguard + category = [11, 16] Could be Werewolf Silencer + category = [22] Could be Blob (non-killing) + category = [22, 23] Could be Serial-Killer +""" + + +ALIGNMENT_TOWN = 1 +ALIGNMENT_WEREWOLF = 2 +ALIGNMENT_NEUTRAL = 3 +ALIGNMENT_MAP = {"Town": 1, "Werewolf": 2, "Neutral": 3} + +# 0-9: Town Role Categories +# 10-19: Werewolf Role Categories +# 20-29: Neutral Role Categories +CATEGORY_TOWN_RANDOM = 1 +CATEGORY_TOWN_INVESTIGATIVE = 2 +CATEGORY_TOWN_PROTECTIVE = 3 +CATEGORY_TOWN_GOVERNMENT = 4 +CATEGORY_TOWN_KILLING = 5 +CATEGORY_TOWN_POWER = 6 + +CATEGORY_WW_RANDOM = 11 +CATEGORY_WW_DECEPTION = 12 +CATEGORY_WW_KILLING = 15 +CATEGORY_WW_SUPPORT = 16 + +CATEGORY_NEUTRAL_BENIGN = 21 +CATEGORY_NEUTRAL_EVIL = 22 +CATEGORY_NEUTRAL_KILLING = 23 + +ROLE_CATEGORY_DESCRIPTIONS = { + CATEGORY_TOWN_RANDOM: "Random", + CATEGORY_TOWN_INVESTIGATIVE: "Investigative", + CATEGORY_TOWN_PROTECTIVE: "Protective", + CATEGORY_TOWN_GOVERNMENT: "Government", + CATEGORY_TOWN_KILLING: "Killing", + CATEGORY_TOWN_POWER: "Power (Special night action)", + CATEGORY_WW_RANDOM: "Random", + CATEGORY_WW_DECEPTION: "Deception", + CATEGORY_WW_KILLING: "Killing", + CATEGORY_WW_SUPPORT: "Support", + CATEGORY_NEUTRAL_BENIGN: "Benign", + CATEGORY_NEUTRAL_EVIL: "Evil", + CATEGORY_NEUTRAL_KILLING: "Killing", +} + + +""" +Listener Actions Priority Guide + + Action priority guide as follows (see listeners.py for wolflistener): + _at_night_start + 0. No Action + 1. Detain actions (Jailer/Kidnapper) + 2. Group discussions and choose targets + + _at_night_end + 0. No Action + 1. Self actions (Veteran) + 2. Target switching and role blocks (bus driver, witch, escort) + 3. Protection / Preempt actions (bodyguard/framer) + 4. Non-disruptive actions (seer/silencer) + 5. Disruptive actions (Killing) + 6. Role altering actions (Cult / Mason / Shifter) +""" diff --git a/werewolf/converters.py b/werewolf/converters.py new file mode 100644 index 0000000..f108666 --- /dev/null +++ b/werewolf/converters.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, Union + +import discord +from discord.ext.commands import BadArgument, Converter +from redbot.core import commands + +from werewolf.player import Player + +if TYPE_CHECKING: + PlayerConverter = Union[int, discord.Member] + CronConverter = str +else: + + class PlayerConverter(Converter): + async def convert(self, ctx, argument) -> Player: + + try: + target = await commands.MemberConverter().convert(ctx, argument) + except BadArgument: + try: + target = int(argument) + assert target >= 0 + except (ValueError, AssertionError): + raise BadArgument + + # TODO: Get the game for context without making a new one + # TODO: Get player from game based on either ID or member object + return target diff --git a/werewolf/game.py b/werewolf/game.py index a64ace1..668bf16 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,20 +1,39 @@ import asyncio +import logging import random -from typing import List, Any, Dict, Set, Union +from collections import deque +from typing import Dict, List, Union import discord from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils import AsyncIter -from .builder import parse_code -from .player import Player -from .role import Role -from .votegroup import VoteGroup +from werewolf.builder import parse_code +from werewolf.constants import ALIGNMENT_NEUTRAL +from werewolf.player import Player +from werewolf.role import Role +from werewolf.votegroup import VoteGroup + +log = logging.getLogger("red.fox_v3.werewolf.game") + +HALF_DAY_LENGTH = 90 # FixMe: Make configurable +HALF_NIGHT_LENGTH = 60 + + +async def anyone_has_role( + member_list: List[discord.Member], role: discord.Role +) -> Union[None, discord.Member]: + return await AsyncIter(member_list).find( + lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) + ) class Game: """ Base class to run a single game of Werewolf """ + vote_groups: Dict[str, VoteGroup] roles: List[Role] players: List[Player] @@ -22,19 +41,29 @@ class Game: default_secret_channel = { "channel": None, "players": [], - "votegroup": None # uninitialized VoteGroup + "votegroup": None, # uninitialized VoteGroup } - morning_messages = [ - "**The sun rises on day {} in the village..**", - "**Morning has arrived on day {}..**" + day_start_messages = [ + "*The sun rises on day {} in the village..*", + "*Morning has arrived on day {}..*", ] + day_end_messages = ["*Dawn falls..*", "*The sun sets on the village*"] + day_vote_count = 3 - def __init__(self, guild: discord.Guild, role: discord.Role = None, - category: discord.CategoryChannel = None, village: discord.TextChannel = None, - log_channel: discord.TextChannel = None, game_code=None): + def __init__( + self, + bot: Red, + guild: discord.Guild, + role: discord.Role = None, + category: discord.CategoryChannel = None, + village: discord.TextChannel = None, + log_channel: discord.TextChannel = None, + game_code=None, + ): + self.bot = bot self.guild = guild self.game_code = game_code @@ -46,7 +75,7 @@ class Game: self.started = False self.game_over = False - self.can_vote = False + self.any_votes_remaining = False self.used_votes = 0 self.day_time = False @@ -68,6 +97,10 @@ class Game: self.loop = asyncio.get_event_loop() + self.action_queue = deque() + self.current_action = None + self.listeners = {} + # def __del__(self): # """ # Cleanup channels as necessary @@ -97,47 +130,72 @@ class Game: await self.get_roles(ctx) if len(self.players) != len(self.roles): - await ctx.maybe_send_embed("Player count does not match role count, cannot start\n" - "Currently **{} / {}**\n" - "Use `{}ww code` to pick a new game" - "".format(len(self.players), len(self.roles), ctx.prefix)) + await ctx.maybe_send_embed( + f"Player count does not match role count, cannot start\n" + f"Currently **{len(self.players)} / {len(self.roles)}**\n" + f"Use `{ctx.prefix}ww code` to pick a game setup\n" + f"Use `{ctx.prefix}buildgame` to generate a new game" + ) self.roles = [] return False + # If there's no game role, make the role and delete it later in `self.to_delete` if self.game_role is None: try: - self.game_role = await ctx.guild.create_role(name="WW Players", - hoist=True, - mentionable=True, - reason="(BOT) Werewolf game role") + self.game_role = await ctx.guild.create_role( + name="WW Players", + hoist=True, + mentionable=True, + reason="(BOT) Werewolf game role", + ) self.to_delete.add(self.game_role) except (discord.Forbidden, discord.HTTPException): - await ctx.maybe_send_embed("Game role not configured and unable to generate one, cannot start") + await ctx.maybe_send_embed( + "Game role not configured and unable to generate one, cannot start" + ) self.roles = [] return False - try: - for player in self.players: - await player.member.add_roles(*[self.game_role]) - except discord.Forbidden: - await ctx.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name)) - return False + + anyone_with_role = await anyone_has_role(self.guild.members, self.game_role) + if anyone_with_role is not None: + await ctx.maybe_send_embed( + f"{anyone_with_role.display_name} has the game role, " + f"can't continue until no one has the role" + ) + return False + + try: + for player in self.players: + await player.member.add_roles(*[self.game_role]) + except discord.Forbidden: + log.exception(f"Unable to add role **{self.game_role.name}**") + await ctx.maybe_send_embed( + f"Unable to add role **{self.game_role.name}**\n" + f"Bot is missing `manage_roles` permissions" + ) + return False await self.assign_roles() # Create category and channel with individual overwrites overwrite = { - self.guild.default_role: discord.PermissionOverwrite(read_messages=True, send_messages=False, - add_reactions=False), - self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True, - manage_messages=True, manage_channels=True, - manage_roles=True), - self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True) + self.guild.default_role: discord.PermissionOverwrite( + read_messages=True, send_messages=False, add_reactions=False + ), + self.guild.me: discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + add_reactions=True, + manage_messages=True, + manage_channels=True, + manage_roles=True, + ), + self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True), } if self.channel_category is None: - self.channel_category = await self.guild.create_category("Werewolf Game", - overwrites=overwrite, - reason="(BOT) New game of werewolf") + self.channel_category = await self.guild.create_category( + "Werewolf Game", overwrites=overwrite, reason="(BOT) New game of werewolf" + ) else: # No need to modify categories pass # await self.channel_category.edit(name="🔴 Werewolf Game (ACTIVE)", reason="(BOT) New game of werewolf") @@ -147,60 +205,76 @@ class Game: # reason="(BOT) New game of werewolf") if self.village_channel is None: try: - self.village_channel = await self.guild.create_text_channel("🔵Werewolf", - overwrites=overwrite, - reason="(BOT) New game of werewolf", - category=self.channel_category) + self.village_channel = await self.guild.create_text_channel( + "🔵Werewolf", + overwrites=overwrite, + reason="(BOT) New game of werewolf", + category=self.channel_category, + ) except discord.Forbidden: - await ctx.send("Unable to create Game Channel and none was provided\n" - "Grant Bot appropriate permissions or assign a game_channel") + await ctx.maybe_send_embed( + "Unable to create Game Channel and none was provided\n" + "Grant Bot appropriate permissions or assign a game_channel" + ) return False else: self.save_perms[self.village_channel] = self.village_channel.overwrites try: - await self.village_channel.edit(name="🔵Werewolf", - category=self.channel_category, - reason="(BOT) New game of werewolf") + await self.village_channel.edit( + name="🔵werewolf", + reason="(BOT) New game of werewolf", + ) except discord.Forbidden as e: - print("Unable to rename Game Channel") - print(e) - await ctx.send("Unable to rename Game Channel, ignoring") + log.exception("Unable to rename Game Channel") + await ctx.maybe_send_embed("Unable to rename Game Channel, ignoring") try: for target, ow in overwrite.items(): curr = self.village_channel.overwrites_for(target) curr.update(**{perm: value for perm, value in ow}) - await self.village_channel.set_permissions(target=target, - overwrite=curr, - reason="(BOT) New game of werewolf") + await self.village_channel.set_permissions( + target=target, overwrite=curr, reason="(BOT) New game of werewolf" + ) except discord.Forbidden: - await ctx.send("Unable to edit Game Channel permissions\n" - "Grant Bot appropriate permissions to manage permissions") + await ctx.maybe_send_embed( + "Unable to edit Game Channel permissions\n" + "Grant Bot appropriate permissions to manage permissions" + ) return self.started = True # Assuming everything worked so far - print("Pre at_game_start") - await self._at_game_start() # This will queue channels and votegroups to be made - print("Post at_game_start") - for channel_id in self.p_channels: - print("Channel id: " + channel_id) + log.debug("Pre at_game_start") + await self._at_game_start() # This will add votegroups to self.p_channels + log.debug("Post at_game_start") + log.debug(f"Private channels: {self.p_channels}") + for channel_id in self.p_channels.keys(): + log.debug("Setup Channel id: " + channel_id) overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=False), - self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True, - manage_messages=True, manage_channels=True, - manage_roles=True) + self.guild.me: discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + add_reactions=True, + manage_messages=True, + manage_channels=True, + manage_roles=True, + ), } for player in self.p_channels[channel_id]["players"]: overwrite[player.member] = discord.PermissionOverwrite(read_messages=True) - channel = await self.guild.create_text_channel(channel_id, - overwrites=overwrite, - reason="(BOT) WW game secret channel", - category=self.channel_category) + channel = await self.guild.create_text_channel( + channel_id, + overwrites=overwrite, + reason="(BOT) WW game secret channel", + category=self.channel_category, + ) self.p_channels[channel_id]["channel"] = channel + self.to_delete.add(channel) + if self.p_channels[channel_id]["votegroup"] is not None: vote_group = self.p_channels[channel_id]["votegroup"](self, channel) @@ -208,28 +282,38 @@ class Game: self.vote_groups[channel_id] = vote_group - print("Pre-cycle") - await asyncio.sleep(1) - asyncio.ensure_future(self._cycle()) # Start the loop + log.debug("Pre-cycle") + await asyncio.sleep(0) + + asyncio.create_task(self._cycle()) # Start the loop + return True - ############START Notify structure############ + # ###########START Notify structure############ async def _cycle(self): """ - Each event calls the next event - - + Each event enqueues the next event _at_day_start() _at_voted() _at_kill() _at_day_end() - _at_night_begin() + _at_night_start() _at_night_end() - + and repeat with _at_day_start() again """ - await self._at_day_start() - # Once cycle ends, this will trigger end_game + + self.action_queue.append(self._at_day_start()) + + while self.action_queue and not self.game_over: + self.current_action = asyncio.create_task(self.action_queue.popleft()) + try: + await self.current_action + except asyncio.CancelledError: + log.debug("Cancelled task") + # + # await self._at_day_start() + # # Once cycle ends, this will trigger end_game await self._end_game() # Handle open channels async def _at_game_start(self): # ID 0 @@ -237,128 +321,156 @@ class Game: return await self.village_channel.send( - embed=discord.Embed(title="Game is starting, please wait for setup to complete")) + embed=discord.Embed(title="Game is starting, please wait for setup to complete") + ) - await self._notify(0) + await self._notify("at_game_start") async def _at_day_start(self): # ID 1 if self.game_over: return + # await self.village_channel.edit(reason="WW Night Start", name="werewolf-🌞") + self.action_queue.append(self._at_day_end()) # Get this ready in case day is cancelled + def check(): - return not self.can_vote or not self.day_time or self.game_over + return not self.any_votes_remaining or not self.day_time or self.game_over self.day_count += 1 - embed = discord.Embed(title=random.choice(self.morning_messages).format(self.day_count)) + + # Print the results of who died during the night + embed = discord.Embed(title=random.choice(self.day_start_messages).format(self.day_count)) for result in self.night_results: embed.add_field(name=result, value="________", inline=False) - self.day_time = True + self.day_time = True # True while day self.night_results = [] # Clear for next day await self.village_channel.send(embed=embed) - await self.generate_targets(self.village_channel) + await self.generate_targets(self.village_channel) # Print remaining players for voting await self.day_perms(self.village_channel) - await self._notify(1) + await self._notify("at_day_start") # Wait for day_start actions await self._check_game_over() - if self.game_over: + if self.game_over: # If game ended because of _notify return - self.can_vote = True - await asyncio.sleep(24) # 4 minute days FixMe to 120 later + self.any_votes_remaining = True + + # Now we sleep and let the day happen. Print the remaining daylight half way through + await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later if check(): return - await self.village_channel.send(embed=discord.Embed(title="**Two minutes of daylight remain...**")) - await asyncio.sleep(24) # 4 minute days FixMe to 120 later + await self.village_channel.send( + embed=discord.Embed(title=f"*{HALF_DAY_LENGTH / 60} minutes of daylight remain...*") + ) + await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later - # Need a loop here to wait for trial to end (can_vote?) + # Need a loop here to wait for trial to end while self.ongoing_vote: - asyncio.sleep(5) - - if check(): - return + await asyncio.sleep(5) - await self._at_day_end() + # Abruptly ends, assuming _day_end is next in queue async def _at_voted(self, target): # ID 2 if self.game_over: return - data = {"player": target} - await self._notify(2, data) + # Notify that a target has been chosen + await self._notify("at_voted", player=target) + + # TODO: Support pre-vote target modifying roles self.ongoing_vote = True self.used_votes += 1 await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( - "**{} will be put to trial and has 30 seconds to defend themselves**".format(target.mention)) + f"*{target.mention} will be put to trial and has 30 seconds to defend themselves**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), + ) await asyncio.sleep(30) await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk - message = await self.village_channel.send( - "Everyone will now vote whether to lynch {}\n" + vote_message: discord.Message = await self.village_channel.send( + f"Everyone will now vote whether to lynch {target.mention}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " - "vote both or neither to abstain, 15 seconds to vote*".format(target.mention)) + "vote both or neither to abstain, 15 seconds to vote*", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), + ) - await message.add_reaction("👍") - await message.add_reaction("👎") + await vote_message.add_reaction("👍") + await vote_message.add_reaction("👎") await asyncio.sleep(15) - reaction_list = message.reactions - up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) - down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) + # Refetch for reactions + vote_message = await self.village_channel.fetch_message(id=vote_message.id) + reaction_list = vote_message.reactions + + log.debug(f"Vote results: {[p.emoji.__repr__() for p in reaction_list]}") + raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) + raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) + + if True: # TODO: Allow customizable vote history deletion. + await vote_message.delete() + + # TODO: Support vote count modifying roles. (Need notify and count function) + voted_to_lynch = raw_down_votes > raw_up_votes - if down_votes > up_votes: - embed = discord.Embed(title="Vote Results", color=0xff0000) + if voted_to_lynch: + embed = discord.Embed( + title="Vote Results", + description=f"**Voted to lynch {target.mention}!**", + color=0xFF0000, + ) else: - embed = discord.Embed(title="Vote Results", color=0x80ff80) + embed = discord.Embed( + title="Vote Results", + description=f"**{target.mention} has been spared!**", + color=0x80FF80, + ) - embed.add_field(name="👎", value="**{}**".format(up_votes), inline=True) - embed.add_field(name="👍", value="**{}**".format(down_votes), inline=True) + embed.add_field(name="👎", value=f"**{raw_up_votes}**", inline=True) + embed.add_field(name="👍", value=f"**{raw_down_votes}**", inline=True) await self.village_channel.send(embed=embed) - if down_votes > up_votes: - await self.village_channel.send("**Voted to lynch {}!**".format(target.mention)) + if voted_to_lynch: await self.lynch(target) - self.can_vote = False + self.any_votes_remaining = False else: - await self.village_channel.send("**{} has been spared!**".format(target.mention)) if self.used_votes >= self.day_vote_count: await self.village_channel.send("**All votes have been used! Day is now over!**") - self.can_vote = False + self.any_votes_remaining = False else: await self.village_channel.send( - "**{}**/**{}** of today's votes have been used!\n" - "Nominate carefully..".format(self.used_votes, self.day_vote_count)) + f"**{self.used_votes}**/**{self.day_vote_count}** of today's votes have been used!\n" + "Nominate carefully.." + ) self.ongoing_vote = False - if not self.can_vote: - await self._at_day_end() + if not self.any_votes_remaining and self.day_time: + self.current_action.cancel() else: await self.normal_perms(self.village_channel) # No point if about to be night async def _at_kill(self, target): # ID 3 if self.game_over: return - data = {"player": target} - await self._notify(3, data) + await self._notify("at_kill", player=target) async def _at_hang(self, target): # ID 4 if self.game_over: return - data = {"player": target} - await self._notify(4, data) + await self._notify("at_hang", player=target) async def _at_day_end(self): # ID 5 await self._check_game_over() @@ -366,79 +478,97 @@ class Game: if self.game_over: return - self.can_vote = False + self.any_votes_remaining = False self.day_vote = {} self.vote_totals = {} self.day_time = False await self.night_perms(self.village_channel) - await self.village_channel.send(embed=discord.Embed(title="**The sun sets on the village...**")) + await self.village_channel.send( + embed=discord.Embed(title=random.choice(self.day_end_messages)) + ) - await self._notify(5) + await self._notify("at_day_end") await asyncio.sleep(5) - await self._at_night_start() + self.action_queue.append(self._at_night_start()) async def _at_night_start(self): # ID 6 if self.game_over: return - await self._notify(6) - await asyncio.sleep(12) # 2 minutes FixMe to 120 later - await self.village_channel.send(embed=discord.Embed(title="**Two minutes of night remain...**")) - await asyncio.sleep(9) # 1.5 minutes FixMe to 90 later - await self.village_channel.send(embed=discord.Embed(title="**Thirty seconds until sunrise...**")) - await asyncio.sleep(3) # .5 minutes FixMe to 3 Later + # await self.village_channel.edit(reason="WW Night Start", name="werewolf-🌑") + + await self._notify("at_night_start") + + await asyncio.sleep(HALF_NIGHT_LENGTH) # 2 minutes FixMe to 120 later + await self.village_channel.send( + embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes of night remain...**") + ) + await asyncio.sleep(HALF_NIGHT_LENGTH) # 1.5 minutes FixMe to 90 later + + await asyncio.sleep(3) # .5 minutes FixMe to 30 Later - await self._at_night_end() + self.action_queue.append(self._at_night_end()) async def _at_night_end(self): # ID 7 if self.game_over: return - await self._notify(7) + await self._notify("at_night_end") await asyncio.sleep(10) - await self._at_day_start() + self.action_queue.append(self._at_day_start()) async def _at_visit(self, target, source): # ID 8 if self.game_over: return - data = {"target": target, "source": source} - await self._notify(8, data) + await self._notify("at_visit", target=target, source=source) - async def _notify(self, event, data=None): + async def _notify(self, event_name, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) tasks = [] - # Role priorities - role_order = [role for role in self.roles if role.action_list[event][1] == i] - for role in role_order: - tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # VoteGroup priorities - vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] - for vote_group in vote_order: - tasks.append(asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop)) - if tasks: - await asyncio.gather(*tasks) + for event in self.listeners.get(event_name, {}).get(i, []): + tasks.append(asyncio.create_task(event(**kwargs))) + + # Run same-priority task simultaneously + await asyncio.gather(*tasks) + + # self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) + # self.bot.extra_events + # tasks = [] + # # Role priorities + # role_order = [role for role in self.roles if role.action_list[event][1] == i] + # for role in role_order: + # tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) + # # VoteGroup priorities + # vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] + # for vote_group in vote_order: + # tasks.append( + # asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + # ) + # if tasks: + # await asyncio.gather(*tasks) # Run same-priority task simultaneously - ############END Notify structure############ + # ###########END Notify structure############ async def generate_targets(self, channel, with_roles=False): - embed = discord.Embed(title="Remaining Players") - for i in range(len(self.players)): - player = self.players[i] + embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]") + for i, player in enumerate(self.players): if player.alive: status = "" else: status = "*[Dead]*-" if with_roles or not player.alive: - embed.add_field(name="ID# **{}**".format(i), - value="{}{}-{}".format(status, player.member.display_name, str(player.role)), - inline=True) + embed.add_field( + name=f"{i} - {status}{player.member.display_name}", + value=f"{player.role}", + inline=False, + ) else: - embed.add_field(name="ID# **{}**".format(i), - value="{}{}".format(status, player.member.display_name), - inline=True) + embed.add_field( + name=f"{i} - {status}{player.member.display_name}", inline=False, value="____" + ) return await channel.send(embed=embed) @@ -453,36 +583,46 @@ class Game: try: await asyncio.sleep(1) # This will have multiple calls self.p_channels[channel_id]["players"].append(role.player) - if votegroup is not None: - self.p_channels[channel_id]["votegroup"] = votegroup except AttributeError: continue else: break - async def join(self, member: discord.Member, channel: discord.TextChannel): + if votegroup is not None: + self.p_channels[channel_id]["votegroup"] = votegroup + + async def join(self, ctx, member: discord.Member): """ Have a member join a game """ if self.started: - await channel.send("**Game has already started!**") + await ctx.maybe_send_embed("Game has already started!") + return + + if member.bot: + await ctx.maybe_send_embed("Bots can't play games") return if await self.get_player_by_member(member) is not None: - await channel.send("{} is already in the game!".format(member.mention)) + await ctx.maybe_send_embed(f"{member.display_name} is already in the game!") return self.players.append(Player(member)) - if self.game_role is not None: - try: - await member.add_roles(*[self.game_role]) - except discord.Forbidden: - await channel.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name)) - - await channel.send("{} has been added to the game, " - "total players is **{}**".format(member.mention, len(self.players))) + # Add the role during setup, not before + # if self.game_role is not None: + # try: + # await member.add_roles(*[self.game_role]) + # except discord.Forbidden: + # await channel.send( + # f"Unable to add role **{self.game_role.name}**\n" + # f"Bot is missing `manage_roles` permissions" + # ) + + await ctx.maybe_send_embed( + f"{member.display_name} has been added to the game, " + f"total players is **{len(self.players)}**" + ) async def quit(self, member: discord.Member, channel: discord.TextChannel = None): """ @@ -495,11 +635,17 @@ class Game: if self.started: await self._quit(player) - await channel.send("{} has left the game".format(member.mention)) + await channel.send( + f"{member.mention} has left the game", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[member]), + ) else: self.players = [player for player in self.players if player.member != member] await member.remove_roles(*[self.game_role]) - await channel.send("{} chickened out, player count is now **{}**".format(member.mention, len(self.players))) + await channel.send( + f"{member.mention} chickened out, player count is now **{len(self.players)}**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[member]), + ) async def choose(self, ctx, data): """ @@ -509,15 +655,15 @@ class Game: player = await self.get_player_by_member(ctx.author) if player is None: - await ctx.send("You're not in this game!") + await ctx.maybe_send_embed("You're not in this game!") return if not player.alive: - await ctx.send("**Corpses** can't participate...") + await ctx.maybe_send_embed("**Corpses** can't participate...") return if player.role.blocked: - await ctx.send("Something is preventing you from doing this...") + await ctx.maybe_send_embed("Something is preventing you from doing this...") return # Let role do target validation, might be alternate targets @@ -529,14 +675,14 @@ class Game: await target.role.visit(source) await self._at_visit(target, source) - async def visit(self, target_id, source): + async def visit(self, target_id, source) -> Union[Player, None]: """ Night visit target_id Returns a target for role information (i.e. Seer) """ if source.role.blocked: # Blocker handles text - return + return None target = await self.get_night_target(target_id, source) await self._visit(target, source) return target @@ -557,7 +703,7 @@ class Game: return if channel == self.village_channel: - if not self.can_vote: + if not self.any_votes_remaining: await channel.send("Voting is not allowed right now") return elif channel.name in self.p_channels: @@ -598,14 +744,16 @@ class Game: required_votes = len([player for player in self.players if player.alive]) // 7 + 2 if self.vote_totals[target_id] < required_votes: - await self.village_channel.send("" - "{} has voted to put {} to trial. " - "{} more votes needed".format(author.mention, - target.member.mention, - required_votes - self.vote_totals[target_id])) + await self.village_channel.send( + f"{author.mention} has voted to put {target.member.mention} to trial. " + f"{required_votes - self.vote_totals[target_id]} more votes needed", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author, target]), + ) else: self.vote_totals[target_id] = 0 - self.day_vote = {k: v for k, v in self.day_vote.items() if v != target_id} # Remove votes for this id + self.day_vote = { + k: v for k, v in self.day_vote.items() if v != target_id + } # Remove votes for this id await self._at_voted(target) async def eval_results(self, target, source=None, method=None): @@ -613,9 +761,9 @@ class Game: out = "**{ID}** - " + method return out.format(ID=target.id, target=target.member.display_name) else: - return "**{ID}** - {target} the {role} was found dead".format(ID=target.id, - target=target.member.display_name, - role=await target.role.get_role()) + return "**{ID}** - {target} the {role} was found dead".format( + ID=target.id, target=target.member.display_name, role=await target.role.get_role() + ) async def _quit(self, player): """ @@ -668,22 +816,22 @@ class Game: Attempt to lynch a target Important to finish execution before triggering notify """ - target = await self.get_day_target(target_id) - target.alive = False + target = await self.get_day_target(target_id) # Allows target modification + target.alive = False # Kill them, await self._at_hang(target) if not target.alive: # Still dead after notifying await self.dead_perms(self.village_channel, target.member) - async def get_night_target(self, target_id, source=None): + async def get_night_target(self, target_id, source=None) -> Player: return self.players[target_id] # ToDo check source - async def get_day_target(self, target_id, source=None): + async def get_day_target(self, target_id, source=None) -> Player: return self.players[target_id] # ToDo check source async def set_code(self, ctx: commands.Context, game_code): if game_code is not None: self.game_code = game_code - await ctx.send("Code has been set") + await ctx.maybe_send_embed("Code has been set") async def get_roles(self, ctx, game_code=None): if game_code is not None: @@ -695,10 +843,12 @@ class Game: try: self.roles = await parse_code(self.game_code, self) except ValueError as e: - await ctx.send("Invalid Code: Code contains unknown character\n{}".format(e)) + await ctx.maybe_send_embed( + "Invalid Code: Code contains unknown character\n{}".format(e) + ) return False except IndexError as e: - await ctx.send("Invalid Code: Code references unknown role\n{}".format(e)) + await ctx.maybe_send_embed("Invalid Code: Code references unknown role\n{}".format(e)) if not self.roles: return False @@ -717,7 +867,7 @@ class Game: # Sorted players, now assign id's await self.players[idx].assign_id(idx) - async def get_player_by_member(self, member): + async def get_player_by_member(self, member: discord.Member): for player in self.players: if player.member == member: return player @@ -748,7 +898,9 @@ class Game: alive_players = [player for player in self.players if player.alive] if len(alive_players) <= 0: - await self.village_channel.send(embed=discord.Embed(title="**Everyone is dead! Game Over!**")) + await self.village_channel.send( + embed=discord.Embed(title="**Everyone is dead! Game Over!**") + ) self.game_over = True elif len(alive_players) == 1: self.game_over = True @@ -758,7 +910,8 @@ class Game: self.game_over = True alignment1 = alive_players[0].role.alignment alignment2 = alive_players[1].role.alignment - if alignment1 == alignment2: # Same team + # Same team and not neutral + if alignment1 == alignment2 and alignment1 != ALIGNMENT_NEUTRAL: winners = alive_players else: winners = [max(alive_players, key=lambda p: p.role.alignment)] @@ -776,31 +929,105 @@ class Game: await self._announce_winners(alive_players) # If no return, cleanup and end game - await self._end_game() + # await self._end_game() async def _announce_winners(self, winnerlist): await self.village_channel.send(self.game_role.mention) - embed = discord.Embed(title='Game Over', description='The Following Players have won!') + embed = discord.Embed(title="Game Over", description="The Following Players have won!") for player in winnerlist: embed.add_field(name=player.member.display_name, value=str(player.role), inline=True) - embed.set_thumbnail(url='https://emojipedia-us.s3.amazonaws.com/thumbs/160/twitter/134/trophy_1f3c6.png') + embed.set_thumbnail( + url="https://emojipedia-us.s3.amazonaws.com/thumbs/160/twitter/134/trophy_1f3c6.png" + ) await self.village_channel.send(embed=embed) await self.generate_targets(self.village_channel, True) async def _end_game(self): # Remove game_role access for potential archiving for now - reason = '(BOT) End of WW game' + reason = "(BOT) End of WW game" for obj in self.to_delete: - print(obj) - await obj.delete(reason=reason) + log.debug(f"End_game: Deleting object {obj.__repr__()}") + try: + await obj.delete(reason=reason) + except discord.NotFound: + # Already deleted + pass try: - await self.village_channel.edit(reason=reason, name="Werewolf") - for target, overwrites in self.save_perms[self.village_channel]: - await self.village_channel.set_permissions(target, overwrite=overwrites, reason=reason) - await self.village_channel.set_permissions(self.game_role, overwrite=None, reason=reason) + asyncio.create_task(self.village_channel.edit(reason=reason, name="werewolf")) + async for channel, overwrites in AsyncIter(self.save_perms.items()): + async for target, overwrite in AsyncIter(overwrites.items()): + await channel.set_permissions(target, overwrite=overwrite, reason=reason) + # for target, overwrites in self.save_perms[self.village_channel]: + # await self.village_channel.set_permissions( + # target, overwrite=overwrites, reason=reason + # ) + await self.village_channel.set_permissions( + self.game_role, overwrite=None, reason=reason + ) except (discord.HTTPException, discord.NotFound, discord.errors.NotFound): pass + for player in self.players: + try: + await player.member.remove_roles(*[self.game_role]) + except discord.Forbidden: + log.exception(f"Unable to add remove **{self.game_role.name}**") + # await ctx.send( + # f"Unable to add role **{self.game_role.name}**\n" + # f"Bot is missing `manage_roles` permissions" + # ) + pass + # Optional dynamic channels/categories + + def add_ww_listener(self, func, priority=0, name=None): + """Adds a listener from the pool of listeners. + + Parameters + ----------- + func: :ref:`coroutine ` + The function to call. + priority: Optional[:class:`int`] + Priority of the listener. Defaults to 0 (no-action) + name: Optional[:class:`str`] + The name of the event to listen for. Defaults to ``func.__name__``. + do_sort: Optional[:class:`bool`] + Whether or not to sort listeners after. Skip sorting during mass appending + + """ + name = func.__name__ if name is None else name + + if not asyncio.iscoroutinefunction(func): + raise TypeError("Listeners must be coroutines") + + if name in self.listeners: + if priority in self.listeners[name]: + self.listeners[name][priority].append(func) + else: + self.listeners[name][priority] = [func] + else: + self.listeners[name] = {priority: [func]} + + # self.listeners[name].sort(reverse=True) + + # def remove_wolf_listener(self, func, name=None): + # """Removes a listener from the pool of listeners. + # + # Parameters + # ----------- + # func + # The function that was used as a listener to remove. + # name: :class:`str` + # The name of the event we want to remove. Defaults to + # ``func.__name__``. + # """ + # + # name = func.__name__ if name is None else name + # + # if name in self.listeners: + # try: + # self.listeners[name].remove(func) + # except ValueError: + # pass diff --git a/werewolf/info.json b/werewolf/info.json index 99bc768..c8ef454 100644 --- a/werewolf/info.json +++ b/werewolf/info.json @@ -4,10 +4,10 @@ ], "min_bot_version": "3.3.0", "description": "Customizable Werewolf Game", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup", "requirements": [], - "short": "Werewolf Game", + "short": "[ALPHA] Play Werewolf (Mafia) Game in discord", "end_user_data_statement": "This stores user IDs in memory while they're actively using the cog, and stores no persistent End User Data.", "tags": [ "mafia", diff --git a/werewolf/listener.py b/werewolf/listener.py new file mode 100644 index 0000000..29ef7dd --- /dev/null +++ b/werewolf/listener.py @@ -0,0 +1,106 @@ +import inspect + + +def wolflistener(name=None, priority=0): + """A decorator that marks a function as a listener. + + This is the werewolf.Game equivalent of :meth:`.Cog.listener`. + + Parameters + ------------ + name: :class:`str` + The name of the event being listened to. If not provided, it + defaults to the function's name. + priority: :class:`int` + The priority of the listener. + Priority guide as follows: + _at_night_start + 0. No Action + 1. Detain actions (Jailer/Kidnapper) + 2. Group discussions and choose targets + + _at_night_end + 0. No Action + 1. Self actions (Veteran) + 2. Target switching and role blocks (bus driver, witch, escort) + 3. Protection / Preempt actions (bodyguard/framer) + 4. Non-disruptive actions (seer/silencer) + 5. Disruptive actions (Killing) + 6. Role altering actions (Cult / Mason / Shifter) + + Raises + -------- + TypeError + The function is not a coroutine function or a string was not passed as + the name. + """ + + if name is not None and not isinstance(name, str): + raise TypeError( + "Game.listener expected str but received {0.__class__.__name__!r} instead.".format( + name + ) + ) + + def decorator(func): + actual = func + if isinstance(actual, staticmethod): + actual = actual.__func__ + if not inspect.iscoroutinefunction(actual): + raise TypeError("Listener function must be a coroutine function.") + actual.__wolf_listener__ = priority + to_assign = name or actual.__name__ + try: + actual.__wolf_listener_names__.append((priority, to_assign)) + except AttributeError: + actual.__wolf_listener_names__ = [(priority, to_assign)] + # we have to return `func` instead of `actual` because + # we need the type to be `staticmethod` for the metaclass + # to pick it up but the metaclass unfurls the function and + # thus the assignments need to be on the actual function + return func + + return decorator + + +class WolfListenerMeta(type): + def __new__(mcs, *args, **kwargs): + name, bases, attrs = args + + listeners = {} + need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" + + new_cls = super().__new__(mcs, name, bases, attrs, **kwargs) + for base in reversed(new_cls.__mro__): + for elem, value in base.__dict__.items(): + if elem in listeners: + del listeners[elem] + + is_static_method = isinstance(value, staticmethod) + if is_static_method: + value = value.__func__ + if inspect.iscoroutinefunction(value): + try: + is_listener = getattr(value, "__wolf_listener__") + except AttributeError: + continue + else: + # if not elem.startswith("at_"): + # raise TypeError(need_at_msg.format(base, elem)) + listeners[elem] = value + + listeners_as_list = [] + for listener in listeners.values(): + for priority, listener_name in listener.__wolf_listener_names__: + # I use __name__ instead of just storing the value so I can inject + # the self attribute when the time comes to add them to the bot + listeners_as_list.append((priority, listener_name, listener.__name__)) + + new_cls.__wolf_listeners__ = listeners_as_list + return new_cls + + +class WolfListener(metaclass=WolfListenerMeta): + def __init__(self, game): + for priority, name, method_name in self.__wolf_listeners__: + game.add_ww_listener(getattr(self, method_name), priority, name) diff --git a/werewolf/night_powers.py b/werewolf/night_powers.py index b50929b..ab82e87 100644 --- a/werewolf/night_powers.py +++ b/werewolf/night_powers.py @@ -1,4 +1,8 @@ -from .role import Role +import logging + +from werewolf.role import Role + +log = logging.getLogger("red.fox_v3.werewolf.night_powers") def night_immune(role: Role): diff --git a/werewolf/player.py b/werewolf/player.py index c84d87f..c574109 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -1,5 +1,9 @@ +import logging + import discord +log = logging.getLogger("red.fox_v3.werewolf.player") + class Player: """ @@ -16,6 +20,9 @@ class Player: self.muted = False self.protected = False + def __repr__(self): + return f"{self.__class__.__name__}({self.member})" + async def assign_role(self, role): """ Give this player a role @@ -28,6 +35,15 @@ class Player: async def send_dm(self, message): try: - await self.member.send(message) # Lets do embeds later + await self.member.send(message) # Lets ToDo embeds later except discord.Forbidden: - await self.role.game.village_channel.send("Couldn't DM {}, uh oh".format(self.mention)) + log.info(f"Unable to mention {self.member.__repr__()}") + await self.role.game.village_channel.send( + f"Couldn't DM {self.mention}, uh oh", + allowed_mentions=discord.AllowedMentions(users=[self.member]), + ) + except AttributeError: + log.exception("Someone messed up and added a bot to the game (I think)") + await self.role.game.village_channel.send( + "Someone messed up and added a bot to the game :eyes:" + ) diff --git a/werewolf/role.py b/werewolf/role.py index 3e4124d..e267283 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,31 +1,41 @@ -class Role: +import inspect +import logging + +from werewolf.listener import WolfListener, wolflistener + +log = logging.getLogger("red.fox_v3.werewolf.role") + + +class Role(WolfListener): """ Base Role class for werewolf game - + Category enrollment guide as follows (category property): Town: 1: Random, 2: Investigative, 3: Protective, 4: Government, 5: Killing, 6: Power (Special night action) - + Werewolf: 11: Random, 12: Deception, 15: Killing, 16: Support - + Neutral: 21: Benign, 22: Evil, 23: Killing - - + + Example category: category = [1, 5, 6] Could be Veteran category = [1, 5] Could be Bodyguard category = [11, 16] Could be Werewolf Silencer - - - Action guide as follows (on_event function): + category = [22] Could be Blob (non-killing) + category = [22, 23] Could be Serial-Killer + + + Action priority guide as follows (on_event function): _at_night_start 0. No Action 1. Detain actions (Jailer/Kidnapper) 2. Group discussions and choose targets - + _at_night_end 0. No Action 1. Self actions (Veteran) @@ -33,13 +43,15 @@ class Role: 3. Protection / Preempt actions (bodyguard/framer) 4. Non-disruptive actions (seer/silencer) 5. Disruptive actions (Killing) - 6. Role altering actions (Cult / Mason) + 6. Role altering actions (Cult / Mason / Shifter) """ - rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) + # Determines if it can be picked as a random role (False for unusually disruptive roles) + rand_choice = False # TODO: Rework random with categories + town_balance = 0 # Guess at power level and it's balance on the town category = [0] # List of enrolled categories (listed above) alignment = 0 # 1: Town, 2: Werewolf, 3: Neutral - channel_id = "" # Empty for no private channel + channel_name = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( "Your role is **Default**\n" @@ -54,32 +66,14 @@ class Role: icon_url = None # Adding a URL here will enable a thumbnail of the role def __init__(self, game): + super().__init__(game) self.game = game self.player = None self.blocked = False self.properties = {} # Extra data for other roles (i.e. arsonist) - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 0), - (self._at_hang, 0), - (self._at_day_end, 0), - (self._at_night_start, 0), - (self._at_night_end, 0), - (self._at_visit, 0) - ] - def __repr__(self): - return self.__class__.__name__ - - async def on_event(self, event, data): - """ - See Game class for event guide - """ - - await self.action_list[event][0](data) + return f"{self.__class__.__name__}({self.player.__repr__()})" async def assign_player(self, player): """ @@ -90,6 +84,8 @@ class Role: player.role = self self.player = player + log.debug(f"Assigned {self} to {player}") + async def get_alignment(self, source=None): """ Interaction for powerful access of alignment @@ -101,7 +97,7 @@ class Role: async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see alignment (Village, Werewolf Other) + to see alignment (Village, Werewolf, Other) """ return "Other" @@ -119,35 +115,16 @@ class Role: """ return "Default" - async def _at_game_start(self, data=None): - if self.channel_id: - await self.game.register_channel(self.channel_id, self) - - await self.player.send_dm(self.game_start_message) # Maybe embeds eventually + @wolflistener("at_game_start", priority=2) + async def _at_game_start(self): + if self.channel_name: + await self.game.register_channel(self.channel_name, self) - async def _at_day_start(self, data=None): - pass - - async def _at_voted(self, data=None): - pass - - async def _at_kill(self, data=None): - pass - - async def _at_hang(self, data=None): - pass - - async def _at_day_end(self, data=None): - pass - - async def _at_night_start(self, data=None): - pass - - async def _at_night_end(self, data=None): - pass - - async def _at_visit(self, data=None): - pass + try: + await self.player.send_dm(self.game_start_message) # Maybe embeds eventually + except AttributeError as e: + log.exception(self.__repr__()) + raise e async def kill(self, source): """ diff --git a/werewolf/roles/__init__.py b/werewolf/roles/__init__.py new file mode 100644 index 0000000..3f58a76 --- /dev/null +++ b/werewolf/roles/__init__.py @@ -0,0 +1,11 @@ +from .villager import Villager +from .seer import Seer + +from .vanillawerewolf import VanillaWerewolf + +from .shifter import Shifter + +# Don't sort these imports. They are unstably in order +# TODO: Replace with unique IDs for roles in the future + +__all__ = ["Seer", "Shifter", "VanillaWerewolf", "Villager"] diff --git a/werewolf/roles/blob.py b/werewolf/roles/blob.py new file mode 100644 index 0000000..af18983 --- /dev/null +++ b/werewolf/roles/blob.py @@ -0,0 +1,101 @@ +import logging +import random + +from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_EVIL +from werewolf.listener import wolflistener +from werewolf.player import Player +from werewolf.role import Role + +log = logging.getLogger("red.fox_v3.werewolf.role.blob") + + +class TheBlob(Role): + rand_choice = True + category = [CATEGORY_NEUTRAL_EVIL] # List of enrolled categories + alignment = ALIGNMENT_NEUTRAL # 1: Town, 2: Werewolf, 3: Neutral + channel_id = "" # Empty for no private channel + unique = True # Only one of this role per game + game_start_message = ( + "Your role is **The Blob**\n" + "You win by absorbing everyone town\n" + "Lynch players during the day with `[p]ww vote `\n" + "Each night you will absorb an adjacent player" + ) + description = ( + "A mysterious green blob of jelly, slowly growing in size.\n" + "The Blob fears no evil, must be dealt with in town" + ) + + def __init__(self, game): + super().__init__(game) + + self.blob_target = None + + async def see_alignment(self, source=None): + """ + Interaction for investigative roles attempting + to see team (Village, Werewolf, Other) + """ + return ALIGNMENT_NEUTRAL + + async def get_role(self, source=None): + """ + Interaction for powerful access of role + Unlikely to be able to deceive this + """ + return "The Blob" + + async def see_role(self, source=None): + """ + Interaction for investigative roles. + More common to be able to deceive these roles + """ + return "The Blob" + + async def kill(self, source): + """ + Called when someone is trying to kill you! + Can you do anything about it? + self.player.alive is now set to False, set to True to stay alive + """ + + # Blob cannot simply be killed + self.player.alive = True + + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): + if not self.player.alive: + return + + self.blob_target = None + idx = self.player.id + left_or_right = random.choice((-1, 1)) + while self.blob_target is None: + idx += left_or_right + if idx >= len(self.game.players): + idx = 0 + + player = self.game.players[idx] + + # you went full circle, everyone is a blob or something else is wrong + if player == self.player: + break + + if player.role.properties.get("been_blobbed", False): + self.blob_target = player + + if self.blob_target is not None: + await self.player.send_dm(f"**You will attempt to absorb {self.blob_target} tonight**") + else: + await self.player.send_dm(f"**No player will be absorbed tonight**") + + @wolflistener("at_night_end", priority=4) + async def _at_night_end(self): + if self.blob_target is None or not self.player.alive: + return + + target: "Player" = await self.game.visit(self.blob_target, self.player) + + if target is not None: + target.role.properties["been_blobbed"] = True + self.game.night_results.append("The Blob grows...") diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 35c8271..983fd14 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,11 +1,26 @@ -from ..night_powers import pick_target -from ..role import Role +import logging + +from werewolf.constants import ( + ALIGNMENT_TOWN, + ALIGNMENT_WEREWOLF, + CATEGORY_TOWN_INVESTIGATIVE, + CATEGORY_TOWN_RANDOM, +) +from werewolf.listener import wolflistener +from werewolf.night_powers import pick_target +from werewolf.role import Role + +log = logging.getLogger("red.fox_v3.werewolf.role.seer") class Seer(Role): - rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) - category = [1, 2] # List of enrolled categories (listed above) - alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral + rand_choice = True + town_balance = 4 + category = [ + CATEGORY_TOWN_RANDOM, + CATEGORY_TOWN_INVESTIGATIVE, + ] # List of enrolled categories (listed above) + alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( @@ -14,8 +29,10 @@ class Seer(Role): "Lynch players during the day with `[p]ww vote `\n" "Check for werewolves at night with `[p]ww choose `" ) - description = "A mystic in search of answers in a chaotic town.\n" \ - "Calls upon the cosmos to discern those of Lycan blood" + description = ( + "A mystic in search of answers in a chaotic town.\n" + "Calls upon the cosmos to discern those of Lycan blood" + ) def __init__(self, game): super().__init__(game) @@ -24,47 +41,49 @@ class Seer(Role): # self.blocked = False # self.properties = {} # Extra data for other roles (i.e. arsonist) self.see_target = None - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 0), - (self._at_hang, 0), - (self._at_day_end, 0), - (self._at_night_start, 2), - (self._at_night_end, 4), - (self._at_visit, 0) - ] + # self.action_list = [ + # (self._at_game_start, 1), # (Action, Priority) + # (self._at_day_start, 0), + # (self._at_voted, 0), + # (self._at_kill, 0), + # (self._at_hang, 0), + # (self._at_day_end, 0), + # (self._at_night_start, 2), + # (self._at_night_end, 4), + # (self._at_visit, 0), + # ] async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see team (Village, Werewolf Other) + to see team (Village, Werewolf, Other) """ - return "Village" + return ALIGNMENT_TOWN async def get_role(self, source=None): """ Interaction for powerful access of role Unlikely to be able to deceive this """ - return "Villager" + return "Seer" async def see_role(self, source=None): """ Interaction for investigative roles. More common to be able to deceive these roles """ - return "Villager" + return "Seer" - async def _at_night_start(self, data=None): + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): if not self.player.alive: return self.see_target = None await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to see tonight**") - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=4) + async def _at_night_end(self): if self.see_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") @@ -75,9 +94,9 @@ class Seer(Role): if target: alignment = await target.role.see_alignment(self.player) - if alignment == "Werewolf": + if alignment == ALIGNMENT_WEREWOLF: out = "Your insight reveals this player to be a **Werewolf!**" - else: + else: # Don't reveal neutrals out = "You fail to find anything suspicious about this player..." await self.player.send_dm(out) @@ -87,4 +106,6 @@ class Seer(Role): await super().choose(ctx, data) self.see_target, target = await pick_target(self, ctx, data) - await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) + await ctx.send( + f"**You will attempt to see the role of {target.member.display_name} tonight...**" + ) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 4c550dc..9685e20 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,35 +1,41 @@ -from ..night_powers import pick_target -from ..role import Role +import logging + +from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_BENIGN +from werewolf.listener import wolflistener +from werewolf.night_powers import pick_target +from werewolf.role import Role + +log = logging.getLogger("red.fox_v3.werewolf.role.shifter") class Shifter(Role): """ Base Role class for werewolf game - + Category enrollment guide as follows (category property): Town: 1: Random, 2: Investigative, 3: Protective, 4: Government, 5: Killing, 6: Power (Special night action) - + Werewolf: 11: Random, 12: Deception, 15: Killing, 16: Support - + Neutral: 21: Benign, 22: Evil, 23: Killing - - + + Example category: category = [1, 5, 6] Could be Veteran category = [1, 5] Could be Bodyguard category = [11, 16] Could be Werewolf Silencer - - + + Action guide as follows (on_event function): _at_night_start 0. No Action 1. Detain actions (Jailer/Kidnapper) 2. Group discussions and choose targets - + _at_night_end 0. No Action 1. Self actions (Veteran) @@ -37,12 +43,13 @@ class Shifter(Role): 3. Protection / Preempt actions (bodyguard/framer) 4. Non-disruptive actions (seer/silencer) 5. Disruptive actions (Killing) - 6. Role altering actions (Cult / Mason) + 6. Role altering actions (Cult / Mason / Shifter) """ rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) - category = [22] # List of enrolled categories (listed above) - alignment = 3 # 1: Town, 2: Werewolf, 3: Neutral + town_balance = -3 + category = [CATEGORY_NEUTRAL_BENIGN] # List of enrolled categories (listed above) + alignment = ALIGNMENT_NEUTRAL # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( @@ -61,22 +68,22 @@ class Shifter(Role): super().__init__(game) self.shift_target = None - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 0), - (self._at_hang, 0), - (self._at_day_end, 0), - (self._at_night_start, 2), # Chooses targets - (self._at_night_end, 6), # Role Swap - (self._at_visit, 0) - ] + # self.action_list = [ + # (self._at_game_start, 1), # (Action, Priority) + # (self._at_day_start, 0), + # (self._at_voted, 0), + # (self._at_kill, 0), + # (self._at_hang, 0), + # (self._at_day_end, 0), + # (self._at_night_start, 2), # Chooses targets + # (self._at_night_end, 6), # Role Swap + # (self._at_visit, 0), + # ] async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see alignment (Village, Werewolf, Other) + to see alignment (Village, Werewolf,, Other) """ return "Other" @@ -94,14 +101,14 @@ class Shifter(Role): """ return "Shifter" - async def _at_night_start(self, data=None): - await super()._at_night_start(data) + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): self.shift_target = None await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to shift into**") - async def _at_night_end(self, data=None): - await super()._at_night_end(data) + @wolflistener("at_night_end", priority=6) + async def _at_night_end(self): if self.shift_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") @@ -114,16 +121,20 @@ class Shifter(Role): # Roles have now been swapped - await self.player.send_dm("Your role has been stolen...\n" - "You are now a **Shifter**.") + await self.player.send_dm( + "Your role has been stolen...\n" "You are now a **Shifter**." + ) await self.player.send_dm(self.game_start_message) await target.send_dm(target.role.game_start_message) else: await self.player.send_dm("**Your shift failed...**") + async def choose(self, ctx, data): """Handle night actions""" await super().choose(ctx, data) self.shift_target, target = await pick_target(self, ctx, data) - await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) + await ctx.send( + f"**You will attempt to see the role of {target.member.display_name} tonight...**" + ) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index c8050da..8abdea2 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,13 +1,19 @@ -from ..role import Role +import logging -from ..votegroups.wolfvote import WolfVote +from werewolf.constants import ALIGNMENT_WEREWOLF, CATEGORY_WW_KILLING, CATEGORY_WW_RANDOM +from werewolf.listener import wolflistener +from werewolf.role import Role +from werewolf.votegroups.wolfvote import WolfVote + +log = logging.getLogger("red.fox_v3.werewolf.role.vanillawerewolf") class VanillaWerewolf(Role): rand_choice = True - category = [11, 15] - alignment = 2 # 1: Town, 2: Werewolf, 3: Neutral - channel_id = "werewolves" + town_balance = -6 + category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING] + alignment = ALIGNMENT_WEREWOLF # 1: Town, 2: Werewolf, 3: Neutral + channel_name = "werewolves" unique = False game_start_message = ( "Your role is **Werewolf**\n" @@ -16,34 +22,19 @@ class VanillaWerewolf(Role): "Vote to kill players at night with `[p]ww vote `" ) - def __init__(self, game): - super().__init__(game) - - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 0), - (self._at_hang, 0), - (self._at_day_end, 0), - (self._at_night_start, 0), - (self._at_night_end, 0), - (self._at_visit, 0) - ] - async def see_alignment(self, source=None): """ Interaction for investigative roles attempting to see team (Village, Werewolf Other) """ - return "Werewolf" + return ALIGNMENT_WEREWOLF async def get_role(self, source=None): """ Interaction for powerful access of role Unlikely to be able to deceive this """ - return "Werewolf" + return "VanillaWerewolf" async def see_role(self, source=None): """ @@ -52,10 +43,13 @@ class VanillaWerewolf(Role): """ return "Werewolf" - async def _at_game_start(self, data=None): - if self.channel_id: - print("Wolf has channel_id: " + self.channel_id) - await self.game.register_channel(self.channel_id, self, WolfVote) # Add VoteGroup WolfVote + @wolflistener("at_game_start", priority=2) + async def _at_game_start(self): + if self.channel_name: + log.debug("Wolf has channel_name: " + self.channel_name) + await self.game.register_channel( + self.channel_name, self, WolfVote + ) # Add VoteGroup WolfVote await self.player.send_dm(self.game_start_message) diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index bda51d2..eb0b2c9 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -1,10 +1,17 @@ -from ..role import Role +import logging + +from werewolf.constants import ALIGNMENT_TOWN, CATEGORY_TOWN_RANDOM +from werewolf.role import Role + +log = logging.getLogger("red.fox_v3.werewolf.role.villager") class Villager(Role): - rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) - category = [1] # List of enrolled categories (listed above) - alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral + # Determines if it can be picked as a random role (False for unusually disruptive roles) + rand_choice = True + town_balance = 1 + category = [CATEGORY_TOWN_RANDOM] # List of enrolled categories (listed above) + alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( @@ -13,15 +20,12 @@ class Villager(Role): "Lynch players during the day with `[p]ww vote `" ) - def __init__(self, game): - super().__init__(game) - async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see team (Village, Werewolf Other) + to see team (Village, Werewolf, Other) """ - return "Village" + return ALIGNMENT_TOWN async def get_role(self, source=None): """ diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index bf07c8c..e651eda 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -1,4 +1,11 @@ -class VoteGroup: +import logging + +from werewolf.listener import WolfListener, wolflistener + +log = logging.getLogger("red.fox_v3.werewolf.votegroup") + + +class VoteGroup(WolfListener): """ Base VoteGroup class for werewolf game Handles secret channels and group decisions @@ -8,58 +15,41 @@ class VoteGroup: channel_id = "" def __init__(self, game, channel): + super().__init__(game) self.game = game self.channel = channel self.players = [] self.vote_results = {} self.properties = {} # Extra data for other options - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 1), - (self._at_hang, 1), - (self._at_day_end, 0), - (self._at_night_start, 2), - (self._at_night_end, 0), - (self._at_visit, 0) - ] - - async def on_event(self, event, data): - """ - See Game class for event guide - """ + def __repr__(self): + return f"{self.__class__.__name__}({self.channel},{self.players})" - await self.action_list[event][0](data) - - async def _at_game_start(self, data=None): + @wolflistener("at_game_start", priority=1) + async def _at_game_start(self): await self.channel.send(" ".join(player.mention for player in self.players)) - async def _at_day_start(self, data=None): - pass - - async def _at_voted(self, data=None): - pass - - async def _at_kill(self, data=None): - if data["player"] in self.players: - self.players.remove(data["player"]) - - async def _at_hang(self, data=None): - if data["player"] in self.players: - self.players.remove(data["player"]) + @wolflistener("at_kill", priority=1) + async def _at_kill(self, player): + if player in self.players: + self.players.remove(player) - async def _at_day_end(self, data=None): - pass + @wolflistener("at_hang", priority=1) + async def _at_hang(self, player): + if player in self.players: + self.players.remove(player) - async def _at_night_start(self, data=None): + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): if self.channel is None: return + self.vote_results = {} + await self.game.generate_targets(self.channel) - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=5) + async def _at_night_end(self): if self.channel is None: return @@ -70,11 +60,8 @@ class VoteGroup: target = max(set(vote_list), key=vote_list.count) if target: - # Do what you voted on - pass - - async def _at_visit(self, data=None): - pass + # Do what the votegroup votes on + raise NotImplementedError async def register_players(self, *players): """ @@ -90,7 +77,7 @@ class VoteGroup: self.players.remove(player) if not self.players: - # ToDo: Trigger deletion of votegroup + # TODO: Confirm deletion pass async def vote(self, target, author, target_id): diff --git a/werewolf/votegroups/__init__.py b/werewolf/votegroups/__init__.py new file mode 100644 index 0000000..6b99b1e --- /dev/null +++ b/werewolf/votegroups/__init__.py @@ -0,0 +1 @@ +from .wolfvote import WolfVote diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 9c068d5..dfb4f32 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -1,6 +1,12 @@ +import logging import random -from ..votegroup import VoteGroup +import discord + +from werewolf.listener import wolflistener +from werewolf.votegroup import VoteGroup + +log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote") class WolfVote(VoteGroup): @@ -13,71 +19,29 @@ class WolfVote(VoteGroup): kill_messages = [ "**{ID}** - {target} was mauled by wolves", - "**{ID}** - {target} was found torn to shreds"] + "**{ID}** - {target} was found torn to shreds", + ] def __init__(self, game, channel): super().__init__(game, channel) - # self.game = game - # self.channel = channel - # self.players = [] - # self.vote_results = {} - # self.properties = {} # Extra data for other options self.killer = None # Added killer - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 1), - (self._at_hang, 1), - (self._at_day_end, 0), - (self._at_night_start, 2), - (self._at_night_end, 5), # Kill priority - (self._at_visit, 0) - ] - - # async def on_event(self, event, data): - - # """ - # See Game class for event guide - # """ - # - # await action_list[event][0](data) - # - # async def _at_game_start(self, data=None): - # await self.channel.send(" ".join(player.mention for player in self.players)) - # - # async def _at_day_start(self, data=None): - # pass - # - # async def _at_voted(self, data=None): - # pass - # - # async def _at_kill(self, data=None): - # if data["player"] in self.players: - # self.players.pop(data["player"]) - # - # async def _at_hang(self, data=None): - # if data["player"] in self.players: - # self.players.pop(data["player"]) - # - # async def _at_day_end(self, data=None): - # pass - - async def _at_night_start(self, data=None): - if self.channel is None: - return + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): + await super()._at_night_start() - await self.game.generate_targets(self.channel) mention_list = " ".join(player.mention for player in self.players) if mention_list != "": await self.channel.send(mention_list) self.killer = random.choice(self.players) - await self.channel.send("{} has been selected as tonight's killer".format(self.killer.member.display_name)) + await self.channel.send( + f"{self.killer.member.display_name} has been selected as tonight's killer" + ) - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=5) + async def _at_night_end(self): if self.channel is None: return @@ -87,34 +51,23 @@ class WolfVote(VoteGroup): if vote_list: target_id = max(set(vote_list), key=vote_list.count) - print("Target id: {}\nKiller: {}".format(target_id, self.killer.member.display_name)) + log.debug(f"Target id: {target_id}\nKiller: {self.killer.member.display_name}") if target_id is not None and self.killer: await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) - await self.channel.send("**{} has left to complete the kill...**".format(self.killer.member.display_name)) + await self.channel.send( + "*{} has left to complete the kill...*".format(self.killer.member.display_name) + ) else: - await self.channel.send("**No kill will be attempted tonight...**") - - # async def _at_visit(self, data=None): - # pass - # - # async def register_players(self, *players): - # """ - # Extend players by passed list - # """ - # self.players.extend(players) - # - # async def remove_player(self, player): - # """ - # Remove a player from player list - # """ - # if player.id in self.players: - # self.players.remove(player) + await self.channel.send("*No kill will be attempted tonight...*") async def vote(self, target, author, target_id): """ Receive vote from game """ - self.vote_results[author.id] = target_id + await super().vote(target, author, target_id) - await self.channel.send("{} has voted to kill {}".format(author.mention, target.member.display_name)) + await self.channel.send( + "{} has voted to kill {}".format(author.mention, target.member.display_name), + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]), + ) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 1f8fc3f..bd68a6f 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,17 +1,31 @@ +import logging +from typing import List, Union + import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import Cog +from redbot.core.utils import AsyncIter from redbot.core.utils.menus import DEFAULT_CONTROLS, menu -from .builder import ( +from werewolf.builder import ( GameBuilder, role_from_alignment, role_from_category, role_from_id, role_from_name, ) -from .game import Game +from werewolf.game import Game + +log = logging.getLogger("red.fox_v3.werewolf") + + +async def anyone_has_role( + member_list: List[discord.Member], role: discord.Role +) -> Union[None, discord.Member]: + return await AsyncIter(member_list).find( + lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) + ) class Werewolf(Cog): @@ -43,7 +57,7 @@ class Werewolf(Cog): return def __unload(self): - print("Unload called") + log.debug("Unload called") for game in self.games.values(): del game @@ -58,9 +72,9 @@ class Werewolf(Cog): code = await gb.build_game(ctx) if code != "": - await ctx.send("Your game code is **{}**".format(code)) + await ctx.maybe_send_embed(f"Your game code is **{code}**") else: - await ctx.send("No code generated") + await ctx.maybe_send_embed("No code generated") @checks.guildowner() @commands.group() @@ -77,31 +91,36 @@ class Werewolf(Cog): """ Lists current guild settings """ - success, role, category, channel, log_channel = await self._get_settings(ctx) - if not success: - await ctx.send("Failed to get settings") - return None + valid, role, category, channel, log_channel = await self._get_settings(ctx) + # if not valid: + # await ctx.send("Failed to get settings") + # return None - embed = discord.Embed(title="Current Guild Settings") + embed = discord.Embed( + title="Current Guild Settings", + description=f"Valid: {valid}", + color=0x008000 if valid else 0xFF0000, + ) embed.add_field(name="Role", value=str(role)) embed.add_field(name="Category", value=str(category)) embed.add_field(name="Channel", value=str(channel)) embed.add_field(name="Log Channel", value=str(log_channel)) + await ctx.send(embed=embed) @commands.guild_only() @wwset.command(name="role") async def wwset_role(self, ctx: commands.Context, role: discord.Role = None): """ - Assign the game role + Set the game role This role should not be manually assigned """ if role is None: await self.config.guild(ctx.guild).role_id.set(None) - await ctx.send("Cleared Game Role") + await ctx.maybe_send_embed("Cleared Game Role") else: await self.config.guild(ctx.guild).role_id.set(role.id) - await ctx.send("Game Role has been set to **{}**".format(role.name)) + await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name)) @commands.guild_only() @wwset.command(name="category") @@ -111,14 +130,16 @@ class Werewolf(Cog): """ if category_id is None: await self.config.guild(ctx.guild).category_id.set(None) - await ctx.send("Cleared Game Channel Category") + await ctx.maybe_send_embed("Cleared Game Channel Category") else: category = discord.utils.get(ctx.guild.categories, id=int(category_id)) if category is None: - await ctx.send("Category not found") + await ctx.maybe_send_embed("Category not found") return await self.config.guild(ctx.guild).category_id.set(category.id) - await ctx.send("Game Channel Category has been set to **{}**".format(category.name)) + await ctx.maybe_send_embed( + "Game Channel Category has been set to **{}**".format(category.name) + ) @commands.guild_only() @wwset.command(name="channel") @@ -128,10 +149,12 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).channel_id.set(None) - await ctx.send("Cleared Game Channel") + await ctx.maybe_send_embed("Cleared Game Channel") else: await self.config.guild(ctx.guild).channel_id.set(channel.id) - await ctx.send("Game Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed( + "Game Channel has been set to **{}**".format(channel.mention) + ) @commands.guild_only() @wwset.command(name="logchannel") @@ -141,10 +164,12 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).log_channel_id.set(None) - await ctx.send("Cleared Game Log Channel") + await ctx.maybe_send_embed("Cleared Game Log Channel") else: await self.config.guild(ctx.guild).log_channel_id.set(channel.id) - await ctx.send("Game Log Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed( + "Game Log Channel has been set to **{}**".format(channel.mention) + ) @commands.group() async def ww(self, ctx: commands.Context): @@ -162,9 +187,9 @@ class Werewolf(Cog): """ game = await self._get_game(ctx, game_code) if not game: - await ctx.send("Failed to start a new game") + await ctx.maybe_send_embed("Failed to start a new game") else: - await ctx.send("Game is ready to join! Use `[p]ww join`") + await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`") @commands.guild_only() @ww.command(name="join") @@ -173,28 +198,49 @@ class Werewolf(Cog): Joins a game of Werewolf """ - game = await self._get_game(ctx) + game: Game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("Failed to join a game!") return - await game.join(ctx.author, ctx.channel) + await game.join(ctx, ctx.author) + await ctx.tick() + + @commands.guild_only() + @commands.admin() + @ww.command(name="forcejoin") + async def ww_forcejoin(self, ctx: commands.Context, target: discord.Member): + """ + Force someone to join a game of Werewolf + """ + + game: Game = await self._get_game(ctx) + + if not game: + await ctx.maybe_send_embed("Failed to join a game!") + return + + await game.join(ctx, target) + await ctx.tick() @commands.guild_only() @ww.command(name="code") async def ww_code(self, ctx: commands.Context, code): """ - Adjust game code + Adjusts the game code. + + See `[p]buildgame` to generate a new code """ game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`") return await game.set_code(ctx, code) + await ctx.tick() @commands.guild_only() @ww.command(name="quit") @@ -206,6 +252,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) await game.quit(ctx.author, ctx.channel) + await ctx.tick() @commands.guild_only() @ww.command(name="start") @@ -215,10 +262,12 @@ class Werewolf(Cog): """ game = await self._get_game(ctx) if not game: - await ctx.send("No game running, cannot start") + await ctx.maybe_send_embed("No game running, cannot start") if not await game.setup(ctx): - pass # Do something? + pass # ToDo something? + + await ctx.tick() @commands.guild_only() @ww.command(name="stop") @@ -226,17 +275,18 @@ class Werewolf(Cog): """ Stops the current game """ - if ctx.guild is None: - # Private message, can't get guild - await ctx.send("Cannot start game from PM!") - return + # if ctx.guild is None: + # # Private message, can't get guild + # await ctx.send("Cannot stop game from PM!") + # return if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: - await ctx.send("No game to stop") + await ctx.maybe_send_embed("No game to stop") return game = await self._get_game(ctx) game.game_over = True - await ctx.send("Game has been stopped") + game.current_action.cancel() + await ctx.maybe_send_embed("Game has been stopped") @commands.guild_only() @ww.command(name="vote") @@ -250,7 +300,7 @@ class Werewolf(Cog): target_id = None if target_id is None: - await ctx.send("`id` must be an integer") + await ctx.maybe_send_embed("`id` must be an integer") return # if ctx.guild is None: @@ -267,7 +317,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) if game is None: - await ctx.send("No game running, cannot vote") + await ctx.maybe_send_embed("No game running, cannot vote") return # Game handles response now @@ -277,7 +327,7 @@ class Werewolf(Cog): elif channel in (c["channel"] for c in game.p_channels.values()): await game.vote(ctx.author, target_id, channel) else: - await ctx.send("Nothing to vote for in this channel") + await ctx.maybe_send_embed("Nothing to vote for in this channel") @ww.command(name="choose") async def ww_choose(self, ctx: commands.Context, data): @@ -288,7 +338,7 @@ class Werewolf(Cog): """ if ctx.guild is not None: - await ctx.send("This action is only available in DM's") + await ctx.maybe_send_embed("This action is only available in DM's") return # DM nonsense, find their game # If multiple games, panic @@ -296,7 +346,7 @@ class Werewolf(Cog): if await game.get_player_by_member(ctx.author): break # game = game else: - await ctx.send("You're not part of any werewolf game") + await ctx.maybe_send_embed("You're not part of any werewolf game") return await game.choose(ctx, data) @@ -317,7 +367,7 @@ class Werewolf(Cog): if from_name: await menu(ctx, from_name, DEFAULT_CONTROLS) else: - await ctx.send("No roles containing that name were found") + await ctx.maybe_send_embed("No roles containing that name were found") @ww_search.command(name="alignment") async def ww_search_alignment(self, ctx: commands.Context, alignment: int): @@ -327,7 +377,7 @@ class Werewolf(Cog): if from_alignment: await menu(ctx, from_alignment, DEFAULT_CONTROLS) else: - await ctx.send("No roles with that alignment were found") + await ctx.maybe_send_embed("No roles with that alignment were found") @ww_search.command(name="category") async def ww_search_category(self, ctx: commands.Context, category: int): @@ -337,7 +387,7 @@ class Werewolf(Cog): if pages: await menu(ctx, pages, DEFAULT_CONTROLS) else: - await ctx.send("No roles in that category were found") + await ctx.maybe_send_embed("No roles in that category were found") @ww_search.command(name="index") async def ww_search_index(self, ctx: commands.Context, idx: int): @@ -347,24 +397,32 @@ class Werewolf(Cog): if idx_embed is not None: await ctx.send(embed=idx_embed) else: - await ctx.send("Role ID not found") + await ctx.maybe_send_embed("Role ID not found") - async def _get_game(self, ctx: commands.Context, game_code=None): - guild: discord.Guild = ctx.guild + async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]: + guild: discord.Guild = getattr(ctx, "guild", None) if guild is None: # Private message, can't get guild - await ctx.send("Cannot start game from PM!") + await ctx.maybe_send_embed("Cannot start game from DM!") return None if guild.id not in self.games or self.games[guild.id].game_over: - await ctx.send("Starting a new game...") - success, role, category, channel, log_channel = await self._get_settings(ctx) + await ctx.maybe_send_embed("Starting a new game...") + valid, role, category, channel, log_channel = await self._get_settings(ctx) - if not success: - await ctx.send("Cannot start a new game") + if not valid: + await ctx.maybe_send_embed("Cannot start a new game") return None - self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) + who_has_the_role = await anyone_has_role(guild.members, role) + if who_has_the_role: + await ctx.maybe_send_embed( + f"Cannot continue, {who_has_the_role.display_name} already has the game role." + ) + return None + self.games[guild.id] = Game( + self.bot, guild, role, category, channel, log_channel, game_code + ) return self.games[guild.id] @@ -385,23 +443,30 @@ class Werewolf(Cog): if role_id is not None: role = discord.utils.get(guild.roles, id=role_id) - if role is None: - await ctx.send("Game Role is invalid") - return False, None, None, None, None + # if role is None: + # # await ctx.send("Game Role is invalid") + # return False, None, None, None, None if category_id is not None: category = discord.utils.get(guild.categories, id=category_id) - if category is None: - await ctx.send("Game Category is invalid") - return False, None, None, None, None + # if category is None: + # # await ctx.send("Game Category is invalid") + # return False, role, None, None, None if channel_id is not None: channel = discord.utils.get(guild.text_channels, id=channel_id) - if channel is None: - await ctx.send("Village Channel is invalid") - return False, None, None, None, None + # if channel is None: + # # await ctx.send("Village Channel is invalid") + # return False, role, category, None, None + if log_channel_id is not None: log_channel = discord.utils.get(guild.text_channels, id=log_channel_id) - if log_channel is None: - await ctx.send("Log Channel is invalid") - return False, None, None, None, None - - return True, role, category, channel, log_channel + # if log_channel is None: + # # await ctx.send("Log Channel is invalid") + # return False, None, None, None, None + + return ( + role is not None and category is not None and channel is not None, + role, + category, + channel, + log_channel, + )