Compare commits

..

No commits in common. "812ae8a9907f9c90d9f624e7ad423d52c4677a39" and "266b2fe81f935b7bce085690cce90000ea473251" have entirely different histories.

18 changed files with 198 additions and 339 deletions

View File

@ -1,62 +0,0 @@
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):
await ctx.send("pong")
@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):
embed = discord.Embed(title=f"Village won!", color=0x00ffff)
won_emoji = ":trophy:"
dead_emoji = ":test:"
tab = "\t"
space = "<:space:705863033871663185>"
embed.add_field(name=str("Name"), value=f"{won_emoji}{space}{dead_emoji}{space}{space}{3}:ballot_box:{tab}role: werewolf{tab}(was: drunk){tab}:point_right: someone", inline=False)
await ctx.send(embed=embed)
await ctx.send(":test::skull:")
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))

View File

@ -1,20 +0,0 @@
from abc import ABC
class Game(ABC):
@classmethod
def name(cls):
return "Game"
def __init__(self, bot, channel):
self.bot = bot
self.channel = channel
self.running = False
self.player_list = []
async def round(self):
pass
async def set_players(self):
pass

View File

@ -1,78 +0,0 @@
"""Has a single class: Game_cog"""
# standard library imports
from typing import Dict, Type
# discord imports
import discord
# 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"""
def __init__(self, bot, game_cls: Type[Game]):
self.bot = bot
self.game_cls = game_cls
self.game_instances = 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 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)
async def pre_game_check(self, ctx):
return self.setup_check(ctx) and self.not_running_check(ctx)
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
async def in_game_check(self, ctx):
return self.setup_check(ctx) and self.running_check(ctx)
async def stop(self, ctx):
self.game_instances[ctx.channel].game.cancel()
await self.send_friendly(ctx, "Game canceled")

View File

@ -1,54 +0,0 @@
"""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"""
@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_game)
@werewolf.command()
async def setup(self, ctx):
"""This function creates an game instance for this channel"""
await super().setup(ctx, Werewolf_game)
@werewolf.command()
async def reset(self, ctx):
"""This function deletes the game instance for this channel"""
await super().reset(ctx)
@werewolf.command()
@commands.check(Game_cog.pre_game_check)
async def players(self, ctx):
"""registers all mentioned players for the game"""
await super().players(ctx)
@werewolf.command()
@commands.check(Game_cog.pre_game_check)
async def start(self, ctx):
"""starts a round of werewolf"""
await super().start(ctx)
@werewolf.command()
@commands.check(Game_cog.in_game_check)
async def stop(self, ctx):
"""aborts the current round of werewolf"""
await super().stop(ctx)
def setup(bot):
bot.add_cog(Werewolf_cog(bot, Werewolf_game))

View File

@ -1,39 +0,0 @@
import discord
"""Has a single class: Werewolf_player"""
# local import
from .player 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

@ -1,18 +0,0 @@
# 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)

View File

