Files
2025-09-16 15:00:16 +02:00

331 lines
13 KiB
Python
Executable File

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 <username>` 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 <username>`."
)
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))