werewolve-bot/werewolve-bot.py

496 lines
12 KiB
Python

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:
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: " + str([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: " + str([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)