Compare commits

...

2 Commits

8 changed files with 216 additions and 64 deletions

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?

View File

@ -41,15 +41,6 @@ class Developer(commands.Cog):
@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)

View File

@ -1,20 +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 round(self):
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
async def set_players(self):
def setup(self):
pass
def end(self):
self.running = False
async def round(self):
pass

View File

@ -1,10 +1,11 @@
"""Has a single class: Game_cog"""
# standard library imports
from typing import Dict, Type
from typing import Dict
# discord imports
import discord
from discord.ext import commands
# local imports
from ..send_message import Send_message
@ -14,10 +15,11 @@ 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]):
game_cls = Game
def __init__(self, bot):
self.bot = bot
self.game_cls = game_cls
self.game_instances = Dict[discord.TextChannel, self.game_cls]
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:
@ -44,7 +46,7 @@ class Game_cog(Send_message):
async def reset(self, ctx):
"""This function deletes the game instance for this channel"""
if self.setup_check(ctx):
if await self.setup_check(ctx):
del self.game_instances[ctx.channel]
# TODO: better info message
@ -60,8 +62,11 @@ class Game_cog(Send_message):
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)
# 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)
@ -70,8 +75,10 @@ class Game_cog(Send_message):
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)
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()

View File

@ -11,6 +11,8 @@ 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()?
@ -19,12 +21,12 @@ class Werewolf_cog(Game_cog, commands.Cog):
@werewolf.command()
async def info(self, ctx):
"""Send information about the subcommands for the game"""
await super().info(ctx, Werewolf_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_game)
await super().setup(ctx)
@werewolf.command()
async def reset(self, ctx):
@ -32,41 +34,42 @@ class Werewolf_cog(Game_cog, commands.Cog):
await super().reset(ctx)
@werewolf.command()
@commands.check(Game_cog.pre_game_check)
@Game_cog.pre_game()
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)
@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()
@commands.check(Game_cog.pre_game_check)
async def minutes(self, ctx, i):
@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()
@commands.check(Game_cog.pre_game_check)
@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()
@commands.check(Game_cog.in_game_check)
@Game_cog.in_game()
async def stop(self, ctx):
"""aborts the current round of werewolf"""
await super().stop(ctx)
@werewolf.command()
@commands.check(Game_cog.in_game_check)
@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, Werewolf_game))
bot.add_cog(Werewolf_cog(bot))

View File

@ -1,53 +1,54 @@
"""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:
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):
self.running = False
self.bot = bot
self.channel = channel
self.player_list = []
super().__init__(bot, channel)
self.role_list = []
self.discussion_time = 301 # seconds
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 Werewolf_player.make(mem, self) for mem in msg.mentions]
await self.send(f"Players: {', '.join(p.name() for p in self.player_list)}") # send confirmation
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, msg):
self.discussion_time = int(msg) * 60 + 1
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):
if not 0 <= len(self.player_list) <= 10:
# 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]:
@ -60,14 +61,15 @@ class Werewolf_game:
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].setRole(role_obj)
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("*The night has begun*"))
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()}**"))
@ -91,9 +93,10 @@ class Werewolf_game:
def remaining_time_string(self):
t = int(self.start_time + self.discussion_time - time.time())
return f"{t//60} minute(s) and {t%60} second(s)"
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)
@ -122,8 +125,8 @@ class Werewolf_game:
p.vote.tally += 1
def who_dead(self):
maxi = max(c.tally for c in self.voting_list)
dead = [p for p in self.player_list if p.tally >= maxi]
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):
@ -152,10 +155,8 @@ class Werewolf_game:
else:
if tanner_dead:
tanner_won = True
if werewolf_dead:
village_won = True
else:
if werewolf_dead:
village_won = True
@ -186,22 +187,29 @@ class Werewolf_game:
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")
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 ":no_mouth:"
embed.add_field(name=str(p), value=f"{won_emoji} {dead_emoji} {p.tally}:ballot_box: role: {str(p.day_role)} (was: {str(p.night_role)}) :point_right: {str(p.vote)}", inline=False)
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)
def end(self):
self.running = False
async def round(self):
try:
self.check()
@ -216,6 +224,5 @@ class Werewolf_game:
await self.vote()
self.tally()
await self.result(*self.who_won(self.who_dead()))
await self.send("Round ended")
finally:
self.end()

View File

@ -4,7 +4,7 @@ This is the main module of the Discord Bot
Mainly loads the Cog's and starts the bot
"""
__version__ = '0.3'
__version__ = '0.4'
__author__ = 'Bibin Muttappillil'
# standard library imports
@ -22,7 +22,7 @@ if TOKEN is None:
print("Missing discord token!")
exit(1)
bot = commands.Bot(command_prefix=commands.when_mentioned_or('$w '))
bot = commands.Bot(command_prefix=commands.when_mentioned_or('$b '))
bot.load_extension('package.developer')
bot.load_extension('package.games.werewolf.cog')
@ -32,6 +32,7 @@ bot.load_extension('package.games.werewolf.cog')
@commands.is_owner()
async def reload(ctx, extension):
bot.reload_extension(f'package.{extension}')
print(f'package.{extension} reloaded')
# checker annotations