diff --git a/.gitignore b/.gitignore index 7e21ea3..4e78b90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ secrets.env .env venv +__pycache__/ +src/test.py \ No newline at end of file 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 new file mode 100644 index 0000000..920a6df --- /dev/null +++ b/src/package/developer.py @@ -0,0 +1,54 @@ +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, i: int): + await ctx.send("pong") + await ctx.send(i + 1) + + @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): + 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/game.py b/src/package/games/game.py new file mode 100644 index 0000000..ac8cf93 --- /dev/null +++ b/src/package/games/game.py @@ -0,0 +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 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 + + 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 new file mode 100644 index 0000000..28a5886 --- /dev/null +++ b/src/package/games/game_cog.py @@ -0,0 +1,85 @@ +"""Has a single class: Game_cog""" + +# standard library imports +from typing import Dict + +# discord imports +import discord +from discord.ext import commands + +# 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""" + + game_cls = Game + + def __init__(self, bot): + self.bot = bot + self.game_instances = {} # TODO: type hint? 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 await 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) + + # 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) + return commands.check(predicate) + + 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 + + def in_game(): + async def predicate(ctx): + return await ctx.cog.setup_check(ctx) and await ctx.cog.running_check(ctx) + return commands.check(predicate) + + 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/players.py b/src/package/games/players.py new file mode 100644 index 0000000..7734f37 --- /dev/null +++ b/src/package/games/players.py @@ -0,0 +1,79 @@ +"""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() + p.member = member + p.dm = member.dm_channel or await member.create_dm() + p.game = game + return p + + def name(self): + return self.member.name + + def __str__(self): + return self.name() + + def reset(self): + pass + + def other_players(self): + return [p for p in self.game.player_list if p != self] + + async def send_normal(self, message): + await self.dm.send(message) + + async def send_embed(self, desc, color): + await self.dm.send(embed=discord.Embed(description=desc, color=color)) + + async def send_wrong(self, message): + await self.send_embed(message, 0xff8000) + + async def send_confirmation(self, message): + await self.send_embed(message, 0x00ff00) + + 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() + + if not len(choice) == n_ans: + await self.send_wrong(f"Please give {n_ans} numbers not {len(choice)}") + continue + try: + for c in choice: + self.check_num(c, len(options)) + except ValueError as error: + await self.send_wrong(str(error)) + continue + + await self.send_confirmation(f"Received: {', '.join(choice)}") + return [int(c) for c in choice] + + async def get_choice(self, question, options): + await self.ask_choice(question, options) + return (await self.receive_choice(options))[0] diff --git a/src/package/games/werewolf/cog.py b/src/package/games/werewolf/cog.py new file mode 100644 index 0000000..210d7b5 --- /dev/null +++ b/src/package/games/werewolf/cog.py @@ -0,0 +1,75 @@ +"""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""" + + 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()? + 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.command() + async def setup(self, ctx): + """This function creates an game instance for this channel""" + await super().setup(ctx) + + @werewolf.command() + async def reset(self, ctx): + """This function deletes the game instance for this channel""" + await super().reset(ctx) + + @werewolf.command() + @Game_cog.pre_game() + async def players(self, ctx): + """registers all mentioned players for the game""" + await super().players(ctx) + + @werewolf.command() + @Game_cog.pre_game() + async def roles(self, ctx, *args): + """registers roles you want to play with""" + await self.game_instances[ctx.channel].set_roles(args) + + @werewolf.command() + @Game_cog.pre_game() + async def minutes(self, ctx, i: int): + """set discussion time""" + await self.game_instances[ctx.channel].set_time(i) + + @werewolf.command() + @Game_cog.pre_game() + async def start(self, ctx): + print(await self.setup_check(ctx), await self.not_running_check(ctx)) + """starts a round of werewolf""" + await super().start(ctx) + + @werewolf.command() + @Game_cog.in_game() + async def stop(self, ctx): + """aborts the current round of werewolf""" + await super().stop(ctx) + + @werewolf.command() + @Game_cog.in_game() + async def time(self, ctx): + """checks how much discussion time there is left""" + await self.send_friendly(ctx, self.game_instances[ctx.channel].remaining_time_string()) + + +def setup(bot): + bot.add_cog(Werewolf_cog(bot)) diff --git a/src/package/games/werewolf/game.py b/src/package/games/werewolf/game.py new file mode 100644 index 0000000..dee4564 --- /dev/null +++ b/src/package/games/werewolf/game.py @@ -0,0 +1,228 @@ +"""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(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): + super().__init__(bot, channel) + self.role_list = [] + self.discussion_time = 301 # seconds + + 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, 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 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]: + self.role[r] = [] + else: + r(self).add_yourself() + self.voting_list = self.player_list + [No_player()] + for c in self.voting_list: + c.reset() + + 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) + role_obj.add_yourself() + 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(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()}**")) + + 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 + await asyncio.gather(*[w.phase() for w in self.role[Werewolf]]) # slow + await asyncio.gather(*[w.send_info() for w in [self.role[Minion]] + self.role[Mason] + [self.role[Seer]]]) + await self.role[Robber].simulate() + await self.role[Robber].send_info() + await self.role[Troublemaker].simulate() + await self.role[Drunk].simulate() + await self.role[Insomniac].send_info() + await self.role[Doppelganger].insomniac() + + async def start_day(self): + await self.send("The day has started") + + def remaining_time_string(self): + t = int(self.start_time + self.discussion_time - time.time()) + 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) + await self.send(f"{self.remaining_time_string()} remaining") + await asyncio.sleep(self.discussion_time / 2 - 60) + await self.send(f"{self.remaining_time_string()} remaining") + await asyncio.sleep(30) + await self.send(f"{self.remaining_time_string()} remaining") + await asyncio.sleep(30) + + async def early_vote(self): + await self.for_all_player(lambda p: p.ready_to_vote()) + + async def discussion_finished(self): + done, pending = await asyncio.wait([self.discussion_timer(), self.early_vote()], return_when=asyncio.FIRST_COMPLETED) + for p in pending: + p.cancel() + await asyncio.wait(pending) + + async def vote(self): + await self.send("Vote in DM") + await self.for_all_player(lambda p: p.cast_vote("Who do you want to kill?", self.voting_list)) + + def tally(self): + for p in self.player_list: + p.vote.tally += 1 + + def who_dead(self): + 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): + dead.append(d.vote) + return dead + + def who_won(self, dead): + + no_dead = (len(dead) == 0) + tanner_dead = any(d.day_role.is_role(Tanner) for d in dead) + werewolf_dead = any(d.day_role.is_role(Werewolf) for d in dead) + werewolf_in_game = any(p.day_role.is_role(Werewolf) for p in self.player_list) + minion_dead = any(d.day_role.is_role(Minion) for d in dead) + minion_in_game = any(p.day_role.is_role(Minion) for p in self.player_list) + + werewolf_won = False + village_won = False + tanner_won = False + + # could make it shorter using boolean algebra + if no_dead: + if werewolf_in_game: + werewolf_won = True + else: + village_won = True + else: + if tanner_dead: + tanner_won = True + if werewolf_dead: + village_won = True + else: + if werewolf_dead: + village_won = True + else: + if minion_dead: + if werewolf_in_game: + werewolf_won = True + else: + village_won = True + else: + if minion_in_game: + werewolf_won = True + + for p in self.player_list: + if p.day_role.is_role(Werewolf) or p.day_role.is_role(Minion): + p.won = werewolf_won + elif p.day_role.is_role(Tanner): + p.won = p.dead + else: + p.won = village_won + + return werewolf_won, village_won, tanner_won, dead + + async def result(self, werewolf_won, village_won, tanner_won, dead): + winnning = [] + if werewolf_won: + winnning.append("Werewolves") + 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") # 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 ":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) + + async def round(self): + try: + self.check() + self.setup() + self.running = True + self.distribute_roles() + await self.start_night() + await self.send_role() + await self.night_phases() + await self.start_day() + await self.discussion_finished() + await self.vote() + self.tally() + await self.result(*self.who_won(self.who_dead())) + finally: + self.end() diff --git a/src/package/games/werewolf/players.py b/src/package/games/werewolf/players.py new file mode 100644 index 0000000..443f075 --- /dev/null +++ b/src/package/games/werewolf/players.py @@ -0,0 +1,37 @@ +"""Has a single class: Werewolf_player""" + +# local import +from ..players 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/games/werewolf/roles.py b/src/package/games/werewolf/roles.py new file mode 100644 index 0000000..8e9b867 --- /dev/null +++ b/src/package/games/werewolf/roles.py @@ -0,0 +1,180 @@ +import functools +from fuzzywuzzy import fuzz +from .players import No_player + + +class Role: + def __init__(self, game, player=No_player()): + self.game = game + self.player = player + + def add_yourself(self): + self.game.role[type(self)] = self + + async def send_role_list(self, cls): + await self.player.send_info(f"{cls.name()}: {', '.join(r.player.name() for r in self.game.role[cls])}") + + def is_role(self, cls): + return isinstance(self, cls) + + @staticmethod + def match(message): + return max(Role.__subclasses__(), key=lambda role_class: fuzz.ratio(message, role_class.name())) + + @staticmethod + def no_player(func): + @functools.wraps(func) + async def wrapper(self, *args, **kwargs): + if not isinstance(self.player, No_player): + return await func(self, *args, **kwargs) + return wrapper + + @classmethod + def name(cls): + return cls.__name__.casefold() + + def __str__(self): + return self.name() + + +class Doppelganger(Role): + @Role.no_player + async def query(self): + self.choice = await self.player.get_choice("Which player role do you want to copy?", self.player.other()) + + @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()}") + + @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]: + await self.copy_role.query(self) + if self.copy_role in [Robber, Troublemaker, Drunk]: + 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: + await self.copy_role.send_info(self) + + def is_role(self, cls): + return self.copy_role == cls + + +class Werewolf(Role): + def add_yourself(self): + self.game.role[Werewolf].append(self) + + @Role.no_player + async def phase(self): + if len(self.game.role[Werewolf]) >= 2: + await self.send_role_list(Werewolf) + else: + await self.player.send_info("You are the only werewolf") + self.choice = await self.player.get_choice("Which card in the middle do you want to look at?", ["left", "middle", "right"]) # 0, 1, 2 + + await self.player.send_info(f"A card in the middle is: {self.game.middle_card[self.choice].name()}") + + +class Minion(Role): + @Role.no_player + async def send_info(self): + if len(self.game.role[Werewolf]) == 0: + await self.player.send_info("There were no werewolves, so you need to kill a villager!") + else: + await self.send_role_list(Werewolf) + + +class Mason(Role): + def add_yourself(self): + self.game.role[Mason].append(self) + + @Role.no_player + async def send_info(self): + await self.send_role_list(Mason) + + +class Seer(Role): + @Role.no_player + async def query(self): + self.choice = await self.player.get_choice("Which 1 player card or 2 middle cards do you want to look at?", self.player.other() + ["left & middle", "middle & right", "left & right"]) + + @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()}") + 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]}") + + +class Robber(Role): + @Role.no_player + async def query(self): + self.choice = await self.player.get_choice("Which player do you want to rob?", self.player.other()) + + @Role.no_player + async def simulate(self): + self.player.swap(self.player.other()[self.choice]) + + @Role.no_player + async def send_info(self): + await self.player.send_info(f"You robbed: {self.player.day_role.name()}") + + +class Troublemaker(Role): + @Role.no_player + async def query(self): + self.A, self.B = await self.player.get_double_choice("Who do you want to exchange? (send two numbers)", self.player.other()) + + @Role.no_player + async def simulate(self): + self.player.other()[self.A].swap(self.player.other()[self.B]) + + +class Drunk(Role): + @Role.no_player + async def query(self): + self.choice = await self.player.get_choice("Which card from the middle do you want to take?", ["left", "middle", "right"]) + + @Role.no_player + async def simulate(self): + self.player.day_role, self.game.middle_card[self.choice] = self.game.middle_card[self.choice], self.player.day_role # swap + + +class Insomniac(Role): + @Role.no_player + async def send_info(self): + await self.player.send_info(f"You are: {self.player.day_role.name()}") + + +class Villager(Role): + pass + + +class Tanner(Role): + pass + + +class Hunter(Role): + pass + + +class No_role(Role): + pass 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 new file mode 100644 index 0000000..94c966e --- /dev/null +++ b/src/werewolf_bot.py @@ -0,0 +1,54 @@ +""" +This is the main module of the Discord Bot + +Mainly loads the Cog's and starts the bot +""" + +__version__ = '0.4' +__author__ = 'Bibin Muttappillil' + +# standard library imports +import os +from dotenv import load_dotenv + +# discord imports +from discord.ext import commands + + +# 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('$b ')) + +bot.load_extension('package.developer') +bot.load_extension('package.games.werewolf.cog') + + +@bot.command() +@commands.is_owner() +async def reload(ctx, extension): + bot.reload_extension(f'package.{extension}') + print(f'package.{extension} reloaded') + + +# checker annotations +# TODO: replace with discord.py error handling? + +''' +def error_handling(command): + @functools.wraps(command) + async def wrapper(ctx, *args, **kwargs): + try: + await command(ctx, *args, **kwargs) + except ValueError as error: + await send_wrong(ctx, str(error)) + except asyncio.TimeoutError: + await send_wrong(ctx, "Error: I got bored waiting for your input") + return wrapper +''' + +bot.run(TOKEN) diff --git a/src/werewolve-bot-old.py b/src/werewolve-bot-old.py deleted file mode 100644 index ba07ef9..0000000 --- a/src/werewolve-bot-old.py +++ /dev/null @@ -1,381 +0,0 @@ -import os -import discord -from enum import Enum -from random import shuffle -import asyncio -from dotenv import load_dotenv - -load_dotenv() -TOKEN = os.getenv('DISCORD_TOKEN') - -class Role(Enum): - doppelganger = 1 - werewolve = 2 - minion = 3 - mason = 4 - seer = 5 - robber = 6 - troublemaker = 7 - drunk = 8 - insomniac = 9 - villiager = 10 - tanner = 11 - hunter = 12 - -class Player: - - def __init__(self, member, role): - self.member = member - self.night_role = role - self.day_role = role - self.vote = -1 - self.tally = 0 - - async def create_dm(self): - self.dm = self.member.dm_channel or await self.member.create_dm() - - async def send(self, message): - await self.dm.send(message) - - async def sendRole(self): - await self.send("Your role: " + self.night_role.name) - - async def sendPoll(self): - await self.send(Player.poll_message) - - async def sendMiddle(self): - await self.send(Player.poll_middle) - - async def receiveChoice(self): - - global bot - - def check(vote): - - return vote.channel == self.dm and vote.content.isdigit() and 0 <= int(vote.content) < Player.size - - vote = await bot.wait_for('message', timeout=30.0, check = check) - await self.send("Received: " + vote.content) - - return int(vote.content) - - -bot = discord.Client() - -@bot.event -async def on_ready(): - print('We have logged in as {0.user}'.format(bot)) - - - -running = False - -async def hello(message): - print("Hello") - await message.channel.send('Hello!:regional_indicator_a:') - - def check(vote): - return vote.content in ["a", "b"] and vote.channel == message.channel - - vote = await bot.wait_for('message', timeout=10.0, check = check) - await message.channel.send("Received! " + vote.content) - -@bot.event -async def on_message(message): - - global running - - if message.author == bot.user: - return - - - if message.content.startswith('$hello'): - await hello(message) - return - - - if message.content.startswith('$logout'): - await bot.logout() - return - - if message.content.startswith('$werewolve'): - - # start (only one instance running) - - if running: - await message.channel.send("Sorry! A game is already running") - return - - - - - """ - if len(players) < 4: - await message.channel.send("To few players!") - return - - """ - - running = True - - - # setup - - members = [mem for mem in message.channel.members if not mem.bot] - - Player.size = len(members) - - - role_set = [Role.werewolve, Role.werewolve, Role.drunk, Role.insomniac, Role.seer, Role.robber, Role.hunter] + (Player.size-4)*[Role.villiager] - shuffle(role_set) - - players = [] - - - werewolve = [] - minion = None - mason = [] - seer = None - robber = None - troublemaker = None - drunk = None - insomniac = None - villiager = [] - tanner = None - hunter = None - - for i in range(Player.size): - - players.append(Player(members[i], role_set[i])) - await players[i].create_dm() - - - if role_set[i] == Role.werewolve: - werewolve.append(players[i]) - - elif role_set[i] == Role.mason: - mason.append(players[i]) - - elif role_set[i] == Role.seer: - seer = players[i] - - elif role_set[i] == Role.robber: - robber = players[i] - - elif role_set[i] == Role.troublemaker: - troublemaker = players[i] - - elif role_set[i] == Role.drunk: - drunk = players[i] - - elif role_set[i] == Role.insomniac: - insomniac = players[i] - - elif role_set[i] == Role.villiager: - villiager.append(players[i]) - - elif role_set[i] == Role.tanner: - tanner = players[i] - - elif role_set[i] == Role.hunter: - hunter = players[i] - - middle = role_set[-3:] - - - Player.poll_middle = "(0) left\n(1) middle\n(2) right" - - Player.poll_message = "" - for i in range(Player.size): - Player.poll_message += "(" + str(i) + ") " + players[i].member.name + "\n" - - # doing phase - - - #send role info to all - send_roles = [p.sendRole() for p in players] - await asyncio.gather(*send_roles) - - - - # query stuff - - #doppelganger stuff - - async def query_seer(): - if seer is None: - return - - await seer.send("Who do you want to look at?") - await seer.sendPoll() - - async def query_robber(): - if robber is None: - return - - await robber.send("Who do you want to rob?") - await robber.sendPoll() - - async def query_troublemaker(): - if troublemaker is None: - return - - await troublemaker.send("Who do you want to exchange?") - await troublemaker.sendPoll() - await troublemaker.sendPoll() - - async def query_drunk(): - if drunk is None: - return - - await drunk.send("Which card from the middle do you want to take?") - await drunk.sendMiddle() - - await asyncio.gather(query_seer(), query_robber(), query_troublemaker(), query_drunk()) - - - - #receive and confirm! - async def receive_seer(): - if seer is not None: - return await seer.receiveChoice() - - async def receive_robber(): - if robber is not None: - return await robber.receiveChoice() - - async def receive_troublemaker(): - if troublemaker is not None: - return await troublemaker.receiveChoice() - - async def receive_drunk(): - if drunk is not None: - return await drunk.receiveChoice() - - seerChoice, robberChoice, troublemakerChoice, drunkChoice = await asyncio.gather(receive_seer(), receive_robber(), receive_troublemaker(), receive_drunk()) - - - - # simulate - - #exchange robber - if robber is not None: - robber.day_role, players[robberChoice].day_role = players[robberChoice].day_role, robber.day_role - #exchange troublemaker - if troublemaker is not None: - A = players[troublemakerChoice[0]] - B = players[troublemakerChoice[1]] - A.day_role, B.day_role = B.day_role, A.day_role - #exchange drunk - if drunk is not None: - drunk.day_role, middle[drunkChoice] = middle[drunkChoice], drunk.day_role - - - - #send werewolves identity to werewolves and minion - async def send_werewolves(): - message = "" - for w in werewolve: - message += w.member.name + " " - - message += "were werewolves" - - sender = [bad.send(message) for bad in werewolve] - if minion is not None: - sender.append(minion.send(message)) - - await asyncio.gather(*sender) - - #send mason identity to masons - async def send_masons(): - message = "" - for m in mason: - message += m.member.name + " " - - message += " were masons" - - sender = [m.send(message) for m in mason] - await asyncio.gather(*sender) - - #send info to seer - async def send_seer(): - if seer is not None: - await seer.send(players[seerChoice].member.name + " was a " + players[seerChoice].night_role.name) - - #send info to robber - async def send_robber(): - if robber is not None: - await robber.send("You stole the role: " + players[robberChoice].night_role.name) - - #send insomniac new role to insomniac - async def send_insomniac(): - if insomniac is not None: - await insomniac.send("You are now a " + insomniac.day_role.name) - - await asyncio.gather(send_werewolves(), send_masons(), send_seer(), send_robber(), send_insomniac()) - - - - # discussion - - # vote - - def check_vote(vote): - return vote.content == "$vote" and vote.channel == message.channel - - await bot.wait_for('message', check = check_vote) - - await message.channel.send("Vote in DM") - - - send_votes = [p.sendPoll() for p in players] - await asyncio.gather(*send_votes) - - receive_votes = [p.receiveChoice() for p in players] - tmp = await asyncio.gather(*receive_votes) - for i in range(Player.size): - players[i].vote = tmp[i] - - for p in players: - players[p.vote].tally += 1 - - maxi = max( [p.tally for p in players] ) - - dead = [p for p in players if p.tally >= maxi] - - if hunter in dead: - dead.append(players[hunter.vote]) - - - - # result and end - # show day-role & night role - - msg = "" - for d in dead: - msg += d.member.name + " " - msg += "are dead!" - await message.channel.send(msg) - - if any(d in werewolve for d in dead): - msg = "The village won!" - else: - msg = "The werewolves won!" - - await message.channel.send(msg) - - msg = "" - for p in players: - msg += p.member.name + ": " + p.day_role.name + " (day) " + p.night_role.name + " (night) and voted for " + players[p.vote].member.name + "\n" - - msg += middle - - await message.channel.send(msg) - - - - running = False - - - -bot.run(TOKEN) \ No newline at end of file diff --git a/src/werewolve-bot.py b/src/werewolve-bot.py deleted file mode 100644 index ccf6601..0000000 --- a/src/werewolve-bot.py +++ /dev/null @@ -1,499 +0,0 @@ -import os -import discord -from enum import Enum -from random import shuffle -import asyncio -from dotenv import load_dotenv - -load_dotenv() -TOKEN = os.getenv('DISCORD_TOKEN') -if TOKEN is None: - print("Missing discord token!") - exit(1) - - - -class Role: - - def __init__(self, game): - self.game = game - self.copy = self - - def setPlayer(self, player): - self.player = player - - async def phase1(self): # query stuff + doppelganger simulation - pass - - async def phase2(self): # werewolf stuff + seer info - pass - - async def phase3(self): # robber simulation & info - pass - - async def phase4(self): # troublemaker simulation - pass - - async def phase5(self): # mostly sending info + drunk simulation - pass - - @staticmethod - def match(message, game): - for role_class in Role.role_set: - if message.casefold() == role_class.name(): - return role_class(game) - - @classmethod - def name(cls): - return cls.__name__.casefold() - - def __str__(self): - return self.name() - -class Doppelganger(Role): - order = 1 - -class Werewolf(Role): - order = 2 - - def setPlayer(self, player): - super().setPlayer(player) - self.game.werewolf_list.append(player) - - async def phase2(self): - if len(self.game.werewolf_list) >= 2: - await self.player.send("Werewolves: " + str(self.game.werewolf_list)) - else: - await self.player.send("You are the only werewolf") - await self.player.send("Which card in the middle do you want to look at?") - self.choice = await self.player.get_choice(["left", "middle", "right"]) - - await self.player.send("A card in the middle is: " + self.game.middle_card[self.choice].name()) - - -class Minion(Role): - order = 3 - - async def phase2(self): - if len(self.game.werewolf_list) == 0: - await self.player.send("There were no werewolves so you became one") - else: - await self.player.send("Werewolves: " + str(self.game.werewolf_list)) - - -class Mason(Role): - order = 4 - - def setPlayer(self, player): - super().setPlayer(player) - self.game.mason_list.append(player) - - async def phase2(self): - await self.player.send("Mason " + str(self.game.mason_list)) - - -class Seer(Role): - order = 5 - - async def phase1(self): - await self.player.send("Which 1 player card or 2 middle cards do you want to look at?") - self.choice = await self.player.get_choice(self.player.other() + ["left & middle", "middle & right", "left & right"]) - - async def phase2(self): - if self.choice < len(self.player.other()): - await self.player.send(self.player.other()[self.choice].night_role) - else: - self.choice -= len(self.player.other()) - if self.choice == 0: - a, b = 0, 1 - elif self.choice == 1: - a, b = 1, 2 - else: - a, b = 0, 2 - - await self.player.send(str(self.game.middle_card[a]) + " " + str(self.game.middle_card[b])) - - -class Robber(Role): - order = 6 - - async def phase1(self): - await self.player.send("Which player do you want to rob?") - self.choice = await self.player.get_choice(self.player.other()) - - async def phase3(self): - Player.swap(self.player, self.player.other()[self.choice]) - await self.player.send("You robbed: " + str(self.player.day_role)) - -class Troublemaker(Role): - order = 7 - - async def phase1(self): - await self.player.send("Who do you want to exchange? (send two separate numbers)") - self.A = await self.player.get_choice(self.player.other()) - self.B = await self.player.get_choice(self.player.other()) - - async def phase4(self): - Player.swap(self.player.other()[self.A], self.player.other()[self.B]) - # receive conformation - await self.player.send("Received " + str(self.A) + " " + str(self.B)) - -class Drunk(Role): - order = 8 - - async def phase1(self): - await self.player.send("Which card from the middle do you want to take?") - self.choice = await self.player.get_choice(["left", "middle", "right"]) - - async def phase5(self): - self.player.day_role, self.game.middle_card[self.choice] = self.game.middle_card[self.choice], self.player.day_role - #receive conformation - await self.player.send("Received " + str(self.choice)) - -class Insomniac(Role): - order = 9 - - async def phase5(self): - await self.player.send("You are now: " + str(self.player.day_role)) - -class Villiager(Role): - order = 10 - -class Tanner(Role): - order = 11 - -class Hunter(Role): - order = 12 - -class No_role(Role): - order = 1000 - - -Role.role_set = [Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Villiager, Tanner, Hunter] - - -class Player: - - @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 - - @staticmethod - def swap(player_A, player_B): - player_A.day_role, player_B.day_role = player_B.day_role, player_A.day_role - - def setRole(self, role): - self.night_role = role - self.day_role = role - - def name(self): - return self.member.name - - def __repr__(self): - return self.name() - - def other(self): - return [p for p in self.game.player_list if p != self] - - async def send(self, message): - await self.dm.send(message) - - async def ask_choice(self, options): - await self.send('\n'.join( "(" + str(i) + ") " + str(options[i]) for i in range(len(options)) )) - - async def receive_choice(self, options): - def check(choice): - return choice.channel == self.dm and choice.content.isdigit() and 0 <= int(choice.content) < len(options) - - return int((await self.game.bot.wait_for('message', timeout=30.0, check = check)).content) - - async def get_choice(self, options): - await self.ask_choice(options) - return await self.receive_choice(options) - - async def cast_vote(self, options): - self.vote = options[await self.get_choice(options)] - -class No_player(Player): - - def __init__(self): - self.day_role = No_role() - - def name(self): - return "no one" - - def __str__(self): - return self.name() - - -class one_night: - - def __init__(self, bot): - self.running = False - self.bot = bot - self.player_list = [] - self.role_list = Role.role_set - - - def set_channel(self, channel): - self.channel = channel - - - async def send(self, message): - await self.channel.send(message) - - - async def receive(self, command): - def check(msg): - return msg.channel == self.channel and msg.content.startswith(command) - - return await bot.wait_for('message', check = check) - - def setup(self): - self.werewolf_list = [] - self.mason_list = [] - - - async def set_players(self): - - await self.send("Who is playing?") - msg = await self.receive('$players') - - # use info from last round otherwise - if not msg.content.startswith('$players last'): - self.player_list = [await Player.make(mem, self) for mem in msg.mentions] - - # check conditions - if not 0 <= len(self.player_list) <= 10: - raise ValueError("Invalid number of players: " + str(len(self.player_list))) - - # send confirmation - await self.send("Players: " + ", ".join(p.name() for p in self.player_list)) - - - async def set_roles(self): - await self.send("With which roles do you want to play?") - msg = await self.receive('$roles') - - # use info from last round otherwise - if not msg.content.startswith('$roles last'): - tmp_role = [Role.match(r, self) for r in msg.content.split()[1:]] - - # invalid input - if None in tmp_role: - raise ValueError("Invalid list of roles: " + str(tmp_role)) - - self.role_list = tmp_role - - # check condition - if not len(self.role_list) == (len(self.player_list) + 3): - raise ValueError("Invalid number of roles: " + str(len(self.role_list)) + " with " + str(len(self.player_list)) + " players") - - # send confirmation - await self.send("Roles: " + ", ".join(r.name() for r in self.role_list)) - - - def distribute_roles(self): - shuffle(self.role_list) - for i in range(len(self.player_list)): - self.player_list[i].setRole(self.role_list[i]) - self.role_list[i].setPlayer(self.player_list[i]) - - self.middle_card = self.role_list[-3:] - self.active_role = sorted(self.role_list[:-3], key = lambda x: x.order) #necessary? - - - async def start_night(self): - await asyncio.gather( *[p.send("The night has begun") for p in self.player_list] ) - - async def send_role(self): - await asyncio.gather( *[p.send("Your role: " + p.night_role.name()) for p in self.player_list] ) - - async def night_phases(self): - await asyncio.gather( *[r.phase1() for r in self.active_role] ) - await asyncio.gather( *[r.phase2() for r in self.active_role] ) - await asyncio.gather( *[r.phase3() for r in self.active_role] ) - await asyncio.gather( *[r.phase4() for r in self.active_role] ) - await asyncio.gather( *[r.phase5() for r in self.active_role] ) - - async def start_day(self): - await self.send("The day has started") - - async def vote(self, options): - - # vote - await self.receive('$vote') - await self.send("Vote in DM") - - await asyncio.gather( *[p.cast_vote(options) for p in self.player_list] ) - - await self.send("Votes\n\n" + '\n'.join(str(p) + " :arrow_right: " + str(p.vote) for p in self.player_list)) - - def tally(self, options): - for o in options: - o.tally = 0 - - for p in self.player_list: - p.vote.tally += 1 - - def who_dead(self, options): - - maxi = max(o.tally for o in options) - dead = [p for p in self.player_list if p.tally >= maxi] - for d in dead: - d.dead = True - if isinstance(d.day_role.copy, Hunter): - dead.append(d.vote) - - return dead - - def who_won(self, dead): - - no_dead = (len(dead) == 0) - tanner_dead = any(isinstance(d.day_role.copy, Tanner) for d in dead) - werewolf_dead = any(isinstance(d.day_role.copy, Werewolf) for d in dead) - werewolf_in_game = any(isinstance(p.day_role.copy, Werewolf) for p in self.player_list) - minion_dead = any(isinstance(d.day_role.copy, Minion) for d in dead) - minion_in_game = any(isinstance(p.day_role.copy, Minion) for p in self.player_list) - - werewolf_won = False - village_won = False - tanner_won = False - - # could make it shorter using boolean algebra - if no_dead: - if werewolf_in_game: - werewolf_won = True - else: - village_won = True - else: - if tanner_dead: - tanner_won = True - - if werewolf_dead: - village_won = True - - else: - if werewolf_dead: - village_won = True - else: - if minion_dead: - if werewolf_in_game: - werewolf_won = True - else: - village_won = True - else: - if minion_in_game: - werewolf_won = True - - return werewolf_won, village_won, tanner_won, dead - - async def result(self, werewolf_won, village_won, tanner_won, dead): - - if werewolf_won: - await self.send("Werewolves won!") - - if village_won: - await self.send("Village won!") - - for d in dead: - if isinstance(d.day_role.copy, Tanner): - await self.send(str(p) + " won a tanner") - - await self.send(":skull: " + ', '.join(str(d) for d in dead)) - await self.send('\n'.join(":ballot_box " + str(p.tally) + " votes for " + str(p) + " who is " + str(p.day_role) + " (was " + str(p.night_role) + ") " for p in self.player_list)) - await self.send("Middle cards: " + ', '.join(str(r) for r in self.middle_card)) - - # debug - await self.send("Success") - - def end(self): - self.running = False - - - async def game(self): - - try: - - self.setup() - await self.set_players() - await self.set_roles() - self.distribute_roles() - await self.start_night() - await self.send_role() - - await self.night_phases() - - await self.start_day() - #discussion timer - - options = self.player_list + [No_role(self)] - await self.vote(options) - self.tally(options) - await self.result(*self.who_won(self.who_dead(options))) - - except ValueError as error: - await self.send(error) - except asyncio.TimeoutError: - await self.send("Error: I got bored waiting for your input") - finally: - self.end() - await self.send("Game ended") - - - -bot = discord.Client() - -@bot.event -async def on_ready(): - print('We have logged in as {0.user}'.format(bot)) - - - -async def hello(message): - print("Hello") - await message.channel.send('Hello!:regional_indicator_a:') - - print(message.mentions) - -@bot.event -async def on_message(message): - - global running - - if message.author == bot.user: - return - - - if message.content.startswith('$hello'): - await hello(message) - return - - - if message.content.startswith('$logout'): - await bot.logout() - return - - - if message.content.startswith('$werewolf'): - - # start (only one instance running) - - if werewolf_game.running: - await message.channel.send("Sorry! A game is already running") - return - - werewolf_game.running = True - werewolf_game.set_channel(message.channel) - - - await werewolf_game.game() - - return - -werewolf_game = one_night(bot) -bot.run(TOKEN)