Compare commits

...

28 Commits

Author SHA1 Message Date
Bibin Muttappillil a4eaa141ac start refactoring again (this time abstracting Discord API and removing dependencies of the game classe 2020-08-10 22:28:20 +02:00
Bibin Muttappillil 14037bf5d8 small fixes + correct checks 2020-07-15 18:32:29 +02:00
Bibin Muttappillil e2b8a20fa6 deleted pycache? 2020-07-15 17:23:41 +02:00
Bibin Muttappillil 519aca51db resolved some name changes 2020-07-15 17:12:10 +02:00
Bibin Muttappillil cbaafe0b85 moved werewolf specific commands to werewolf.cog 2020-07-15 16:59:30 +02:00
Bibin Muttappillil 812ae8a990 moved general player functions to super class 2020-07-15 16:50:26 +02:00
Bibin Muttappillil e7b37217f9 changed name var to name func 2020-07-15 16:49:19 +02:00
Bibin Muttappillil 54f8eaf4c2 changed constant name to a function returning name 2020-07-15 16:48:00 +02:00
Bibin Muttappillil 13064a3af0 more refactoring 2020-07-15 16:09:38 +02:00
Bibin Muttappillil b85c20b65a started generalizing player 2020-07-15 16:09:17 +02:00
Bibin Muttappillil eff057da8c lot's of refactoring and packaging 2020-07-13 23:47:56 +02:00
Bibin Muttappillil 9cdc8d742c added dev cog 2020-07-12 15:54:17 +02:00
Bibin Muttappillil a4652642d1 added TODO's 2020-07-12 15:53:49 +02:00
Bibin Muttappillil ec446785ee forgotten commits 2020-07-12 13:17:39 +02:00
Bibin Muttappillil 266b2fe81f update gitignore 2020-04-15 19:21:00 +02:00
Bibin Muttappillil b474d41087 added decorator, decorator with args, time command added 2020-04-15 18:42:56 +02:00
Bibin Muttappillil e56a5d1629 fix middle_cards, fix async, discussion timer added 2020-04-15 17:23:24 +02:00
Bibin Muttappillil be93817063 more elegant setup function 2020-04-15 14:20:30 +02:00
Bibin Muttappillil caca8bc2e5 added stop command to restart round 2020-04-15 02:04:37 +02:00
Bibin Muttappillil 240ad44e4c more generous role match function 2020-04-15 01:10:27 +02:00
Bibin Muttappillil e0029118f9 doppelganger + refactored code 2020-04-14 02:55:51 +02:00
Bibin Muttappillil 42275c7ab5 beautified output 2020-04-13 20:55:17 +02:00
Bibin Muttappillil 7495336269 commands instead of on_message 2020-04-12 23:01:47 +02:00
Bibin Muttappillil eaf1686c5d on_message -> commands 2020-04-03 16:09:39 +02:00
Bibin Muttappillil 534013a7dc Merge branch 'master' of https://git.soi.ch/bibin/werewolve-bot 2020-04-02 20:50:15 +02:00
Bibin Muttappillil 9b367001b4 organize in several files 2020-04-02 20:47:11 +02:00
Bibin Muttappillil 15b695851d Merge branch 'master' of https://git.soi.ch/bibin/werewolve-bot
x
2020-04-01 00:50:22 +02:00
Bibin Muttappillil ff655eecdd small fixes 2020-04-01 00:50:17 +02:00
15 changed files with 975 additions and 880 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
secrets.env
.env
venv
__pycache__/
src/test.py

View File

@ -0,0 +1,57 @@
"""This module is a wrapper for messaging functionality Discord to play text based multi-player games"""
# discord imports
import discord
from discord.ext import commands
class Client:
def create_task():
pass
class User:
def name() -> str:
pass
def DM() -> DM:
pass
class Message:
def mentions() -> User:
pass
class Communication:
def send():
pass
def send_embed():
pass
def command():
pass
def wait_for_message() -> Message:
pass
class DM(Communication):
pass
class Room(Communication):
pass
class Context:
pass
def choices():
pass
def choice():
pass

View File

@ -0,0 +1,57 @@
"""This module is a wrapper template for messaging functionality to play text based multi-player games"""
class Interaction:
def query(communication, question: str, options: list, exp_ans=1):
pass
class Client:
def create_task(func):
pass
class User:
def name():
pass
def DM():
pass
class Communication: # better name
def send(str):
pass
def send_embed(embed):
pass
def command():
pass
def wait_for_message():
pass
class DM(Communication):
pass
class Room(Communication):
pass
class Context:
def User():
pass
def Communication():
pass
def Message():
pass
def mentions():
pass
# args?

54
src/package/developer.py Normal file
View File

@ -0,0 +1,54 @@
import discord
from discord.ext import commands
class Developer(commands.Cog):
"""This class is intended only for the developer, mainly for testing purposes"""
def __init__(self, bot):
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
await self.bot.change_presence(status=discord.Status.online, activity=discord.Game('One Night Ultimate Werewolf'))
print('We have logged in as {0.user}'.format(self.bot))
async def is_dev(ctx):
if ctx.author.id == 461892912821698562:
return True
await ctx.send("This command is not for you!")
return False
@commands.command()
async def hello(self, ctx):
await ctx.send(f"Hello {ctx.message.author.name} :wave:")
@commands.group(name="gg")
async def group(self, ctx):
pass
@group.command()
@commands.check(is_dev)
async def ping(self, ctx, i: int):
await ctx.send("pong")
await ctx.send(i + 1)
@commands.command()
@commands.check(is_dev)
async def logout(self, ctx):
await self.bot.logout()
@commands.command()
@commands.check(is_dev)
async def debug(self, ctx, *args):
for emoji in ctx.guild.emojis:
await ctx.send(emoji)
print(emoji.id)
@debug.error
async def debug_error(self, ctx, error):
await ctx.send(error)
def setup(bot):
bot.add_cog(Developer(bot))

49
src/package/games/game.py Normal file
View File

@ -0,0 +1,49 @@
"""Has a single class: Game"""
# standard library imports
from abc import ABC
import asyncio
# discord imports
import discord
# local imports
from .players import Player
class Game(ABC):
"""This (abstract) class is a template for main-game-loop objects for games"""
@classmethod
def name(cls):
return "Game"
player_cls = Player
def __init__(self, bot, channel):
self.bot = bot
self.channel = channel
self.running = False
self.player_list = []
async def send(self, message):
await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff))
async def for_all_player(self, call):
await asyncio.gather(*[call(p) for p in self.player_list])
async def set_players(self, msg):
self.player_list = [await self.player_cls.make(mem, self) for mem in msg.mentions]
await self.send(f"Players: {', '.join(p.name() for p in self.player_list)}") # send confirmation
def check(self):
pass
def setup(self):
pass
def end(self):
self.running = False
async def round(self):
pass

