banned/src/gamemodes.py
jacob1 268129bd9d fix potential recursion error during maelstrom
when calling chk_win_conditions, maelstrom uses a rolemap where all players are usually '0', this is casted to a set which makes it think there is only one player
2017-02-23 23:43:32 -05:00

1339 lines
68 KiB
Python

import random
import math
import threading
import copy
from datetime import datetime
from collections import defaultdict, OrderedDict
import botconfig
import src.settings as var
from src.utilities import *
from src.messages import messages
from src.decorators import handle_error
from src import events, channels, users
def game_mode(name, minp, maxp, likelihood = 0):
def decor(c):
c.name = name
var.GAME_MODES[name] = (c, minp, maxp, likelihood)
return c
return decor
reset_roles = lambda i: OrderedDict([(role, (0,) * len(i)) for role in var.ROLE_GUIDE])
def get_lovers():
lovers = []
pl = list_players()
for lover in var.LOVERS:
done = None
for i, lset in enumerate(lovers):
if lover in pl and lover in lset:
if done is not None: # plot twist! two clusters turn out to be linked!
done.update(lset)
for lvr in var.LOVERS[lover]:
if lvr in pl:
done.add(lvr)
lset.clear()
continue
for lvr in var.LOVERS[lover]:
if lvr in pl:
lset.add(lvr)
done = lset
if done is None and lover in pl:
lovers.append(set())
lovers[-1].add(lover)
for lvr in var.LOVERS[lover]:
if lvr in pl:
lovers[-1].add(lvr)
while set() in lovers:
lovers.remove(set())
return lovers
class GameMode:
def __init__(self, arg=""):
if not arg:
return
arg = arg.replace("=", ":").replace(";", ",")
pairs = [arg]
while pairs:
pair, *pairs = pairs[0].split(",", 1)
change = pair.lower().replace(":", " ").strip().rsplit(None, 1)
if len(change) != 2:
raise InvalidModeException(messages["invalid_mode_args"].format(arg))
key, val = change
if key in ("role reveal", "reveal roles"):
if val not in ("on", "off", "team"):
raise InvalidModeException(messages["invalid_reveal"].format(val))
self.ROLE_REVEAL = val
if val == "off" and not hasattr(self, "STATS_TYPE"):
self.STATS_TYPE = "disabled"
elif val == "team" and not hasattr(self, "STATS_TYPE"):
self.STATS_TYPE = "team"
elif key in ("stats type", "stats"):
if val not in ("default", "accurate", "team", "disabled"):
raise InvalidModeException(messages["invalid_stats"].format(val))
self.STATS_TYPE = val
elif key == "abstain":
if val not in ("enabled", "restricted", "disabled"):
raise InvalidModeException(messages["invalid_abstain"].format(val))
if val == "enabled":
self.ABSTAIN_ENABLED = True
self.LIMIT_ABSTAIN = False
elif val == "restricted":
self.ABSTAIN_ENABLED = True
self.LIMIT_ABSTAIN = True
elif val == "disabled":
self.ABSTAIN_ENABLED = False
elif key == "lover wins with fool":
if val not in ("true", "false"):
raise InvalidModeException(messages["invalid_lover_wins_with_fool"].format(val))
self.LOVER_WINS_WITH_FOOL = True if val == "true" else False
def startup(self):
pass
def teardown(self):
pass
# Here so any game mode can use it
def lovers_chk_win(self, evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
winner = evt.data["winner"]
if winner is not None and winner.startswith("@"):
return # fool won, lovers can't win even if they would
all_lovers = get_lovers()
if len(all_lovers) != 1:
return # we need exactly one cluster alive for this to trigger
lovers = all_lovers[0]
if len(lovers) == lpl:
evt.data["winner"] = "lovers"
evt.data["additional_winners"] = list(lovers)
evt.data["message"] = messages["lovers_win"]
def all_dead_chk_win(self, evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
if evt.data["winner"] == "no_team_wins":
evt.data["winner"] = "everyone"
evt.data["message"] = messages["everyone_died_won"]
@game_mode("roles", minp = 4, maxp = 35)
class ChangedRolesMode(GameMode):
"""Example: !fgame roles=wolf:1,seer:0,guardian angel:1"""
def __init__(self, arg=""):
super().__init__(arg)
self.MAX_PLAYERS = 35
self.ROLE_GUIDE = var.ROLE_GUIDE.copy()
self.ROLE_INDEX = (var.MIN_PLAYERS,)
arg = arg.replace("=", ":").replace(";", ",")
for role in self.ROLE_GUIDE.keys():
self.ROLE_GUIDE[role] = (0,)
pairs = [arg]
while pairs:
pair, *pairs = pairs[0].split(",", 1)
change = pair.replace(":", " ").strip().rsplit(None, 1)
if len(change) != 2:
raise InvalidModeException(messages["invalid_mode_roles"].format(arg))
role, num = change
try:
if role.lower() in var.DISABLED_ROLES:
raise InvalidModeException(messages["role_disabled"].format(role))
elif role.lower() in self.ROLE_GUIDE:
self.ROLE_GUIDE[role.lower()] = tuple([int(num)] * len(var.ROLE_INDEX))
elif role.lower() == "default" and num.lower() in self.ROLE_GUIDE:
self.DEFAULT_ROLE = num.lower()
elif role.lower() in ("role reveal", "reveal roles", "stats type", "stats", "abstain", "lover wins with fool"):
# handled in parent constructor
pass
else:
raise InvalidModeException(messages["specific_invalid_role"].format(role))
except ValueError:
raise InvalidModeException(messages["bad_role_value"])
@game_mode("default", minp = 4, maxp = 24, likelihood = 20)
class DefaultMode(GameMode):
"""Default game mode."""
def __init__(self, arg="", role_index=var.ROLE_INDEX, role_guide=var.ROLE_GUIDE.copy()):
# No extra settings, just an explicit way to revert to default settings
super().__init__(arg)
self.ROLE_INDEX = role_index
self.ROLE_GUIDE = role_guide
@game_mode("villagergame", minp = 4, maxp = 9, likelihood = 0)
class VillagergameMode(GameMode):
"""This mode definitely does not exist, now please go away."""
def __init__(self, arg=""):
super().__init__(arg)
self.fake_index = var.ROLE_INDEX
self.fake_guide = var.ROLE_GUIDE.copy()
self.ROLE_INDEX = ( 4 , 6 , 7 , 8 , 9 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({
"seer" : ( 1 , 1 , 1 , 1 , 1 ),
"shaman" : ( 0 , 0 , 1 , 1 , 1 ),
"harlot" : ( 0 , 0 , 0 , 1 , 1 ),
"crazed shaman" : ( 0 , 0 , 0 , 0 , 1 ),
"cursed villager" : ( 0 , 1 , 1 , 1 , 1 ),
})
def startup(self):
events.add_listener("chk_win", self.chk_win)
events.add_listener("chk_nightdone", self.chk_nightdone)
events.add_listener("transition_day_begin", self.transition_day)
events.add_listener("retribution_kill", self.on_retribution_kill, priority=4)
def teardown(self):
events.remove_listener("chk_win", self.chk_win)
events.remove_listener("chk_nightdone", self.chk_nightdone)
events.remove_listener("transition_day_begin", self.transition_day)
events.remove_listener("retribution_kill", self.on_retribution_kill, priority=4)
def chk_win(self, evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
# village can only win via unanimous vote on the bot nick
# villagergame_lose should probably explain that mechanic
# Note: not implemented here since that needs to work in default too
pc = len(var.ALL_PLAYERS)
if (pc >= 8 and lpl <= 4) or lpl <= 2:
evt.data["winner"] = ""
evt.data["message"] = messages["villagergame_lose"].format(botconfig.CMD_CHAR, botconfig.NICK)
else:
evt.data["winner"] = None
def chk_nightdone(self, evt, cli, var):
transition_day = evt.data["transition_day"]
evt.data["transition_day"] = lambda cli2, gameid=0: self.prolong_night(cli2, var, gameid, transition_day)
def prolong_night(self, cli, var, gameid, transition_day):
nspecials = len(var.ROLES["seer"] | var.ROLES["harlot"] | var.ROLES["shaman"] | var.ROLES["crazed shaman"])
rand = random.gauss(5, 3)
if rand <= 0 and nspecials > 0:
transition_day(cli, gameid=gameid)
else:
t = threading.Timer(abs(rand), transition_day, args=(cli,), kwargs={"gameid": gameid})
t.start()
def transition_day(self, evt, cli, var):
# 30% chance we kill a safe, otherwise kill at random
# when killing safes, go after seer, then harlot, then shaman
self.delaying_night = False
pl = list_players()
tgt = None
seer = None
hlt = None
hvst = None
shmn = None
if len(var.ROLES["seer"]) == 1:
seer = list(var.ROLES["seer"])[0]
if len(var.ROLES["harlot"]) == 1:
hlt = list(var.ROLES["harlot"])[0]
hvst = var.HVISITED.get(hlt)
if hvst:
pl.remove(hlt)
if len(var.ROLES["shaman"]) == 1:
shmn = list(var.ROLES["shaman"])[0]
if random.random() < 0.3:
if seer:
tgt = seer
elif hvst:
tgt = hvst
elif shmn:
tgt = shmn
elif hlt and not hvst:
tgt = hlt
if not tgt:
tgt = random.choice(pl)
from src.roles import wolf
wolf.KILLS[botconfig.NICK] = [tgt]
def on_retribution_kill(self, evt, cli, var, victim, orig_target):
# There are no wolves for this totem to kill
if orig_target == "@wolves":
evt.data["target"] = None
evt.stop_processing = True
@game_mode("foolish", minp = 8, maxp = 24, likelihood = 8)
class FoolishMode(GameMode):
"""Contains the fool, be careful not to lynch them!"""
def __init__(self, arg=""):
super().__init__(arg)
self.ROLE_INDEX = ( 8 , 9 , 10 , 11 , 12 , 15 , 17 , 20 , 21 , 22 , 24 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"oracle" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"harlot" : ( 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 2 ),
"bodyguard" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
"augur" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
"hunter" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"shaman" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# wolf roles
"wolf" : ( 1 , 1 , 2 , 2 , 2 , 2 , 3 , 3 , 3 , 3 , 4 ),
"traitor" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 ),
"wolf cub" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"sorcerer" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
# neutral roles
"clone" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"fool" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"gunner" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
"sharpshooter" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"mayor" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
})
@game_mode("mad", minp = 7, maxp = 22, likelihood = 4)
class MadMode(GameMode):
"""This game mode has mad scientist and many things that may kill you."""
def __init__(self, arg=""):
super().__init__(arg)
# gunner and sharpshooter always get 1 bullet
self.SHOTS_MULTIPLIER = 0.0001
self.SHARPSHOOTER_MULTIPLIER = 0.0001
self.ROLE_INDEX = ( 7 , 8 , 10 , 12 , 14 , 15 , 17 , 18 , 20 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"mad scientist" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"detective" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
"guardian angel" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ),
"hunter" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 ),
"harlot" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
"village drunk" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 2 ),
"traitor" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"werecrow" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"wolf cub" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 2 ),
"cultist" : ( 1 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
# neutral roles
"vengeful ghost" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
"jester" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"gunner" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"sharpshooter" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"assassin" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
})
@game_mode("evilvillage", minp = 6, maxp = 18, likelihood = 1)
class EvilVillageMode(GameMode):
"""Majority of the village is wolf aligned, safes must secretly try to kill the wolves."""
def __init__(self, arg=""):
self.ABSTAIN_ENABLED = False
super().__init__(arg)
self.DEFAULT_ROLE = "cultist"
self.DEFAULT_SEEN_AS_VILL = False
self.ROLE_INDEX = ( 6 , 8 , 10 , 12 , 15 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 0 , 1 , 1 , 1 , 1 ),
"guardian angel" : ( 0 , 0 , 1 , 1 , 1 ),
"shaman" : ( 0 , 0 , 0 , 1 , 1 ),
"hunter" : ( 1 , 1 , 1 , 1 , 2 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 1 , 2 ),
"minion" : ( 0 , 0 , 1 , 1 , 1 ),
# neutral roles
"fool" : ( 0 , 0 , 1 , 1 , 1 ),
})
def startup(self):
events.add_listener("chk_win", self.chk_win)
def teardown(self):
events.remove_listener("chk_win", self.chk_win)
def chk_win(self, evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
lsafes = len(list_players(["oracle", "seer", "guardian angel", "shaman", "hunter", "villager"]))
lcultists = len(list_players(["cultist"]))
evt.stop_processing = True
if lrealwolves == 0 and lsafes == 0:
evt.data["winner"] = "no_team_wins"
evt.data["message"] = messages["evil_no_win"]
elif lrealwolves == 0:
evt.data["winner"] = "villagers"
evt.data["message"] = messages["evil_villager_win"]
elif lsafes == 0:
evt.data["winner"] = "wolves"
evt.data["message"] = messages["evil_wolf_win"]
elif lcultists == 0:
evt.data["winner"] = "villagers"
evt.data["message"] = messages["evil_cultists_dead"]
elif lsafes == lpl / 2:
evt.data["winner"] = "villagers"
evt.data["message"] = messages["evil_villager_tie"]
elif lsafes > lpl / 2:
evt.data["winner"] = "villagers"
evt.data["message"] = messages["evil_more_villagers"]
else:
try:
if evt.data["winner"][0] != "@":
evt.data["winner"] = None
except TypeError:
evt.data["winner"] = None
@game_mode("classic", minp = 4, maxp = 21, likelihood = 0)
class ClassicMode(GameMode):
"""Classic game mode from before all the changes."""
def __init__(self, arg=""):
super().__init__(arg)
self.ABSTAIN_ENABLED = False
self.ROLE_INDEX = ( 4 , 6 , 8 , 10 , 12 , 15 , 17 , 18 , 20 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"village drunk" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"harlot" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"bodyguard" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
"detective" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 2 , 2 , 3 , 3 , 3 , 4 ),
"traitor" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"werecrow" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
"gunner" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
})
@game_mode("rapidfire", minp = 6, maxp = 24, likelihood = 0)
class RapidFireMode(GameMode):
"""Many roles that lead to multiple chain deaths."""
def __init__(self, arg=""):
super().__init__(arg)
self.SHARPSHOOTER_CHANCE = 1
self.DAY_TIME_LIMIT = 480
self.DAY_TIME_WARN = 360
self.SHORT_DAY_LIMIT = 240
self.SHORT_DAY_WARN = 180
self.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS = 0
self.ROLE_INDEX = ( 6 , 8 , 10 , 12 , 15 , 18 , 22 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"mad scientist" : ( 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
"matchmaker" : ( 0 , 0 , 1 , 1 , 1 , 1 , 2 ),
"hunter" : ( 0 , 1 , 1 , 1 , 1 , 2 , 2 ),
"augur" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
"time lord" : ( 0 , 0 , 1 , 1 , 1 , 2 , 2 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 2 , 2 , 3 , 4 ),
"wolf cub" : ( 0 , 1 , 1 , 1 , 2 , 2 , 2 ),
"traitor" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
# neutral roles
"vengeful ghost" : ( 0 , 0 , 0 , 1 , 1 , 1 , 2 ),
"amnesiac" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
"assassin" : ( 0 , 1 , 1 , 1 , 2 , 2 , 2 ),
"gunner" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
"sharpshooter" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
})
def startup(self):
events.add_listener("chk_win", self.all_dead_chk_win)
def teardown(self):
events.remove_listener("chk_win", self.all_dead_chk_win)
@game_mode("drunkfire", minp = 8, maxp = 17, likelihood = 0)
class DrunkFireMode(GameMode):
"""Most players get a gun, quickly shoot all the wolves!"""
def __init__(self, arg=""):
super().__init__(arg)
self.SHARPSHOOTER_CHANCE = 1
self.DAY_TIME_LIMIT = 480
self.DAY_TIME_WARN = 360
self.SHORT_DAY_LIMIT = 240
self.SHORT_DAY_WARN = 180
self.NIGHT_TIME_LIMIT = 60
self.NIGHT_TIME_WARN = 40 # HIT MISS SUICIDE HEADSHOT
self.GUN_CHANCES = ( 3/7 , 3/7 , 1/7 , 4/5 )
self.WOLF_GUN_CHANCES = ( 4/7 , 3/7 , 0/7 , 1 )
self.ROLE_INDEX = ( 8 , 10 , 12 , 14 , 16 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 1 , 1 , 1 , 2 , 2 ),
"village drunk" : ( 2 , 3 , 4 , 4 , 5 ),
# wolf roles
"wolf" : ( 1 , 2 , 2 , 3 , 3 ),
"traitor" : ( 1 , 1 , 1 , 1 , 2 ),
"hag" : ( 0 , 0 , 1 , 1 , 1 ),
# neutral roles
"crazed shaman" : ( 0 , 0 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 1 , 1 , 1 , 1 , 1 ),
"assassin" : ( 0 , 0 , 0 , 1 , 1 ),
"gunner" : ( 5 , 6 , 7 , 8 , 9 ),
"sharpshooter" : ( 2 , 2 , 3 , 3 , 4 ),
})
def startup(self):
events.add_listener("chk_win", self.all_dead_chk_win)
def teardown(self):
events.remove_listener("chk_win", self.all_dead_chk_win)
@game_mode("noreveal", minp = 4, maxp = 21, likelihood = 2)
class NoRevealMode(GameMode):
"""Roles are not revealed when players die."""
def __init__(self, arg=""):
self.ROLE_REVEAL = "off"
self.STATS_TYPE = "disabled"
super().__init__(arg)
self.ROLE_INDEX = ( 4 , 6 , 8 , 10 , 12 , 15 , 17 , 19 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"guardian angel" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
"mystic" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
"detective" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
"hunter" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 1 , 2 , 2 , 2 , 3 ),
"wolf mystic" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
"traitor" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
"werecrow" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
# neutral roles
"clone" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
"lycan" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 ),
"amnesiac" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 ),
# templates
"cursed villager" : ( 0 , 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
})
@game_mode("lycan", minp = 7, maxp = 21, likelihood = 6)
class LycanMode(GameMode):
"""Many lycans will turn into wolves. Hunt them down before the wolves overpower the village."""
def __init__(self, arg=""):
super().__init__(arg)
self.ROLE_INDEX = ( 7 , 8 , 9 , 10 , 11 , 12 , 15 , 17 , 19 , 20 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
"bodyguard" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
"matchmaker" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
"hunter" : ( 1 , 1 , 1 , 2 , 2 , 2 , 2 , 2 , 2 , 2 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"wolf shaman" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"traitor" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# neutral roles
"clone" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 ),
"lycan" : ( 1 , 1 , 1 , 2 , 2 , 3 , 4 , 4 , 4 , 5 ),
# templates
"cursed villager" : ( 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 2 ),
"gunner" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
"sharpshooter" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
"mayor" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
})
@game_mode("valentines", minp = 8, maxp = 24, likelihood = 0)
class MatchmakerMode(GameMode):
"""Love is in the air!"""
def __init__(self, arg=""):
super().__init__(arg)
self.NIGHT_TIME_LIMIT = 150
self.NIGHT_TIME_WARN = 105
self.ROLE_INDEX = range(8, 25)
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({
"wolf" : [math.ceil((i ** 1.4) * 0.06) for i in self.ROLE_INDEX],
"matchmaker" : [i - math.ceil((i ** 1.4) * 0.06) - (i >= 12) - (i >= 18) for i in self.ROLE_INDEX],
"monster" : [i >= 12 for i in self.ROLE_INDEX],
"mad scientist" : [i >= 18 for i in self.ROLE_INDEX],
})
def startup(self):
events.add_listener("chk_win", self.lovers_chk_win)
def teardown(self):
events.remove_listener("chk_win", self.lovers_chk_win)
@game_mode("random", minp = 8, maxp = 24, likelihood = 0)
class RandomMode(GameMode):
"""Completely random and hidden roles."""
def __init__(self, arg=""):
self.ROLE_REVEAL = random.choice(("on", "off", "team"))
self.STATS_TYPE = "disabled" if self.ROLE_REVEAL == "off" else random.choice(("disabled", "team"))
super().__init__(arg)
self.LOVER_WINS_WITH_FOOL = True
self.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS = 0 # always make it happen
self.TEMPLATE_RESTRICTIONS = OrderedDict((template, frozenset()) for template in var.TEMPLATE_RESTRICTIONS)
self.TOTEM_CHANCES = { # shaman , crazed , wolf
"death": ( 8 , 1 , 1 ),
"protection": ( 6 , 1 , 6 ),
"silence": ( 4 , 1 , 3 ),
"revealing": ( 2 , 1 , 5 ),
"desperation": ( 4 , 1 , 7 ),
"impatience": ( 7 , 1 , 2 ),
"pacifism": ( 7 , 1 , 2 ),
"influence": ( 7 , 1 , 2 ),
"narcolepsy": ( 4 , 1 , 3 ),
"exchange": ( 1 , 1 , 1 ),
"lycanthropy": ( 1 , 1 , 3 ),
"luck": ( 6 , 1 , 7 ),
"pestilence": ( 3 , 1 , 1 ),
"retribution": ( 5 , 1 , 6 ),
"misdirection": ( 6 , 1 , 4 ),
"deceit": ( 3 , 1 , 6 ),
}
def startup(self):
events.add_listener("role_attribution", self.role_attribution)
events.add_listener("chk_win", self.lovers_chk_win)
def teardown(self):
events.remove_listener("role_attribution", self.role_attribution)
events.remove_listener("chk_win", self.lovers_chk_win)
def role_attribution(self, evt, cli, var, chk_win_conditions, villagers):
lpl = len(villagers) - 1
addroles = evt.data["addroles"]
for role in var.ROLE_GUIDE:
addroles[role] = 0
wolves = var.WOLF_ROLES - {"wolf cub"}
addroles[random.choice(list(wolves))] += 1 # make sure there's at least one wolf role
roles = list(var.ROLE_GUIDE.keys() - var.TEMPLATE_RESTRICTIONS.keys() - {"villager", "cultist", "amnesiac"})
while lpl:
addroles[random.choice(roles)] += 1
lpl -= 1
addroles["gunner"] = random.randrange(int(len(villagers) ** 1.2 / 4))
addroles["assassin"] = random.randrange(max(int(len(villagers) ** 1.2 / 8), 1))
rolemap = defaultdict(set)
for r,c in addroles.items():
if c > 0:
rolemap[r] = set(range(c))
if chk_win_conditions(cli, rolemap, end_game=False):
return self.role_attribution(evt, cli, var, chk_win_conditions, villagers)
evt.prevent_default = True
# Credits to Metacity for designing and current name
# Blame arkiwitect for the original name of KrabbyPatty
@game_mode("aleatoire", minp = 8, maxp = 24, likelihood = 4)
class AleatoireMode(GameMode):
"""Game mode created by Metacity and balanced by woffle."""
def __init__(self, arg=""):
super().__init__(arg)
self.SHARPSHOOTER_CHANCE = 1
# SHAMAN , CRAZED SHAMAN , WOLF SHAMAN
self.TOTEM_CHANCES = { "death": ( 4 , 1 , 0 ),
"protection": ( 8 , 1 , 0 ),
"silence": ( 2 , 1 , 0 ),
"revealing": ( 0 , 1 , 0 ),
"desperation": ( 1 , 1 , 0 ),
"impatience": ( 0 , 1 , 0 ),
"pacifism": ( 0 , 1 , 0 ),
"influence": ( 0 , 1 , 0 ),
"narcolepsy": ( 0 , 1 , 0 ),
"exchange": ( 0 , 1 , 0 ),
"lycanthropy": ( 0 , 1 , 0 ),
"luck": ( 0 , 1 , 0 ),
"pestilence": ( 1 , 1 , 0 ),
"retribution": ( 4 , 1 , 0 ),
"misdirection": ( 0 , 1 , 0 ),
"deceit": ( 0 , 1 , 0 ),
}
# get default values for wolf shaman's chances
for totem, (s, cs, ws) in self.TOTEM_CHANCES.items():
self.TOTEM_CHANCES[totem] = (s, cs, var.TOTEM_CHANCES[totem][2])
self.ROLE_INDEX = ( 8 , 10 , 12 , 13 , 14 , 15 , 17 , 18 , 21 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({ # village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"shaman" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"matchmaker" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"hunter" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
"augur" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
"time lord" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ),
"guardian angel" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# wolf roles
"wolf" : ( 1 , 2 , 2 , 2 , 2 , 2 , 3 , 3 , 3 ),
"wolf cub" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ),
"traitor" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"werecrow" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
"hag" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# neutral roles
"vengeful ghost" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
"amnesiac" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"turncoat" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 ),
"assassin" : ( 0 , 1 , 1 , 2 , 2 , 2 , 2 , 2 , 2 ),
"gunner" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"mayor" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
})
@game_mode("alpha", minp=10, maxp=24, likelihood=5)
class AlphaMode(GameMode):
"""Features the alpha wolf who can turn other people into wolves, be careful whom you trust!"""
def __init__(self, arg=""):
super().__init__(arg)
self.ROLE_INDEX = ( 10 , 12 , 13 , 14 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 24 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({
#village roles
"oracle" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"doctor" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"harlot" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"guardian angel" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 ),
"matchmaker" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"augur" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"vigilante" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
# wolf roles
"wolf" : ( 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 3 , 4 ),
"alpha wolf" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"traitor" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"werecrow" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# neutral roles
"amnesiac" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"lycan" : ( 2 , 2 , 2 , 2 , 3 , 3 , 3 , 3 , 4 , 4 , 4 , 4 ),
"crazed shaman" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"clone" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 1 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 2 , 2 , 3 , 3 ),
"mayor" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"assassin" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
})
# original idea by Rossweisse, implemented by Vgr with help from woffle and jacob1
@game_mode("guardian", minp = 8, maxp = 16, likelihood = 0)
class GuardianMode(GameMode):
"""Game mode full of guardian angels, wolves need to pick them apart!"""
def __init__(self, arg=""):
self.LIMIT_ABSTAIN = False
super().__init__(arg)
self.ROLE_INDEX = ( 8 , 10 , 12 , 13 , 15 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({
# village roles
"bodyguard" : ( 0 , 0 , 0 , 0 , 1 ),
"guardian angel" : ( 1 , 1 , 2 , 2 , 2 ),
"shaman" : ( 0 , 1 , 1 , 1 , 1 ),
"seer" : ( 1 , 1 , 1 , 1 , 1 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 1 , 2 ),
"werecrow" : ( 0 , 1 , 1 , 1 , 1 ),
"werekitten" : ( 1 , 1 , 1 , 1 , 1 ),
"alpha wolf" : ( 0 , 0 , 1 , 1 , 1 ),
# neutral roles
"jester" : ( 0 , 0 , 0 , 1 , 1 ),
# templates
"gunner" : ( 0 , 0 , 0 , 1 , 1 ),
"cursed villager" : ( 1 , 1 , 2 , 2 , 2 ),
})
self.TOTEM_CHANCES = { # shaman , crazed , wolf
"death": ( 4 , 1 , 0 ),
"protection": ( 8 , 1 , 0 ),
"silence": ( 2 , 1 , 0 ),
"revealing": ( 0 , 1 , 0 ),
"desperation": ( 0 , 1 , 0 ),
"impatience": ( 0 , 1 , 0 ),
"pacifism": ( 0 , 1 , 0 ),
"influence": ( 0 , 1 , 0 ),
"narcolepsy": ( 0 , 1 , 0 ),
"exchange": ( 0 , 1 , 0 ),
"lycanthropy": ( 0 , 1 , 0 ),
"luck": ( 3 , 1 , 0 ),
"pestilence": ( 0 , 1 , 0 ),
"retribution": ( 6 , 1 , 0 ),
"misdirection": ( 4 , 1 , 0 ),
"deceit": ( 0 , 1 , 0 ),
}
for totem, (s, cs, ws) in self.TOTEM_CHANCES.items():
self.TOTEM_CHANCES[totem] = (s, cs, var.TOTEM_CHANCES[totem][2])
def startup(self):
events.add_listener("chk_win", self.chk_win)
def teardown(self):
events.remove_listener("chk_win", self.chk_win)
def chk_win(self, evt, cli, var, rolemap, lpl, lwolves, lrealwolves):
lguardians = len(list_players(["guardian angel", "bodyguard"]))
if lpl < 1:
# handled by default win cond checking
return
elif not lguardians and lwolves > lpl / 2:
evt.data["winner"] = "wolves"
evt.data["message"] = messages["guardian_wolf_win"]
elif not lguardians and lwolves == lpl / 2:
evt.data["winner"] = "wolves"
evt.data["message"] = messages["guardian_wolf_tie_no_guards"]
elif not lrealwolves and lguardians:
evt.data["winner"] = "villagers"
evt.data["message"] = messages["guardian_villager_win"]
elif not lrealwolves and not lguardians:
evt.data["winner"] = "villagers"
evt.data["message"] = messages["guardian_lose_no_guards"]
elif lwolves == lguardians and lpl - lwolves - lguardians == 0:
evt.data["winner"] = "wolves"
evt.data["message"] = messages["guardian_lose_with_guards"]
else:
evt.data["winner"] = None
@game_mode("charming", minp = 6, maxp = 24, likelihood = 4)
class CharmingMode(GameMode):
"""Charmed players must band together to find the piper in this game mode."""
def __init__(self, arg=""):
super().__init__(arg)
self.ROLE_INDEX = ( 6 , 8 , 10 , 11 , 12 , 14 , 16 , 18 , 19 , 22 , 24 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({# village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"harlot" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"shaman" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 ),
"detective" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"bodyguard" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 2 , 2 , 2 , 2 ),
# wolf roles
"wolf" : ( 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 , 3 , 3 ),
"traitor" : ( 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"werekitten" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"warlock" : ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"sorcerer" : ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ),
# neutral roles
"piper" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"vengeful ghost" : ( 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
# templates
"cursed villager" : ( 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
"gunner" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 ),
"sharpshooter" : ( 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 ),
"mayor" : ( 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ),
"assassin" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
})
@game_mode("sleepy", minp=8, maxp=24, likelihood=5)
class SleepyMode(GameMode):
"""A small village has become the playing ground for all sorts of supernatural beings."""
def __init__(self, arg=""):
super().__init__(arg)
self.ROLE_INDEX = ( 8 , 10 , 12 , 15 , 18 , 21 )
self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX)
self.ROLE_GUIDE.update({
# village roles
"seer" : ( 1 , 1 , 1 , 1 , 1 , 1 ),
"priest" : ( 0 , 1 , 1 , 1 , 1 , 1 ),
"harlot" : ( 0 , 0 , 0 , 0 , 1 , 1 ),
"detective" : ( 0 , 0 , 0 , 1 , 1 , 1 ),
"vigilante" : ( 0 , 0 , 1 , 1 , 1 , 1 ),
"village drunk" : ( 0 , 0 , 0 , 0 , 0 , 1 ),
# wolf roles
"wolf" : ( 1 , 1 , 2 , 3 , 4 , 5 ),
"werecrow" : ( 0 , 1 , 1 , 1 , 1 , 1 ),
"traitor" : ( 1 , 1 , 1 , 1 , 1 , 1 ),
"cultist" : ( 0 , 1 , 1 , 1 , 1 , 1 ),
# neutral roles
"dullahan" : ( 1 , 1 , 1 , 1 , 1 , 1 ),
"vengeful ghost" : ( 0 , 0 , 0 , 1 , 1 , 1 ),
"monster" : ( 0 , 0 , 0 , 0 , 1 , 2 ),
# templates
"cursed villager" : ( 1 , 1 , 1 , 1 , 1 , 1 ),
"blessed villager" : ( 0 , 1 , 1 , 1 , 1 , 1 ),
"prophet" : ( 0 , 1 , 1 , 1 , 1 , 1 ),
"gunner" : ( 0 , 0 , 0 , 0 , 0 , 1 ),
})
# this ensures that priest will always receive the blessed villager and prophet templates
# prophet is normally a role by itself, but we're turning it into a template for this mode
self.TEMPLATE_RESTRICTIONS = var.TEMPLATE_RESTRICTIONS.copy()
self.TEMPLATE_RESTRICTIONS["cursed villager"] |= {"priest"}
self.TEMPLATE_RESTRICTIONS["blessed villager"] = frozenset(self.ROLE_GUIDE.keys()) - {"priest", "blessed villager", "prophet"}
self.TEMPLATE_RESTRICTIONS["prophet"] = frozenset(self.ROLE_GUIDE.keys()) - {"priest", "blessed villager", "prophet"}
# this ensures that village drunk will always receive the gunner template
self.TEMPLATE_RESTRICTIONS["gunner"] = frozenset(self.ROLE_GUIDE.keys()) - {"village drunk", "cursed villager", "gunner"}
# disable wolfchat
#self.RESTRICT_WOLFCHAT = 0x0f
self.having_nightmare = None
def startup(self):
from src import decorators
events.add_listener("dullahan_targets", self.dullahan_targets)
events.add_listener("transition_night_begin", self.setup_nightmares)
events.add_listener("chk_nightdone", self.prolong_night)
events.add_listener("transition_day_begin", self.nightmare_kill)
events.add_listener("del_player", self.happy_fun_times)
events.add_listener("rename_player", self.rename_player)
self.north_cmd = decorators.cmd("north", "n", chan=False, pm=True, playing=True, phases=("night",))(self.north)
self.east_cmd = decorators.cmd("east", "e", chan=False, pm=True, playing=True, phases=("night",))(self.east)
self.south_cmd = decorators.cmd("south", "s", chan=False, pm=True, playing=True, phases=("night",))(self.south)
self.west_cmd = decorators.cmd("west", "w", chan=False, pm=True, playing=True, phases=("night",))(self.west)
def teardown(self):
from src import decorators
events.remove_listener("dullahan_targets", self.dullahan_targets)
events.remove_listener("transition_night_begin", self.setup_nightmares)
events.remove_listener("chk_nightdone", self.prolong_night)
events.remove_listener("transition_day_begin", self.nightmare_kill)
events.remove_listener("del_player", self.happy_fun_times)
events.remove_listener("rename_player", self.rename_player)
def remove_command(name, command):
if len(decorators.COMMANDS[name]) > 1:
decorators.COMMANDS[name].remove(command)
else:
del decorators.COMMANDS[name]
remove_command("north", self.north_cmd)
remove_command("n", self.north_cmd)
remove_command("east", self.east_cmd)
remove_command("e", self.east_cmd)
remove_command("south", self.south_cmd)
remove_command("s", self.south_cmd)
remove_command("west", self.west_cmd)
remove_command("w", self.west_cmd)
def dullahan_targets(self, evt, cli, var, dullahans, max_targets):
for dull in dullahans:
evt.data["targets"][dull] = set(var.ROLES["priest"])
def setup_nightmares(self, evt, cli, var):
if random.random() < 1/5:
self.having_nightmare = True
with var.WARNING_LOCK:
t = threading.Timer(60, self.do_nightmare, (cli, var, random.choice(list_players()), var.NIGHT_COUNT))
t.daemon = True
t.start()
else:
self.having_nightmare = None
def rename_player(self, evt, cli, var, prefix, nick):
if self.having_nightmare == prefix:
self.having_nightmare = nick
@handle_error
def do_nightmare(self, cli, var, target, night):
if var.PHASE != "night" or var.NIGHT_COUNT != night:
return
if target not in list_players():
return
self.having_nightmare = target
pm(cli, self.having_nightmare, messages["sleepy_nightmare_begin"])
pm(cli, self.having_nightmare, messages["sleepy_nightmare_navigate"])
self.correct = [None, None, None]
self.fake1 = [None, None, None]
self.fake2 = [None, None, None]
directions = ["n", "e", "s", "w"]
self.step = 0
self.prev_direction = None
opposite = {"n": "s", "e": "w", "s": "n", "w": "e"}
for i in range(3):
corrdir = directions[:]
f1dir = directions[:]
f2dir = directions[:]
if i > 0:
corrdir.remove(opposite[self.correct[i-1]])
f1dir.remove(opposite[self.fake1[i-1]])
f2dir.remove(opposite[self.fake2[i-1]])
else:
corrdir.remove("s")
f1dir.remove("s")
f2dir.remove("s")
self.correct[i] = random.choice(corrdir)
self.fake1[i] = random.choice(f1dir)
self.fake2[i] = random.choice(f2dir)
self.prev_direction = "n"
self.start_direction = "n"
self.on_path = set()
self.nightmare_step(cli)
def nightmare_step(self, cli):
if self.prev_direction == "n":
directions = "north, east, and west"
elif self.prev_direction == "e":
directions = "north, east, and south"
elif self.prev_direction == "s":
directions = "east, south, and west"
elif self.prev_direction == "w":
directions = "north, south, and west"
if self.step == 0:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_0"].format(directions))
elif self.step == 1:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_1"].format(directions))
elif self.step == 2:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_2"].format(directions))
elif self.step == 3:
if "correct" in self.on_path:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_wake"])
self.having_nightmare = None
chk_nightdone(cli)
elif "fake1" in self.on_path:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_fake_1"])
self.step = 0
self.on_path = set()
self.prev_direction = self.start_direction
self.nightmare_step(cli)
elif "fake2" in self.on_path:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_fake_2"])
self.step = 0
self.on_path = set()
self.prev_direction = self.start_direction
self.nightmare_step(cli)
def north(self, cli, nick, chan, rest):
if nick != self.having_nightmare:
return
if self.prev_direction == "s":
pm(cli, nick, messages["sleepy_nightmare_invalid_direction"])
return
advance = False
if ("correct" in self.on_path or self.step == 0) and self.correct[self.step] == "n":
self.on_path.add("correct")
advance = True
else:
self.on_path.discard("correct")
if ("fake1" in self.on_path or self.step == 0) and self.fake1[self.step] == "n":
self.on_path.add("fake1")
advance = True
else:
self.on_path.discard("fake1")
if ("fake2" in self.on_path or self.step == 0) and self.fake2[self.step] == "n":
self.on_path.add("fake2")
advance = True
else:
self.on_path.discard("fake2")
if advance:
self.step += 1
self.prev_direction = "n"
else:
self.step = 0
self.on_path = set()
self.prev_direction = self.start_direction
pm(cli, self.having_nightmare, messages["sleepy_nightmare_restart"])
self.nightmare_step(cli)
def east(self, cli, nick, chan, rest):
if nick != self.having_nightmare:
return
if self.prev_direction == "w":
pm(cli, nick, messages["sleepy_nightmare_invalid_direction"])
return
advance = False
if ("correct" in self.on_path or self.step == 0) and self.correct[self.step] == "e":
self.on_path.add("correct")
advance = True
else:
self.on_path.discard("correct")
if ("fake1" in self.on_path or self.step == 0) and self.fake1[self.step] == "e":
self.on_path.add("fake1")
advance = True
else:
self.on_path.discard("fake1")
if ("fake2" in self.on_path or self.step == 0) and self.fake2[self.step] == "e":
self.on_path.add("fake2")
advance = True
else:
self.on_path.discard("fake2")
if advance:
self.step += 1
self.prev_direction = "e"
else:
self.step = 0
self.on_path = set()
self.prev_direction = self.start_direction
pm(cli, self.having_nightmare, messages["sleepy_nightmare_restart"])
self.nightmare_step(cli)
def south(self, cli, nick, chan, rest):
if nick != self.having_nightmare:
return
if self.prev_direction == "n":
pm(cli, nick, messages["sleepy_nightmare_invalid_direction"])
return
advance = False
if ("correct" in self.on_path or self.step == 0) and self.correct[self.step] == "s":
self.on_path.add("correct")
advance = True
else:
self.on_path.discard("correct")
if ("fake1" in self.on_path or self.step == 0) and self.fake1[self.step] == "s":
self.on_path.add("fake1")
advance = True
else:
self.on_path.discard("fake1")
if ("fake2" in self.on_path or self.step == 0) and self.fake2[self.step] == "s":
self.on_path.add("fake2")
advance = True
else:
self.on_path.discard("fake2")
if advance:
self.step += 1
self.prev_direction = "s"
else:
self.step = 0
self.on_path = set()
self.prev_direction = self.start_direction
pm(cli, self.having_nightmare, messages["sleepy_nightmare_restart"])
self.nightmare_step(cli)
def west(self, cli, nick, chan, rest):
if nick != self.having_nightmare:
return
if self.prev_direction == "e":
pm(cli, nick, messages["sleepy_nightmare_invalid_direction"])
return
advance = False
if ("correct" in self.on_path or self.step == 0) and self.correct[self.step] == "w":
self.on_path.add("correct")
advance = True
else:
self.on_path.discard("correct")
if ("fake1" in self.on_path or self.step == 0) and self.fake1[self.step] == "w":
self.on_path.add("fake1")
advance = True
else:
self.on_path.discard("fake1")
if ("fake2" in self.on_path or self.step == 0) and self.fake2[self.step] == "w":
self.on_path.add("fake2")
advance = True
else:
self.on_path.discard("fake2")
if advance:
self.step += 1
self.prev_direction = "w"
else:
self.step = 0
self.on_path = set()
self.prev_direction = self.start_direction
pm(cli, self.having_nightmare, messages["sleepy_nightmare_restart"])
self.nightmare_step(cli)
def prolong_night(self, evt, cli, var):
if self.having_nightmare is not None:
evt.data["actedcount"] = -1
def nightmare_kill(self, evt, cli, var):
# if True, it means night ended before 1 minute
if self.having_nightmare is not None and self.having_nightmare is not True and self.having_nightmare in list_players():
var.DYING.add(self.having_nightmare)
pm(cli, self.having_nightmare, messages["sleepy_nightmare_death"])
def happy_fun_times(self, evt, cli, var, nick, nickrole, nicktpls, death_triggers):
if death_triggers:
if nickrole == "priest":
pl = evt.data["pl"]
turn_chance = 3/4
seers = [p for p in var.ROLES["seer"] if p in pl and random.random() < turn_chance]
harlots = [p for p in var.ROLES["harlot"] if p in pl and random.random() < turn_chance]
cultists = [p for p in var.ROLES["cultist"] if p in pl and random.random() < turn_chance]
cli.msg(botconfig.CHANNEL, messages["sleepy_priest_death"])
for seer in seers:
var.ROLES["seer"].remove(seer)
var.ROLES["doomsayer"].add(seer)
var.FINAL_ROLES[seer] = "doomsayer"
pm(cli, seer, messages["sleepy_doomsayer_turn"])
relay_wolfchat_command(cli, seer, messages["sleepy_doomsayer_wolfchat"].format(seer), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True)
for harlot in harlots:
var.ROLES["harlot"].remove(harlot)
var.ROLES["succubus"].add(harlot)
var.FINAL_ROLES[harlot] = "succubus"
pm(cli, harlot, messages["sleepy_succubus_turn"])
for cultist in cultists:
var.ROLES["cultist"].remove(cultist)
var.ROLES["demoniac"].add(cultist)
var.FINAL_ROLES[cultist] = "demoniac"
pm(cli, cultist, messages["sleepy_demoniac_turn"])
# NOTE: chk_win is called by del_player, don't need to call it here even though this has a chance of ending game
@game_mode("maelstrom", minp = 8, maxp = 24, likelihood = 0)
class MaelstromMode(GameMode):
"""Some people just want to watch the world burn."""
def __init__(self, arg=""):
self.ROLE_REVEAL = "on"
self.STATS_TYPE = "disabled"
super().__init__(arg)
self.LOVER_WINS_WITH_FOOL = True
self.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS = 0 # always make it happen
self.ALWAYS_PM_ROLE = True
# clone is pointless in this mode
# dullahan doesn't really work in this mode either, if enabling anyway special logic to determine kill list
# needs to be added above for when dulls are added during the game
# matchmaker is conditionally enabled during night 1 only
# monster and demoniac are nearly impossible to counter and don't add any interesting gameplay
# succubus keeps around entranced people, who are then unable to win even if there are later no succubi (not very fun)
self.roles = list(var.ROLE_GUIDE.keys() - var.TEMPLATE_RESTRICTIONS.keys() - {"amnesiac", "clone", "dullahan", "matchmaker", "monster", "demoniac", "wild child", "succubus"})
self.DEAD_ACCOUNTS = set()
self.DEAD_HOSTS = set()
def startup(self):
events.add_listener("role_attribution", self.role_attribution)
events.add_listener("transition_night_begin", self.transition_night_begin)
events.add_listener("del_player", self.on_del_player)
events.add_listener("join", self.on_join)
def teardown(self):
events.remove_listener("role_attribution", self.role_attribution)
events.remove_listener("transition_night_begin", self.transition_night_begin)
events.remove_listener("del_player", self.on_del_player)
events.remove_listener("join", self.on_join)
def on_del_player(self, evt, cli, var, nick, nickrole, nicktpls, death_triggers):
if is_fake_nick(nick):
return
if not var.DISABLE_ACCOUNTS:
self.DEAD_ACCOUNTS.add(irc_lower(var.USERS[nick]["account"]))
if not var.ACCOUNTS_ONLY:
self.DEAD_HOSTS.add(var.USERS[nick]["host"].lower())
def on_join(self, evt, var, wrapper, message, forced=False):
if var.PHASE != "day" or (wrapper.public and wrapper.target is not channels.Main):
return
temp = wrapper.source.lower()
if (wrapper.source in var.ALL_PLAYERS or
temp.account in self.DEAD_ACCOUNTS or
temp.host in self.DEAD_HOSTS):
wrapper.pm(messages["maelstrom_dead"])
return
if not forced and evt.data["join_player"](var, type(wrapper)(wrapper.source, channels.Main), sanity=False):
self._on_join(var, wrapper)
evt.prevent_default = True
elif forced:
# in fjoin, handle this differently
jp = evt.data["join_player"]
evt.data["join_player"] = lambda var, wrapper, who=None, forced=False: jp(var, wrapper, who=who, forced=forced, sanity=False) and self._on_join(var, wrapper)
def _on_join(self, var, wrapper):
role = random.choice(self.roles)
newlist = copy.deepcopy(var.ROLES)
newlist[role].add(wrapper.source)
if self.chk_win_conditions(wrapper.client, newlist, end_game=False):
return self._on_join(var, wrapper)
var.ROLES[role].add(wrapper.source.nick) # FIXME: add user instead of nick
var.ORIGINAL_ROLES[role].add(wrapper.source.nick)
var.FINAL_ROLES[wrapper.source.nick] = role
var.LAST_SAID_TIME[wrapper.source.nick] = datetime.now()
if wrapper.source.nick in var.USERS:
var.PLAYERS[wrapper.source.nick] = var.USERS[wrapper.source.nick]
if role == "doctor":
lpl = len(list_players())
var.DOCTORS[wrapper.source.nick] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * lpl)
# let them know their role
from src.decorators import COMMANDS
COMMANDS["myrole"][0].caller(wrapper.source.client, wrapper.source.nick, wrapper.target.name, "") # FIXME: New/old API
# if they're a wolfchat role, alert the other wolves
if role in var.WOLFCHAT_ROLES:
relay_wolfchat_command(wrapper.source.client, wrapper.source.nick, messages["wolfchat_new_member"].format(wrapper.source.nick, role), var.WOLFCHAT_ROLES, is_wolf_command=True, is_kill_command=True)
# TODO: make this part of !myrole instead, no reason we can't give out wofllist in that
wolves = list_players(var.WOLFCHAT_ROLES)
pl = list_players()
random.shuffle(pl)
pl.remove(wrapper.source.nick)
for i, player in enumerate(pl):
prole = get_role(player)
if prole in var.WOLFCHAT_ROLES:
cursed = ""
if player in var.ROLES["cursed villager"]:
cursed = "cursed "
pl[i] = "\u0002{0}\u0002 ({1}{2})".format(player, cursed, prole)
elif player in var.ROLES["cursed villager"]:
pl[i] = player + " (cursed)"
wrapper.pm("Players: " + ", ".join(pl))
def role_attribution(self, evt, cli, var, chk_win_conditions, villagers):
self.chk_win_conditions = chk_win_conditions
evt.data["addroles"] = self._role_attribution(cli, var, villagers, True)
def transition_night_begin(self, evt, cli, var):
# don't do this n1
if var.FIRST_NIGHT:
return
villagers = list_players()
lpl = len(villagers)
addroles = self._role_attribution(cli, var, villagers, False)
# shameless copy/paste of regular role attribution
for role, count in addroles.items():
selected = random.sample(villagers, count)
var.ROLES[role] = set(selected)
for x in selected:
villagers.remove(x)
# Handle roles that need extra help
for doctor in var.ROLES["doctor"]:
var.DOCTORS[doctor] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * lpl)
# Clear totem tracking; this would let someone that gets shaman twice in a row to give
# out a totem to the same person twice in a row, but oh well
var.LASTGIVEN = {}
# for end of game stats to show what everyone ended up as on game end
for role, pl in var.ROLES.items():
if role in var.TEMPLATE_RESTRICTIONS.keys():
continue
for p in pl:
# discard them from all non-template roles, we don't have a reliable
# means of tracking their previous role (due to traitor turning, exchange
# totem, etc.), so we need to iterate through everything.
for r in var.ORIGINAL_ROLES.keys():
if r in var.TEMPLATE_RESTRICTIONS.keys():
continue
var.ORIGINAL_ROLES[r].discard(p)
var.ORIGINAL_ROLES[role].add(p)
var.FINAL_ROLES[p] = role
def _role_attribution(self, cli, var, villagers, do_templates):
lpl = len(villagers) - 1
addroles = {}
for role in var.ROLE_GUIDE:
if role in var.TEMPLATE_RESTRICTIONS.keys() and not do_templates:
continue
addroles[role] = 0
wolves = var.WOLF_ROLES - {"wolf cub"}
addroles[random.choice(list(wolves))] += 1 # make sure there's at least one wolf role
roles = self.roles[:]
if do_templates:
# mm only works night 1, do_templates is also only true n1
roles.append("matchmaker")
while lpl:
addroles[random.choice(roles)] += 1
lpl -= 1
if do_templates:
addroles["gunner"] = random.randrange(4)
addroles["sharpshooter"] = random.randrange(addroles["gunner"] + 1)
addroles["assassin"] = random.randrange(3)
addroles["cursed villager"] = random.randrange(3)
addroles["mayor"] = random.randrange(2)
if random.randrange(100) == 0 and addroles.get("villager", 0) > 0:
addroles["blessed villager"] = 1
rolemap = defaultdict(list)
pcount = 0
for r,c in addroles.items():
if c > 0:
rolemap[r] = list(range(pcount, pcount+c))
pcount += c
if self.chk_win_conditions(cli, rolemap, end_game=False):
return self._role_attribution(cli, var, villagers, do_templates)
return addroles
# vim: set sw=4 expandtab: