diff --git a/src/package/__pycache__/developer.cpython-37.pyc b/src/package/__pycache__/developer.cpython-37.pyc deleted file mode 100644 index e2450da..0000000 Binary files a/src/package/__pycache__/developer.cpython-37.pyc and /dev/null differ diff --git a/src/package/__pycache__/send_message.cpython-37.pyc b/src/package/__pycache__/send_message.cpython-37.pyc deleted file mode 100644 index 96a6f4f..0000000 Binary files a/src/package/__pycache__/send_message.cpython-37.pyc and /dev/null differ diff --git a/src/package/developer.py b/src/package/developer.py deleted file mode 100644 index d55d30d..0000000 --- a/src/package/developer.py +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 75ffe46..0000000 Binary files a/src/package/games/__pycache__/game.cpython-37.pyc and /dev/null differ diff --git a/src/package/games/__pycache__/game_cog.cpython-37.pyc b/src/package/games/__pycache__/game_cog.cpython-37.pyc deleted file mode 100644 index bdaa947..0000000 Binary files a/src/package/games/__pycache__/game_cog.cpython-37.pyc and /dev/null differ diff --git a/src/package/games/game.py b/src/package/games/game.py deleted file mode 100644 index 7a4ccf6..0000000 --- a/src/package/games/game.py +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 7c9083e..0000000 --- a/src/package/games/game_cog.py +++ /dev/null @@ -1,78 +0,0 @@ -"""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/package/games/werewolf/__pycache__/cog.cpython-37.pyc b/src/package/games/werewolf/__pycache__/cog.cpython-37.pyc deleted file mode 100644 index 5ddac64..0000000 Binary files a/src/package/games/werewolf/__pycache__/cog.cpython-37.pyc and /dev/null differ diff --git a/src/package/games/werewolf/__pycache__/game.cpython-37.pyc b/src/package/games/werewolf/__pycache__/game.cpython-37.pyc deleted file mode 100644 index 3217184..0000000 Binary files a/src/package/games/werewolf/__pycache__/game.cpython-37.pyc and /dev/null differ diff --git a/src/package/games/werewolf/__pycache__/players.cpython-37.pyc b/src/package/games/werewolf/__pycache__/players.cpython-37.pyc deleted file mode 100644 index 5173188..0000000 Binary files a/src/package/games/werewolf/__pycache__/players.cpython-37.pyc and /dev/null differ diff --git a/src/package/games/werewolf/__pycache__/roles.cpython-37.pyc b/src/package/games/werewolf/__pycache__/roles.cpython-37.pyc deleted file mode 100644 index 71a5402..0000000 Binary files a/src/package/games/werewolf/__pycache__/roles.cpython-37.pyc and /dev/null differ diff --git a/src/package/games/werewolf/cog.py b/src/package/games/werewolf/cog.py deleted file mode 100644 index 65b88ca..0000000 --- a/src/package/games/werewolf/cog.py +++ /dev/null @@ -1,54 +0,0 @@ -"""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/package/games/werewolf/players.py b/src/package/games/werewolf/players.py deleted file mode 100644 index a17ab9c..0000000 --- a/src/package/games/werewolf/players.py +++ /dev/null @@ -1,39 +0,0 @@ -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/package/send_message.py b/src/package/send_message.py deleted file mode 100644 index 136f7de..0000000 --- a/src/package/send_message.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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 9fb47b9..bfaf7cb 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -1,43 +1,107 @@ -""" -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 - -# discord imports +import functools +import asyncio +import discord 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 ')) -bot.load_extension('package.developer') -bot.load_extension('package.games.werewolf.cog') +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.command() -@commands.is_owner() -async def reload(ctx, extension): - bot.reload_extension(f'package.{extension}') +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) -# checker annotations -# TODO: replace with discord.py error handling? +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 + -''' def error_handling(command): @functools.wraps(command) async def wrapper(ctx, *args, **kwargs): @@ -48,11 +112,30 @@ def error_handling(command): except asyncio.TimeoutError: await send_wrong(ctx, "Error: I got bored waiting for your input") return wrapper -''' -''' -# TODO: (specifig game) werewolf COG +@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) + @game.command() @game_not_running @@ -73,6 +156,45 @@ 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) diff --git a/src/package/games/werewolf/game.py b/src/werewolf_game.py similarity index 95% rename from src/package/games/werewolf/game.py rename to src/werewolf_game.py index 4d1b9ee..255bef0 100644 --- a/src/package/games/werewolf/game.py +++ b/src/werewolf_game.py @@ -2,15 +2,11 @@ from random import shuffle import time import asyncio import discord -from .roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role -from .players import Player, No_player +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 -class Werewolf_game: - - @classmethod - def name(cls): - return "One Night Ultimate Werewolf" +class Game: def __init__(self, bot, channel): self.running = False @@ -18,7 +14,7 @@ class Werewolf_game: self.channel = channel self.player_list = [] self.role_list = [] - self.discussion_time = 301 # seconds + self.discussion_time = 300 # seconds async def send(self, message): await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff)) @@ -73,7 +69,6 @@ class Werewolf_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/players.py b/src/werewolf_players.py similarity index 65% rename from src/package/games/players.py rename to src/werewolf_players.py index 7734f37..13af25b 100644 --- a/src/package/games/players.py +++ b/src/werewolf_players.py @@ -1,30 +1,33 @@ -"""Has a single class: Player""" - -# discord imports import discord class Player: - """This (abstract) class is a template for player objects for games""" - @classmethod - async def make(cls, member, game): - p = cls() + @staticmethod + async def make(member, game): + p = Player() 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 reset(self): - pass + def swap(self, player_B): + self.day_role, player_B.day_role = player_B.day_role, self.day_role - def other_players(self): + def other(self): return [p for p in self.game.player_list if p != self] async def send_normal(self, message): @@ -42,24 +45,21 @@ 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', check=check)).content.split() + choice = (await self.game.bot.wait_for('message', timeout=30.0, check=check)).content.split() if not len(choice) == n_ans: await self.send_wrong(f"Please give {n_ans} numbers not {len(choice)}") @@ -77,3 +77,21 @@ 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/roles.py b/src/werewolf_roles.py similarity index 87% rename from src/package/games/werewolf/roles.py rename to src/werewolf_roles.py index 8e9b867..ce14fb1 100644 --- a/src/package/games/werewolf/roles.py +++ b/src/werewolf_roles.py @@ -1,6 +1,6 @@ import functools from fuzzywuzzy import fuzz -from .players import No_player +from werewolf_players import No_player class Role: @@ -45,33 +45,28 @@ 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.player.send_info(f"You copied: {self.copy_role.name()}") + await self.send_info(f"You copied: {self.copy_role}") @Role.no_player async def simulate(self): if self.copy_role in [Werewolf, Mason]: self.copy_role.add_yourself(self) - elif self.copy_role in [Seer, Robber, Troublemaker, Drunk]: + 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]: await self.copy_role.query(self) if self.copy_role in [Robber, Troublemaker, Drunk]: - await self.copy_role.simulate(self) + 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: - await self.copy_role.send_info(self) + self.copy_role.send_info(self) def is_role(self, cls): return self.copy_role == cls @@ -118,10 +113,10 @@ class Seer(Role): @Role.no_player async def send_info(self): if self.choice < len(self.player.other()): - await self.player.send_info(f"You saw: {self.player.other()[self.choice].night_role.name()}") + await self.player.send_info(self.player.other()[self.choice].night_role) else: a, b = [(0, 1), (1, 2), (0, 2)][self.choice - len(self.player.other())] - await self.player.send_info(f"You saw: {self.game.middle_card[a]} {self.game.middle_card[b]}") + await self.player.send_info(f"{self.game.middle_card[a]} {self.game.middle_card[b]}") class Robber(Role): @@ -135,7 +130,7 @@ class Robber(Role): @Role.no_player async def send_info(self): - await self.player.send_info(f"You robbed: {self.player.day_role.name()}") + await self.player.send_info(f"You robbed: {self.player.day_role}") class Troublemaker(Role): @@ -161,7 +156,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.name()}") + await self.player.send_info(f"You are: {self.player.day_role}") class Villager(Role):