View File

@ -0,0 +1,85 @@
"""Has a single class: Game_cog"""
# standard library imports
from typing import Dict
# discord imports
import discord
from discord.ext import commands
# local imports
from ..send_message import Send_message
from .game import Game
class Game_cog(Send_message):
"""This (abstract) class is are common function for the Game Cog's (setup-game, pre-game, in-game), mainly has checker functions"""
game_cls = Game
def __init__(self, bot):
self.bot = bot
self.game_instances = {} # TODO: type hint? Dict[discord.TextChannel, self.game_cls]
async def setup_check(self, ctx):
if ctx.channel not in self.game_instances:
await self.send_wrong(ctx, f"The channel is not setup yet.")
return ctx.channel in self.game_instances
async def not_running_check(self, ctx):
if self.game_instances[ctx.channel].running:
await self.send_wrong(ctx, "Sorry! A game is already running")
return not self.game_instances[ctx.channel].running
async def running_check(self, ctx):
if not self.game_instances[ctx.channel].running:
await self.send_wrong(ctx, "No game is running")
return self.game_instances[ctx.channel].running
async def setup(self, ctx):
"""This function creates an game instance for this channel"""
if ctx.channel in self.game_instances:
await self.send_wrong(ctx, f"A game '{self.game_cls.name()}' is already setup in this channel")
else:
self.game_instances[ctx.channel] = self.game_cls(self.bot, ctx.channel)
await self.send_friendly(ctx, f"This channel can now play: {self.game_cls.name()}")
async def reset(self, ctx):
"""This function deletes the game instance for this channel"""
if await self.setup_check(ctx):
del self.game_instances[ctx.channel]
# TODO: better info message
async def info(self, ctx):
"""Send information about the subcommands for the game"""
embed = discord.Embed(title="How to play?", description="You will need to set up the game and its information in a channel and start the game there. Afterwards the player mainly interact with the bot in DM.", color=0x00ffff)
embed.set_author(name=f"With this bot you can play {self.game_cls.name()}")
# embed.set_thumbnail(url="https://images-na.ssl-images-amazon.com/images/I/717GrDtFKCL._AC_SL1000_.jpg")
embed.add_field(name="$w game setup", value="Make this channel playable.", inline=False)
embed.add_field(name="$w game players", value="Set mentioned users as players", inline=False)
embed.add_field(name="$w game roles", value="Set the roles to play with", inline=False)
embed.add_field(name="$w game start", value="Play one round", inline=False)
embed.set_footer(text="Have fun!")
await ctx.send(embed=embed)
# TODO: can't one access self instead of ctx.cog?
def pre_game():
async def predicate(ctx):
return await ctx.cog.setup_check(ctx) and await ctx.cog.not_running_check(ctx)
return commands.check(predicate)
async def players(self, ctx):
await self.game_instances[ctx.channel].set_players(ctx.message)
async def start(self, ctx):
self.game_instances[ctx.channel].game = self.bot.loop.create_task(self.game_instances[ctx.channel].round())
await self.game_instances[ctx.channel].game
def in_game():
async def predicate(ctx):
return await ctx.cog.setup_check(ctx) and await ctx.cog.running_check(ctx)
return commands.check(predicate)
async def stop(self, ctx):
self.game_instances[ctx.channel].game.cancel()
await self.send_friendly(ctx, "Game canceled")

View File

@ -0,0 +1,79 @@
"""Has a single class: Player"""
# discord imports
import discord
class Player:
"""This (abstract) class is a template for player objects for games"""
@classmethod
async def make(cls, member, game):
p = cls()
p.member = member
p.dm = member.dm_channel or await member.create_dm()
p.game = game
return p
def name(self):
return self.member.name
def __str__(self):
return self.name()
def reset(self):
pass
def other_players(self):
return [p for p in self.game.player_list if p != self]
async def send_normal(self, message):
await self.dm.send(message)
async def send_embed(self, desc, color):
await self.dm.send(embed=discord.Embed(description=desc, color=color))
async def send_wrong(self, message):
await self.send_embed(message, 0xff8000)
async def send_confirmation(self, message):
await self.send_embed(message, 0x00ff00)
async def send_info(self, message):
await self.send_embed(message, 0x00ffff)
# TODO: refactor this function to make it understandable
async def ask_choice(self, question, options):
text = f"{question}\n" + f"{'='*len(question)}\n\n" + '\n'.join(f"[{str(i)}]({str(options[i])})" for i in range(len(options)))
await self.dm.send(f"```md\n{text}```")
# TODO: Basic Converters (https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html#basic-converters)
def check_num(self, choice, N):
if not choice.isdigit():
raise ValueError(f"Your choice {choice} is not a number")
if not 0 <= int(choice) < N:
raise ValueError(f"Your choice {choice} is not in range 0 - {N-1}")
# TODO: seems hacky, figure out a nicer way
async def receive_choice(self, options, n_ans=1):
while True:
def check(choice):
return choice.channel == self.dm and choice.author == self.member
choice = (await self.game.bot.wait_for('message', check=check)).content.split()
if not len(choice) == n_ans:
await self.send_wrong(f"Please give {n_ans} numbers not {len(choice)}")
continue
try:
for c in choice:
self.check_num(c, len(options))
except ValueError as error:
await self.send_wrong(str(error))
continue
await self.send_confirmation(f"Received: {', '.join(choice)}")
return [int(c) for c in choice]
async def get_choice(self, question, options):
await self.ask_choice(question, options)
return (await self.receive_choice(options))[0]

View File

@ -0,0 +1,75 @@
"""Has a single class: Werewolf_Cog"""
# discord imports
from discord.ext import commands
# local imports
from ..game_cog import Game_cog
from .game import Werewolf_game
class Werewolf_cog(Game_cog, commands.Cog):
"""This singleton class is a Discord Cog for the interaction in the werewolf game"""
game_cls = Werewolf_game
@commands.group(invoke_without_command=True)
async def werewolf(self, ctx):
# TODO: isn't there a better way to have a default subcommand? Maybe invoke super().info()?
await ctx.invoke(self.bot.get_command('werewolf info'))
@werewolf.command()
async def info(self, ctx):
"""Send information about the subcommands for the game"""
await super().info(ctx)
@werewolf.command()
async def setup(self, ctx):
"""This function creates an game instance for this channel"""
await super().setup(ctx)
@werewolf.command()
async def reset(self, ctx):
"""This function deletes the game instance for this channel"""
await super().reset(ctx)
@werewolf.command()
@Game_cog.pre_game()
async def players(self, ctx):
"""registers all mentioned players for the game"""
await super().players(ctx)
@werewolf.command()
@Game_cog.pre_game()
async def roles(self, ctx, *args):
"""registers roles you want to play with"""
await self.game_instances[ctx.channel].set_roles(args)
@werewolf.command()
@Game_cog.pre_game()
async def minutes(self, ctx, i: int):
"""set discussion time"""
await self.game_instances[ctx.channel].set_time(i)
@werewolf.command()
@Game_cog.pre_game()
async def start(self, ctx):
print(await self.setup_check(ctx), await self.not_running_check(ctx))
"""starts a round of werewolf"""
await super().start(ctx)
@werewolf.command()
@Game_cog.in_game()
async def stop(self, ctx):
"""aborts the current round of werewolf"""
await super().stop(ctx)
@werewolf.command()
@Game_cog.in_game()
async def time(self, ctx):
"""checks how much discussion time there is left"""
await self.send_friendly(ctx, self.game_instances[ctx.channel].remaining_time_string())
def setup(bot):
bot.add_cog(Werewolf_cog(bot))

View File

