import math import re import random from collections import defaultdict, deque import src.settings as var from src.utilities import * from src import debuglog, errlog, plog from src.decorators import cmd, event_listener from src.messages import messages from src.events import Event import botconfig KILLS = {} # type: Dict[str, str] TARGETS = {} # type: Dict[str, Set[str]] @cmd("kill", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("dullahan",)) def dullahan_kill(cli, nick, chan, rest): """Kill someone at night as a dullahan until everyone on your list is dead.""" if not TARGETS[nick] & set(list_players()): pm(cli, nick, messages["dullahan_targets_dead"]) return victim = get_victim(cli, nick, re.split(" +",rest)[0], False) if not victim: return if victim == nick: pm(cli, nick, messages["no_suicide"]) return orig = victim evt = Event("targeted_command", {"target": victim, "misdirection": True, "exchange": True}) evt.dispatch(cli, var, "kill", nick, victim, frozenset({"detrimental"})) if evt.prevent_default: return victim = evt.data["target"] KILLS[nick] = victim msg = messages["wolf_target"].format(orig) pm(cli, nick, messages["player"].format(msg)) debuglog("{0} ({1}) KILL: {2} ({3})".format(nick, get_role(nick), victim, get_role(victim))) chk_nightdone(cli) @cmd("retract", "r", chan=False, pm=True, playing=True, phases=("night",), roles=("dullahan",)) def dullahan_retract(cli, nick, chan, rest): """Removes a dullahan's kill selection.""" if nick not in KILLS: return if nick in KILLS: del KILLS[nick] pm(cli, nick, messages["retracted_kill"]) @event_listener("player_win") def on_player_win(evt, cli, var, nick, role, winner, survived): if role != "dullahan": return alive = set(list_players()) if nick in var.ENTRANCED: alive -= var.ROLES["succubus"] if not TARGETS[nick] & alive: evt.data["iwon"] = True @event_listener("del_player") def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers): for h,v in list(KILLS.items()): if v == nick: pm(cli, h, messages["hunter_discard"]) del KILLS[h] elif h == nick: del KILLS[h] if death_triggers and nickrole == "dullahan": pl = evt.data["pl"] targets = TARGETS[nick] & set(pl) if targets: target = random.choice(list(targets)) prots = deque(var.ACTIVE_PROTECTIONS[target]) aevt = Event("assassinate", {"pl": evt.data["pl"]}, del_player=evt.params.del_player, deadlist=evt.params.deadlist, original=evt.params.original, refresh_pl=evt.params.refresh_pl, message_prefix="dullahan_die_", nickrole=nickrole, nicktpls=nicktpls, prots=prots) while len(prots) > 0: # an event can read the current active protection and cancel the totem # if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS # so that it cannot be used again (if the protection is meant to be usable once-only) if not aevt.dispatch(cli, var, nick, target, prots[0]): evt.data["pl"] = aevt.data["pl"] return prots.popleft() if var.ROLE_REVEAL in ("on", "team"): role = get_reveal_role(target) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" cli.msg(botconfig.CHANNEL, messages["dullahan_die_success"].format(nick, target, an, role)) else: cli.msg(botconfig.CHANNEL, messages["dullahan_die_success_noreveal"].format(nick, target)) debuglog("{0} ({1}) DULLAHAN ASSASSINATE: {2} ({3})".format(nick, nickrole, target, get_role(target))) evt.params.del_player(cli, target, True, end_game=False, killer_role=nickrole, deadlist=evt.params.deadlist, original=evt.params.original, ismain=False) evt.data["pl"] = evt.params.refresh_pl(pl) @event_listener("rename_player") def on_rename(evt, cli, var, prefix, nick): kvp = [] for a,b in KILLS.items(): if a == prefix: a = nick if b == prefix: b = nick kvp.append((a,b)) KILLS.update(kvp) if prefix in KILLS: del KILLS[prefix] kvp = [] for a,b in TARGETS.items(): nl = set() for n in b: if n == prefix: n = nick nl.add(n) if a == prefix: a = nick kvp.append((a,nl)) TARGETS.update(kvp) if prefix in TARGETS: del TARGETS[prefix] @event_listener("night_acted") def on_acted(evt, cli, var, nick, sender): if nick in KILLS: evt.data["acted"] = True @event_listener("get_special") def on_get_special(evt, cli, var): evt.data["special"].update(list_players(("dullahan",))) @event_listener("transition_day", priority=2) def on_transition_day(evt, cli, var): for k, d in list(KILLS.items()): evt.data["victims"].append(d) evt.data["onlybywolves"].discard(d) evt.data["killers"][d].append(k) del KILLS[k] @event_listener("exchange_roles") def on_exchange(evt, cli, var, actor, nick, actor_role, nick_role): if actor in KILLS: del KILLS[actor] if nick in KILLS: del KILLS[nick] if actor_role == "dullahan" and nick_role != "dullahan" and actor in TARGETS: TARGETS[nick] = TARGETS[actor] - {nick} del TARGETS[actor] elif nick_role == "dullahan" and actor_role != "dullahan" and nick in TARGETS: TARGETS[actor] = TARGETS[nick] - {actor} del TARGETS[nick] @event_listener("chk_nightdone") def on_chk_nightdone(evt, cli, var): spl = set(list_players()) evt.data["actedcount"] += len(KILLS) for p in var.ROLES["dullahan"]: if TARGETS[p] & spl: evt.data["nightroles"].append(p) @event_listener("transition_night_end", priority=2) def on_transition_night_end(evt, cli, var): for dullahan in var.ROLES["dullahan"]: targets = list(TARGETS[dullahan]) for target in var.DEAD: if target in targets: targets.remove(target) if not targets: # already all dead pm(cli, dullahan, "{0} {1}".format(messages["dullahan_simple"], messages["dullahan_targets_dead"])) continue random.shuffle(targets) if dullahan in var.PLAYERS and not is_user_simple(dullahan): pm(cli, dullahan, messages["dullahan_notify"]) else: pm(cli, dullahan, messages["dullahan_simple"]) t = messages["dullahan_targets"] if var.FIRST_NIGHT else messages["dullahan_remaining_targets"] pm(cli, dullahan, t + ", ".join(targets)) @event_listener("role_assignment") def on_role_assignment(evt, cli, var, gamemode, pl, restart): # assign random targets to dullahan to kill if var.ROLES["dullahan"]: max_targets = math.ceil(8.1 * math.log(len(pl), 10) - 5) for dull in var.ROLES["dullahan"]: TARGETS[dull] = set() dull_targets = Event("dullahan_targets", {"targets": TARGETS}) # support sleepy dull_targets.dispatch(cli, var, var.ROLES["dullahan"], max_targets) for dull, ts in TARGETS.items(): ps = pl[:] ps.remove(dull) while len(ts) < max_targets: target = random.choice(ps) ps.remove(target) ts.add(target) @event_listener("myrole") def on_myrole(evt, cli, var, nick): role = get_role(nick) # Remind dullahans of their targets if role == "dullahan": targets = list(TARGETS[nick]) for target in var.DEAD: if target in targets: targets.remove(target) random.shuffle(targets) if targets: t = messages["dullahan_targets"] if var.FIRST_NIGHT else messages["dullahan_remaining_targets"] evt.data["messages"].append(t + ", ".join(targets)) else: evt.data["messages"].append(messages["dullahan_targets_dead"]) @event_listener("revealroles_role") def on_revealroles_role(evt, cli, var, nickname, role): if role == "dullahan" and nickname in TARGETS: targets = TARGETS[nickname] - var.DEAD if targets: evt.data["special_case"].append("need to kill {0}".format(", ".join(TARGETS[nickname] - var.DEAD))) else: evt.data["special_case"].append("All targets dead") @event_listener("begin_day") def on_begin_day(evt, cli, var): KILLS.clear() @event_listener("reset") def on_reset(evt, var): KILLS.clear() TARGETS.clear() # vim: set sw=4 expandtab: