From a4eaa141ace4830576d8ef26df60eb6a6cbbb3c8 Mon Sep 17 00:00:00 2001 From: bibin Date: Mon, 10 Aug 2020 22:28:20 +0200 Subject: [PATCH] start refactoring again (this time abstracting Discord API and removing dependencies of the game classe --- src/interaction/discord_wrapper.py | 57 +++++++++++++++++++++++++ src/interaction/template.py | 57 +++++++++++++++++++++++++ src/package/developer.py | 11 +---- src/package/games/game.py | 33 ++++++++++++++- src/package/games/game_cog.py | 10 +++-- src/package/games/werewolf/cog.py | 4 +- src/package/games/werewolf/game.py | 68 ++++++++++++++++-------------- src/werewolf_bot.py | 5 ++- 8 files changed, 195 insertions(+), 50 deletions(-) create mode 100644 src/interaction/discord_wrapper.py create mode 100644 src/interaction/template.py diff --git a/src/interaction/discord_wrapper.py b/src/interaction/discord_wrapper.py new file mode 100644 index 0000000..3f21e9f --- /dev/null +++ b/src/interaction/discord_wrapper.py @@ -0,0 +1,57 @@ +"""This module is a wrapper for messaging functionality Discord to play text based multi-player games""" + +# discord imports +import discord +from discord.ext import commands + + +class Client: + def create_task(): + pass + + +class User: + def name() -> str: + pass + + def DM() -> DM: + pass + + +class Message: + def mentions() -> User: + pass + + +class Communication: + def send(): + pass + + def send_embed(): + pass + + def command(): + pass + + def wait_for_message() -> Message: + pass + + +class DM(Communication): + pass + + +class Room(Communication): + pass + + +class Context: + pass + + +def choices(): + pass + + +def choice(): + pass diff --git a/src/interaction/template.py b/src/interaction/template.py new file mode 100644 index 0000000..5cdcdea --- /dev/null +++ b/src/interaction/template.py @@ -0,0 +1,57 @@ +"""This module is a wrapper template for messaging functionality to play text based multi-player games""" + + +class Interaction: + def query(communication, question: str, options: list, exp_ans=1): + pass + + +class Client: + def create_task(func): + pass + + +class User: + def name(): + pass + + def DM(): + pass + + +class Communication: # better name + def send(str): + pass + + def send_embed(embed): + pass + + def command(): + pass + + def wait_for_message(): + pass + + +class DM(Communication): + pass + + +class Room(Communication): + pass + + +class Context: + def User(): + pass + + def Communication(): + pass + + def Message(): + pass + + def mentions(): + pass + + # args? \ No newline at end of file diff --git a/src/package/developer.py b/src/package/developer.py index b572206..920a6df 100644 --- a/src/package/developer.py +++ b/src/package/developer.py @@ -13,7 +13,7 @@ class Developer(commands.Cog): await self.bot.change_presence(status=discord.Status.online, activity=discord.Game('One Night Ultimate Werewolf')) print('We have logged in as {0.user}'.format(self.bot)) - async def is_dev(self, ctx): + async def is_dev(ctx): if ctx.author.id == 461892912821698562: return True await ctx.send("This command is not for you!") @@ -41,15 +41,6 @@ class Developer(commands.Cog): @commands.command() @commands.check(is_dev) async def debug(self, ctx, *args): - embed = discord.Embed(title=f"Village won!", color=0x00ffff) - won_emoji = ":trophy:" - dead_emoji = ":test:" - tab = "\t" - space = "<:space:705863033871663185>" - embed.add_field(name=str("Name"), value=f"{won_emoji}{space}{dead_emoji}{space}{space}{3}:ballot_box:{tab}role: werewolf{tab}(was: drunk){tab}:point_right: someone", inline=False) - await ctx.send(embed=embed) - await ctx.send(":test::skull:") - for emoji in ctx.guild.emojis: await ctx.send(emoji) print(emoji.id) diff --git a/src/package/games/game.py b/src/package/games/game.py index 7a4ccf6..ac8cf93 100644 --- a/src/package/games/game.py +++ b/src/package/games/game.py @@ -1,20 +1,49 @@ +"""Has a single class: Game""" + +# standard library imports from abc import ABC +import asyncio + +# discord imports +import discord + +# local imports +from .players import Player class Game(ABC): + """This (abstract) class is a template for main-game-loop objects for games""" @classmethod def name(cls): return "Game" + player_cls = Player + def __init__(self, bot, channel): self.bot = bot self.channel = channel self.running = False self.player_list = [] - async def round(self): + async def send(self, message): + await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff)) + + async def for_all_player(self, call): + await asyncio.gather(*[call(p) for p in self.player_list]) + + async def set_players(self, msg): + self.player_list = [await self.player_cls.make(mem, self) for mem in msg.mentions] + await self.send(f"Players: {', '.join(p.name() for p in self.player_list)}") # send confirmation + + def check(self): pass - async def set_players(self): + def setup(self): + pass + + def end(self): + self.running = False + + async def round(self): pass diff --git a/src/package/games/game_cog.py b/src/package/games/game_cog.py index 4744b5a..28a5886 100644 --- a/src/package/games/game_cog.py +++ b/src/package/games/game_cog.py @@ -1,7 +1,7 @@ """Has a single class: Game_cog""" # standard library imports -from typing import Dict, Type +from typing import Dict # discord imports import discord @@ -15,9 +15,10 @@ from .game import Game class Game_cog(Send_message): """This (abstract) class is are common function for the Game Cog's (setup-game, pre-game, in-game), mainly has checker functions""" - def __init__(self, bot, game_cls: Type[Game]): + game_cls = Game + + def __init__(self, bot): self.bot = bot - self.game_cls = game_cls self.game_instances = {} # TODO: type hint? Dict[discord.TextChannel, self.game_cls] async def setup_check(self, ctx): @@ -45,7 +46,7 @@ class Game_cog(Send_message): async def reset(self, ctx): """This function deletes the game instance for this channel""" - if self.setup_check(ctx): + if await self.setup_check(ctx): del self.game_instances[ctx.channel] # TODO: better info message @@ -61,6 +62,7 @@ class Game_cog(Send_message): embed.set_footer(text="Have fun!") await ctx.send(embed=embed) + # TODO: can't one access self instead of ctx.cog? def pre_game(): async def predicate(ctx): return await ctx.cog.setup_check(ctx) and await ctx.cog.not_running_check(ctx) diff --git a/src/package/games/werewolf/cog.py b/src/package/games/werewolf/cog.py index de02287..210d7b5 100644 --- a/src/package/games/werewolf/cog.py +++ b/src/package/games/werewolf/cog.py @@ -11,6 +11,8 @@ from .game import Werewolf_game class Werewolf_cog(Game_cog, commands.Cog): """This singleton class is a Discord Cog for the interaction in the werewolf game""" + game_cls = Werewolf_game + @commands.group(invoke_without_command=True) async def werewolf(self, ctx): # TODO: isn't there a better way to have a default subcommand? Maybe invoke super().info()? @@ -70,4 +72,4 @@ class Werewolf_cog(Game_cog, commands.Cog): def setup(bot): - bot.add_cog(Werewolf_cog(bot, Werewolf_game)) + bot.add_cog(Werewolf_cog(bot)) diff --git a/src/package/games/werewolf/game.py b/src/package/games/werewolf/game.py index 2be4aab..dee4564 100644 --- a/src/package/games/werewolf/game.py +++ b/src/package/games/werewolf/game.py @@ -1,54 +1,54 @@ +"""Has a single class: Werewolf_game""" + +# standard library imports from random import shuffle import time import asyncio + +# discord imports import discord + +# local imports from .roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role from .players import Werewolf_player, No_player +from ..game import Game -class Werewolf_game: +class Werewolf_game(Game): + """This class simulates One Night Ultimate Werewolf with the help of Werewolf_player and Werewolf_role""" @classmethod def name(cls): return "One Night Ultimate Werewolf" + player_cls = Werewolf_player + def __init__(self, bot, channel): - self.running = False - self.bot = bot - self.channel = channel - self.player_list = [] + super().__init__(bot, channel) self.role_list = [] self.discussion_time = 301 # seconds - async def send(self, message): - await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff)) - - async def for_all_player(self, call): - await asyncio.gather(*[call(p) for p in self.player_list]) - - async def set_players(self, msg): - self.player_list = [await Werewolf_player.make(mem, self) for mem in msg.mentions] - await self.send(f"Players: {', '.join(p.name() for p in self.player_list)}") # send confirmation - async def set_roles(self, suggestions): self.role_list = [Role.match(r) for r in suggestions] # raises ValueError await self.send(f"Roles: {', '.join(r.name() for r in self.role_list)}") # send confirmation - async def set_time(self, msg): - self.discussion_time = int(msg) * 60 + 1 + async def set_time(self, minutes: int): + self.discussion_time = minutes * 60 + 1 await self.send(f"You have set the discussion time to {self.discussion_time//60} minutes") # send confirmation def check(self): # TODO: min. player - if not 0 <= len(self.player_list) <= 10: + if not 1 <= len(self.player_list) <= 10: raise ValueError(f"Invalid number of players: {len(self.player_list)}") if not len(self.role_list) == (len(self.player_list) + 3): raise ValueError(f"Invalid number of roles: {len(self.role_list)} with {len(self.player_list)} players") def setup(self): + # TODO: better... self.role = dict() # setting default value for r in Role.__subclasses__(): + # TODO: every role should be a list (not only werewolves, masons, ...) if r == No_role: continue if r in [Werewolf, Mason]: @@ -61,6 +61,7 @@ class Werewolf_game: def distribute_roles(self): shuffle(self.role_list) + # TODO: use zip for i in range(len(self.player_list)): role_obj = self.role_list[i](self, self.player_list[i]) self.player_list[i].set_role(role_obj) @@ -68,7 +69,7 @@ class Werewolf_game: self.middle_card = [r(self) for r in self.role_list[-3:]] async def start_night(self): - await self.for_all_player(lambda p: p.send_normal("*The night has begun*")) + await self.for_all_player(lambda p: p.send_normal(f"*The night has begun* {':full_moon:'*10}")) async def send_role(self): await self.for_all_player(lambda p: p.send_info(f"Your role: **{p.night_role.name()}**")) @@ -92,9 +93,10 @@ class Werewolf_game: def remaining_time_string(self): t = int(self.start_time + self.discussion_time - time.time()) - return f"{t//60} minute(s) and {t%60} second(s)" + return f"{t//60:02d}:{t%60:02d}" async def discussion_timer(self): + # TODO: better? self.start_time = time.time() await self.send(f"You have {self.remaining_time_string()} to discuss") await asyncio.sleep(self.discussion_time / 2) @@ -123,8 +125,8 @@ class Werewolf_game: p.vote.tally += 1 def who_dead(self): - maxi = max(c.tally for c in self.voting_list) - dead = [p for p in self.player_list if p.tally >= maxi] + maxi = max(c.tally for c in self.voting_list) # most votes + dead = [p for p in self.player_list if p.tally >= maxi] # who has most votes for d in dead: d.dead = True if d.day_role.is_role(Hunter): @@ -153,10 +155,8 @@ class Werewolf_game: else: if tanner_dead: tanner_won = True - if werewolf_dead: village_won = True - else: if werewolf_dead: village_won = True @@ -187,22 +187,29 @@ class Werewolf_game: if village_won: winnning.append("Village") if tanner_won: - winnning.append(f"{sum(1 for d in dead if d.day_role.is_role(Tanner))} tanner") + winnning.append(f"{sum(1 for d in dead if d.day_role.is_role(Tanner))} tanner") # number of tanners dead if len(winnning) == 0: winnning = ["No one"] + # TODO: better emoji code? + space = "<:space:705863033871663185>" embed = discord.Embed(title=f"{' and '.join(winnning)} won!", color=0x00ffff) for p in self.player_list: won_emoji = ":trophy:" if p.won else ":frowning2:" - dead_emoji = ":skull:" if p.dead else ":no_mouth:" - embed.add_field(name=str(p), value=f"{won_emoji} {dead_emoji} {p.tally}:ballot_box: role: {str(p.day_role)} (was: {str(p.night_role)}) :point_right: {str(p.vote)}", inline=False) + dead_emoji = ":skull:" if p.dead else ":eyes:" + val = [ + won_emoji, + dead_emoji, + f"{p.tally}:ballot_box:", + f"role: {str(p.day_role)}", + f"(was: {str(p.night_role)})", + f":point_right: {str(p.vote)}" + ] + embed.add_field(name=str(p), value=space.join(v for v in val), inline=False) embed.add_field(name="Middle cards", value=', '.join(r.name() for r in self.middle_card), inline=False) await self.channel.send(embed=embed) - def end(self): - self.running = False - async def round(self): try: self.check() @@ -217,6 +224,5 @@ class Werewolf_game: await self.vote() self.tally() await self.result(*self.who_won(self.who_dead())) - await self.send("Round ended") finally: self.end() diff --git a/src/werewolf_bot.py b/src/werewolf_bot.py index 31a4a22..94c966e 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -4,7 +4,7 @@ This is the main module of the Discord Bot Mainly loads the Cog's and starts the bot """ -__version__ = '0.3' +__version__ = '0.4' __author__ = 'Bibin Muttappillil' # standard library imports @@ -22,7 +22,7 @@ if TOKEN is None: print("Missing discord token!") exit(1) -bot = commands.Bot(command_prefix=commands.when_mentioned_or('$w ')) +bot = commands.Bot(command_prefix=commands.when_mentioned_or('$b ')) bot.load_extension('package.developer') bot.load_extension('package.games.werewolf.cog') @@ -32,6 +32,7 @@ bot.load_extension('package.games.werewolf.cog') @commands.is_owner() async def reload(ctx, extension): bot.reload_extension(f'package.{extension}') + print(f'package.{extension} reloaded') # checker annotations