@ -1,43 +1,107 @@
"""
This is the main module of the Discord Bot
Mainly loads the Cog's and starts the bot
"""
__version__ = '0.3'
__author__ = 'Bibin Muttappillil'
# standard library imports
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
import functools
# discord imports import asyncio
import discord
from discord.ext import commands from discord.ext import commands
from werewolf_game import Game as Werewolf_Game
# Token stuff
load_dotenv() load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN') TOKEN = os.getenv('DISCORD_TOKEN')
if TOKEN is None: if TOKEN is None:
print("Missing discord token!") print("Missing discord token!")
exit(1) exit(1)
bot = commands.Bot(command_prefix=commands.when_mentioned_or('$w '))
bot.load_extension('package.developer') PREFIX = '$w '
bot.load_extension('package.games.werewolf.cog') bot = commands.Bot(command_prefix=commands.when_mentioned_or(PREFIX))
bot.remove_command('help')
@bot.event
async def on_ready():
await bot.change_presence(status=discord.Status.online, activity=discord.Game('One Night Ultimate Werewolf'))
print('We have logged in as {0.user}'.format(bot))
@bot.command() @bot.command()
@commands.is_owner() async def help(ctx):
async def reload(ctx, extension): 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)
bot.reload_extension(f'package.{extension}') embed.set_author(name="With this bot you can play One Night Ultimate Werewolf")
# 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)
# checker annotations async def send_embed(ctx, desc, color):
# TODO: replace with discord.py error handling? await ctx.send(embed=discord.Embed(description=desc, color=color))
async def send_friendly(ctx, desc):
await send_embed(ctx, desc, 0x00ff00)
async def send_wrong(ctx, desc):
await send_embed(ctx, desc, 0xff8000)
# game commands
game_instances = {}
@bot.group()
async def game(ctx):
if ctx.invoked_subcommand is None:
await send_wrong(ctx, 'Invalid sub command passed...')
@game.command()
async def setup(ctx):
if ctx.channel in game_instances:
await send_wrong(ctx, "Game already setup in this channel")
else:
game_instances[ctx.channel] = Werewolf_Game(bot, ctx.channel)
await send_friendly(ctx, "This channel can now play Werewolf")
def channel_setup(command):
@functools.wraps(command)
async def wrapper(ctx, *args, **kwargs):
if ctx.channel not in game_instances:
await send_wrong(ctx, f"No game setup yet. Use {PREFIX}game setup")
else:
await command(ctx, *args, **kwargs)
return wrapper
def game_not_running(command):
@functools.wraps(command)
@channel_setup
async def wrapper(ctx, *args, **kwargs):
if game_instances[ctx.channel].running:
await send_wrong(ctx, "Sorry! A game is already running")
else:
await command(ctx, *args, **kwargs)
return wrapper
def game_running(command):
@functools.wraps(command)
@channel_setup
async def wrapper(ctx, *args, **kwargs):
if not game_instances[ctx.channel].running:
await send_wrong(ctx, "No game is running")
else:
await command(ctx, *args, **kwargs)
return wrapper
'''
def error_handling(command): def error_handling(command):
@functools.wraps(command) @functools.wraps(command)
async def wrapper(ctx, *args, **kwargs): async def wrapper(ctx, *args, **kwargs):
@ -48,11 +112,30 @@ def error_handling(command):
except asyncio.TimeoutError: except asyncio.TimeoutError:
await send_wrong(ctx, "Error: I got bored waiting for your input") await send_wrong(ctx, "Error: I got bored waiting for your input")
return wrapper return wrapper
'''
'''
# TODO: (specifig game) werewolf COG @game.command()
@game_not_running
@error_handling
async def start(ctx):
game_instances[ctx.channel].game = bot.loop.create_task(game_instances[ctx.channel].round())
await game_instances[ctx.channel].game
@game.command()
@game_running
@channel_setup
async def stop(ctx):
game_instances[ctx.channel].game.cancel()
await send_friendly(ctx, "Game canceled")
@game.command()
@game_not_running
@error_handling
async def players(ctx):
await game_instances[ctx.channel].set_players(ctx.message)
@game.command() @game.command()
@game_not_running @game_not_running
@ -73,6 +156,45 @@ async def minutes(ctx, i):
@error_handling @error_handling
async def time(ctx): async def time(ctx):
await send_friendly(ctx, game_instances[ctx.channel].remaining_time_string()) await send_friendly(ctx, game_instances[ctx.channel].remaining_time_string())
'''
# smaller commands
@bot.command()
async def hello(ctx):
await send_friendly(ctx, f"Hello {ctx.message.author.name} :wave:")
@bot.command()
async def ping(ctx):
print("pong")
await send_friendly(ctx, "pong")
# developer commands
def developer(command):
@functools.wraps(command)
async def wrapper(ctx, *args, **kwargs):
DEV_ID = 461892912821698562
if ctx.author.id == DEV_ID:
await command(ctx, *args, **kwargs)
else:
await send_wrong(ctx, "This command is not for you!")
return wrapper
@bot.command()
@developer
async def logout(ctx):
await bot.logout()
@bot.command()
@developer
async def debug(ctx, *args):
print("DEBUG")
print(ctx.args)
print(ctx.kwargs)
bot.run(TOKEN) bot.run(TOKEN)

View File

@ -2,15 +2,11 @@ from random import shuffle
import time import time
import asyncio import asyncio
import discord import discord
from .roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role from werewolf_roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role
from .players import Player, No_player from werewolf_players import Player, No_player
class Werewolf_game: class Game:
@classmethod
def name(cls):
return "One Night Ultimate Werewolf"
def __init__(self, bot, channel): def __init__(self, bot, channel):
self.running = False self.running = False
@ -18,7 +14,7 @@ class Werewolf_game:
self.channel = channel self.channel = channel
self.player_list = [] self.player_list = []
self.role_list = [] self.role_list = []
self.discussion_time = 301 # seconds self.discussion_time = 300 # seconds
async def send(self, message): async def send(self, message):
await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff)) await self.channel.send(embed=discord.Embed(description=message, color=0x00ffff))
@ -73,7 +69,6 @@ class Werewolf_game:
await self.for_all_player(lambda p: p.send_info(f"Your role: **{p.night_role.name()}**")) await self.for_all_player(lambda p: p.send_info(f"Your role: **{p.night_role.name()}**"))
async def night_phases(self): 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 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].send_copy_info()
await self.role[Doppelganger].simulate() # slow await self.role[Doppelganger].simulate() # slow

View File

