import discord from discord.ext import commands import random import asyncio from collections import Counter ROLES = ["Werewolf", "Seer", "Doctor", "Villager"] ROLE_DESCRIPTIONS = { "Werewolf": "You are a **Werewolf**! Work with your fellow werewolves at night to eliminate villagers. Your goal is to outnumber the villagers.", "Seer": "You are the **Seer**! Each night, you may select a player to learn their true role.", "Doctor": "You are the **Doctor**! Each night, you may choose a player to protect from elimination.", "Villager": "You are a **Villager**! Try to find and vote out the werewolves during the day.", } class WerewolvesGame: def __init__(self, channel): self.channel = channel self.players = [] self.started = False self.roles = {} # user_id: role self.phase = "lobby" # or "night", "day" self.role_threads = {} # role: thread self.votes = {} self.vote_targets = [] self.alive = set() def add_player(self, user): if user not in self.players: self.players.append(user) def assign_roles(self): # For 6+ players: 2 werewolves, 1 seer, 1 doctor, rest villagers n = len(self.players) roles_list = ["Werewolf"] * 2 + ["Seer", "Doctor"] roles_list += ["Villager"] * (n - len(roles_list)) random.shuffle(roles_list) self.roles = {player.id: role for player, role in zip(self.players, roles_list)} def get_players_by_role(self, role): return [p for p in self.players if self.roles.get(p.id) == role] def reset_votes(self): self.votes = {} self.vote_targets = [] def vote(self, voter, target): self.votes[voter.id] = target.id def tally_votes(self): if not self.votes: return None count = Counter(self.votes.values()) most_common = count.most_common(1) if most_common: return most_common[0][0] # user_id of voted out return None class Games(commands.Cog): def __init__(self, client): self.client = client self.active_games = {} # channel_id: WerewolvesGame @commands.command(name="joinwerewolves", hidden=True) async def join_werewolves(self, ctx): game = self.active_games.get(ctx.channel.id) if not game: game = WerewolvesGame(ctx.channel) self.active_games[ctx.channel.id] = game if game.started: await ctx.reply("Game already started!") return game.add_player(ctx.author) await ctx.reply(f"{ctx.author.display_name} joined the game!") @commands.command(name="startwerewolves", hidden=True) async def start_werewolves(self, ctx): game = self.active_games.get(ctx.channel.id) if not game or len(game.players) < 2: await ctx.reply("Need at least 6 players to start!") return if game.started: await ctx.reply("Game already started!") return game.started = True game.assign_roles() # Create threads for roles await self.create_role_threads(game) # Announce roles in threads (description only once per thread) # Werewolf thread: mention all werewolves, send description once werewolves = game.get_players_by_role("Werewolf") if werewolves: thread = game.role_threads["Werewolf"] mentions = " ".join([p.mention for p in werewolves]) desc = ROLE_DESCRIPTIONS["Werewolf"] await thread.send(f"{mentions}\nYou are **Werewolves**!\n\n{desc}") # Seer thread seer = game.get_players_by_role("Seer") if seer: thread = game.role_threads["Seer"] desc = ROLE_DESCRIPTIONS["Seer"] await thread.send(f"{seer[0].mention}\nYou are the **Seer**!\n\n{desc}") # Doctor thread doctor = game.get_players_by_role("Doctor") if doctor: thread = game.role_threads["Doctor"] desc = ROLE_DESCRIPTIONS["Doctor"] await thread.send(f"{doctor[0].mention}\nYou are the **Doctor**!\n\n{desc}") await ctx.send( "Game started! Roles have been assigned in private threads.\nThe game cycle will begin in 30 seconds..." ) # Start the game cycle after 30 seconds await asyncio.sleep(30) await self.start_cycle(ctx) async def create_role_threads(self, game: WerewolvesGame): # Werewolves share a thread, others get solo threads channel = game.channel werewolves = game.get_players_by_role("Werewolf") seer = game.get_players_by_role("Seer") doctor = game.get_players_by_role("Doctor") # Create werewolf group thread if werewolves: thread = await channel.create_thread( name="Werewolves Actions", type=discord.ChannelType.private_thread, auto_archive_duration=60, reason="Werewolves game: Werewolves thread", ) for member in werewolves: await thread.add_user(member) game.role_threads["Werewolf"] = thread # Create seer thread if seer: thread = await channel.create_thread( name="Seer Actions", type=discord.ChannelType.private_thread, auto_archive_duration=60, reason="Werewolves game: Seer thread", ) await thread.add_user(seer[0]) game.role_threads["Seer"] = thread # Create doctor thread if doctor: thread = await channel.create_thread( name="Doctor Actions", type=discord.ChannelType.private_thread, auto_archive_duration=60, reason="Werewolves game: Doctor thread", ) await thread.add_user(doctor[0]) game.role_threads["Doctor"] = thread @commands.command(name="playwerewolves") @commands.guild_only() async def play_werewolves(self, ctx): """Create a private game room for Werewolves and start the game setup.""" guild = ctx.guild overwrites = { guild.default_role: discord.PermissionOverwrite(read_messages=False), ctx.author: discord.PermissionOverwrite( read_messages=True, send_messages=True ), } category = ctx.channel.category channel_name = f"werewolves-room-{ctx.author.display_name}".replace( " ", "-" ).lower() channel = await guild.create_text_channel( name=channel_name, overwrites=overwrites, category=category, reason="Private Werewolves game room", ) await channel.send( f"{ctx.author.mention} Your private **Werewolves** game room has been created!\n" f"Use `py addwerewolves ` in this channel to add others.\n" f"Use `py joinwerewolves` in this channel to join the game.\n" f"Once enough players have joined, use `py startwerewolves` to start the game." ) # Optionally, auto-join the creator to the game game = WerewolvesGame(channel) game.add_player(ctx.author) self.active_games[channel.id] = game @commands.command(name="addwerewolves", hidden=True) @commands.guild_only() async def add_werewolves( self, ctx, member: discord.Member | None = None, *, identifier: str | None = None, ): """Add a user to the current Werewolves game room by username (case-insensitive, not mention).""" game = self.active_games.get(ctx.channel.id) if not game: await ctx.reply("This is not a Werewolves game room.") return # Only allow the creator (first player) to add others if ctx.author != game.players[0]: await ctx.reply("Only the game creator can add players to the room.") return # Find member by username (case-insensitive) if member is None and identifier: try: member = ctx.guild.get_member(int(identifier)) except ValueError: member = discord.utils.find( lambda m: m.name.lower() == identifier.lower() or m.display_name.lower() == identifier.lower(), ctx.guild.members, ) if not member: await ctx.reply(f"User '{member}' not found in this server.") return await ctx.channel.set_permissions( member, read_messages=True, send_messages=True, ) await ctx.send( f"{member.mention} has been added to the game room! They can now join the game with `py joinwerewolves`." ) @commands.command(name="startcycle", hidden=True) async def start_cycle(self, ctx): """Start the Werewolves game cycle (night/day loop).""" game = self.active_games.get(ctx.channel.id) if not game or not game.started: await ctx.send("No active game to start the cycle.") return await ctx.send("The game cycle is starting!") game.phase = "night" game.alive = set(game.players) game.reset_votes() while True: # NIGHT PHASE await ctx.send( "🌙 **Night falls!** Werewolves, Seer, and Doctor, check your threads." ) await asyncio.sleep(10) # Replace with your night action logic and timing # DAY PHASE await ctx.send( "☀️ **Day breaks!** Discuss and vote to eliminate a player. Use `py vote `." ) game.reset_votes() await self.day_voting(ctx, game) voted_out_id = game.tally_votes() if voted_out_id: voted_out = discord.utils.get(ctx.guild.members, id=voted_out_id) if voted_out in game.alive: game.alive.remove(voted_out) await ctx.send(f"{voted_out.mention} has been eliminated!") else: await ctx.send("No one was eliminated today.") # Check win condition (example: only werewolves or villagers left) werewolves = [p for p in game.alive if game.roles.get(p.id) == "Werewolf"] villagers = [p for p in game.alive if game.roles.get(p.id) != "Werewolf"] if not werewolves: await ctx.send("Villagers win! 🎉") break if len(werewolves) >= len(villagers): await ctx.send("Werewolves win! 🐺") break await asyncio.sleep(5) # Short pause before next night @commands.command(name="vote", hidden=True) async def vote( self, ctx, member: discord.Member | None = None, *, identifier: str | None = None, ): """Vote to eliminate a player during the day phase.""" game = self.active_games.get(ctx.channel.id) if not game or ctx.author not in game.alive: await ctx.reply("You are not in the game or not alive.") return # Find member by username (case-insensitive) if member is None and identifier: try: member = ctx.guild.get_member(int(identifier)) except ValueError: member = discord.utils.find( lambda m: m.name.lower() == identifier.lower() or m.display_name.lower() == identifier.lower(), ctx.guild.members, ) if not member: await ctx.reply(f"User '{member}' not found in this server.") return if not member or member not in game.alive: await ctx.reply("That player is not alive or not found.") return game.vote(ctx.author, member) await ctx.send( f"{ctx.author.display_name} voted to eliminate {member.display_name}." ) async def day_voting(self, ctx, game, timeout=30): """Wait for votes during the day phase.""" await ctx.send(f"You have {timeout} seconds to vote.") await asyncio.sleep(timeout) async def setup(client): await client.add_cog(Games(client))