From 240ad44e4cb6ce888ffed5ae2ddcd82ddd1ac28c Mon Sep 17 00:00:00 2001 From: bibin Date: Wed, 15 Apr 2020 01:10:27 +0200 Subject: [PATCH 1/5] more generous role match function --- src/werewolf_bot.py | 2 +- src/werewolf_roles.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/werewolf_bot.py b/src/werewolf_bot.py index 7ee894e..0f398a3 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -159,6 +159,6 @@ async def logout(ctx): @developer async def debug(ctx): print("DEBUG") - print(ctx.message.author.id) + print(ctx.message.content) bot.run(TOKEN) diff --git a/src/werewolf_roles.py b/src/werewolf_roles.py index 0c5b213..6c7ef22 100644 --- a/src/werewolf_roles.py +++ b/src/werewolf_roles.py @@ -1,4 +1,5 @@ import functools +from fuzzywuzzy import fuzz from werewolf_players import No_player @@ -20,10 +21,7 @@ class Role: @staticmethod def match(message): - for role_class in Role.__subclasses__(): - if message.casefold() == role_class.name(): - return role_class - raise ValueError(f"Invalid role: {message}") + return max(Role.__subclasses__(), key=lambda role_class: fuzz.ratio(message, role_class.name())) @staticmethod def no_player(func): @@ -160,7 +158,7 @@ class Drunk(Role): class Insomniac(Role): @Role.no_player async def send_info(self): - await self.player.send_info(f"You are now: {self.player.day_role}") + await self.player.send_info(f"You are: {self.player.day_role}") class Villager(Role): From caca8bc2e5917e2e22a5fcfc16e6e0d55e7d4a04 Mon Sep 17 00:00:00 2001 From: bibin Date: Wed, 15 Apr 2020 02:04:37 +0200 Subject: [PATCH 2/5] added stop command to restart round --- src/werewolf_bot.py | 25 ++++++++++++++++++++++--- src/werewolf_game.py | 2 +- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/werewolf_bot.py b/src/werewolf_bot.py index 0f398a3..4c5ee4a 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -70,12 +70,21 @@ async def setup(ctx): await send_friendly(ctx, "This channel can now play Werewolf") -def game_running(command): +def channel_setup(command): @functools.wraps(command) async def wrapper(ctx): if ctx.channel not in game_instances: await send_wrong(ctx, f"No game setup yet. Use {PREFIX}game setup") - elif game_instances[ctx.channel].running: + else: + await command(ctx) + return wrapper + + +def game_running(command): + @functools.wraps(command) + @channel_setup + async def wrapper(ctx): + if game_instances[ctx.channel].running: await send_wrong(ctx, "Sorry! A game is already running") else: await command(ctx) @@ -98,7 +107,17 @@ def error_handling(command): @game_running @error_handling async def start(ctx): - await game_instances[ctx.channel].game() + game_instances[ctx.channel].game = bot.loop.create_task(game_instances[ctx.channel].round()) + + +@game.command() +@channel_setup +async def stop(ctx): + if not game_instances[ctx.channel].running: + await send_wrong(ctx, "No game is running") + else: + game_instances[ctx.channel].game.cancel() + await send_friendly(ctx, "Game canceled") @game.command() diff --git a/src/werewolf_game.py b/src/werewolf_game.py index 8abf581..4f5388f 100644 --- a/src/werewolf_game.py +++ b/src/werewolf_game.py @@ -165,7 +165,7 @@ class Game: def end(self): self.running = False - async def game(self): + async def round(self): try: self.check() self.running = True From be93817063bf907bd071f64187422db02cad1b66 Mon Sep 17 00:00:00 2001 From: bibin Date: Wed, 15 Apr 2020 14:20:30 +0200 Subject: [PATCH 3/5] more elegant setup function --- src/werewolf_game.py | 19 ++++++++++++------- src/werewolf_players.py | 4 ++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/werewolf_game.py b/src/werewolf_game.py index 4f5388f..6ba781d 100644 --- a/src/werewolf_game.py +++ b/src/werewolf_game.py @@ -36,15 +36,18 @@ class Game: def setup(self): self.role = dict() + # setting default value for r in Role.__subclasses__(): - if r not in [Werewolf, Mason, No_role]: + if r == No_role: + continue + if r in [Werewolf, Mason]: + self.role[r] = [] + else: 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 - c.won = c.dead = False + c.reset() + self.time = 0 def distribute_roles(self): shuffle(self.role_list) @@ -74,6 +77,9 @@ class Game: async def start_day(self): await self.send("The day has started") + async def discussion_timer(self): + pass + async def vote(self): # vote @@ -168,8 +174,8 @@ class Game: async def round(self): try: self.check() - self.running = True self.setup() + self.running = True self.distribute_roles() await self.start_night() await self.send_role() @@ -177,7 +183,6 @@ class Game: await self.night_phases() await self.start_day() - # discussion timer await self.vote() self.tally() diff --git a/src/werewolf_players.py b/src/werewolf_players.py index 5c7f89e..8d4fd1c 100644 --- a/src/werewolf_players.py +++ b/src/werewolf_players.py @@ -14,6 +14,10 @@ class Player: 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 From e56a5d1629229dded33d2900bdfaf38be3e87804 Mon Sep 17 00:00:00 2001 From: bibin Date: Wed, 15 Apr 2020 17:23:24 +0200 Subject: [PATCH 4/5] fix middle_cards, fix async, discussion timer added --- src/werewolf_bot.py | 8 +------ src/werewolf_game.py | 48 ++++++++++++++++++++++++++--------------- src/werewolf_players.py | 6 ++++++ src/werewolf_roles.py | 8 +++---- 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/werewolf_bot.py b/src/werewolf_bot.py index 4c5ee4a..650840f 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -108,6 +108,7 @@ def error_handling(command): @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() @@ -133,13 +134,6 @@ async def players(ctx): async def roles(ctx): 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 diff --git a/src/werewolf_game.py b/src/werewolf_game.py index 6ba781d..f2a2e0f 100644 --- a/src/werewolf_game.py +++ b/src/werewolf_game.py @@ -13,6 +13,7 @@ class Game: self.channel = channel self.player_list = [] self.role_list = [] + self.discussion_time = 300 # seconds async def send(self, message): await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff)) @@ -43,7 +44,7 @@ class Game: if r in [Werewolf, Mason]: self.role[r] = [] else: - r(self) + r(self).add_yourself() self.voting_list = self.player_list + [No_player()] for c in self.voting_list: c.reset() @@ -52,8 +53,10 @@ class Game: def distribute_roles(self): shuffle(self.role_list) for i in range(len(self.player_list)): - self.role_list[i](self, self.player_list[i]) - self.middle_card = self.role_list[-3:] + role_obj = self.role_list[i](self, self.player_list[i]) + self.player_list[i].setRole(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("*The night has begun*")) @@ -78,14 +81,26 @@ class Game: await self.send("The day has started") async def discussion_timer(self): - pass + await self.send(f"You have {self.discussion_time / 60} minutes to discuss") + await asyncio.sleep(self.discussion_time / 2) + await self.send("Half of the time has passed") + await asyncio.sleep(self.discussion_time / 2 - 60) + await self.send("One minute remaining") + await asyncio.sleep(30) + await self.send("30 seconds 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): - - # vote - # replace with dm: await self.receive('$vote') 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): @@ -151,15 +166,17 @@ class Game: return werewolf_won, village_won, tanner_won, dead async def result(self, werewolf_won, village_won, tanner_won, dead): - + winnning = [] if werewolf_won: - winnning = ["Werewolves won!"] + winnning.append("Werewolves") if village_won: - winnning = ["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 won") + winnning.append(f"{sum(1 for d in dead if d.day_role.is_role(Tanner))} tanner") + if len(winnning) == 0: + winnning = ["No one"] - embed = discord.Embed(title=' and '.join(winnning), color=0x00ffff) + embed = discord.Embed(title=f"{' and '.join(winnning)} won!", color=0x00ffff) for p in self.player_list: won_emoji = ":trophy:" if p.won else ":frowning2:" dead_emoji = ":skull:" if p.dead else ":no_mouth:" @@ -179,15 +196,12 @@ class Game: 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())) - await self.send("Round ended") finally: self.end() diff --git a/src/werewolf_players.py b/src/werewolf_players.py index 8d4fd1c..13af25b 100644 --- a/src/werewolf_players.py +++ b/src/werewolf_players.py @@ -85,6 +85,12 @@ class Player: 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): diff --git a/src/werewolf_roles.py b/src/werewolf_roles.py index 6c7ef22..ce14fb1 100644 --- a/src/werewolf_roles.py +++ b/src/werewolf_roles.py @@ -7,8 +7,6 @@ class Role: def __init__(self, game, player=No_player()): self.game = game self.player = player - self.player.setRole(self) - self.add_yourself() def add_yourself(self): self.game.role[type(self)] = self @@ -127,7 +125,7 @@ class Robber(Role): self.choice = await self.player.get_choice("Which player do you want to rob?", self.player.other()) @Role.no_player - def simulate(self): + async def simulate(self): self.player.swap(self.player.other()[self.choice]) @Role.no_player @@ -141,7 +139,7 @@ class Troublemaker(Role): 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 - def simulate(self): + async def simulate(self): self.player.other()[self.A].swap(self.player.other()[self.B]) @@ -151,7 +149,7 @@ class Drunk(Role): self.choice = await self.player.get_choice("Which card from the middle do you want to take?", ["left", "middle", "right"]) @Role.no_player - def simulate(self): + 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 From b474d410874023c39f43d57f88046bba364bf9ea Mon Sep 17 00:00:00 2001 From: bibin Date: Wed, 15 Apr 2020 18:42:56 +0200 Subject: [PATCH 5/5] added decorator, decorator with args, time command added --- src/werewolf_bot.py | 66 ++++++++++++++++++++++++++++++-------------- src/werewolf_game.py | 19 +++++++++---- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/werewolf_bot.py b/src/werewolf_bot.py index 650840f..3ef0819 100644 --- a/src/werewolf_bot.py +++ b/src/werewolf_bot.py @@ -72,30 +72,41 @@ async def setup(ctx): def channel_setup(command): @functools.wraps(command) - async def wrapper(ctx): + 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) + 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): - if game_instances[ctx.channel].running: - await send_wrong(ctx, "Sorry! A game is already running") + 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) + await command(ctx, *args, **kwargs) return wrapper def error_handling(command): @functools.wraps(command) - async def wrapper(ctx): + async def wrapper(ctx, *args, **kwargs): try: - await command(ctx) + await command(ctx, *args, **kwargs) except ValueError as error: await send_wrong(ctx, str(error)) except asyncio.TimeoutError: @@ -104,7 +115,7 @@ def error_handling(command): @game.command() -@game_running +@game_not_running @error_handling async def start(ctx): game_instances[ctx.channel].game = bot.loop.create_task(game_instances[ctx.channel].round()) @@ -112,27 +123,39 @@ async def start(ctx): @game.command() +@game_running @channel_setup async def stop(ctx): - if not game_instances[ctx.channel].running: - await send_wrong(ctx, "No game is running") - else: - game_instances[ctx.channel].game.cancel() - await send_friendly(ctx, "Game canceled") + game_instances[ctx.channel].game.cancel() + await send_friendly(ctx, "Game canceled") @game.command() -@game_running +@game_not_running @error_handling async def players(ctx): await game_instances[ctx.channel].set_players(ctx.message) +@game.command() +@game_not_running +@error_handling +async def roles(ctx, *args): + await game_instances[ctx.channel].set_roles(args) + + +@game.command() +@game_not_running +@error_handling +async def minutes(ctx, i): + await game_instances[ctx.channel].set_time(i) + + @game.command() @game_running @error_handling -async def roles(ctx): - await game_instances[ctx.channel].set_roles(ctx.message.content.split()[3:]) # exclude commands +async def time(ctx): + await send_friendly(ctx, game_instances[ctx.channel].remaining_time_string()) # smaller commands @@ -153,10 +176,10 @@ async def ping(ctx): def developer(command): @functools.wraps(command) - async def wrapper(ctx): + async def wrapper(ctx, *args, **kwargs): DEV_ID = 461892912821698562 if ctx.author.id == DEV_ID: - await command(ctx) + await command(ctx, *args, **kwargs) else: await send_wrong(ctx, "This command is not for you!") return wrapper @@ -170,8 +193,9 @@ async def logout(ctx): @bot.command() @developer -async def debug(ctx): +async def debug(ctx, *args): print("DEBUG") - print(ctx.message.content) + print(ctx.args) + print(ctx.kwargs) bot.run(TOKEN) diff --git a/src/werewolf_game.py b/src/werewolf_game.py index f2a2e0f..255bef0 100644 --- a/src/werewolf_game.py +++ b/src/werewolf_game.py @@ -1,4 +1,5 @@ from random import shuffle +import time import asyncio import discord from werewolf_roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role @@ -29,6 +30,10 @@ class Game: self.role_list = [Role.match(r) for r in suggestions] # raises ValueError await self.send(f"Roles: {', '.join(r.name() for r in self.role_list)}") # send confirmation + async def set_time(self, msg): + self.discussion_time = int(msg) * 60 + 1 + await self.send(f"You have set the discussion time to {self.discussion_time//60} minutes") # send confirmation + def check(self): if not 0 <= len(self.player_list) <= 10: raise ValueError(f"Invalid number of players: {len(self.player_list)}") @@ -48,7 +53,6 @@ class Game: self.voting_list = self.player_list + [No_player()] for c in self.voting_list: c.reset() - self.time = 0 def distribute_roles(self): shuffle(self.role_list) @@ -80,14 +84,19 @@ class Game: 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} minute(s) and {t%60} second(s)" + async def discussion_timer(self): - await self.send(f"You have {self.discussion_time / 60} minutes to discuss") + 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("Half of the time has passed") + await self.send(f"{self.remaining_time_string()} remaining") await asyncio.sleep(self.discussion_time / 2 - 60) - await self.send("One minute remaining") + await self.send(f"{self.remaining_time_string()} remaining") await asyncio.sleep(30) - await self.send("30 seconds remaining") + await self.send(f"{self.remaining_time_string()} remaining") await asyncio.sleep(30) async def early_vote(self):