331 lines
13 KiB
Python
Executable File
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))
|