From 1723dc381d4a49fc5f2c14a666499967c8bd7f33 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 13:42:43 -0400 Subject: [PATCH] More listener --- werewolf/game.py | 183 ++++++++++++++++++++++++++++++++++++++--------- werewolf/role.py | 84 +++++++++++----------- 2 files changed, 192 insertions(+), 75 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c3aa813..e9d3763 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,4 +1,5 @@ import asyncio +import inspect import logging import random from collections import deque @@ -36,9 +37,7 @@ class Game: "**Morning has arrived on day {}..**", ] - night_messages = [ - "**Dawn falls on day {}..****" - ] + night_messages = ["**Dawn falls on day {}..****"] day_vote_count = 3 @@ -87,6 +86,7 @@ class Game: self.loop = asyncio.get_event_loop() self.action_queue = deque() + self.listeners = {} # def __del__(self): # """ @@ -265,9 +265,7 @@ class Game: ############START Notify structure############ async def _cycle(self): """ - Each event calls the next event - - + Each event enqueues the next event _at_day_start() _at_voted() @@ -296,7 +294,7 @@ class Game: 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: @@ -318,7 +316,7 @@ class Game: await self.generate_targets(self.village_channel) await self.day_perms(self.village_channel) - await self._notify(1) + await self._notify("at_day_start") await self._check_game_over() if self.game_over: @@ -346,7 +344,7 @@ class Game: if self.game_over: return data = {"player": target} - await self._notify(2, data) + await self._notify("at_voted", player=target) self.ongoing_vote = True @@ -357,9 +355,7 @@ class Game: "**{} will be put to trial and has 30 seconds to defend themselves**".format( target.mention ), - allowed_mentions=discord.AllowedMentions( - everyone=False, users=[target] - ) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) await asyncio.sleep(30) @@ -371,9 +367,7 @@ class Game: "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " "vote both or neither to abstain, 15 seconds to vote*".format(target.mention), - allowed_mentions=discord.AllowedMentions( - everyone=False, users=[target] - ) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) await message.add_reaction("👍") @@ -422,13 +416,13 @@ class Game: 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() @@ -447,14 +441,14 @@ class Game: embed=discord.Embed(title="**The sun sets on the village...**") ) - await self._notify(5) + await self._notify("at_day_end") await asyncio.sleep(5) 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 self._notify("at_night_start") await asyncio.sleep(12) # 2 minutes FixMe to 120 later await self.village_channel.send( @@ -471,7 +465,7 @@ class Game: 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) self.action_queue.append(self._at_day_start()) @@ -480,25 +474,30 @@ class Game: 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, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) + tasks = [] + for event in self.listeners.get(event, []): + tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) + 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) + # 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############ @@ -911,3 +910,117 @@ class Game: pass # Optional dynamic channels/categories + + @classmethod + def wolflistener(cls, name=None): + """A decorator that marks a function as a listener. + + This is the cog equivalent of :meth:`.Bot.listen`. + + Parameters + ------------ + name: :class:`str` + The name of the event being listened to. If not provided, it + defaults to the function's name. + + 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( + "Cog.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.__werewolf_listener__ = True + to_assign = name or actual.__name__ + try: + actual.__cog_listener_names__.append(to_assign) + except AttributeError: + actual.__cog_listener_names__ = [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 + + def wolflisten(self, name=None): + """A decorator that registers another function as an external + event listener. Basically this allows you to listen to multiple + events from different places e.g. such as :func:`.on_ready` + + The functions being listened to must be a :ref:`coroutine `. + + Example + -------- + + .. code-block:: python3 + + @bot.listen() + async def on_message(message): + print('one') + + # in some other file... + + @bot.listen('on_message') + async def my_message(message): + print('two') + + Would print one and two in an unspecified order. + + Raises + ------- + TypeError + The function being listened to is not a coroutine. + """ + + def decorator(func): + self.add_wolflistener(func, name) + return func + + return decorator + + def add_wolflistener(self, func, name=None): + """The non decorator alternative to :meth:`.listen`. + + Parameters + ----------- + func: :ref:`coroutine ` + The function to call. + name: Optional[:class:`str`] + The name of the event to listen for. Defaults to ``func.__name__``. + + Example + -------- + + .. code-block:: python3 + + async def on_ready(): pass + async def my_message(message): pass + + bot.add_listener(on_ready) + bot.add_listener(my_message, 'on_message') + + """ + 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: + self.listeners[name].append(func) + else: + self.listeners[name] = [func] diff --git a/werewolf/role.py b/werewolf/role.py index 2a08f71..bdcec71 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,5 +1,8 @@ +import inspect import logging +from werewolf import Werewolf + log = logging.getLogger("red.fox_v3.werewolf.role") @@ -64,27 +67,27 @@ class Role: 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), - ] + # 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) + # async def on_event(self, event, data): + # """ + # See Game class for event guide + # """ + # + # await self.action_list[event][0](data) async def assign_player(self, player): """ @@ -124,35 +127,36 @@ class Role: """ return "Default" + @wolflistener("at_game_start") 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 - 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 + # 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 async def kill(self, source): """