@ -0,0 +1,228 @@
"""Has a single class: Werewolf_game"""
# standard library imports
from random import shuffle
import time
import asyncio
# discord imports
import discord
# local imports
from .roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role
from .players import Werewolf_player, No_player
from ..game import Game
class Werewolf_game(Game):
"""This class simulates One Night Ultimate Werewolf with the help of Werewolf_player and Werewolf_role"""
@classmethod
def name(cls):
return "One Night Ultimate Werewolf"
player_cls = Werewolf_player
def __init__(self, bot, channel):
super().__init__(bot, channel)
self.role_list = []
self.discussion_time = 301 # seconds
async def set_roles(self, suggestions):
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, minutes: int):
self.discussion_time = minutes * 60 + 1
await self.send(f"You have set the discussion time to {self.discussion_time//60} minutes") # send confirmation
def check(self):
# TODO: min. player
if not 1 <= len(self.player_list) <= 10:
raise ValueError(f"Invalid number of players: {len(self.player_list)}")
if not len(self.role_list) == (len(self.player_list) + 3):
raise ValueError(f"Invalid number of roles: {len(self.role_list)} with {len(self.player_list)} players")
def setup(self):
# TODO: better...
self.role = dict()
# setting default value
for r in Role.__subclasses__():
# TODO: every role should be a list (not only werewolves, masons, ...)
if r == No_role:
continue
if r in [Werewolf, Mason]:
self.role[r] = []
else:
r(self).add_yourself()
self.voting_list = self.player_list + [No_player()]
for c in self.voting_list:
c.reset()
def distribute_roles(self):
shuffle(self.role_list)
# TODO: use zip
for i in range(len(self.player_list)):
role_obj = self.role_list[i](self, self.player_list[i])
self.player_list[i].set_role(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(f"*The night has begun* {':full_moon:'*10}"))
async def send_role(self):
await self.for_all_player(lambda p: p.send_info(f"Your role: **{p.night_role.name()}**"))
async def night_phases(self):
# TODO: implement waiting if role in middle
await asyncio.gather(*[self.role[r].query() for r in [Doppelganger, Seer, Robber, Troublemaker, Drunk]]) # slow
await self.role[Doppelganger].send_copy_info()
await self.role[Doppelganger].simulate() # slow
await asyncio.gather(*[w.phase() for w in self.role[Werewolf]]) # slow
await asyncio.gather(*[w.send_info() for w in [self.role[Minion]] + self.role[Mason] + [self.role[Seer]]])
await self.role[Robber].simulate()
await self.role[Robber].send_info()
await self.role[Troublemaker].simulate()
await self.role[Drunk].simulate()
await self.role[Insomniac].send_info()
await self.role[Doppelganger].insomniac()
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:02d}:{t%60:02d}"
async def discussion_timer(self):
# TODO: better?
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(f"{self.remaining_time_string()} remaining")
await asyncio.sleep(self.discussion_time / 2 - 60)
await self.send(f"{self.remaining_time_string()} remaining")
await asyncio.sleep(30)
await self.send(f"{self.remaining_time_string()} 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):
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):
for p in self.player_list:
p.vote.tally += 1
def who_dead(self):
maxi = max(c.tally for c in self.voting_list) # most votes
dead = [p for p in self.player_list if p.tally >= maxi] # who has most votes
for d in dead:
d.dead = True
if d.day_role.is_role(Hunter):
dead.append(d.vote)
return dead
def who_won(self, dead):
no_dead = (len(dead) == 0)
tanner_dead = any(d.day_role.is_role(Tanner) for d in dead)
werewolf_dead = any(d.day_role.is_role(Werewolf) for d in dead)
werewolf_in_game = any(p.day_role.is_role(Werewolf) for p in self.player_list)
minion_dead = any(d.day_role.is_role(Minion) for d in dead)
minion_in_game = any(p.day_role.is_role(Minion) for p in self.player_list)
werewolf_won = False
village_won = False
tanner_won = False
# could make it shorter using boolean algebra
if no_dead:
if werewolf_in_game:
werewolf_won = True
else:
village_won = True
else:
if tanner_dead:
tanner_won = True
if werewolf_dead:
village_won = True
else:
if werewolf_dead:
village_won = True
else:
if minion_dead:
if werewolf_in_game:
werewolf_won = True
else:
village_won = True
else:
if minion_in_game:
werewolf_won = True
for p in self.player_list:
if p.day_role.is_role(Werewolf) or p.day_role.is_role(Minion):
p.won = werewolf_won
elif p.day_role.is_role(Tanner):
p.won = p.dead
else:
p.won = village_won
return werewolf_won, village_won, tanner_won, dead
async def result(self, werewolf_won, village_won, tanner_won, dead):
winnning = []
if werewolf_won:
winnning.append("Werewolves")
if 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") # number of tanners dead
if len(winnning) == 0:
winnning = ["No one"]
# TODO: better emoji code?
space = "<:space:705863033871663185>"
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 ":eyes:"
val = [
won_emoji,
dead_emoji,
f"{p.tally}:ballot_box:",
f"role: {str(p.day_role)}",
f"(was: {str(p.night_role)})",
f":point_right: {str(p.vote)}"
]
embed.add_field(name=str(p), value=space.join(v for v in val), inline=False)
embed.add_field(name="Middle cards", value=', '.join(r.name() for r in self.middle_card), inline=False)
await self.channel.send(embed=embed)
async def round(self):
try:
self.check()
self.setup()
self.running = True
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()))
finally:
self.end()

View File

@ -0,0 +1,37 @@
"""Has a single class: Werewolf_player"""
# local import
from ..players import Player
class Werewolf_player(Player):
"""This class is for simulating non-role-specific werewolf players"""
def set_role(self, role):
self.day_role = self.night_role = role
def reset(self):
self.tally = 0
self.won = self.dead = False
def swap(self, player_B):
self.day_role, player_B.day_role = player_B.day_role, self.day_role
async def get_double_choice(self, question, options):
await self.ask_choice(question, options)
return await self.receive_choice(options, 2)
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")
# TODO: this seems hacky, find another approach
class No_player(Werewolf_player):
def name(self):
return "no one"

View File

@ -0,0 +1,180 @@
import functools
from fuzzywuzzy import fuzz
from .players import No_player
class Role:
def __init__(self, game, player=No_player()):
self.game = game
self.player = player
def add_yourself(self):
self.game.role[type(self)] = self
async def send_role_list(self, cls):
await self.player.send_info(f"{cls.name()}: {', '.join(r.player.name() for r in self.game.role[cls])}")
def is_role(self, cls):
return isinstance(self, cls)
@staticmethod
def match(message):
return max(Role.__subclasses__(), key=lambda role_class: fuzz.ratio(message, role_class.name()))
@staticmethod
def no_player(func):
@functools.wraps(func)
async def wrapper(self, *args, **kwargs):
if not isinstance(self.player, No_player):
return await func(self, *args, **kwargs)
return wrapper
@classmethod
def name(cls):
return cls.__name__.casefold()
def __str__(self):
return self.name()
class Doppelganger(Role):
@Role.no_player
async def query(self):
self.choice = await self.player.get_choice("Which player role do you want to copy?", self.player.other())
@Role.no_player
async def send_copy_info(self):
self.copy_role = type(self.player.other()[self.choice].day_role)
await self.player.send_info(f"You copied: {self.copy_role.name()}")
@Role.no_player
async def simulate(self):
if self.copy_role in [Werewolf, Mason]:
self.copy_role.add_yourself(self)
elif self.copy_role in [Seer, Robber, Troublemaker, Drunk]:
await self.copy_role.query(self)
if self.copy_role in [Robber, Troublemaker, Drunk]:
await self.copy_role.simulate(self)
if self.copy_role in [Seer, Robber]:
await self.copy_role.send_info(self)
@Role.no_player
async def phase(self):
if self.copy_role == Werewolf:
await self.copy_role.phase(self)
@Role.no_player
async def send_info(self):
if self.copy_role in [Mason, Minion]:
await self.copy_role.send_info(self)
@Role.no_player
async def insomniac(self):
if self.copy_role == Insomniac:
await self.copy_role.send_info(self)
def is_role(self, cls):
return self.copy_role == cls
class Werewolf(Role):
def add_yourself(self):
self.game.role[Werewolf].append(self)
@Role.no_player
async def phase(self):
if len(self.game.role[Werewolf]) >= 2:
await self.send_role_list(Werewolf)
else:
await self.player.send_info("You are the only werewolf")
self.choice = await self.player.get_choice("Which card in the middle do you want to look at?", ["left", "middle", "right"]) # 0, 1, 2
await self.player.send_info(f"A card in the middle is: {self.game.middle_card[self.choice].name()}")
class Minion(Role):
@Role.no_player
async def send_info(self):
if len(self.game.role[Werewolf]) == 0:
await self.player.send_info("There were no werewolves, so you need to kill a villager!")
else:
await self.send_role_list(Werewolf)
class Mason(Role):
def add_yourself(self):
self.game.role[Mason].append(self)
@Role.no_player
async def send_info(self):
await self.send_role_list(Mason)
class Seer(Role):
@Role.no_player
async def query(self):
self.choice = await self.player.get_choice("Which 1 player card or 2 middle cards do you want to look at?", self.player.other() + ["left & middle", "middle & right", "left & right"])
@Role.no_player
async def send_info(self):
if self.choice < len(self.player.other()):
await self.player.send_info(f"You saw: {self.player.other()[self.choice].night_role.name()}")
else:
a, b = [(0, 1), (1, 2), (0, 2)][self.choice - len(self.player.other())]
await self.player.send_info(f"You saw: {self.game.middle_card[a]} {self.game.middle_card[b]}")
class Robber(Role):
@Role.no_player
async def query(self):
self.choice = await self.player.get_choice("Which player do you want to rob?", self.player.other())
@Role.no_player
async def simulate(self):
self.player.swap(self.player.other()[self.choice])
@Role.no_player
async def send_info(self):
await self.player.send_info(f"You robbed: {self.player.day_role.name()}")
class Troublemaker(Role):
@Role.no_player
async def query(self):
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
async def simulate(self):
self.player.other()[self.A].swap(self.player.other()[self.B])
class Drunk(Role):
@Role.no_player
async def query(self):
self.choice = await self.player.get_choice("Which card from the middle do you want to take?", ["left", "middle", "right"])
@Role.no_player
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
class Insomniac(Role):
@Role.no_player
async def send_info(self):
await self.player.send_info(f"You are: {self.player.day_role.name()}")
class Villager(Role):
pass
class Tanner(Role):
pass
class Hunter(Role):
pass
class No_role(Role):
pass

View File

@ -0,0 +1,18 @@
# discord import
import discord
class Send_message:
"""This (abstract) class for sending formatted messages"""
def __init__(self, bot):
self.bot = bot
async def send_embed(self, ctx, desc, color):
await ctx.send(embed=discord.Embed(description=desc, color=color))
async def send_friendly(self, ctx, desc):
await self.send_embed(ctx, desc, 0x00ff00)
async def send_wrong(self, ctx, desc):
await self.send_embed(ctx, desc, 0xff8000)

54
src/werewolf_bot.py Normal file
View File

@ -0,0 +1,54 @@
"""
This is the main module of the Discord Bot
Mainly loads the Cog's and starts the bot
"""
__version__ = '0.4'
__author__ = 'Bibin Muttappillil'
# standard library imports
import os
from dotenv import load_dotenv
# discord imports
from discord.ext import commands
# Token stuff
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
if TOKEN is None:
print("Missing discord token!")
exit(1)
bot = commands.Bot(command_prefix=commands.when_mentioned_or('$b '))
bot.load_extension('package.developer')
bot.load_extension('package.games.werewolf.cog')
@bot.command()
@commands.is_owner()
async def reload(ctx, extension):
bot.reload_extension(f'package.{extension}')
print(f'package.{extension} reloaded')
# checker annotations
# TODO: replace with discord.py error handling?
'''
def error_handling(command):
@functools.wraps(command)
async def wrapper(ctx, *args, **kwargs):
try:
await command(ctx, *args, **kwargs)
except ValueError as error:
await send_wrong(ctx, str(error))
except asyncio.TimeoutError:
await send_wrong(ctx, "Error: I got bored waiting for your input")
return wrapper
'''
bot.run(TOKEN)

View File

