lot's of refactoring and packaging
This commit is contained in:
parent
9cdc8d742c
commit
eff057da8c
17 changed files with 190 additions and 134 deletions
BIN
src/package/games/werewolf/__pycache__/cog.cpython-37.pyc
Normal file
BIN
src/package/games/werewolf/__pycache__/cog.cpython-37.pyc
Normal file
Binary file not shown.
BIN
src/package/games/werewolf/__pycache__/game.cpython-37.pyc
Normal file
BIN
src/package/games/werewolf/__pycache__/game.cpython-37.pyc
Normal file
Binary file not shown.
BIN
src/package/games/werewolf/__pycache__/players.cpython-37.pyc
Normal file
BIN
src/package/games/werewolf/__pycache__/players.cpython-37.pyc
Normal file
Binary file not shown.
BIN
src/package/games/werewolf/__pycache__/roles.cpython-37.pyc
Normal file
BIN
src/package/games/werewolf/__pycache__/roles.cpython-37.pyc
Normal file
Binary file not shown.
34
src/package/games/werewolf/cog.py
Normal file
34
src/package/games/werewolf/cog.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# 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):
|
||||
"""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?
|
||||
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)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Werewolf_cog(bot, None))
|
||||
219
src/package/games/werewolf/game.py
Normal file
219
src/package/games/werewolf/game.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
from random import shuffle
|
||||
import time
|
||||
import asyncio
|
||||
import discord
|
||||
from .roles import Role, Doppelganger, Werewolf, Minion, Mason, Seer, Robber, Troublemaker, Drunk, Insomniac, Tanner, Hunter, No_role
|
||||
from .players import Player, No_player
|
||||
|
||||
|
||||
class Werewolf_game:
|
||||
|
||||
name = "One Night Ultimate Werewolf"
|
||||
|
||||
def __init__(self, bot, channel):
|
||||
self.running = False
|
||||
self.bot = bot
|
||||
self.channel = channel
|
||||
self.player_list = []
|
||||
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 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
|
||||
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:
|
||||
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):
|
||||
self.role = dict()
|
||||
# setting default value
|
||||
for r in Role.__subclasses__():
|
||||
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)
|
||||
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)
|
||||
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*"))
|
||||
|
||||
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} minute(s) and {t%60} second(s)"
|
||||
|
||||
async def discussion_timer(self):
|
||||
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)
|
||||
dead = [p for p in self.player_list if p.tally >= maxi]
|
||||
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")
|
||||
if len(winnning) == 0:
|
||||
winnning = ["No one"]
|
||||
|
||||
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)
|
||||
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()
|
||||
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()))
|
||||
await self.send("Round ended")
|
||||
finally:
|
||||
self.end()
|
||||
98
src/package/games/werewolf/players.py
Normal file
98
src/package/games/werewolf/players.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import discord
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 other(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)
|
||||
|
||||
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}")
|
||||
|
||||
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]
|
||||
|
||||
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"
|
||||
180
src/package/games/werewolf/roles.py
Normal file
180
src/package/games/werewolf/roles.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue