diff --git a/src/werewolf_bot.py b/src/werewolf_bot.py index 20cf469..1816add 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -13,8 +13,10 @@ if TOKEN is None: print("Missing discord token!") exit(1) + PREFIX = '$w ' bot = commands.Bot(command_prefix=commands.when_mentioned_or(PREFIX)) +bot.remove_command('help') @bot.event @@ -23,76 +25,114 @@ async def on_ready(): print('We have logged in as {0.user}'.format(bot)) +@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) + + +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(help="werewolf game", description="Prefix for the One Night Ultimate Werewolf commands.") +@bot.group() async def game(ctx): if ctx.invoked_subcommand is None: - await ctx.send('Invalid sub command passed...') + await send_wrong(ctx, 'Invalid sub command passed...') -@game.command(help="start werewolf game", description="Start One Night Ultimate Werewolf game.") +@game.command() async def setup(ctx): if ctx.channel in game_instances: - await ctx.send("Game already setup in this channel") + 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 game_running(command): @functools.wraps(command) async def wrapper(ctx): if ctx.channel not in game_instances: - await ctx.send(f"No game setup yet. Use {PREFIX}game setup") + await send_wrong(ctx, f"No game setup yet. Use {PREFIX}game setup") elif game_instances[ctx.channel].running: - await ctx.send("Sorry! A game is already running") + await send_wrong(ctx, "Sorry! A game is already running") else: await command(ctx) return wrapper -@game.command(help="start a round", description="Play one round of One Night Ultimate Werewolf") +def error_handling(command): + @functools.wraps(command) + async def wrapper(ctx): + try: + await command(ctx) + 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 + + +@game.command() @game_running +@error_handling async def start(ctx): await game_instances[ctx.channel].game() -@game.command(help="set players", description="Set the players for the next round.") +@game.command() @game_running +@error_handling async def players(ctx): await game_instances[ctx.channel].set_players(ctx.message) -@game.command(help="set roles", description="Set the roles for the next round.") +@game.command() @game_running +@error_handling async def roles(ctx): - try: - await game_instances[ctx.channel].set_roles(ctx.message.content.split()[3:]) - except ValueError as error: - await ctx.send(error) + await game_instances[ctx.channel].set_roles(ctx.message.content.split()[3:]) # exclude commands # ONLY FOR TESTING @game.command() @game_running +@error_handling async def vote(ctx): await game_instances[ctx.channel].vote() # smaller commands - -@bot.command(help="greets you", description="This just says hello back to the message author.") +@bot.command() async def hello(ctx): - await ctx.send(f"Hello {ctx.message.author.name} :wave:") + await send_friendly(ctx, f"Hello {ctx.message.author.name} :wave:") -@bot.command(help="test bot responsiveness", description="This is a debug function to see if the bot listens to a command.") +@bot.command() async def ping(ctx): print("pong") - await ctx.send("pong") + await send_friendly(ctx, "pong") # developer commands @@ -105,17 +145,17 @@ def developer(command): if ctx.author.id == DEV_ID: await command(ctx) else: - await ctx.send("This command is not for you!") + await send_wrong(ctx, "This command is not for you!") return wrapper -@bot.command(help="not for you", description="Shut down the bot.") +@bot.command() @developer async def logout(ctx): await bot.logout() -@bot.command(help="debug") +@bot.command() @developer async def debug(ctx): print("DEBUG") diff --git a/src/werewolf_game.py b/src/werewolf_game.py index c0b4df9..5cf0b55 100644 --- a/src/werewolf_game.py +++ b/src/werewolf_game.py @@ -1,6 +1,7 @@ from random import shuffle import asyncio -from werewolf_roles import Role, Werewolf, Minion, Tanner, Hunter +import discord +from werewolf_roles import Role, Werewolf, Mason from werewolf_players import Player, No_player @@ -14,46 +15,47 @@ class Game: self.role_list = [] async def send(self, message): - await self.channel.send(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 Player.make(mem, self) for mem in msg.mentions] - - # send confirmation - await self.send("Players: " + ", ".join(p.name() for p in self.player_list)) + await self.send(f"Players: {', '.join(p.name() for p in self.player_list)}") # send confirmation async def set_roles(self, suggestions): - self.role_list = [Role.match(r, self) for r in suggestions] # raises ValueError - - # send confirmation - await self.send("Roles: " + ", ".join(r.name() for r in self.role_list)) + 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 def check(self): if not 0 <= 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): - self.werewolf_list = [] - self.mason_list = [] - self.running = True + self.role = dict() + for r in Role.__subclasses__(): + if r not in [Werewolf, Mason]: + r(self) + self.role[Werewolf] = [] + self.role[Mason] = [] + self.voting_list = self.player_list + [No_player()] + for c in self.voting_list: + c.tally = 0 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.role_list[i](self, self.player_list[i]) self.middle_card = self.role_list[-3:] - self.active_role = sorted(self.role_list[:-3], key=lambda x: x.order) async def start_night(self): - await asyncio.gather(*[p.send("The night has begun") for p in self.player_list]) + await self.for_all_player(lambda p: p.send_normal("*The night has begun*")) async def send_role(self): - await asyncio.gather(*[p.send("Your role: " + p.night_role.name()) for p in self.player_list]) + await self.for_all_player(lambda p: p.send_info(f"Your role: **{p.night_role.name()}**")) async def night_phases(self): await asyncio.gather(*[r.phase1() for r in self.active_role]) @@ -65,42 +67,35 @@ class Game: async def start_day(self): await self.send("The day has started") - async def vote(self, options): + async def vote(self): # vote # replace with dm: 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 + await self.for_all_player(lambda p: p.cast_vote(self.voting_list)) + def tally(self): for p in self.player_list: p.vote.tally += 1 - def who_dead(self, options): - - maxi = max(o.tally for o in options) + def who_dead(self): + maxi = max(c.tally for c in self.voting_list) dead = [p for p in self.player_list if p.tally >= maxi] for d in dead: d.dead = True - if isinstance(d.day_role.copy, Hunter): + if d.day_role.is_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) + tanner_dead = any(d.day_role.is_Tanner for d in dead) + werewolf_dead = any(d.day_role.is_Werewolf for d in dead) + werewolf_in_game = any(p.day_role.is_Werewolf for p in self.player_list) + minion_dead = any(d.day_role.is_Minion for d in dead) + minion_in_game = any(p.day_role.is_Minion for p in self.player_list) werewolf_won = False village_won = False @@ -132,35 +127,41 @@ class Game: if minion_in_game: werewolf_won = True + for p in self.player_list: + if p.day_role.is_Werewolf or p.day_role.is_Minion: + p.won = werewolf_won + elif p.day_role.is_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): if werewolf_won: - await self.send("Werewolves won!") - + winnning = ["Werewolves won!"] if village_won: - await self.send("Village won!") + winnning = ["Village won!"] + if tanner_won: + winnning.append(f"{sum(1 for d in dead if d.day_role.is_Tanner)} tanner won") - for d in dead: - if isinstance(d.day_role.copy, Tanner): - await self.send(str(d) + " won a tanner") + embed = discord.Embed(title=' and '.join(winnning), color=0x00ffff) + for p in self.player_list: + won_emoji = ":trophy:" if p.won else "frowning2" + dead_emoji = ":skull:" if p.dead else ":no_mouth:" + embed.add_field(name="", value=f"{won_emoji} {dead_emoji} {p.tally}:ballot_box: {str(p)} :sunrise_over_mountains:{str(p.day_role)} (:full_moon:{str(p.night_role)}) :point_right:{str(p.vote)}", inline=False) + embed.add_field(name="Middle cards", value=', '.join(str(r) for r in self.middle_card)) - 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") + await self.channel.send(embed=embed) def end(self): self.running = False async def game(self): - try: - self.check() + self.running = True self.setup() self.distribute_roles() await self.start_night() @@ -171,15 +172,10 @@ class Game: await self.start_day() # discussion timer - options = self.player_list + [No_player(self)] - await self.vote(options) - self.tally(options) - await self.result(*self.who_won(self.who_dead(options))) + await self.vote() + self.tally() + await self.result(*self.who_won(self.who_dead())) - except ValueError as error: - await self.send(error) - except asyncio.TimeoutError: - await self.send("Error: I got bored waiting for your input") + await self.send("Round ended") finally: self.end() - await self.send("Round ended") diff --git a/src/werewolf_players.py b/src/werewolf_players.py index cacef4a..9e1b3e5 100644 --- a/src/werewolf_players.py +++ b/src/werewolf_players.py @@ -1,4 +1,4 @@ -from werewolf_roles import No_role +import discord class Player: @@ -11,12 +11,8 @@ class Player: p.game = game return p - def swap(self, player_B): - self.day_role, player_B.day_role = player_B.day_role, self.day_role - def setRole(self, role): - self.night_role = role - self.day_role = role + self.day_role = self.night_role = role def name(self): return self.member.name @@ -24,33 +20,66 @@ class Player: 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 other(self): return [p for p in self.game.player_list if p != self] - async def send(self, message): + async def send_normal(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 send_embed(self, desc, color): + await self.dm.send(embed=discord.Embed(description=desc, color=color)) - 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) + async def send_wrong(self, message): + await self.send_embed(message, 0xff8000) - return int((await self.game.bot.wait_for('message', timeout=30.0, check=check)).content) + async def send_confirmation(self, message): + await self.send_embed(message, 0x00ff00) - async def get_choice(self, options): - await self.ask_choice(options) - return await self.receive_choice(options) + async def send_info(self, message): + await self.send_embed(message, 0x00ffff) - async def cast_vote(self, options): - self.vote = options[await self.get_choice(options)] + 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}```") + + async def check_num(self, choice, N): + if not choice.isdigit(): + await self.send_wrong(f"Your choice {choice} is not a number") + return False + if not 0 <= int(choice) < N: + await self.send_wrong(f"Your choice {choice} is not in range 0 - {N-1}") + return False + + async def receive_choice(self, options, n_ans=1): + while True: + def check(choice): + return choice.channel == self.dm + 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)}") + continue + if not all(self.check_num(c, len(options)) for c in choice): + 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] + + 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)] class No_player(Player): - - def __init__(self): - self.day_role = No_role() - def name(self): return "no one" diff --git a/src/werewolf_roles.py b/src/werewolf_roles.py index 4e7173a..40b6d20 100644 --- a/src/werewolf_roles.py +++ b/src/werewolf_roles.py @@ -1,32 +1,25 @@ +from werewolf_players import No_player + + class Role: - - def __init__(self, game): + def __init__(self, game, player=No_player()): self.game = game - self.copy = self - - def setPlayer(self, player): self.player = player + self.player.setRole(self) + self.add_yourself() + self.is_Hunter = self.is_Tanner = self.is_Werewolf = self.is_Minion = False - async def phase1(self): # query stuff + doppelganger simulation - pass + def add_yourself(self): + self.game.role[type(self)] = self - 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 + async def send_role_list(self, cls): + await self.player.send_info(f"{cls}: {', '.join(str(p) for p in self.game.role[cls])}") @staticmethod - def match(message, game): - for role_class in Role.role_set: + def match(message): + for role_class in Role.__subclasses__(): if message.casefold() == role_class.name(): - return role_class(game) + return role_class raise ValueError(f"Invalid role: {message}") @classmethod @@ -38,58 +31,54 @@ class Role: class Doppelganger(Role): - order = 1 + pass class Werewolf(Role): - order = 2 + def __init__(self, game, player=No_player()): + super().__init__(game, player) + self.is_Werewolf = True - def setPlayer(self, player): - super().setPlayer(player) - self.game.werewolf_list.append(player) + def add_yourself(self): + self.game.role[Werewolf].append(self) - async def phase2(self): - if len(self.game.werewolf_list) >= 2: - await self.player.send("Werewolves: " + str(self.game.werewolf_list)) + async def phase(self): + if len(self.game.role[Werewolf]) >= 2: + await self.send_role_list(Werewolf) 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_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("A card in the middle is: " + self.game.middle_card[self.choice].name()) + await self.player.send_info(f"A card in the middle is: {self.game.middle_card[self.choice].name()}") class Minion(Role): - order = 3 + def __init__(self, game, player=No_player()): + super().__init__(game, player) + self.is_Minion = True - async def phase2(self): + async def send_info(self): if len(self.game.werewolf_list) == 0: - await self.player.send("There were no werewolves so you became one") + await self.player.send_info("There were no werewolves, so you need to kill a villager!") else: - await self.player.send("Werewolves: " + str(self.game.werewolf_list)) + await self.send_role_list(Werewolf) class Mason(Role): - order = 4 + def add_yourself(self): + self.game.role[Mason].append(self) - 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)) + async def send_info(self): + await self.send_role_list(Mason) class Seer(Role): - order = 5 + 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"]) - 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): + async def send_info(self): if self.choice < len(self.player.other()): - await self.player.send(self.player.other()[self.choice].night_role) + await self.player.send_info(self.player.other()[self.choice].night_role) else: self.choice -= len(self.player.other()) if self.choice == 0: @@ -99,69 +88,56 @@ class Seer(Role): else: a, b = 0, 2 - await self.player.send(str(self.game.middle_card[a]) + " " + str(self.game.middle_card[b])) + await self.player.send_info(f"{self.game.middle_card[a]} {self.game.middle_card[b]}") class Robber(Role): - order = 6 + async def query(self): + self.choice = await self.player.get_choice("Which player do you want to rob?", self.player.other()) - 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): + async def simulate(self): self.player.swap(self.player.other()[self.choice]) - await self.player.send("You robbed: " + str(self.player.day_role)) + + async def send_info(self): + await self.player.send_info(f"You robbed: {self.player.day_role}") class Troublemaker(Role): - order = 7 + 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()) - 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): + async def simulate(self): self.player.other()[self.A].swap(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 query(self): + self.choice = await self.player.get_choice("Which card from the middle do you want to take?", ["left", "middle", "right"]) - 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)) + 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): - order = 9 - - async def phase5(self): - await self.player.send("You are now: " + str(self.player.day_role)) + async def send_info(self): + await self.player.send_info(f"You are now: {self.player.day_role}") -class Villiager(Role): - order = 10 +class Villager(Role): + pass class Tanner(Role): - order = 11 + def __init__(self, game, player=No_player()): + super().__init__(game, player) + self.is_Tanner = True class Hunter(Role): - order = 12 + def __init__(self, game, player=No_player()): + super().__init__(game, player) + self.is_Hunter = True class No_role(Role): - order = 1000 - - -Role.role_set = [Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Villiager, Tanner, Hunter] + pass