@ -1,381 +0,0 @@
import os
import discord
from enum import Enum
from random import shuffle
import asyncio
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
class Role(Enum):
doppelganger = 1
werewolve = 2
minion = 3
mason = 4
seer = 5
robber = 6
troublemaker = 7
drunk = 8
insomniac = 9
villiager = 10
tanner = 11
hunter = 12
class Player:
def __init__(self, member, role):
self.member = member
self.night_role = role
self.day_role = role
self.vote = -1
self.tally = 0
async def create_dm(self):
self.dm = self.member.dm_channel or await self.member.create_dm()
async def send(self, message):
await self.dm.send(message)
async def sendRole(self):
await self.send("Your role: " + self.night_role.name)
async def sendPoll(self):
await self.send(Player.poll_message)
async def sendMiddle(self):
await self.send(Player.poll_middle)
async def receiveChoice(self):
global bot
def check(vote):
return vote.channel == self.dm and vote.content.isdigit() and 0 <= int(vote.content) < Player.size
vote = await bot.wait_for('message', timeout=30.0, check = check)
await self.send("Received: " + vote.content)
return int(vote.content)
bot = discord.Client()
@bot.event
async def on_ready():
print('We have logged in as {0.user}'.format(bot))
running = False
async def hello(message):
print("Hello")
await message.channel.send('Hello!:regional_indicator_a:')
def check(vote):
return vote.content in ["a", "b"] and vote.channel == message.channel
vote = await bot.wait_for('message', timeout=10.0, check = check)
await message.channel.send("Received! " + vote.content)
@bot.event
async def on_message(message):
global running
if message.author == bot.user:
return
if message.content.startswith('$hello'):
await hello(message)
return
if message.content.startswith('$logout'):
await bot.logout()
return
if message.content.startswith('$werewolve'):
# start (only one instance running)
if running:
await message.channel.send("Sorry! A game is already running")
return
"""
if len(players) < 4:
await message.channel.send("To few players!")
return
"""
running = True
# setup
members = [mem for mem in message.channel.members if not mem.bot]
Player.size = len(members)
role_set = [Role.werewolve, Role.werewolve, Role.drunk, Role.insomniac, Role.seer, Role.robber, Role.hunter] + (Player.size-4)*[Role.villiager]
shuffle(role_set)
players = []
werewolve = []
minion = None
mason = []
seer = None
robber = None
troublemaker = None
drunk = None
insomniac = None
villiager = []
tanner = None
hunter = None
for i in range(Player.size):
players.append(Player(members[i], role_set[i]))
await players[i].create_dm()
if role_set[i] == Role.werewolve:
werewolve.append(players[i])
elif role_set[i] == Role.mason:
mason.append(players[i])
elif role_set[i] == Role.seer:
seer = players[i]
elif role_set[i] == Role.robber:
robber = players[i]
elif role_set[i] == Role.troublemaker:
troublemaker = players[i]
elif role_set[i] == Role.drunk:
drunk = players[i]
elif role_set[i] == Role.insomniac:
insomniac = players[i]
elif role_set[i] == Role.villiager:
villiager.append(players[i])
elif role_set[i] == Role.tanner:
tanner = players[i]
elif role_set[i] == Role.hunter:
hunter = players[i]
middle = role_set[-3:]
Player.poll_middle = "(0) left\n(1) middle\n(2) right"
Player.poll_message = ""
for i in range(Player.size):
Player.poll_message += "(" + str(i) + ") " + players[i].member.name + "\n"
# doing phase
#send role info to all
send_roles = [p.sendRole() for p in players]
await asyncio.gather(*send_roles)
# query stuff
#doppelganger stuff
async def query_seer():
if seer is None:
return
await seer.send("Who do you want to look at?")
await seer.sendPoll()
async def query_robber():
if robber is None:
return
await robber.send("Who do you want to rob?")
await robber.sendPoll()
async def query_troublemaker():
if troublemaker is None:
return
await troublemaker.send("Who do you want to exchange?")
await troublemaker.sendPoll()
await troublemaker.sendPoll()
async def query_drunk():
if drunk is None:
return
await drunk.send("Which card from the middle do you want to take?")
await drunk.sendMiddle()
await asyncio.gather(query_seer(), query_robber(), query_troublemaker(), query_drunk())
#receive and confirm!
async def receive_seer():
if seer is not None:
return await seer.receiveChoice()
async def receive_robber():
if robber is not None:
return await robber.receiveChoice()
async def receive_troublemaker():
if troublemaker is not None:
return await troublemaker.receiveChoice()
async def receive_drunk():
if drunk is not None:
return await drunk.receiveChoice()
seerChoice, robberChoice, troublemakerChoice, drunkChoice = await asyncio.gather(receive_seer(), receive_robber(), receive_troublemaker(), receive_drunk())
# simulate
#exchange robber
if robber is not None:
robber.day_role, players[robberChoice].day_role = players[robberChoice].day_role, robber.day_role
#exchange troublemaker
if troublemaker is not None:
A = players[troublemakerChoice[0]]
B = players[troublemakerChoice[1]]
A.day_role, B.day_role = B.day_role, A.day_role
#exchange drunk
if drunk is not None:
drunk.day_role, middle[drunkChoice] = middle[drunkChoice], drunk.day_role
#send werewolves identity to werewolves and minion
async def send_werewolves():
message = ""
for w in werewolve:
message += w.member.name + " "
message += "were werewolves"
sender = [bad.send(message) for bad in werewolve]
if minion is not None:
sender.append(minion.send(message))
await asyncio.gather(*sender)
#send mason identity to masons
async def send_masons():
message = ""
for m in mason:
message += m.member.name + " "
message += " were masons"
sender = [m.send(message) for m in mason]
await asyncio.gather(*sender)
#send info to seer
async def send_seer():
if seer is not None:
await seer.send(players[seerChoice].member.name + " was a " + players[seerChoice].night_role.name)
#send info to robber
async def send_robber():
if robber is not None:
await robber.send("You stole the role: " + players[robberChoice].night_role.name)
#send insomniac new role to insomniac
async def send_insomniac():
if insomniac is not None:
await insomniac.send("You are now a " + insomniac.day_role.name)
await asyncio.gather(send_werewolves(), send_masons(), send_seer(), send_robber(), send_insomniac())
# discussion
# vote
def check_vote(vote):
return vote.content == "$vote" and vote.channel == message.channel
await bot.wait_for('message', check = check_vote)
await message.channel.send("Vote in DM")
send_votes = [p.sendPoll() for p in players]
await asyncio.gather(*send_votes)
receive_votes = [p.receiveChoice() for p in players]
tmp = await asyncio.gather(*receive_votes)
for i in range(Player.size):
players[i].vote = tmp[i]
for p in players:
players[p.vote].tally += 1
maxi = max( [p.tally for p in players] )
dead = [p for p in players if p.tally >= maxi]
if hunter in dead:
dead.append(players[hunter.vote])
# result and end
# show day-role & night role
msg = ""
for d in dead:
msg += d.member.name + " "
msg += "are dead!"
await message.channel.send(msg)
if any(d in werewolve for d in dead):
msg = "The village won!"
else:
msg = "The werewolves won!"
await message.channel.send(msg)
msg = ""
for p in players:
msg += p.member.name + ": " + p.day_role.name + " (day) " + p.night_role.name + " (night) and voted for " + players[p.vote].member.name + "\n"
msg += middle
await message.channel.send(msg)
running = False
bot.run(TOKEN)

