Compare commits

...

9 Commits

18 changed files with 339 additions and 198 deletions

Binary file not shown.

Binary file not shown.

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

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

Binary file not shown.

Binary file not shown.

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

@ -0,0 +1,20 @@
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

@ -0,0 +1,78 @@
"""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,33 +1,30 @@
"""Has a single class: Player"""
# discord imports
import discord
class Player:
"""This (abstract) class is a template for player objects for games"""
@staticmethod
async def make(member, game):
p = Player()
@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 setRole(self, role):
self.day_role = self.night_role = role
def reset(self):
self.tally = 0
self.won = self.dead = False
def name(self):
return self.member.name
def __str__(self):
return self.name()
def swap(self, player_B):
self.day_role, player_B.day_role = player_B.day_role, self.day_role
def reset(self):
pass
def other(self):
def other_players(self):
return [p for p in self.game.player_list if p != self]
async def send_normal(self, message):
@ -45,21 +42,24 @@ class Player:
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', timeout=30.0, check=check)).content.split()
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)}")
@ -77,21 +77,3 @@ class Player:
async def get_choice(self, question, options):
await self.ask_choice(question, options)
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

@ -0,0 +1,54 @@
"""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

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

View File

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

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)

View File

@ -1,107 +1,43 @@
"""
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
from dotenv import load_dotenv
import functools
import asyncio
import discord
# discord imports
from discord.ext import commands
from werewolf_game import Game as Werewolf_Game
# 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('$w '))
PREFIX = '$w '
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.load_extension('package.developer')
bot.load_extension('package.games.werewolf.cog')
@bot.command()
async def help(ctx):
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="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)
@commands.is_owner()
async def reload(ctx, extension):
bot.reload_extension(f'package.{extension}')
async def send_embed(ctx, desc, color):
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
# checker annotations
# TODO: replace with discord.py error handling?
'''
def error_handling(command):
@functools.wraps(command)
async def wrapper(ctx, *args, **kwargs):
@ -112,30 +48,11 @@ def error_handling(command):
except asyncio.TimeoutError:
await send_wrong(ctx, "Error: I got bored waiting for your input")
return wrapper
'''
'''
@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)
# TODO: (specifig game) werewolf COG
@game.command()
@game_not_running
@ -156,45 +73,6 @@ async def minutes(ctx, i):
@error_handling
async def time(ctx):
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)