diff --git a/GameFiles/Game.py b/GameFiles/Game.py index 219868a..1fd5df9 100644 --- a/GameFiles/Game.py +++ b/GameFiles/Game.py @@ -50,6 +50,20 @@ def vote_running(func): return decorated +def policies_drawn(func): + """ + Decorator for *methods* of Game that need policies to have been drawn (i.e. the legislative phase has started) + """ + + @wraps(func) + def decorated(obj: 'Game', *args, **kwargs): + if obj.config["drawn"] is None: + raise RuntimeError("This function only works when policies have been drawn (i.e. the legislative phase has started)") + return func(obj, *args, **kwargs) + + return decorated + + def save_on_success(func): """ Decorator for *async methods* of game that calls the save function after the method has executed without exceptions @@ -191,6 +205,14 @@ class Game: def get_player_channel(self, player: Union[int, discord.Member]) -> discord.TextChannel: return self.guild.get_channel(self.get_player_channel_id(player)) + @game_started + def is_legislative_phase(self) -> bool: + return self.config["drawn"] is not None + + @game_started + def get_enacted_policies(self) -> List[Policy]: + return [Policy(policy_str) for policy_str in self.config["enacted"]] + @save_on_success async def start(self, player_role: discord.Role, bot_user_id: int): if self.is_started(): @@ -371,11 +393,10 @@ class Game: @save_on_success async def stop_vote(self): logging.debug(f"[{self.guild.name}] Stopping the vote") - tasks = [] passed = self.is_vote_passing() self.config["vote"]["revealed"] = True await self.update_vote_message() - tasks.append(asyncio.create_task(self.get_votes_channel().send("**The vote has ended**"))) + await self.get_votes_channel().send("**The vote has ended**") announcement_content = [ f"<@&{self.get_player_role_id()}> <@&{self.get_gm_role_id()}> the vote has ended!", f"{':green_square:' if passed else ':red_square:'} The vote has **{'' if passed else 'not '}passed**" @@ -387,25 +408,32 @@ class Game: if self.config["chaos"] > 0: # If there was some chaos announcement_content.append(":relaxed: The country has calmed and the chaos counter has been reset") self.config["chaos"] = 0 # Anyway, the chaos is reset by a successful vote - - else: - chaos = self.config["chaos"] + 1 - if chaos < 3: - self.config["chaos"] = chaos - announcement_content.append( - ":fire: The country slowly descends into chaos " + " ".join([":fire:" for _ in range(chaos)] + [":black_small_square:" for _ in range(3 - chaos)]) - ) - else: # Too many rejected votes throw the country into chaos - announcement_content.append(":fire: :fire: :fire: **The country is thrown into chaos by too many rejected votes** :fire: :fire: :fire:") - tasks.append(asyncio.create_task(self.enact_top_policy(delay = 10))) - - tasks.append(asyncio.create_task(self.get_announcements_channel().send("\n".join(announcement_content), allowed_mentions = discord.AllowedMentions(roles = True)))) - await asyncio.wait(tasks) + await self.get_announcements_channel().send("\n".join(announcement_content), allowed_mentions = discord.AllowedMentions(roles = True)) self.config["vote"] = None + # After announcing a non-passing vote, we increase the chaos and announce it + if not passed: + await self.increase_chaos() + + @game_started + @save_on_success + async def increase_chaos(self): + new_chaos = self.config["chaos"] + 1 + if new_chaos < 3: + self.config["chaos"] = new_chaos + await self.get_announcements_channel().send( + ":fire: The country slowly descends into chaos " + " ".join([":fire:" for _ in range(new_chaos)] + [":black_small_square:" for _ in range(3 - new_chaos)]) + ) + else: # Chaos is too high! + await self.get_announcements_channel().send(":fire: :fire: :fire: **The country is thrown into chaos** :fire: :fire: :fire:") + await self.enact_top_policy(delay = 10) + await self.get_announcements_channel().send(":relaxed: The country has calmed and the chaos counter has been reset") + self.config["chaos"] = 0 # We reset the counter @game_started @save_on_success async def draw_policies(self) -> List[Policy]: + if self.is_legislative_phase(): + raise RuntimeError("Can not draw cards if some are already in hand. Enact one or veto instead.") self.config["drawn"] = [self.config["deck"].pop(0) for _ in range(3)] return [Policy(p) for p in self.config["drawn"]] @@ -414,10 +442,9 @@ class Game: return [Policy(self.config["deck"][i]) for i in range(3)] @game_started + @policies_drawn @save_on_success async def enact_drawn_policy(self, index: int): - if self.config["drawn"] is None: - raise RuntimeError("Can only enact a policy when they have been drawn") if not (0 <= index < len(self.config["drawn"])): raise IndexError(f"Expected policy index between 0 and {len(self.config['drawn'])}, got {index}") for i, policy_str in enumerate(self.config["drawn"]): @@ -427,12 +454,12 @@ class Game: self.config["discard"].append(policy_str) self.config["drawn"] = None if len(self.config["deck"]) < 3: - self.shuffle_discard_into_deck() + await self.shuffle_discard_into_deck() await self.announce_latest_enacted_policy() @game_started @save_on_success - def shuffle_discard_into_deck(self): + async def shuffle_discard_into_deck(self): self.config["deck"].extend(self.config["discard"]) self.config["discard"] = [] random.shuffle(self.config["deck"]) @@ -442,26 +469,41 @@ class Game: async def enact_top_policy(self, delay = 10): logger.debug(f"[{self.guild.name}] Enacting top policy in {delay} seconds...") await asyncio.sleep(delay) - self.config["chaos"] = 0 # We reset the counter self.config["enacted"].append(self.config["deck"].pop(0)) if len(self.config["deck"]) < 3: - self.shuffle_discard_into_deck() + await self.shuffle_discard_into_deck() await self.announce_latest_enacted_policy() @game_started async def announce_latest_enacted_policy(self): last_enacted = Policy(self.config["enacted"][-1]) enacted_count = defaultdict(int) - for policy_str in self.config["enacted"]: - enacted_count[Policy(policy_str)] += 1 + for policy in self.get_enacted_policies(): + enacted_count[policy] += 1 message_content = [ f"{self.get_player_role().mention} A **{last_enacted.name}** policy {last_enacted.square_emoji()} has been enacted!", f"In total, **{enacted_count[Policy.LIBERAL]} {Policy.LIBERAL.name}** policies and **{enacted_count[Policy.FASCIST]} {Policy.FASCIST.name}** policies have been enacted", " ".join([Policy.LIBERAL.square_emoji()] * enacted_count[Policy.LIBERAL] + [":black_small_square:"] * (5 - enacted_count[Policy.LIBERAL])), " ".join([Policy.FASCIST.square_emoji()] * enacted_count[Policy.FASCIST] + [":black_small_square:"] * (6 - enacted_count[Policy.FASCIST])), ] + if last_enacted == Policy.FASCIST: + if enacted_count[Policy.FASCIST] == 3: + message_content.append("Be careful about who you elect as chancellor!") + elif enacted_count[Policy.FASCIST] == 5: + message_content.append(":person_gesturing_no: Veto power unlocked :person_gesturing_no:") await self.get_announcements_channel().send("\n".join(message_content), allowed_mentions = discord.AllowedMentions(roles = True)) + @game_started + @policies_drawn + @save_on_success + async def veto(self): + self.config["discard"].extend(self.config["drawn"]) + self.config["drawn"] = None + if len(self.config["deck"]) < 3: + await self.shuffle_discard_into_deck() + await self.get_announcements_channel().send(f"<@&{self.get_player_role_id()}>\n:person_gesturing_no: The government used the veto power! No policies have been enacted. :person_gesturing_no:", allowed_mentions = discord.AllowedMentions(roles = True)) + await self.increase_chaos() + @game_started @save_on_success async def kill_player(self, player: discord.Member): diff --git a/SecretBot.py b/SecretBot.py index 340e412..a138309 100755 --- a/SecretBot.py +++ b/SecretBot.py @@ -261,13 +261,16 @@ class SecretBot(commands.Cog): game = await self.get_running_game_or_error_message(ctx) await self.check_is_administrator_or_gm(ctx) await self.check_in_admin_channel_or_error_message(ctx, game) - policies = await game.draw_policies() - message_content = [ - "The following policies have been drawn:", - " ".join([f"{num + 1}) {':blue_square:' if policy == Policy.LIBERAL else ':red_square:'} " for num, policy in enumerate(policies)]), - "Send them to the president and chancellor and type `!Enact ` to enact one of them when you are finished" - ] - await ctx.reply("\n".join(message_content)) + if game.is_legislative_phase(): + await ctx.reply(":x: The game is already in a legislative phase. Enact a policy or use veto power") + else: + policies = await game.draw_policies() + message_content = [ + "The following policies have been drawn:", + " ".join([f"{num + 1}) {':blue_square:' if policy == Policy.LIBERAL else ':red_square:'} " for num, policy in enumerate(policies)]), + "Send them to the president and chancellor and type `!Enact ` to enact one of them when you are finished" + ] + await ctx.reply("\n".join(message_content)) @commands.command("Enact", help = "Legislative session only: enact one of the previously drawn policies") async def enact_drawn_policy(self, ctx: commands.Context, policy_number: int): @@ -303,6 +306,29 @@ class SecretBot(commands.Cog): await game.kill_player(player) await ctx.reply(":dagger: The order has been executed.") + @commands.command("Veto") + async def veto_policy_with_confirmation_if_veto_locked(self, ctx: commands.Context): + game = await self.get_running_game_or_error_message(ctx) + await self.check_is_administrator_or_gm(ctx) + if game.is_legislative_phase(): + nb_fascist_policies = len([policy for policy in game.get_enacted_policies() if policy == Policy.FASCIST]) + if nb_fascist_policies < 5: + await self.confirm_action( + f"Are you sure that you want to use the veto power with less than 5 enacted fascist policies? ({nb_fascist_policies} enacted)", + ctx.channel, + self.veto_policy(ctx), + ctx.message + ) + else: + await self.veto_policy(ctx) + else: + await ctx.reply(":x: We are not in a legislative phase") + + async def veto_policy(self, ctx: commands.Context): + game = await self.get_running_game_or_error_message(ctx) + await self.check_is_administrator_or_gm(ctx) + await game.veto() + if __name__ == '__main__': argparser = argparse.ArgumentParser(description = "Secret Hitler helper bot", formatter_class = argparse.ArgumentDefaultsHelpFormatter)