View File

@ -1,499 +0,0 @@
import os
import discord
from enum import Enum
from random import shuffle
import asyncio
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
if TOKEN is None:
print("Missing discord token!")
exit(1)
class Role:
def __init__(self, game):
self.game = game
self.copy = self
def setPlayer(self, player):
self.player = player
async def phase1(self): # query stuff + doppelganger simulation
pass
async def phase2(self): # werewolf stuff + seer info
pass
async def phase3(self): # robber simulation & info
pass
async def phase4(self): # troublemaker simulation
pass
async def phase5(self): # mostly sending info + drunk simulation
pass
@staticmethod
def match(message, game):
for role_class in Role.role_set:
if message.casefold() == role_class.name():
return role_class(game)
@classmethod
def name(cls):
return cls.__name__.casefold()
def __str__(self):
return self.name()
class Doppelganger(Role):
order = 1
class Werewolf(Role):
order = 2
def setPlayer(self, player):
super().setPlayer(player)
self.game.werewolf_list.append(player)
async def phase2(self):
if len(self.game.werewolf_list) >= 2:
await self.player.send("Werewolves: " + str(self.game.werewolf_list))
else:
await self.player.send("You are the only werewolf")
await self.player.send("Which card in the middle do you want to look at?")
self.choice = await self.player.get_choice(["left", "middle", "right"])
await self.player.send("A card in the middle is: " + self.game.middle_card[self.choice].name())
class Minion(Role):
order = 3
async def phase2(self):
if len(self.game.werewolf_list) == 0:
await self.player.send("There were no werewolves so you became one")
else:
await self.player.send("Werewolves: " + str(self.game.werewolf_list))
class Mason(Role):
order = 4
def setPlayer(self, player):
super().setPlayer(player)
self.game.mason_list.append(player)
async def phase2(self):
await self.player.send("Mason " + str(self.game.mason_list))
class Seer(Role):
order = 5
async def phase1(self):
await self.player.send("Which 1 player card or 2 middle cards do you want to look at?")
self.choice = await self.player.get_choice(self.player.other() + ["left & middle", "middle & right", "left & right"])
async def phase2(self):
if self.choice < len(self.player.other()):
await self.player.send(self.player.other()[self.choice].night_role)
else:
self.choice -= len(self.player.other())
if self.choice == 0:
a, b = 0, 1
elif self.choice == 1:
a, b = 1, 2
else:
a, b = 0, 2
await self.player.send(str(self.game.middle_card[a]) + " " + str(self.game.middle_card[b]))
class Robber(Role):
order = 6
async def phase1(self):
await self.player.send("Which player do you want to rob?")
self.choice = await self.player.get_choice(self.player.other())
async def phase3(self):
Player.swap(self.player, self.player.other()[self.choice])
await self.player.send("You robbed: " + str(self.player.day_role))
class Troublemaker(Role):
order = 7
async def phase1(self):
await self.player.send("Who do you want to exchange? (send two separate numbers)")
self.A = await self.player.get_choice(self.player.other())
self.B = await self.player.get_choice(self.player.other())
async def phase4(self):
Player.swap(self.player.other()[self.A], self.player.other()[self.B])
# receive conformation
await self.player.send("Received " + str(self.A) + " " + str(self.B))
class Drunk(Role):
order = 8
async def phase1(self):
await self.player.send("Which card from the middle do you want to take?")
self.choice = await self.player.get_choice(["left", "middle", "right"])
async def phase5(self):
self.player.day_role, self.game.middle_card[self.choice] = self.game.middle_card[self.choice], self.player.day_role
#receive conformation
await self.player.send("Received " + str(self.choice))
class Insomniac(Role):
order = 9
async def phase5(self):
await self.player.send("You are now: " + str(self.player.day_role))
class Villiager(Role):
order = 10
class Tanner(Role):
order = 11
class Hunter(Role):
order = 12
class No_role(Role):
order = 1000
Role.role_set = [Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Villiager, Tanner, Hunter]
class Player:
@staticmethod
async def make(member, game):
p = Player()
p.member = member
p.dm = member.dm_channel or await member.create_dm()
p.game = game
return p
@staticmethod
def swap(player_A, player_B):
player_A.day_role, player_B.day_role = player_B.day_role, player_A.day_role
def setRole(self, role):
self.night_role = role
self.day_role = role
def name(self):
return self.member.name
def __repr__(self):
return self.name()
def other(self):
return [p for p in self.game.player_list if p != self]
async def send(self, message):
await self.dm.send(message)
async def ask_choice(self, options):
await self.send('\n'.join( "(" + str(i) + ") " + str(options[i]) for i in range(len(options)) ))
async def receive_choice(self, options):
def check(choice):
return choice.channel == self.dm and choice.content.isdigit() and 0 <= int(choice.content) < len(options)
return int((await self.game.bot.wait_for('message', timeout=30.0, check = check)).content)
async def get_choice(self, options):
await self.ask_choice(options)
return await self.receive_choice(options)
async def cast_vote(self, options):
self.vote = options[await self.get_choice(options)]
class No_player(Player):
def __init__(self):
self.day_role = No_role()
def name(self):
return "no one"
def __str__(self):
return self.name()
class one_night:
def __init__(self, bot):
self.running = False
self.bot = bot
self.player_list = []
self.role_list = Role.role_set
def set_channel(self, channel):
self.channel = channel
async def send(self, message):
await self.channel.send(message)
async def receive(self, command):
def check(msg):
return msg.channel == self.channel and msg.content.startswith(command)
return await bot.wait_for('message', check = check)
def setup(self):
self.werewolf_list = []
self.mason_list = []
async def set_players(self):
await self.send("Who is playing?")
msg = await self.receive('$players')
# use info from last round otherwise
if not msg.content.startswith('$players last'):
self.player_list = [await Player.make(mem, self) for mem in msg.mentions]
# check conditions
if not 0 <= len(self.player_list) <= 10:
raise ValueError("Invalid number of players: " + str(len(self.player_list)))
# send confirmation
await self.send("Players: " + ", ".join(p.name() for p in self.player_list))
async def set_roles(self):
await self.send("With which roles do you want to play?")
msg = await self.receive('$roles')
# use info from last round otherwise
if not msg.content.startswith('$roles last'):
tmp_role = [Role.match(r, self) for r in msg.content.split()[1:]]
# invalid input
if None in tmp_role:
raise ValueError("Invalid list of roles: " + str(tmp_role))
self.role_list = tmp_role
# check condition
if not len(self.role_list) == (len(self.player_list) + 3):
raise ValueError("Invalid number of roles: " + str(len(self.role_list)) + " with " + str(len(self.player_list)) + " players")
# send confirmation
await self.send("Roles: " + ", ".join(r.name() for r in self.role_list))
def distribute_roles(self):
shuffle(self.role_list)
for i in range(len(self.player_list)):
self.player_list[i].setRole(self.role_list[i])
self.role_list[i].setPlayer(self.player_list[i])
self.middle_card = self.role_list[-3:]
self.active_role = sorted(self.role_list[:-3], key = lambda x: x.order) #necessary?
async def start_night(self):
await asyncio.gather( *[p.send("The night has begun") for p in self.player_list] )
async def send_role(self):
await asyncio.gather( *[p.send("Your role: " + p.night_role.name()) for p in self.player_list] )
async def night_phases(self):
await asyncio.gather( *[r.phase1() for r in self.active_role] )
await asyncio.gather( *[r.phase2() for r in self.active_role] )
await asyncio.gather( *[r.phase3() for r in self.active_role] )
await asyncio.gather( *[r.phase4() for r in self.active_role] )
await asyncio.gather( *[r.phase5() for r in self.active_role] )
async def start_day(self):
await self.send("The day has started")
async def vote(self, options):
# vote
await self.receive('$vote')
await self.send("Vote in DM")
await asyncio.gather( *[p.cast_vote(options) for p in self.player_list] )
await self.send("Votes\n\n" + '\n'.join(str(p) + " :arrow_right: " + str(p.vote) for p in self.player_list))
def tally(self, options):
for o in options:
o.tally = 0
for p in self.player_list:
p.vote.tally += 1
def who_dead(self, options):
maxi = max(o.tally for o in options)
dead = [p for p in self.player_list if p.tally >= maxi]
for d in dead:
d.dead = True
if isinstance(d.day_role.copy, Hunter):
dead.append(d.vote)
return dead
def who_won(self, dead):
no_dead = (len(dead) == 0)
tanner_dead = any(isinstance(d.day_role.copy, Tanner) for d in dead)
werewolf_dead = any(isinstance(d.day_role.copy, Werewolf) for d in dead)
werewolf_in_game = any(isinstance(p.day_role.copy, Werewolf) for p in self.player_list)
minion_dead = any(isinstance(d.day_role.copy, Minion) for d in dead)
minion_in_game = any(isinstance(p.day_role.copy, Minion) for p in self.player_list)
werewolf_won = False
village_won = False
tanner_won = False
# could make it shorter using boolean algebra
if no_dead:
if werewolf_in_game:
werewolf_won = True
else:
village_won = True
else:
if tanner_dead:
tanner_won = True
if werewolf_dead:
village_won = True
else:
if werewolf_dead:
village_won = True
else:
if minion_dead:
if werewolf_in_game:
werewolf_won = True
else:
village_won = True
else:
if minion_in_game:
werewolf_won = True
return werewolf_won, village_won, tanner_won, dead
async def result(self, werewolf_won, village_won, tanner_won, dead):
if werewolf_won:
await self.send("Werewolves won!")
if village_won:
await self.send("Village won!")
for d in dead:
if isinstance(d.day_role.copy, Tanner):
await self.send(str(p) + " won a tanner")
await self.send(":skull: " + ', '.join(str(d) for d in dead))
await self.send('\n'.join(":ballot_box " + str(p.tally) + " votes for " + str(p) + " who is " + str(p.day_role) + " (was " + str(p.night_role) + ") " for p in self.player_list))
await self.send("Middle cards: " + ', '.join(str(r) for r in self.middle_card))
# debug
await self.send("Success")
def end(self):
self.running = False
async def game(self):
try:
self.setup()
await self.set_players()
await self.set_roles()
self.distribute_roles()
await self.start_night()
await self.send_role()
await self.night_phases()
await self.start_day()
#discussion timer
options = self.player_list + [No_role(self)]
await self.vote(options)
self.tally(options)
await self.result(*self.who_won(self.who_dead(options)))
except ValueError as error:
await self.send(error)
except asyncio.TimeoutError:
await self.send("Error: I got bored waiting for your input")
finally:
self.end()
await self.send("Game ended")
bot = discord.Client()
@bot.event
async def on_ready():
print('We have logged in as {0.user}'.format(bot))
async def hello(message):
print("Hello")
await message.channel.send('Hello!:regional_indicator_a:')
print(message.mentions)
@bot.event
async def on_message(message):
global running
if message.author == bot.user:
return
if message.content.startswith('$hello'):
await hello(message)
return
if message.content.startswith('$logout'):
await bot.logout()
return
if message.content.startswith('$werewolf'):
# start (only one instance running)
if werewolf_game.running:
await message.channel.send("Sorry! A game is already running")
return
werewolf_game.running = True
werewolf_game.set_channel(message.channel)
await werewolf_game.game()
return
werewolf_game = one_night(bot)
bot.run(TOKEN)