@ -1,30 +1,33 @@
"""Has a single class: Player"""
# discord imports
import discord import discord
class Player: class Player:
"""This (abstract) class is a template for player objects for games"""
@classmethod @staticmethod
async def make(cls, member, game): async def make(member, game):
p = cls() p = Player()
p.member = member p.member = member
p.dm = member.dm_channel or await member.create_dm() p.dm = member.dm_channel or await member.create_dm()
p.game = game p.game = game
return p return p
def setRole(self, role):
self.day_role = self.night_role = role
def reset(self):
self.tally = 0
self.won = self.dead = False
def name(self): def name(self):
return self.member.name return self.member.name
def __str__(self): def __str__(self):
return self.name() return self.name()
def reset(self): def swap(self, player_B):
pass self.day_role, player_B.day_role = player_B.day_role, self.day_role
def other_players(self): def other(self):
return [p for p in self.game.player_list if p != self] return [p for p in self.game.player_list if p != self]
async def send_normal(self, message): async def send_normal(self, message):
@ -42,24 +45,21 @@ class Player:
async def send_info(self, message): async def send_info(self, message):
await self.send_embed(message, 0x00ffff) await self.send_embed(message, 0x00ffff)
# TODO: refactor this function to make it understandable
async def ask_choice(self, question, options): 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))) 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}```") 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): def check_num(self, choice, N):
if not choice.isdigit(): if not choice.isdigit():
raise ValueError(f"Your choice {choice} is not a number") raise ValueError(f"Your choice {choice} is not a number")
if not 0 <= int(choice) < N: if not 0 <= int(choice) < N:
raise ValueError(f"Your choice {choice} is not in range 0 - {N-1}") 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): async def receive_choice(self, options, n_ans=1):
while True: while True:
def check(choice): def check(choice):
return choice.channel == self.dm and choice.author == self.member return choice.channel == self.dm and choice.author == self.member
choice = (await self.game.bot.wait_for('message', check=check)).content.split() choice = (await self.game.bot.wait_for('message', timeout=30.0, check=check)).content.split()
if not len(choice) == n_ans: if not len(choice) == n_ans:
await self.send_wrong(f"Please give {n_ans} numbers not {len(choice)}") await self.send_wrong(f"Please give {n_ans} numbers not {len(choice)}")
@ -77,3 +77,21 @@ class Player:
async def get_choice(self, question, options): async def get_choice(self, question, options):
await self.ask_choice(question, options) await self.ask_choice(question, options)
return (await self.receive_choice(options))[0] return (await self.receive_choice(options))[0]
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")
class No_player(Player):
def name(self):
return "no one"

View File

@ -1,6 +1,6 @@
import functools import functools
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from .players import No_player from werewolf_players import No_player
class Role: class Role:
@ -45,33 +45,28 @@ class Doppelganger(Role):
@Role.no_player @Role.no_player
async def send_copy_info(self): async def send_copy_info(self):
self.copy_role = type(self.player.other()[self.choice].day_role) self.copy_role = type(self.player.other()[self.choice].day_role)
await self.player.send_info(f"You copied: {self.copy_role.name()}") await self.send_info(f"You copied: {self.copy_role}")
@Role.no_player @Role.no_player
async def simulate(self): async def simulate(self):
if self.copy_role in [Werewolf, Mason]: if self.copy_role in [Werewolf, Mason]:
self.copy_role.add_yourself(self) self.copy_role.add_yourself(self)
elif self.copy_role in [Seer, Robber, Troublemaker, Drunk]: if self.copy_role == Werewolf:
await self.copy_role.phase(self)
if self.copy_role in [Mason, Minion]:
await self.copy_role.send_info(self)
if self.copy_role in [Seer, Robber, Troublemaker, Drunk]:
await self.copy_role.query(self) await self.copy_role.query(self)
if self.copy_role in [Robber, Troublemaker, Drunk]: if self.copy_role in [Robber, Troublemaker, Drunk]:
await self.copy_role.simulate(self) self.copy_role.simulate(self)
if self.copy_role in [Seer, Robber]: if self.copy_role in [Seer, Robber]:
await self.copy_role.send_info(self) 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 @Role.no_player
async def insomniac(self): async def insomniac(self):
if self.copy_role == Insomniac: if self.copy_role == Insomniac:
await self.copy_role.send_info(self) self.copy_role.send_info(self)
def is_role(self, cls): def is_role(self, cls):
return self.copy_role == cls return self.copy_role == cls
@ -118,10 +113,10 @@ class Seer(Role):
@Role.no_player @Role.no_player
async def send_info(self): async def send_info(self):
if self.choice < len(self.player.other()): if self.choice < len(self.player.other()):
await self.player.send_info(f"You saw: {self.player.other()[self.choice].night_role.name()}") await self.player.send_info(self.player.other()[self.choice].night_role)
else: else:
a, b = [(0, 1), (1, 2), (0, 2)][self.choice - len(self.player.other())] 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]}") await self.player.send_info(f"{self.game.middle_card[a]} {self.game.middle_card[b]}")
class Robber(Role): class Robber(Role):
@ -135,7 +130,7 @@ class Robber(Role):
@Role.no_player @Role.no_player
async def send_info(self): async def send_info(self):
await self.player.send_info(f"You robbed: {self.player.day_role.name()}") await self.player.send_info(f"You robbed: {self.player.day_role}")
class Troublemaker(Role): class Troublemaker(Role):
@ -161,7 +156,7 @@ class Drunk(Role):
class Insomniac(Role): class Insomniac(Role):
@Role.no_player @Role.no_player
async def send_info(self): async def send_info(self):
await self.player.send_info(f"You are: {self.player.day_role.name()}") await self.player.send_info(f"You are: {self.player.day_role}")
class Villager(Role): class Villager(Role):