def __init__(self, arg=""): super().__init__(arg) self.LIMIT_ABSTAIN = False self.DEFAULT_ROLE = "shaman" # If you add non-wolfteam, non-shaman roles, be sure to update update_stats to account for it! # otherwise !stats will break if they turn into VG. self.ROLE_GUIDE = { 6: ["wolf shaman"], 10: ["wolf shaman(2)"], 15: ["wolf shaman(3)"], 20: ["wolf shaman(4)"] } self.EVENTS = { "transition_night_end": EventListener(self.on_transition_night_end, priority=1), "wolf_numkills": EventListener(self.on_wolf_numkills), "totem_assignment": EventListener(self.on_totem_assignment), "transition_day_begin": EventListener(self.on_transition_day_begin, priority=8), "transition_day_resolve_end": EventListener(self.on_transition_day_resolve_end, priority=2), "del_player": EventListener(self.on_del_player), "apply_totem": EventListener(self.on_apply_totem), "lynch": EventListener(self.on_lynch), "chk_win": EventListener(self.on_chk_win), "revealroles_role": EventListener(self.on_revealroles_role), "update_stats": EventListener(self.on_update_stats), "begin_night": EventListener(self.on_begin_night) } self.TOTEM_CHANCES = {totem: {} for totem in self.DEFAULT_TOTEM_CHANCES} self.set_default_totem_chances() for totem, roles in self.TOTEM_CHANCES.items(): for role in roles: self.TOTEM_CHANCES[totem][role] = 0 # custom totems self.TOTEM_CHANCES["sustenance"] = {"shaman": 12, "wolf shaman": 0, "crazed shaman": 0} self.TOTEM_CHANCES["hunger"] = {"shaman": 0, "wolf shaman": 6, "crazed shaman": 0} # extra shaman totems self.TOTEM_CHANCES["revealing"]["shaman"] = 4 self.TOTEM_CHANCES["death"]["shaman"] = 1 self.TOTEM_CHANCES["pacifism"]["shaman"] = 2 self.TOTEM_CHANCES["silence"]["shaman"] = 1 # extra WS totems: note that each WS automatically gets a hunger totem in addition to this in phase 1 self.TOTEM_CHANCES["death"]["wolf shaman"] = 2 self.TOTEM_CHANCES["revealing"]["wolf shaman"] = 2 self.TOTEM_CHANCES["luck"]["wolf shaman"] = 4 self.TOTEM_CHANCES["silence"]["wolf shaman"] = 1 self.TOTEM_CHANCES["pacifism"]["wolf shaman"] = 2 self.TOTEM_CHANCES["impatience"]["wolf shaman"] = 3 self.hunger_levels = DefaultUserDict(int) self.totem_tracking = defaultdict(int) # no need to make a user container, this is only non-empty a very short time self.phase = 1 self.max_nights = 7 self.village_hunger = 0 self.village_hunger_percent_base = 0.3 self.village_hunger_percent_adj = 0.03 self.village_starve = 0 self.max_village_starve = 3 self.num_retribution = 0 self.saved_messages = {} # type: Dict[str, str] self.feed_command = None
def add_protection(var, target, protector, protector_role, scope=All): """Add a protection to the target affecting the relevant scope.""" if target not in get_players(): return if target not in PROTECTIONS: PROTECTIONS[target] = DefaultUserDict(list) prot_entry = (scope, protector_role) PROTECTIONS[target][protector].append(prot_entry)
from src.containers import DefaultUserDict from src.decorators import event_listener from src.functions import get_players from src.events import Event __all__ = ["add_lynch_immunity", "try_lynch_immunity"] IMMUNITY = DefaultUserDict(set) # type: UserDict[User, set] def add_lynch_immunity(var, user, reason): """Make user immune to lynching for one day.""" if user not in get_players(): return IMMUNITY[user].add(reason) def try_lynch_immunity(var, user) -> bool: if user in IMMUNITY: reason = IMMUNITY[user].pop() # get a random reason evt = Event("lynch_immunity", {"immune": False}) evt.dispatch(var, user, reason) return evt.data["immune"] return False @event_listener("revealroles") def on_revealroles(evt, var, wrapper): if IMMUNITY: evt.data["output"].append("\u0002lynch immunity\u0002: {0}".format(
class BorealMode(GameMode): """Some shamans are working against you. Exile them before you starve!""" def __init__(self, arg=""): super().__init__(arg) self.LIMIT_ABSTAIN = False self.SELF_LYNCH_ALLOWED = False self.DEFAULT_ROLE = "shaman" # If you add non-wolfteam, non-shaman roles, be sure to update update_stats to account for it! # otherwise !stats will break if they turn into VG. self.ROLE_GUIDE = { 6: ["wolf shaman", "wolf shaman(2)"], 10: ["wolf shaman(3)"], 15: ["wolf shaman(4)"], 20: ["wolf shaman(5)"] } self.EVENTS = { "transition_night_begin": EventListener(self.on_transition_night_begin), "transition_night_end": EventListener(self.on_transition_night_end, priority=1), "wolf_numkills": EventListener(self.on_wolf_numkills), "totem_assignment": EventListener(self.on_totem_assignment), "transition_day_begin": EventListener(self.on_transition_day_begin, priority=8), "transition_day_resolve_end": EventListener(self.on_transition_day_resolve_end, priority=2), "del_player": EventListener(self.on_del_player), "apply_totem": EventListener(self.on_apply_totem), "lynch": EventListener(self.on_lynch), "chk_win": EventListener(self.on_chk_win), "revealroles_role": EventListener(self.on_revealroles_role), "update_stats": EventListener(self.on_update_stats), "begin_night": EventListener(self.on_begin_night), "num_totems": EventListener(self.on_num_totems) } self.TOTEM_CHANCES = { totem: {} for totem in self.DEFAULT_TOTEM_CHANCES } self.set_default_totem_chances() for totem, roles in self.TOTEM_CHANCES.items(): for role in roles: self.TOTEM_CHANCES[totem][role] = 0 # custom totems self.TOTEM_CHANCES["sustenance"] = { "shaman": 60, "wolf shaman": 10, "crazed shaman": 0 } self.TOTEM_CHANCES["hunger"] = { "shaman": 0, "wolf shaman": 40, "crazed shaman": 0 } # extra shaman totems self.TOTEM_CHANCES["revealing"]["shaman"] = 10 self.TOTEM_CHANCES["death"]["shaman"] = 10 self.TOTEM_CHANCES["pacifism"]["shaman"] = 10 self.TOTEM_CHANCES["silence"]["shaman"] = 10 # extra WS totems self.TOTEM_CHANCES["death"]["wolf shaman"] = 10 self.TOTEM_CHANCES["revealing"]["wolf shaman"] = 10 self.TOTEM_CHANCES["luck"]["wolf shaman"] = 10 self.TOTEM_CHANCES["silence"]["wolf shaman"] = 10 self.TOTEM_CHANCES["pacifism"]["wolf shaman"] = 10 self.hunger_levels = DefaultUserDict(int) self.totem_tracking = defaultdict( int ) # no need to make a user container, this is only non-empty a very short time self.phase = 1 self.max_nights = 7 self.village_hunger = 0 self.village_hunger_percent_base = 0.4 self.village_hunger_percent_adj = 0.03 self.ws_num_totem_percent = 0.5 self.ws_extra_totem = 0 self.village_starve = 0 self.max_village_starve = 3 self.num_retribution = 0 self.saved_messages = {} # type: Dict[str, str] self.feed_command = None def startup(self): super().startup() self.phase = 1 self.village_starve = 0 self.hunger_levels.clear() self.saved_messages = { "wolf_shaman_notify": messages.messages["wolf_shaman_notify"], "vengeful_turn": messages.messages["vengeful_turn"], "lynch_reveal": messages.messages["lynch_reveal"] } messages.messages[ "wolf_shaman_notify"] = "" # don't tell WS they can kill messages.messages["vengeful_turn"] = messages.messages["boreal_turn"] messages.messages["lynch_reveal"] = messages.messages["boreal_exile"] kwargs = dict(chan=False, pm=True, playing=True, silenced=True, phases=("night", ), roles=("shaman", "wolf shaman")) self.feed_command = command("feed", **kwargs)(self.feed) def teardown(self): from src import decorators super().teardown() def remove_command(name, command): if len(decorators.COMMANDS[name]) > 1: decorators.COMMANDS[name].remove(command) else: del decorators.COMMANDS[name] self.hunger_levels.clear() for key, value in self.saved_messages.items(): messages.messages[key] = value remove_command("feed", self.feed_command) def on_totem_assignment(self, evt, var, player, role): if role == "shaman": # In phase 2, we want to hand out as many retribution totems as there are active VGs (if possible) if self.num_retribution > 0: self.num_retribution -= 1 evt.data["totems"] = {"retribution": 1} def on_transition_night_begin(self, evt, var): num_s = len( get_players(("shaman", ), mainroles=var.ORIGINAL_MAIN_ROLES)) num_ws = len(get_players(("wolf shaman", ))) # as wolf shamans die, we want to pass some extras onto the remaining ones; each ws caps at 2 totems though self.ws_extra_totem = int(num_s * self.ws_num_totem_percent) - num_ws def on_transition_night_end(self, evt, var): from src.roles import vengefulghost # determine how many retribution totems we need to hand out tonight self.num_retribution = sum(1 for p in vengefulghost.GHOSTS if vengefulghost.GHOSTS[p][0] != "!") if self.num_retribution > 0: self.phase = 2 # determine how many tribe members need to be fed. It's a percentage of remaining shamans # Each alive WS reduces the percentage needed; the number is rounded off (.5 rounding to even) percent = self.village_hunger_percent_base - self.village_hunger_percent_adj * len( get_players(("wolf shaman", ))) self.village_hunger = round(len(get_players(("shaman", ))) * percent) def on_wolf_numkills(self, evt, var): evt.data["numkills"] = 0 def on_num_totems(self, evt, var, player, role): if role == "wolf shaman" and self.ws_extra_totem > 0: self.ws_extra_totem -= 1 evt.data["num"] = 2 def on_transition_day_begin(self, evt, var): from src.roles import vengefulghost num_wendigos = len(vengefulghost.GHOSTS) num_wolf_shamans = len(get_players(("wolf shaman", ))) ps = get_players() for p in ps: if get_main_role(p) in Wolfteam: continue # wolf shamans can't starve if self.totem_tracking[p] > 0: # if sustenance totem made it through, fully feed player self.hunger_levels[p] = 0 elif self.totem_tracking[p] < 0: # if hunger totem made it through, fast-track player to starvation if self.hunger_levels[p] < 3: self.hunger_levels[p] = 3 # apply natural hunger self.hunger_levels[p] += 1 if self.hunger_levels[p] >= 5: # if they hit 5, they die of starvation # if there are less VGs than alive wolf shamans, they become a wendigo as well if num_wendigos < num_wolf_shamans: num_wendigos += 1 change_role(var, p, get_main_role(p), "vengeful ghost", message=None) add_dying(var, p, killer_role="villager", reason="boreal_starvation") elif self.hunger_levels[p] >= 3: # if they are at 3 or 4, alert them that they are hungry p.send(messages["boreal_hungry"]) self.totem_tracking.clear() def on_transition_day_resolve_end(self, evt, var, victims): if len(evt.data["dead"]) == 0: evt.data["novictmsg"] = False # say if the village went hungry last night (and apply those effects if it did) if self.village_hunger > 0: self.village_starve += 1 evt.data["message"]["*"].append(messages["boreal_village_hungry"]) # say how many days remaining remain = self.max_nights - var.NIGHT_COUNT if remain > 0: evt.data["message"]["*"].append( messages["boreal_day_count"].format(remain)) def on_lynch(self, evt, var, votee, voters): if get_main_role(votee) not in Wolfteam: # if there are less VGs than alive wolf shamans, they become a wendigo as well from src.roles import vengefulghost num_wendigos = len(vengefulghost.GHOSTS) num_wolf_shamans = len(get_players(("wolf shaman", ))) if num_wendigos < num_wolf_shamans: change_role(var, votee, get_main_role(votee), "vengeful ghost", message=None) def on_del_player(self, evt, var, player, all_roles, death_triggers): for a, b in list(self.hunger_levels.items()): if player in (a, b): del self.hunger_levels[a] def on_apply_totem(self, evt, var, role, totem, shaman, target): if totem == "sustenance": if target is users.Bot: # fed the village self.village_hunger -= 1 else: # gave to a player self.totem_tracking[target] += 1 elif totem == "hunger": if target is users.Bot: # tried to starve the village self.village_hunger += 1 else: # gave to a player self.totem_tracking[target] -= 1 def on_chk_win(self, evt, var, rolemap, mainroles, lpl, lwolves, lrealwolves): if self.village_starve == self.max_village_starve and var.PHASE == "day": # if village didn't feed the NPCs enough nights, the starving tribe members destroy themselves from within # this overrides built-in win conds (such as all wolves being dead) evt.data["winner"] = "wolves" evt.data["message"] = messages["boreal_village_starve"] elif var.NIGHT_COUNT == self.max_nights and var.PHASE == "day": # if village survived for N nights without losing, they outlast the storm and win # this overrides built-in win conds (such as same number of wolves as villagers) evt.data["winner"] = "villagers" evt.data["message"] = messages["boreal_time_up"] elif evt.data["winner"] == "villagers": evt.data["message"] = messages["boreal_village_win"] elif evt.data["winner"] == "wolves": evt.data["message"] = messages["boreal_wolf_win"] def on_revealroles_role(self, evt, var, player, role): if player in self.hunger_levels: evt.data["special_case"].append( messages["boreal_revealroles"].format( self.hunger_levels[player])) def on_update_stats(self, evt, var, player, main_role, reveal_role, all_roles): if main_role == "vengeful ghost": evt.data["possible"].add("shaman") def on_begin_night(self, evt, var): evt.data["messages"].append(messages["boreal_night_reminder"].format( self.village_hunger, self.village_starve)) def feed(self, var, wrapper, message): """Give your totem to the tribe members.""" from src.roles.shaman import TOTEMS as s_totems, SHAMANS as s_shamans from src.roles.wolfshaman import TOTEMS as ws_totems, SHAMANS as ws_shamans pieces = re.split(" +", message) valid = ("sustenance", "hunger") state_vars = ((s_totems, s_shamans), (ws_totems, ws_shamans)) for TOTEMS, SHAMANS in state_vars: if wrapper.source not in TOTEMS: continue totem_types = list(TOTEMS[wrapper.source].keys()) given = complete_one_match(pieces[0], totem_types) if not given and TOTEMS[wrapper.source].get( "sustenance", 0) + TOTEMS[wrapper.source].get("hunger", 0) > 1: wrapper.send(messages["boreal_ambiguous_feed"]) return for totem in valid: if (given and totem != given) or TOTEMS[wrapper.source].get( totem, 0) == 0: continue # doesn't have a totem that can be used to feed tribe SHAMANS[wrapper.source][totem].append(users.Bot) if len(SHAMANS[wrapper.source][totem]) > TOTEMS[ wrapper.source][totem]: SHAMANS[wrapper.source][totem].pop(0) wrapper.pm(messages["boreal_feed_success"].format(totem)) # send_wolfchat_message already takes care of checking whether the player has access to wolfchat, so this will only be sent for wolf shamans send_wolfchat_message(var, wrapper.source, messages["boreal_wolfchat_feed"].format( wrapper.source), {"wolf shaman"}, role="wolf shaman", command="feed") return
def ulf(): # Factory method to create a DefaultUserDict[*, UserList] # this can be passed into a DefaultUserDict constructor so we can make nested defaultdicts easily return DefaultUserDict(UserList)
def setup_variables(rolename, *, knows_totem): """Setup role variables and shared events.""" def ulf(): # Factory method to create a DefaultUserDict[*, UserList] # this can be passed into a DefaultUserDict constructor so we can make nested defaultdicts easily return DefaultUserDict(UserList) TOTEMS = DefaultUserDict( dict) # type: DefaultUserDict[users.User, Dict[str, int]] LASTGIVEN = DefaultUserDict( ulf ) # type: DefaultUserDict[users.User, DefaultUserDict[str, UserList]] SHAMANS = DefaultUserDict( ulf ) # type: DefaultUserDict[users.User, DefaultUserDict[str, UserList]] RETARGET = DefaultUserDict( UserDict ) # type: DefaultUserDict[users.User, UserDict[users.User, users.User]] _rolestate[rolename] = { "TOTEMS": TOTEMS, "LASTGIVEN": LASTGIVEN, "SHAMANS": SHAMANS, "RETARGET": RETARGET } @event_listener("reset", listener_id="shamans.<{}>.on_reset".format(rolename)) def on_reset(evt, var): TOTEMS.clear() LASTGIVEN.clear() SHAMANS.clear() RETARGET.clear() @event_listener("begin_day", listener_id="shamans.<{}>.on_begin_day".format(rolename)) def on_begin_day(evt, var): SHAMANS.clear() RETARGET.clear() @event_listener( "revealroles_role", listener_id="shamans.<{}>.revealroles_role".format(rolename)) def on_revealroles(evt, var, user, role): if role == rolename and user in TOTEMS: if var.PHASE == "night": evt.data["special_case"].append( messages["shaman_revealroles_night"].format( (messages["shaman_revealroles_night_totem"].format( num, totem) for num, totem in TOTEMS[user].items()), sum(TOTEMS[user].values()))) elif user in LASTGIVEN and LASTGIVEN[user]: given = [] for totem, recips in LASTGIVEN[user].items(): for recip in recips: given.append( messages["shaman_revealroles_day_totem"].format( totem, recip)) evt.data["special_case"].append( messages["shaman_revealroles_day"].format(given)) @event_listener( "transition_day_begin", priority=7, listener_id="shamans.<{}>.transition_day_begin".format(rolename)) def on_transition_day_begin2(evt, var): LASTGIVEN.clear() for shaman, given in SHAMANS.items(): for totem, targets in given.items(): for target in targets: victim = RETARGET[shaman].get(target, target) if not victim: continue if totem == "death": # this totem stacks if shaman not in DEATH: DEATH[shaman] = UserList() DEATH[shaman].append(victim) elif totem == "protection": # this totem stacks PROTECTION.append(victim) elif totem == "revealing": REVEALING.add(victim) elif totem == "narcolepsy": NARCOLEPSY.add(victim) elif totem == "silence": SILENCE.add(victim) elif totem == "desperation": DESPERATION.add(victim) elif totem == "impatience": # this totem stacks IMPATIENCE.append(victim) elif totem == "pacifism": # this totem stacks PACIFISM.append(victim) elif totem == "influence": INFLUENCE.add(victim) elif totem == "exchange": EXCHANGE.add(victim) elif totem == "lycanthropy": LYCANTHROPY.add(victim) elif totem == "luck": LUCK.add(victim) elif totem == "pestilence": PESTILENCE.add(victim) elif totem == "retribution": RETRIBUTION.add(victim) elif totem == "misdirection": MISDIRECTION.add(victim) elif totem == "deceit": DECEIT.add(victim) else: event = Event("apply_totem", {}) event.dispatch(var, rolename, totem, shaman, victim) if target is not victim: shaman.send(messages["totem_retarget"].format( victim, target)) LASTGIVEN[shaman][totem].append(victim) havetotem.append(victim) @event_listener("del_player", listener_id="shamans.<{}>.del_player".format(rolename)) def on_del_player(evt, var, player, all_roles, death_triggers): for a, b in list(SHAMANS.items()): if player is a: del SHAMANS[a] else: for totem, c in b.items(): if player in c: SHAMANS[a][totem].remove(player) del RETARGET[:player:] for a, b in list(RETARGET.items()): for c, d in list(b.items()): if player in (c, d): del RETARGET[a][c] @event_listener("chk_nightdone", listener_id="shamans.<{}>.chk_nightdone".format(rolename)) def on_chk_nightdone(evt, var): # only count shaman as acted if they've given out all of their totems for shaman in SHAMANS: totemcount = sum(TOTEMS[shaman].values()) given = len( list(itertools.chain.from_iterable(SHAMANS[shaman].values()))) if given == totemcount: evt.data["acted"].append(shaman) evt.data["nightroles"].extend(get_all_players((rolename, ))) @event_listener( "get_role_metadata", listener_id="shamans.<{}>.get_role_metadata".format(rolename)) def on_get_role_metadata(evt, var, kind): if kind == "night_kills": # only add shamans here if they were given a death totem # even though retribution kills, it is given a special kill message evt.data[rolename] = list( itertools.chain.from_iterable(TOTEMS.values())).count("death") @event_listener("new_role", listener_id="shamans.<{}>.new_role".format(rolename)) def on_new_role(evt, var, player, old_role): if evt.params.inherit_from in TOTEMS and old_role != rolename and evt.data[ "role"] == rolename: totems = TOTEMS.pop(evt.params.inherit_from) del SHAMANS[:evt.params.inherit_from:] del LASTGIVEN[:evt.params.inherit_from:] if knows_totem: evt.data["messages"].append(totem_message(totems)) TOTEMS[player] = totems @event_listener("swap_role_state", listener_id="shamans.<{}>.swap_role_state".format(rolename) ) def on_swap_role_state(evt, var, actor, target, role): if role == rolename and actor in TOTEMS and target in TOTEMS: TOTEMS[actor], TOTEMS[target] = TOTEMS[target], TOTEMS[actor] del SHAMANS[:actor:] del SHAMANS[:target:] del LASTGIVEN[:actor:] del LASTGIVEN[:target:] if knows_totem: evt.data["actor_messages"].append(totem_message(TOTEMS[actor])) evt.data["target_messages"].append( totem_message(TOTEMS[target])) @event_listener("default_totems", priority=3, listener_id="shamans.<{}>.default_totems".format(rolename)) def add_shaman(evt, chances): evt.data["shaman_roles"].add(rolename) @event_listener( "transition_night_end", listener_id="shamans.<{}>.on_transition_night_end".format(rolename)) def on_transition_night_end(evt, var): if var.NIGHT_COUNT == 0 or not get_all_players((rolename, )): return if var.CURRENT_GAMEMODE.TOTEM_CHANCES["lycanthropy"][rolename] > 0: status.add_lycanthropy_scope(var, All) if var.CURRENT_GAMEMODE.TOTEM_CHANCES["luck"][rolename] > 0: status.add_misdirection_scope(var, All, as_target=True) if var.CURRENT_GAMEMODE.TOTEM_CHANCES["misdirection"][rolename] > 0: status.add_misdirection_scope(var, All, as_actor=True) if knows_totem: @event_listener("myrole", listener_id="shamans.<{}>.on_myrole".format(rolename)) def on_myrole(evt, var, user): if evt.data[ "role"] == rolename and var.PHASE == "night" and user not in SHAMANS: evt.data["messages"].append(totem_message(TOTEMS[user])) return (TOTEMS, LASTGIVEN, SHAMANS, RETARGET)
class BorealMode(GameMode): """Some shamans are working against you. Exile them before you starve!""" def __init__(self, arg=""): super().__init__(arg) self.LIMIT_ABSTAIN = False self.DEFAULT_ROLE = "shaman" # If you add non-wolfteam, non-shaman roles, be sure to update update_stats to account for it! # otherwise !stats will break if they turn into VG. self.ROLE_GUIDE = { 6: ["wolf shaman"], 10: ["wolf shaman(2)"], 15: ["wolf shaman(3)"], 20: ["wolf shaman(4)"] } self.EVENTS = { "transition_night_end": EventListener(self.on_transition_night_end, priority=1), "wolf_numkills": EventListener(self.on_wolf_numkills), "totem_assignment": EventListener(self.on_totem_assignment), "transition_day_begin": EventListener(self.on_transition_day_begin, priority=8), "transition_day_resolve_end": EventListener(self.on_transition_day_resolve_end, priority=2), "del_player": EventListener(self.on_del_player), "apply_totem": EventListener(self.on_apply_totem), "lynch": EventListener(self.on_lynch), "chk_win": EventListener(self.on_chk_win), "revealroles_role": EventListener(self.on_revealroles_role), "update_stats": EventListener(self.on_update_stats), "begin_night": EventListener(self.on_begin_night) } self.TOTEM_CHANCES = {totem: {} for totem in self.DEFAULT_TOTEM_CHANCES} self.set_default_totem_chances() for totem, roles in self.TOTEM_CHANCES.items(): for role in roles: self.TOTEM_CHANCES[totem][role] = 0 # custom totems self.TOTEM_CHANCES["sustenance"] = {"shaman": 12, "wolf shaman": 0, "crazed shaman": 0} self.TOTEM_CHANCES["hunger"] = {"shaman": 0, "wolf shaman": 6, "crazed shaman": 0} # extra shaman totems self.TOTEM_CHANCES["revealing"]["shaman"] = 4 self.TOTEM_CHANCES["death"]["shaman"] = 1 self.TOTEM_CHANCES["pacifism"]["shaman"] = 2 self.TOTEM_CHANCES["silence"]["shaman"] = 1 # extra WS totems: note that each WS automatically gets a hunger totem in addition to this in phase 1 self.TOTEM_CHANCES["death"]["wolf shaman"] = 2 self.TOTEM_CHANCES["revealing"]["wolf shaman"] = 2 self.TOTEM_CHANCES["luck"]["wolf shaman"] = 4 self.TOTEM_CHANCES["silence"]["wolf shaman"] = 1 self.TOTEM_CHANCES["pacifism"]["wolf shaman"] = 2 self.TOTEM_CHANCES["impatience"]["wolf shaman"] = 3 self.hunger_levels = DefaultUserDict(int) self.totem_tracking = defaultdict(int) # no need to make a user container, this is only non-empty a very short time self.phase = 1 self.max_nights = 7 self.village_hunger = 0 self.village_hunger_percent_base = 0.3 self.village_hunger_percent_adj = 0.03 self.village_starve = 0 self.max_village_starve = 3 self.num_retribution = 0 self.saved_messages = {} # type: Dict[str, str] self.feed_command = None def startup(self): super().startup() self.phase = 1 self.village_starve = 0 self.hunger_levels.clear() self.saved_messages = { "wolf_shaman_notify": messages.messages["wolf_shaman_notify"], "vengeful_turn": messages.messages["vengeful_turn"], "lynch_reveal": messages.messages["lynch_reveal"] } messages.messages["wolf_shaman_notify"] = "" # don't tell WS they can kill messages.messages["vengeful_turn"] = messages.messages["boreal_turn"] messages.messages["lynch_reveal"] = messages.messages["boreal_exile"] kwargs = dict(chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("shaman",)) self.feed_command = command("feed", **kwargs)(self.feed) def teardown(self): from src import decorators super().teardown() def remove_command(name, command): if len(decorators.COMMANDS[name]) > 1: decorators.COMMANDS[name].remove(command) else: del decorators.COMMANDS[name] self.hunger_levels.clear() for key, value in self.saved_messages.items(): messages.messages[key] = value remove_command("feed", self.feed_command) def on_totem_assignment(self, evt, var, role): if role == "shaman": # In phase 2, we want to hand out as many retribution totems as there are active VGs (if possible) if self.num_retribution > 0: self.num_retribution -= 1 evt.data["totems"] = {"retribution": 1} elif role == "wolf shaman": # In phase 1, wolf shamans get a bonus hunger totem if self.phase == 1: if "hunger" in evt.data["totems"]: evt.data["totems"]["hunger"] += 1 else: evt.data["totems"]["hunger"] = 1 def on_transition_night_end(self, evt, var): from src.roles import vengefulghost # determine how many retribution totems we need to hand out tonight self.num_retribution = sum(1 for p in vengefulghost.GHOSTS if vengefulghost.GHOSTS[p][0] != "!") if self.num_retribution > 0: self.phase = 2 # determine how many tribe members need to be fed. It's a percentage of remaining shamans # Each alive WS reduces the percentage needed; the number is rounded off (.5 rounding to even) percent = self.village_hunger_percent_base - self.village_hunger_percent_adj * len(get_players(("wolf shaman",))) self.village_hunger = round(len(get_players(("shaman",))) * percent) def on_wolf_numkills(self, evt, var): evt.data["numkills"] = 0 def on_transition_day_begin(self, evt, var): ps = get_players() for p in ps: if get_main_role(p) in Wolfteam: continue # wolf shamans can't starve if self.totem_tracking[p] > 0: # if sustenance totem made it through, fully feed player self.hunger_levels[p] = 0 elif self.totem_tracking[p] < 0: # if hunger totem made it through, fast-track player to starvation if self.hunger_levels[p] < 3: self.hunger_levels[p] = 3 # apply natural hunger self.hunger_levels[p] += 1 if self.hunger_levels[p] >= 5: # if they hit 5, they die of starvation # if there are less VGs than alive wolf shamans, they become a wendigo as well self.maybe_make_wendigo(var, p) add_dying(var, p, killer_role="villager", reason="boreal_starvation") elif self.hunger_levels[p] >= 3: # if they are at 3 or 4, alert them that they are hungry p.send(messages["boreal_hungry"]) self.totem_tracking.clear() def on_transition_day_resolve_end(self, evt, var, victims): if len(evt.data["dead"]) == 0: evt.data["novictmsg"] = False # say if the village went hungry last night (and apply those effects if it did) if self.village_hunger > 0: self.village_starve += 1 evt.data["message"]["*"].append(messages["boreal_village_hungry"]) # say how many days remaining remain = self.max_nights - var.NIGHT_COUNT if remain > 0: evt.data["message"]["*"].append(messages["boreal_day_count"].format(remain, "s" if remain != 1 else "")) def maybe_make_wendigo(self, var, player): from src.roles import vengefulghost num_wendigos = len(vengefulghost.GHOSTS) num_wolf_shamans = len(get_players(("wolf shaman",))) if num_wendigos < num_wolf_shamans: change_role(var, player, get_main_role(player), "vengeful ghost", message=None) def on_lynch(self, evt, var, votee, voters): if get_main_role(votee) not in Wolfteam: # if there are less VGs than alive wolf shamans, they become a wendigo as well self.maybe_make_wendigo(var, votee) def on_del_player(self, evt, var, player, all_roles, death_triggers): for a, b in list(self.hunger_levels.items()): if player in (a, b): del self.hunger_levels[a] def on_apply_totem(self, evt, var, role, totem, shaman, target): if totem == "sustenance": if target is users.Bot: # fed the village self.village_hunger = max(0, self.village_hunger - 1) else: # gave to a player self.totem_tracking[target] += 1 elif totem == "hunger": self.totem_tracking[target] -= 1 def on_chk_win(self, evt, var, rolemap, mainroles, lpl, lwolves, lrealwolves): if self.village_starve == self.max_village_starve and var.PHASE == "day": # if village didn't feed the NPCs enough nights, the starving tribe members destroy themselves from within if evt.data["winner"] is None: evt.data["winner"] = "wolves" evt.data["message"] = messages["boreal_village_starve"] elif var.NIGHT_COUNT == self.max_nights and var.PHASE == "day": # if village survived for N nights without losing, they outlast the storm and win if evt.data["winner"] is None: evt.data["winner"] = "villagers" evt.data["message"] = messages["boreal_time_up"] elif evt.data["winner"] == "villagers": evt.data["message"] = messages["boreal_village_win"] elif evt.data["winner"] == "wolves": evt.data["message"] = messages["boreal_wolf_win"] def on_revealroles_role(self, evt, var, player, role): if player in self.hunger_levels: evt.data["special_case"].append(messages["boreal_revealroles"].format(self.hunger_levels[player])) def on_update_stats(self, evt, var, player, main_role, reveal_role, all_roles): if main_role == "vengeful ghost": evt.data["possible"].add("shaman") def on_begin_night(self, evt, var): evt.data["messages"].append(messages["boreal_night_reminder"].format( self.village_hunger, "s" if self.village_hunger != 1 else "", self.village_starve, "s" if self.village_starve != 1 else "" )) def feed(self, var, wrapper, message): """Give your sustenance totem to the tribe members so they don't starve.""" from src.roles.shaman import TOTEMS, SHAMANS if TOTEMS[wrapper.source].get("sustenance", 0) == 0: return # doesn't have a sustenance totem SHAMANS[wrapper.source]["sustenance"].append(users.Bot) if len(SHAMANS[wrapper.source]["sustenance"]) > TOTEMS[wrapper.source]["sustenance"]: SHAMANS[wrapper.source]["sustenance"].pop(0) wrapper.pm(messages["boreal_feed_success"])