diff --git a/werewolf/game.py b/werewolf/game.py index e9d3763..df5d263 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -16,6 +16,7 @@ from .votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") +HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days class Game: """ @@ -262,7 +263,7 @@ class Game: await asyncio.sleep(1) await asyncio.ensure_future(self._cycle()) # Start the loop - ############START Notify structure############ + # ###########START Notify structure############ async def _cycle(self): """ Each event enqueues the next event @@ -323,13 +324,13 @@ class Game: return self.can_vote = True - await asyncio.sleep(24) # 4 minute days FixMe to 120 later + 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 asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later # Need a loop here to wait for trial to end (can_vote?) while self.ongoing_vote: @@ -500,7 +501,7 @@ class Game: # 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") @@ -911,89 +912,7 @@ class Game: # 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): + def add_listener(self, func, name=None): """The non decorator alternative to :meth:`.listen`. Parameters @@ -1024,3 +943,23 @@ class Game: self.listeners[name].append(func) else: self.listeners[name] = [func] + + def remove_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/listener.py b/werewolf/listener.py new file mode 100644 index 0000000..9c36400 --- /dev/null +++ b/werewolf/listener.py @@ -0,0 +1,91 @@ +import inspect + + +def wolflistener(name=None): + """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. + + 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__ = True + to_assign = name or actual.__name__ + try: + actual.__wolf_listener_names__.append(to_assign) + except AttributeError: + actual.__wolf_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 + + +class WolfListenerMeta(type): + def __new__(mcs, cls, *args, **kwargs): + name, bases = args + + commands = {} + listeners = {} + need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" + + new_cls = super().__new__(cls, name, bases, **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(mcs, elem)) + listeners[elem] = value + + listeners_as_list = [] + for listener in listeners.values(): + for 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((listener_name, listener.__name__)) + + new_cls.__wolf_listeners__ = listeners_as_list + return new_cls + + +class WolfListener(metaclass=WolfListenerMeta): + def __init__(self, game): + for name, method_name in self.__wolf_listeners__: + game.add_listener(getattr(self, method_name), name) diff --git a/werewolf/role.py b/werewolf/role.py index bdcec71..4ae10ad 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,12 +1,12 @@ import inspect import logging -from werewolf import Werewolf +from werewolf.listener import WolfListener, wolflistener log = logging.getLogger("red.fox_v3.werewolf.role") -class Role: +class Role(WolfListener): """ Base Role class for werewolf game @@ -28,7 +28,7 @@ class Role: category = [11, 16] Could be Werewolf Silencer - Action guide as follows (on_event function): + Action priority guide as follows (on_event function): _at_night_start 0. No Action 1. Detain actions (Jailer/Kidnapper) @@ -62,6 +62,7 @@ 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 diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 8d0fc1f..603d197 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..night_powers import pick_target from ..role import Role @@ -59,6 +60,7 @@ class Seer(Role): """ return "Villager" + @wolflistener("at_night_start") async def _at_night_start(self, data=None): if not self.player.alive: return @@ -66,6 +68,7 @@ class Seer(Role): await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to see tonight**") + @wolflistener("at_night_end") async def _at_night_end(self, data=None): if self.see_target is None: if self.player.alive: diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 8d4b4f5..85c3ec5 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..night_powers import pick_target from ..role import Role @@ -5,31 +6,31 @@ from ..role import Role 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) @@ -61,17 +62,17 @@ 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): """ @@ -94,14 +95,14 @@ class Shifter(Role): """ return "Shifter" + @wolflistener("at_night_start") async def _at_night_start(self, data=None): - await super()._at_night_start(data) self.shift_target = None await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to shift into**") + @wolflistener("at_night_end") async def _at_night_end(self, data=None): - await super()._at_night_end(data) if self.shift_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") @@ -114,16 +115,22 @@ 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( + "**You will attempt to see the role of {} tonight...**".format( + target.member.display_name + ) + ) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index c8050da..5f7407b 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..role import Role from ..votegroups.wolfvote import WolfVote @@ -19,17 +20,17 @@ class VanillaWerewolf(Role): 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) - ] + # 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): """ @@ -52,6 +53,7 @@ class VanillaWerewolf(Role): """ return "Werewolf" + @wolflistener("at_game_start") async def _at_game_start(self, data=None): if self.channel_id: print("Wolf has channel_id: " + self.channel_id) diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index d60f451..19ebd9e 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -1,9 +1,11 @@ import logging +from werewolf.listener import WolfListener, wolflistener + log = logging.getLogger("red.fox_v3.werewolf.votegroup") -class VoteGroup: +class VoteGroup(WolfListener): """ Base VoteGroup class for werewolf game Handles secret channels and group decisions @@ -13,57 +15,55 @@ 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 - """ - - await self.action_list[event][0](data) - + # 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 + # """ + # + # await self.action_list[event][0](data) + + @wolflistener("at_game_start") 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 - + @wolflistener("at_kill") 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"]) - - async def _at_day_end(self, data=None): - pass + # Removed, only if they actually die + # @wolflistener("at_hang") + # async def _at_hang(self, data=None): + # if data["player"] in self.players: + # self.players.remove(data["player"]) + @wolflistener("at_night_start") async def _at_night_start(self, data=None): if self.channel is None: return await self.game.generate_targets(self.channel) + @wolflistener("at_night_end") async def _at_night_end(self, data=None): if self.channel is None: return @@ -78,9 +78,6 @@ class VoteGroup: # Do what you voted on pass - async def _at_visit(self, data=None): - pass - async def register_players(self, *players): """ Extend players by passed list