diff --git a/src/package/__pycache__/developer.cpython-37.pyc b/src/package/__pycache__/developer.cpython-37.pyc new file mode 100644 index 0000000..e2450da Binary files /dev/null and b/src/package/__pycache__/developer.cpython-37.pyc differ diff --git a/src/package/__pycache__/send_message.cpython-37.pyc b/src/package/__pycache__/send_message.cpython-37.pyc new file mode 100644 index 0000000..96a6f4f Binary files /dev/null and b/src/package/__pycache__/send_message.cpython-37.pyc differ diff --git a/src/package/developer.py b/src/package/developer.py new file mode 100644 index 0000000..d55d30d --- /dev/null +++ b/src/package/developer.py @@ -0,0 +1,62 @@ +import discord +from discord.ext import commands + + +class Developer(commands.Cog): + """This class is intended only for the developer, mainly for testing purposes""" + + def __init__(self, bot): + self.bot = bot + + @commands.Cog.listener() + async def on_ready(self): + 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(ctx): + if ctx.author.id == 461892912821698562: + return True + await ctx.send("This command is not for you!") + return False + + @commands.command() + async def hello(self, ctx): + await ctx.send(f"Hello {ctx.message.author.name} :wave:") + + @commands.group(name="gg") + async def group(self, ctx): + pass + + @group.command() + @commands.check(is_dev) + async def ping(self, ctx): + await ctx.send("pong") + + @commands.command() + @commands.check(is_dev) + async def logout(self, ctx): + await self.bot.logout() + + @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) + + @debug.error + async def debug_error(self, ctx, error): + await ctx.send(error) + + +def setup(bot): + bot.add_cog(Developer(bot)) diff --git a/src/package/games/__pycache__/game.cpython-37.pyc b/src/package/games/__pycache__/game.cpython-37.pyc new file mode 100644 index 0000000..75ffe46 Binary files /dev/null and b/src/package/games/__pycache__/game.cpython-37.pyc differ diff --git a/src/package/games/__pycache__/game_cog.cpython-37.pyc b/src/package/games/__pycache__/game_cog.cpython-37.pyc new file mode 100644 index 0000000..bdaa947 Binary files /dev/null and b/src/package/games/__pycache__/game_cog.cpython-37.pyc differ diff --git a/src/package/games/game.py b/src/package/games/game.py new file mode 100644 index 0000000..7a4ccf6 --- /dev/null +++ b/src/package/games/game.py @@ -0,0 +1,20 @@ +from abc import ABC + + +class Game(ABC): + + @classmethod + def name(cls): + return "Game" + + def __init__(self, bot, channel): + self.bot = bot + self.channel = channel + self.running = False + self.player_list = [] + + async def round(self): + pass + + async def set_players(self): + pass diff --git a/src/package/games/game_cog.py b/src/package/games/game_cog.py new file mode 100644 index 0000000..7c9083e --- /dev/null +++ b/src/package/games/game_cog.py @@ -0,0 +1,78 @@ +"""Has a single class: Game_cog""" + +# standard library imports +from typing import Dict, Type + +# discord imports +import discord + +# local imports +from ..send_message import Send_message +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]): + self.bot = bot + self.game_cls = game_cls + self.game_instances = Dict[discord.TextChannel, self.game_cls] + + async def setup_check(self, ctx): + if ctx.channel not in self.game_instances: + await self.send_wrong(ctx, f"The channel is not setup yet.") + return ctx.channel in self.game_instances + + async def not_running_check(self, ctx): + if self.game_instances[ctx.channel].running: + await self.send_wrong(ctx, "Sorry! A game is already running") + return not self.game_instances[ctx.channel].running + + async def running_check(self, ctx): + if not self.game_instances[ctx.channel].running: + await self.send_wrong(ctx, "No game is running") + return self.game_instances[ctx.channel].running + + async def setup(self, ctx): + """This function creates an game instance for this channel""" + if ctx.channel in self.game_instances: + await self.send_wrong(ctx, f"A game '{self.game_cls.name()}' is already setup in this channel") + else: + self.game_instances[ctx.channel] = self.game_cls(self.bot, ctx.channel) + await self.send_friendly(ctx, f"This channel can now play: {self.game_cls.name()}") + + async def reset(self, ctx): + """This function deletes the game instance for this channel""" + if self.setup_check(ctx): + del self.game_instances[ctx.channel] + + # TODO: better info message + async def info(self, ctx): + """Send information about the subcommands for the game""" + embed = discord.Embed(title="How to play?", description="You will need to set up the game and its information in a channel and start the game there. Afterwards the player mainly interact with the bot in DM.", color=0x00ffff) + embed.set_author(name=f"With this bot you can play {self.game_cls.name()}") + # embed.set_thumbnail(url="https://images-na.ssl-images-amazon.com/images/I/717GrDtFKCL._AC_SL1000_.jpg") + embed.add_field(name="$w game setup", value="Make this channel playable.", inline=False) + embed.add_field(name="$w game players", value="Set mentioned users as players", inline=False) + embed.add_field(name="$w game roles", value="Set the roles to play with", inline=False) + embed.add_field(name="$w game start", value="Play one round", inline=False) + embed.set_footer(text="Have fun!") + await ctx.send(embed=embed) + + async def pre_game_check(self, ctx): + return self.setup_check(ctx) and self.not_running_check(ctx) + + async def players(self, ctx): + await self.game_instances[ctx.channel].set_players(ctx.message) + + async def start(self, ctx): + self.game_instances[ctx.channel].game = self.bot.loop.create_task(self.game_instances[ctx.channel].round()) + await self.game_instances[ctx.channel].game + + async def in_game_check(self, ctx): + return self.setup_check(ctx) and self.running_check(ctx) + + async def stop(self, ctx): + self.game_instances[ctx.channel].game.cancel() + await self.send_friendly(ctx, "Game canceled") diff --git a/src/werewolf_players.py b/src/package/games/players.py similarity index 65% rename from src/werewolf_players.py rename to src/package/games/players.py index 13af25b..7734f37 100644 --- a/src/werewolf_players.py +++ b/src/package/games/players.py @@ -1,33 +1,30 @@ +"""Has a single class: Player""" + +# discord imports import discord class Player: + """This (abstract) class is a template for player objects for games""" - @staticmethod - async def make(member, game): - p = Player() + @classmethod + async def make(cls, member, game): + p = cls() p.member = member p.dm = member.dm_channel or await member.create_dm() p.game = game return p - def setRole(self, role): - self.day_role = self.night_role = role - - def reset(self): - self.tally = 0 - self.won = self.dead = False - def name(self): return self.member.name def __str__(self): return self.name() - def swap(self, player_B): - self.day_role, player_B.day_role = player_B.day_role, self.day_role + def reset(self): + pass - def other(self): + def other_players(self): return [p for p in self.game.player_list if p != self] async def send_normal(self, message): @@ -45,21 +42,24 @@ class Player: async def send_info(self, message): await self.send_embed(message, 0x00ffff) + # TODO: refactor this function to make it understandable async def ask_choice(self, question, options): text = f"{question}\n" + f"{'='*len(question)}\n\n" + '\n'.join(f"[{str(i)}]({str(options[i])})" for i in range(len(options))) await self.dm.send(f"```md\n{text}```") + # TODO: Basic Converters (https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html#basic-converters) def check_num(self, choice, N): if not choice.isdigit(): raise ValueError(f"Your choice {choice} is not a number") if not 0 <= int(choice) < N: raise ValueError(f"Your choice {choice} is not in range 0 - {N-1}") + # TODO: seems hacky, figure out a nicer way async def receive_choice(self, options, n_ans=1): while True: def check(choice): return choice.channel == self.dm and choice.author == self.member - choice = (await self.game.bot.wait_for('message', timeout=30.0, check=check)).content.split() + choice = (await self.game.bot.wait_for('message', check=check)).content.split() if not len(choice) == n_ans: await self.send_wrong(f"Please give {n_ans} numbers not {len(choice)}") @@ -77,21 +77,3 @@ class Player: async def get_choice(self, question, options): await self.ask_choice(question, options) return (await self.receive_choice(options))[0] - - async def get_double_choice(self, question, options): - await self.ask_choice(question, options) - return await self.receive_choice(options, 2) - - async def cast_vote(self, question, options): - self.vote = options[await self.get_choice(question, options)] - - async def ready_to_vote(self): - def check(msg): - return msg.channel == self.dm and msg.author == self.member and msg.content.casefold() == "vote" - await self.game.bot.wait_for('message', check=check) - await self.send_confirmation("You are ready to vote") - - -class No_player(Player): - def name(self): - return "no one" diff --git a/src/package/games/werewolf/__pycache__/cog.cpython-37.pyc b/src/package/games/werewolf/__pycache__/cog.cpython-37.pyc new file mode 100644 index 0000000..5ddac64 Binary files /dev/null and b/src/package/games/werewolf/__pycache__/cog.cpython-37.pyc differ diff --git a/src/package/games/werewolf/__pycache__/game.cpython-37.pyc b/src/package/games/werewolf/__pycache__/game.cpython-37.pyc new file mode 100644 index 0000000..3217184 Binary files /dev/null and b/src/package/games/werewolf/__pycache__/game.cpython-37.pyc differ diff --git a/src/package/games/werewolf/__pycache__/players.cpython-37.pyc b/src/package/games/werewolf/__pycache__/players.cpython-37.pyc new file mode 100644 index 0000000..5173188 Binary files /dev/null and b/src/package/games/werewolf/__pycache__/players.cpython-37.pyc differ diff --git a/src/package/games/werewolf/__pycache__/roles.cpython-37.pyc b/src/package/games/werewolf/__pycache__/roles.cpython-37.pyc new file mode 100644 index 0000000..71a5402 Binary files /dev/null and b/src/package/games/werewolf/__pycache__/roles.cpython-37.pyc differ diff --git a/src/package/games/werewolf/cog.py b/src/package/games/werewolf/cog.py new file mode 100644 index 0000000..65b88ca --- /dev/null +++ b/src/package/games/werewolf/cog.py @@ -0,0 +1,54 @@ +"""Has a single class: Werewolf_Cog""" + +# discord imports +from discord.ext import commands + +# local imports +from ..game_cog import Game_cog +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""" + + @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()? + await ctx.invoke(self.bot.get_command('werewolf info')) + + @werewolf.command() + async def info(self, ctx): + """Send information about the subcommands for the game""" + await super().info(ctx, Werewolf_game) + + @werewolf.command() + async def setup(self, ctx): + """This function creates an game instance for this channel""" + await super().setup(ctx, Werewolf_game) + + @werewolf.command() + async def reset(self, ctx): + """This function deletes the game instance for this channel""" + await super().reset(ctx) + + @werewolf.command() + @commands.check(Game_cog.pre_game_check) + async def players(self, ctx): + """registers all mentioned players for the game""" + await super().players(ctx) + + @werewolf.command() + @commands.check(Game_cog.pre_game_check) + async def start(self, ctx): + """starts a round of werewolf""" + await super().start(ctx) + + @werewolf.command() + @commands.check(Game_cog.in_game_check) + async def stop(self, ctx): + """aborts the current round of werewolf""" + await super().stop(ctx) + + +def setup(bot): + bot.add_cog(Werewolf_cog(bot, Werewolf_game)) diff --git a/src/werewolf_game.py b/src/package/games/werewolf/game.py similarity index 95% rename from src/werewolf_game.py rename to src/package/games/werewolf/game.py index 255bef0..4d1b9ee 100644 --- a/src/werewolf_game.py +++ b/src/package/games/werewolf/game.py @@ -2,11 +2,15 @@ from random import shuffle import time import asyncio import discord -from werewolf_roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role -from werewolf_players import Player, No_player +from .roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role +from .players import Player, No_player -class Game: +class Werewolf_game: + + @classmethod + def name(cls): + return "One Night Ultimate Werewolf" def __init__(self, bot, channel): self.running = False @@ -14,7 +18,7 @@ class Game: self.channel = channel self.player_list = [] self.role_list = [] - self.discussion_time = 300 # seconds + self.discussion_time = 301 # seconds async def send(self, message): await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff)) @@ -69,6 +73,7 @@ class Game: await self.for_all_player(lambda p: p.send_info(f"Your role: **{p.night_role.name()}**")) async def night_phases(self): + # TODO: implement waiting if role in middle await asyncio.gather(*[self.role[r].query() for r in [Doppelganger, Seer, Robber, Troublemaker, Drunk]]) # slow await self.role[Doppelganger].send_copy_info() await self.role[Doppelganger].simulate() # slow diff --git a/src/package/games/werewolf/players.py b/src/package/games/werewolf/players.py new file mode 100644 index 0000000..a17ab9c --- /dev/null +++ b/src/package/games/werewolf/players.py @@ -0,0 +1,39 @@ +import discord + +"""Has a single class: Werewolf_player""" + +# local import +from .player import Player + + +class Werewolf_player(Player): + """This class is for simulating non-role-specific werewolf players""" + + def set_role(self, role): + self.day_role = self.night_role = role + + def reset(self): + self.tally = 0 + self.won = self.dead = False + + def swap(self, player_B): + self.day_role, player_B.day_role = player_B.day_role, self.day_role + + async def get_double_choice(self, question, options): + await self.ask_choice(question, options) + return await self.receive_choice(options, 2) + + async def cast_vote(self, question, options): + self.vote = options[await self.get_choice(question, options)] + + async def ready_to_vote(self): + def check(msg): + return msg.channel == self.dm and msg.author == self.member and msg.content.casefold() == "vote" + await self.game.bot.wait_for('message', check=check) + await self.send_confirmation("You are ready to vote") + + +# TODO: this seems hacky, find another approach +class No_player(Werewolf_player): + def name(self): + return "no one" diff --git a/src/werewolf_roles.py b/src/package/games/werewolf/roles.py similarity index 87% rename from src/werewolf_roles.py rename to src/package/games/werewolf/roles.py index ce14fb1..8e9b867 100644 --- a/src/werewolf_roles.py +++ b/src/package/games/werewolf/roles.py @@ -1,6 +1,6 @@ import functools from fuzzywuzzy import fuzz -from werewolf_players import No_player +from .players import No_player class Role: @@ -45,28 +45,33 @@ class Doppelganger(Role): @Role.no_player async def send_copy_info(self): self.copy_role = type(self.player.other()[self.choice].day_role) - await self.send_info(f"You copied: {self.copy_role}") + await self.player.send_info(f"You copied: {self.copy_role.name()}") @Role.no_player async def simulate(self): if self.copy_role in [Werewolf, Mason]: self.copy_role.add_yourself(self) - if self.copy_role == Werewolf: - await self.copy_role.phase(self) - if self.copy_role in [Mason, Minion]: - await self.copy_role.send_info(self) - - if self.copy_role in [Seer, Robber, Troublemaker, Drunk]: + elif self.copy_role in [Seer, Robber, Troublemaker, Drunk]: await self.copy_role.query(self) if self.copy_role in [Robber, Troublemaker, Drunk]: - self.copy_role.simulate(self) + await self.copy_role.simulate(self) if self.copy_role in [Seer, Robber]: await self.copy_role.send_info(self) + @Role.no_player + async def phase(self): + if self.copy_role == Werewolf: + await self.copy_role.phase(self) + + @Role.no_player + async def send_info(self): + if self.copy_role in [Mason, Minion]: + await self.copy_role.send_info(self) + @Role.no_player async def insomniac(self): if self.copy_role == Insomniac: - self.copy_role.send_info(self) + await self.copy_role.send_info(self) def is_role(self, cls): return self.copy_role == cls @@ -113,10 +118,10 @@ class Seer(Role): @Role.no_player async def send_info(self): if self.choice < len(self.player.other()): - await self.player.send_info(self.player.other()[self.choice].night_role) + await self.player.send_info(f"You saw: {self.player.other()[self.choice].night_role.name()}") else: a, b = [(0, 1), (1, 2), (0, 2)][self.choice - len(self.player.other())] - await self.player.send_info(f"{self.game.middle_card[a]} {self.game.middle_card[b]}") + await self.player.send_info(f"You saw: {self.game.middle_card[a]} {self.game.middle_card[b]}") class Robber(Role): @@ -130,7 +135,7 @@ class Robber(Role): @Role.no_player async def send_info(self): - await self.player.send_info(f"You robbed: {self.player.day_role}") + await self.player.send_info(f"You robbed: {self.player.day_role.name()}") class Troublemaker(Role): @@ -156,7 +161,7 @@ class Drunk(Role): class Insomniac(Role): @Role.no_player async def send_info(self): - await self.player.send_info(f"You are: {self.player.day_role}") + await self.player.send_info(f"You are: {self.player.day_role.name()}") class Villager(Role): diff --git a/src/package/send_message.py b/src/package/send_message.py new file mode 100644 index 0000000..136f7de --- /dev/null +++ b/src/package/send_message.py @@ -0,0 +1,18 @@ +# discord import +import discord + + +class Send_message: + """This (abstract) class for sending formatted messages""" + + def __init__(self, bot): + self.bot = bot + + async def send_embed(self, ctx, desc, color): + await ctx.send(embed=discord.Embed(description=desc, color=color)) + + async def send_friendly(self, ctx, desc): + await self.send_embed(ctx, desc, 0x00ff00) + + async def send_wrong(self, ctx, desc): + await self.send_embed(ctx, desc, 0xff8000) diff --git a/src/werewolf_bot.py b/src/werewolf_bot.py index bfaf7cb..9fb47b9 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -1,107 +1,43 @@ +""" +This is the main module of the Discord Bot + +Mainly loads the Cog's and starts the bot +""" + +__version__ = '0.3' +__author__ = 'Bibin Muttappillil' + +# standard library imports import os from dotenv import load_dotenv -import functools -import asyncio -import discord + +# discord imports from discord.ext import commands -from werewolf_game import Game as Werewolf_Game +# Token stuff load_dotenv() TOKEN = os.getenv('DISCORD_TOKEN') if TOKEN is None: print("Missing discord token!") exit(1) +bot = commands.Bot(command_prefix=commands.when_mentioned_or('$w ')) -PREFIX = '$w ' -bot = commands.Bot(command_prefix=commands.when_mentioned_or(PREFIX)) -bot.remove_command('help') - - -@bot.event -async def on_ready(): - await bot.change_presence(status=discord.Status.online, activity=discord.Game('One Night Ultimate Werewolf')) - print('We have logged in as {0.user}'.format(bot)) +bot.load_extension('package.developer') +bot.load_extension('package.games.werewolf.cog') @bot.command() -async def help(ctx): - embed = discord.Embed(title="How to play?", description="You will need to set up the game and its information in a channel and start the game there. Afterwards the player mainly interact with the bot in DM.", color=0x00ffff) - embed.set_author(name="With this bot you can play One Night Ultimate Werewolf") - # embed.set_thumbnail(url="https://images-na.ssl-images-amazon.com/images/I/717GrDtFKCL._AC_SL1000_.jpg") - embed.add_field(name="$w game setup", value="Make this channel playable.", inline=False) - embed.add_field(name="$w game players", value="Set mentioned users as players", inline=False) - embed.add_field(name="$w game roles", value="Set the roles to play with", inline=False) - embed.add_field(name="$w game start", value="Play one round", inline=False) - embed.set_footer(text="Have fun!") - await ctx.send(embed=embed) +@commands.is_owner() +async def reload(ctx, extension): + bot.reload_extension(f'package.{extension}') -async def send_embed(ctx, desc, color): - await ctx.send(embed=discord.Embed(description=desc, color=color)) - - -async def send_friendly(ctx, desc): - await send_embed(ctx, desc, 0x00ff00) - - -async def send_wrong(ctx, desc): - await send_embed(ctx, desc, 0xff8000) - - -# game commands - -game_instances = {} - - -@bot.group() -async def game(ctx): - if ctx.invoked_subcommand is None: - await send_wrong(ctx, 'Invalid sub command passed...') - - -@game.command() -async def setup(ctx): - if ctx.channel in game_instances: - await send_wrong(ctx, "Game already setup in this channel") - else: - game_instances[ctx.channel] = Werewolf_Game(bot, ctx.channel) - await send_friendly(ctx, "This channel can now play Werewolf") - - -def channel_setup(command): - @functools.wraps(command) - async def wrapper(ctx, *args, **kwargs): - if ctx.channel not in game_instances: - await send_wrong(ctx, f"No game setup yet. Use {PREFIX}game setup") - else: - await command(ctx, *args, **kwargs) - return wrapper - - -def game_not_running(command): - @functools.wraps(command) - @channel_setup - async def wrapper(ctx, *args, **kwargs): - if game_instances[ctx.channel].running: - await send_wrong(ctx, "Sorry! A game is already running") - else: - await command(ctx, *args, **kwargs) - return wrapper - - -def game_running(command): - @functools.wraps(command) - @channel_setup - async def wrapper(ctx, *args, **kwargs): - if not game_instances[ctx.channel].running: - await send_wrong(ctx, "No game is running") - else: - await command(ctx, *args, **kwargs) - return wrapper - +# checker annotations +# TODO: replace with discord.py error handling? +''' def error_handling(command): @functools.wraps(command) async def wrapper(ctx, *args, **kwargs): @@ -112,30 +48,11 @@ def error_handling(command): except asyncio.TimeoutError: await send_wrong(ctx, "Error: I got bored waiting for your input") return wrapper +''' +''' -@game.command() -@game_not_running -@error_handling -async def start(ctx): - game_instances[ctx.channel].game = bot.loop.create_task(game_instances[ctx.channel].round()) - await game_instances[ctx.channel].game - - -@game.command() -@game_running -@channel_setup -async def stop(ctx): - game_instances[ctx.channel].game.cancel() - await send_friendly(ctx, "Game canceled") - - -@game.command() -@game_not_running -@error_handling -async def players(ctx): - await game_instances[ctx.channel].set_players(ctx.message) - +# TODO: (specifig game) werewolf COG @game.command() @game_not_running @@ -156,45 +73,6 @@ async def minutes(ctx, i): @error_handling async def time(ctx): await send_friendly(ctx, game_instances[ctx.channel].remaining_time_string()) - - -# smaller commands - -@bot.command() -async def hello(ctx): - await send_friendly(ctx, f"Hello {ctx.message.author.name} :wave:") - - -@bot.command() -async def ping(ctx): - print("pong") - await send_friendly(ctx, "pong") - - -# developer commands - -def developer(command): - @functools.wraps(command) - async def wrapper(ctx, *args, **kwargs): - DEV_ID = 461892912821698562 - if ctx.author.id == DEV_ID: - await command(ctx, *args, **kwargs) - else: - await send_wrong(ctx, "This command is not for you!") - return wrapper - - -@bot.command() -@developer -async def logout(ctx): - await bot.logout() - - -@bot.command() -@developer -async def debug(ctx, *args): - print("DEBUG") - print(ctx.args) - print(ctx.kwargs) +''' bot.run(TOKEN)