import re import random import itertools import math from collections import defaultdict import botconfig import src.settings as var from src.utilities import * from src import channels, users, debuglog, errlog, plog from src.functions import get_players, get_all_players, get_main_role from src.decorators import cmd, event_listener from src.messages import messages from src.events import Event ENTRANCED = set() ENTRANCED_DYING = set() VISITED = {} ALL_SUCC_IDLE = True @cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("succubus",)) def hvisit(cli, nick, chan, rest): """Entrance a player, converting them to your team.""" if VISITED.get(nick): pm(cli, nick, messages["succubus_already_visited"].format(VISITED[nick])) return victim = get_victim(cli, nick, re.split(" +",rest)[0], False, True) if not victim: return if nick == victim: pm(cli, nick, messages["succubus_not_self"]) return evt = Event("targeted_command", {"target": victim, "misdirection": True, "exchange": False}) evt.dispatch(cli, var, "visit", nick, victim, frozenset({"detrimental", "immediate"})) if evt.prevent_default: return victim = evt.data["target"] VISITED[nick] = victim if victim not in var.ROLES["succubus"]: ENTRANCED.add(victim) pm(cli, nick, messages["succubus_target_success"].format(victim)) else: pm(cli, nick, messages["harlot_success"].format(victim)) if nick != victim: if victim not in var.ROLES["succubus"]: pm(cli, victim, messages["notify_succubus_target"].format(nick)) else: pm(cli, victim, messages["harlot_success"].format(nick)) revt = Event("succubus_visit", {}) revt.dispatch(cli, var, nick, victim) # TODO: split these into assassin, hag, and alpha wolf when they are split off if var.TARGETED.get(victim) in var.ROLES["succubus"]: msg = messages["no_target_succubus"].format(var.TARGETED[victim]) del var.TARGETED[victim] if victim in var.ROLES["village drunk"]: target = random.choice(list(set(list_players()) - var.ROLES["succubus"] - {victim})) msg += messages["drunk_target"].format(target) var.TARGETED[victim] = target pm(cli, victim, nick) if victim in var.HEXED and var.LASTHEXED[victim] in var.ROLES["succubus"]: pm(cli, victim, messages["retract_hex_succubus"].format(var.LASTHEXED[victim])) var.TOBESILENCED.remove(nick) var.HEXED.remove(victim) del var.LASTHEXED[victim] if var.BITE_PREFERENCES.get(victim) in var.ROLES["succubus"]: pm(cli, victim, messages["no_kill_succubus"].format(var.BITE_PREFERENCES[victim])) del var.BITE_PREFERENCES[victim] debuglog("{0} (succubus) VISIT: {1} ({2})".format(nick, victim, get_role(victim))) chk_nightdone(cli) @cmd("pass", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("succubus",)) def pass_cmd(cli, nick, chan, rest): """Do not entrance someone tonight.""" if VISITED.get(nick): pm(cli, nick, messages["succubus_already_visited"].format(VISITED[nick])) return VISITED[nick] = None pm(cli, nick, messages["succubus_pass"]) debuglog("{0} (succubus) PASS".format(nick)) chk_nightdone(cli) @event_listener("harlot_visit") def on_harlot_visit(evt, cli, var, nick, victim): if victim in var.ROLES["succubus"]: pm(cli, nick, messages["notify_succubus_target"].format(victim)) pm(cli, victim, messages["succubus_harlot_success"].format(nick)) ENTRANCED.add(nick) @event_listener("get_random_totem_targets") def on_get_random_totem_targets(evt, var, shaman): if shaman.nick in ENTRANCED: for succubus in get_all_players(("succubus",)): if succubus in evt.data["targets"]: evt.data["targets"].remove(succubus) @event_listener("chk_decision", priority=0) def on_chk_decision(evt, cli, var, force): for votee, voters in evt.data["votelist"].items(): if votee in var.ROLES["succubus"]: for vtr in ENTRANCED: if vtr in voters: voters.remove(vtr) def _kill_entranced_voters(var, votelist, not_lynching, votee): if not var.ROLES["succubus"] & (set(itertools.chain(*votelist.values())) | not_lynching): # none of the succubi voted (or there aren't any succubi), so short-circuit return # kill off everyone entranced that did not follow one of the succubi's votes or abstain # unless a succubus successfully voted the target, then people that didn't follow are spared ENTRANCED_DYING.update(ENTRANCED - var.DEAD) for other_votee, other_voters in votelist.items(): if var.ROLES["succubus"] & set(other_voters): if votee == other_votee: ENTRANCED_DYING.clear() return ENTRANCED_DYING.difference_update(other_voters) if var.ROLES["succubus"] & not_lynching: if votee is None: ENTRANCED_DYING.clear() return ENTRANCED_DYING.difference_update(not_lynching) @event_listener("chk_decision_lynch", priority=5) def on_chk_decision_lynch(evt, cli, var, voters): # a different event may override the original votee, but people voting along with succubus # won't necessarily know that, so base whether or not they risk death on the person originally voted _kill_entranced_voters(var, evt.params.votelist, evt.params.not_lynching, evt.params.original_votee) @event_listener("chk_decision_abstain") def on_chk_decision_abstain(evt, cli, var, not_lynching): _kill_entranced_voters(var, evt.params.votelist, not_lynching, None) # entranced logic should run after team wins have already been determined (aka run last) # we do not want to override the win conditions for neutral roles should they win while entranced # For example, entranced monsters should win with other monsters should mosnters win, and be # properly credited with a team win in that event. @event_listener("player_win", priority=6) def on_player_win(evt, var, user, role, winner, survived): nick = user.nick if nick in ENTRANCED: evt.data["special"].append("entranced") if winner != "succubi" and role not in var.TRUE_NEUTRAL_ROLES: evt.data["won"] = False else: evt.data["iwon"] = True if role == "succubus" and winner == "succubi": evt.data["won"] = True @event_listener("chk_win", priority=2) def on_chk_win(evt, cli, var, rolemap, mainroles, lpl, lwolves, lrealwolves): lsuccubi = len(rolemap.get("succubus", ())) lentranced = len(ENTRANCED - var.DEAD) if lsuccubi and var.PHASE == "day" and lpl - lsuccubi == lentranced: evt.data["winner"] = "succubi" evt.data["message"] = messages["succubus_win"].format(plural("succubus", lsuccubi), plural("has", lsuccubi), plural("master's", lsuccubi)) @event_listener("can_exchange") def on_can_exchange(evt, var, user, target): if user.nick in var.ROLES["succubus"] or target.nick in var.ROLES["succubus"]: evt.prevent_default = True evt.stop_processing = True @event_listener("del_player") def on_del_player(evt, var, user, mainrole, allroles, death_triggers): global ALL_SUCC_IDLE if "succubus" not in allroles: return if user.nick in VISITED: # if it's night, also unentrance the person they visited if var.PHASE == "night" and var.GAMEPHASE == "night": if VISITED[user.nick] in ENTRANCED: ENTRANCED.discard(VISITED[user.nick]) ENTRANCED_DYING.discard(VISITED[user.nick]) pm(user.client, VISITED[user.nick], messages["entranced_revert_win"]) del VISITED[user.nick] # if all succubi are dead, one of two things happen: # 1. if all succubi idled out (every last one of them), un-entrance people # 2. otherwise, kill all entranced people immediately, they still remain entranced (and therefore lose) # death_triggers is False for an idle-out, so we use that to determine which it is if death_triggers: ALL_SUCC_IDLE = False if len(var.ROLES["succubus"]) == 0: entranced_alive = {users._get(x) for x in ENTRANCED}.difference(evt.params.deadlist).intersection(get_players()) # FIXME if ALL_SUCC_IDLE: while ENTRANCED: e = ENTRANCED.pop() pm(user.client, e, messages["entranced_revert_win"]) elif entranced_alive: msg = [] # Run in two loops so we can play the message for everyone dying at once before we actually # kill any of them off (if we killed off first, the message order would be wrong wrt death chains) comma = "" if var.ROLE_REVEAL in ("on", "team"): comma = "," for e in entranced_alive: if var.ROLE_REVEAL in ("on", "team"): role = get_reveal_role(e.nick) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" msg.append("\u0002{0}\u0002, a{1} \u0002{2}\u0002".format(e, an, role)) else: msg.append("\u0002{0}\u0002".format(e)) if len(msg) == 1: channels.Main.send(messages["succubus_die_kill"].format(msg[0] + comma)) elif len(msg) == 2: channels.Main.send(messages["succubus_die_kill"].format(msg[0] + comma + " and " + msg[1] + comma)) else: channels.Main.send(messages["succubus_die_kill"].format(", ".join(msg[:-1]) + ", and " + msg[-1] + comma)) for e in entranced_alive: # to ensure we do not double-kill someone, notify all child deaths that we'll be # killing off everyone else that is entranced so they don't need to bother dlc = list(evt.params.deadlist) dlc.extend(entranced_alive - {e}) debuglog("{0} (succubus) SUCCUBUS DEATH KILL: {1} ({2})".format(user, e, get_main_role(e))) evt.params.del_player(e, end_game=False, killer_role="succubus", deadlist=dlc, original=evt.params.original, ismain=False) evt.data["pl"] = evt.params.refresh_pl(evt.data["pl"]) ENTRANCED_DYING.clear() @event_listener("transition_day_resolve", priority=1) def on_transition_day_resolve(evt, var, victim): if victim.nick in var.ROLES["succubus"] and VISITED.get(victim.nick) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]: # TODO: check if this is necessary for succubus, it's to prevent a message playing if alpha bites # a harlot that is visiting a wolf, since the bite succeeds in that case. if victim not in evt.data["bitten"]: evt.data["message"].append(messages["target_not_home"]) evt.data["novictmsg"] = False evt.stop_processing = True evt.prevent_default = True @event_listener("transition_day_resolve_end", priority=1) def on_transition_day_resolve_end(evt, var, victims): for victim in victims + evt.data["bitten"]: if victim in evt.data["dead"] and victim.nick in VISITED.values() and (victim in evt.data["bywolves"] or victim in evt.data["bitten"]): for succ in VISITED: user = users._get(succ) # FIXME if VISITED[succ] == victim.nick and user not in evt.data["bitten"] and user not in evt.data["dead"]: if var.ROLE_REVEAL in ("on", "team"): evt.data["message"].append(messages["visited_victim"].format(succ, get_reveal_role(succ))) else: evt.data["message"].append(messages["visited_victim_noreveal"].format(succ)) evt.data["bywolves"].add(user) evt.data["onlybywolves"].add(user) evt.data["dead"].append(user) @event_listener("night_acted") def on_night_acted(evt, var, user, actor): if VISITED.get(user.nick): evt.data["acted"] = True @event_listener("chk_nightdone") def on_chk_nightdone(evt, var): evt.data["actedcount"] += len(VISITED) evt.data["nightroles"].extend(get_all_players(("succubus",))) @event_listener("targeted_command") def on_targeted_command(evt, cli, var, cmd, actor, orig_target, tags): if "beneficial" not in tags and actor in ENTRANCED and evt.data["target"] in var.ROLES["succubus"]: try: what = evt.params.action except AttributeError: what = cmd pm(cli, actor, messages["no_acting_on_succubus"].format(what)) evt.stop_processing = True evt.prevent_default = True @event_listener("transition_night_end", priority=2) def on_transition_night_end(evt, var): succubi = get_all_players(("succubus",)) for succubus in succubi: pl = get_players() random.shuffle(pl) pl.remove(succubus) to_send = "succubus_notify" if succubus.prefers_simple(): to_send = "succubus_simple" succ = [] for p in pl: if p in succubi: succ.append("{0} (succubus)".format(p)) else: succ.append(p.nick) succubus.send(messages[to_send], "Players: " + ", ".join(succ), sep="\n") @event_listener("begin_day") def on_begin_day(evt, var): VISITED.clear() ENTRANCED_DYING.clear() @event_listener("transition_day", priority=2) def on_transition_day(evt, var): for v in ENTRANCED_DYING: user = users._get(v) # FIXME var.DYING.add(user) # indicate that the death bypasses protections evt.data["victims"].append(user) evt.data["onlybywolves"].discard(user) # we do not add to killers as retribution totem should not work on entranced not following succubus @event_listener("get_special") def on_get_special(evt, var): evt.data["special"].update(get_players(("succubus",))) @event_listener("vg_kill") def on_vg_kill(evt, var, ghost, target): if ghost.nick in ENTRANCED: evt.data["pl"] -= var.ROLES["succubus"] @event_listener("rename_player") def on_rename(evt, cli, var, prefix, nick): if prefix in ENTRANCED: ENTRANCED.remove(prefix) ENTRANCED.add(nick) if prefix in ENTRANCED_DYING: ENTRANCED_DYING.remove(prefix) ENTRANCED_DYING.add(nick) kvp = {} for a,b in VISITED.items(): s = nick if a == prefix else a t = nick if b == prefix else b kvp[s] = t VISITED.update(kvp) if prefix in VISITED: del VISITED[prefix] @event_listener("reset") def on_reset(evt, var): global ALL_SUCC_IDLE ALL_SUCC_IDLE = True ENTRANCED.clear() ENTRANCED_DYING.clear() VISITED.clear() @event_listener("revealroles") def on_revealroles(evt, var, wrapper): if ENTRANCED: evt.data["output"].append("\u0002entranced players\u0002: {0}".format(", ".join(ENTRANCED))) if ENTRANCED_DYING: evt.data["output"].append("\u0002dying entranced players\u0002: {0}".format(", ".join(ENTRANCED_DYING))) # vim: set sw=4 expandtab: