From ecd68d15bf5caefef815d3fa21e411927b1e7d42 Mon Sep 17 00:00:00 2001 From: "Vgr E. Barry" Date: Tue, 24 Apr 2018 13:24:38 -0400 Subject: [PATCH] Split and convert assassin --- src/roles/assassin.py | 163 ++++++++++++++++++++++++++++++++++++++++++ src/roles/succubus.py | 11 +-- src/roles/villager.py | 6 ++ src/wolfgame.py | 130 ++------------------------------- 4 files changed, 175 insertions(+), 135 deletions(-) create mode 100644 src/roles/assassin.py diff --git a/src/roles/assassin.py b/src/roles/assassin.py new file mode 100644 index 0000000..953c1e2 --- /dev/null +++ b/src/roles/assassin.py @@ -0,0 +1,163 @@ +import re +import random +import itertools +import math +from collections import defaultdict, deque + +import botconfig +from src.utilities import * +from src import channels, users, debuglog, errlog, plog +from src.functions import get_players, get_all_players, get_main_role, get_reveal_role, get_target +from src.decorators import command, event_listener +from src.containers import UserList, UserSet, UserDict, DefaultUserDict +from src.messages import messages +from src.events import Event + +TARGETED = UserDict() # type: Dict[users.User, users.User] + +@command("target", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("assassin",)) +def target(var, wrapper, message): + """Pick a player as your target, killing them if you die.""" + if wrapper.source in TARGETED: + wrapper.send(messages["assassin_already_targeted"]) + return + + target = get_target(var, wrapper, re.split(" +", message)[0]) + if not target: + return + + evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True}) + if not evt.dispatch(var, "target", wrapper.source, target, frozenset({"detrimental"})): + return + target = evt.data["target"] + + TARGETED[wrapper.source] = target + + wrapper.send(messages["assassin_target_success"].format(target)) + + debuglog("{0} (assassin) TARGET: {1} ({2})".format(wrapper.source, target, get_main_role(target))) + +@event_listener("chk_nightdone") +def on_chk_nightdone(evt, var): + evt.data["nightroles"].extend(get_all_players(("assassin",))) + evt.data["actedcount"] += len(TARGETED) + +@event_listener("transition_day", priority=8) +def on_transition_day_resolve(evt, var): + # Select a random target for assassin that isn't already going to die if they didn't target + pl = get_players() + for ass in get_all_players(("assassin",)): + if ass not in TARGETED and ass.nick not in var.SILENCED: + ps = pl[:] + ps.remove(ass) + for victim in set(evt.data["victims"]): + if victim in ps: + ps.remove(victim) + if len(ps) > 0: + target = random.choice(ps) + TARGETED[ass] = target + ass.send(messages["assassin_random"].format(target)) + +@event_listener("transition_night_end") +def on_transition_night_end(evt, var): + for ass in get_all_players(("assassin",)): + if ass in TARGETED: + continue # someone already targeted + + pl = get_players() + random.shuffle(pl) + pl.remove(ass) + + if ass in get_all_players(("village drunk",)): # FIXME: Make into an event when village drunk is split + TARGETED[ass] = random.choice(pl) + message = messages["drunken_assassin_notification"].format(TARGETED[ass]) + if not ass.prefers_simple(): + message += messages["assassin_info"] + ass.send(message) + + else: + if ass.prefers_simple(): + ass.send(messages["assassin_simple"]) + else: + ass.send(messages["assassin_notify"]) + ass.send("Players: " + ", ".join(p.nick for p in pl)) + +@event_listener("del_player") +def on_del_player(evt, var, player, mainrole, allroles, death_triggers): + if player in TARGETED.values(): + for x, y in list(TARGETED.items()): + if y is player: + del TARGETED[x] + + if death_triggers and "assassin" in allroles and player in TARGETED: + target = TARGETED[player] + del TARGETED[player] + if target in evt.data["pl"]: + prots = deque(var.ACTIVE_PROTECTIONS[target.nick]) + aevt = Event("assassinate", {"pl": evt.data["pl"], "target": target}, + del_player=evt.params.del_player, + deadlist=evt.params.deadlist, + original=evt.params.original, + refresh_pl=evt.params.refresh_pl, + message_prefix="assassin_fail_", + source="assassin", + killer=player, + killer_mainrole=mainrole, + killer_allroles=allroles, + prots=prots) + + while len(prots) > 0: + # an event can read the current active protection and cancel the assassination + # 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(var, player, target, prots[0]): + pl = aevt.data["pl"] + if target is not aevt.data["target"]: + target = aevt.data["target"] + prots = deque(var.ACTIVE_PROTECTIONS[target.nick]) + aevt.params.prots = prots + continue + break + prots.popleft() + + if not prots: + if var.ROLE_REVEAL in ("on", "team"): + role = get_reveal_role(target) + an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" + message = messages["assassin_success"].format(player, target, an, role) + else: + message = messages["assassin_success_no_reveal"].format(player, target) + channels.Main.send(message) + debuglog("{0} (assassin) ASSASSINATE: {1} ({2})".format(player, target, get_main_role(target))) + evt.params.del_player(target, end_game=False, killer_role=mainrole, deadlist=evt.params.deadlist, original=evt.params.original, ismain=False) + evt.data["pl"] = evt.params.refresh_pl(aevt.data["pl"]) + +@event_listener("succubus_visit") +def on_succubus_visit(evt, var, actor, target): + if target in TARGETED and TARGETED[target] in get_all_players(("succubus",)): + msg = messages["no_target_succubus"].format(TARGETED[target]) + del TARGETED[target] + if target in get_all_players(("village drunk",)): + victim = random.choice(list(get_all_players() - get_all_players(("succubus",)) - {target})) + msg += messages["drunk_target"].format(victim) + TARGETED[target] = victim + target.send(msg) + +@event_listener("myrole") +def on_myrole(evt, var, user): + if user in get_all_players(("assassin",)): + msg = "" + if user in TARGETED: + msg = messages["assassin_targeting"].format(TARGETED[user]) + user.send(messages["assassin_role_info"].format(msg)) + +@event_listener("revealroles_role") +def on_revealroles_role(evt, var, user, role): + if role == "assassin" and user in TARGETED: + evt.data["special_case"].append("targeting {0}".format(TARGETED[user])) + +@event_listener("reset") +def on_reset(evt, var): + TARGETED.clear() + +# vim: set sw=4 expandtab: diff --git a/src/roles/succubus.py b/src/roles/succubus.py index 1faacc9..2c6c7a9 100644 --- a/src/roles/succubus.py +++ b/src/roles/succubus.py @@ -55,16 +55,7 @@ def hvisit(var, wrapper, message): revt = Event("succubus_visit", {}) revt.dispatch(var, wrapper.source, target) - # TODO: split these into assassin, hag, and alpha wolf when they are split off - if users._get(var.TARGETED.get(target.nick), allow_none=True) in get_all_players(("succubus",)): # FIXME - msg = messages["no_target_succubus"].format(var.TARGETED[target.nick]) - del var.TARGETED[target.nick] - if target in get_all_players(("village drunk",)): - victim = random.choice(list(get_all_players() - get_all_players(("succubus",)) - {target})) - msg += messages["drunk_target"].format(victim) - var.TARGETED[target.nick] = victim.nick - target.send(msg) - + # TODO: split these into hag and alpha wolf when they are split off if target.nick in var.HEXED and users._get(var.LASTHEXED[target.nick]) in get_all_players(("succubus",)): # FIXME target.send(messages["retract_hex_succubus"].format(var.LASTHEXED[target.nick])) var.TOBESILENCED.remove(wrapper.source.nick) diff --git a/src/roles/villager.py b/src/roles/villager.py index 1801a76..0359e03 100644 --- a/src/roles/villager.py +++ b/src/roles/villager.py @@ -8,6 +8,12 @@ from src.events import Event # handles villager and cultist +@event_listener("transition_day", priority=7) +def on_transition_day(evt, var): + for player in var.DYING: + evt.data["victims"].append(player) + evt.data["onlybywolves"].discard(player) + @event_listener("transition_night_end", priority=2) def on_transition_night_end(evt, var): if var.FIRST_NIGHT or var.ALWAYS_PM_ROLE: diff --git a/src/wolfgame.py b/src/wolfgame.py index f33f2dc..f7d6cd1 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -2456,51 +2456,6 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill debuglog("{0} ({1}) LOVE SUICIDE: {2} ({3})".format(lover, get_main_role(lover), player, mainrole)) del_player(lover, end_game=False, killer_role=killer_role, deadlist=deadlist, original=original, ismain=False) pl = refresh_pl(pl) - if "assassin" in allroles: - if player.nick in var.TARGETED: - targetnick = var.TARGETED[player.nick] - del var.TARGETED[player.nick] - if targetnick is None: - target = None - else: - target = users._get(targetnick) # FIXME - if target is not None and target in pl: - prots = deque(var.ACTIVE_PROTECTIONS[target.nick]) - aevt = Event("assassinate", {"pl": pl, "target": target}, - del_player=del_player, - deadlist=deadlist, - original=original, - refresh_pl=refresh_pl, - message_prefix="assassin_fail_", - source="assassin", - killer=player, - killer_mainrole=mainrole, - killer_allroles=allroles, - prots=prots) - while len(prots) > 0: - # an event can read the current active protection and cancel the assassination - # 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(var, player, target, prots[0]): - pl = aevt.data["pl"] - if target is not aevt.data["target"]: - target = aevt.data["target"] - prots = deque(var.ACTIVE_PROTECTIONS[target.nick]) - aevt.params.prots = prots - continue - break - prots.popleft() - if len(prots) == 0: - if var.ROLE_REVEAL in ("on", "team"): - role = get_reveal_role(target) - an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" - message = messages["assassin_success"].format(player, target, an, role) - else: - message = messages["assassin_success_no_reveal"].format(player, target) - channels.Main.send(message) - debuglog("{0} (assassin) ASSASSINATE: {1} ({2})".format(player, target, get_main_role(target))) - del_player(target, end_game=False, killer_role=mainrole, deadlist=deadlist, original=original, ismain=False) - pl = refresh_pl(pl) if mainrole == "time lord": if "DAY_TIME_LIMIT" not in var.ORIGINAL_SETTINGS: var.ORIGINAL_SETTINGS["DAY_TIME_LIMIT"] = var.DAY_TIME_LIMIT @@ -2636,7 +2591,7 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill if var.PHASE in var.GAME_PHASES: # remove the player from variables if they're in there if ret: - for x in (var.OBSERVED, var.TARGETED, var.LASTHEXED): + for x in (var.OBSERVED, var.LASTHEXED): for k in list(x): if player.nick in (k, x[k]): del x[k] @@ -2954,7 +2909,7 @@ def rename_player(var, user, prefix): if prefix in var.PRAYED.keys(): del var.PRAYED[prefix] - for dictvar in (var.OBSERVED, var.TARGETED, var.CLONED, var.LASTHEXED, var.BITE_PREFERENCES): + for dictvar in (var.OBSERVED, var.CLONED, var.LASTHEXED, var.BITE_PREFERENCES): kvp = [] for a,b in dictvar.items(): if a == prefix: @@ -3340,6 +3295,8 @@ def transition_day(gameid=0): # 5 = alpha wolf bite, other custom events that trigger after all protection stuff is resolved # 6 = rearranging victim list (ensure bodyguard/harlot messages plays), # fixing killers dict priority again (in case step 4 or 5 added to it) + # 7 = killer-less deaths (i.e. var.DYING) + # 8 = read-only operations # Actually killing off the victims happens in transition_day_resolve # We set the variables here first; listeners should mutate, not replace # We don't need to use User containers here, as these don't persist long enough @@ -3363,10 +3320,6 @@ def transition_day(gameid=0): }) evt.dispatch(var) - for player in var.DYING: - victims.append(player) - onlybywolves.discard(player) - # remove duplicates victims_set = set(victims) vappend = [] @@ -3474,20 +3427,6 @@ def transition_day(gameid=0): vappend.remove(v) victims.append(v) - # Select a random target for assassin that isn't already going to die if they didn't target - pl = get_players() - for ass in get_all_players(("assassin",)): - if ass.nick not in var.TARGETED and ass.nick not in var.SILENCED: - ps = pl[:] - ps.remove(ass) - for victim in victims: - if victim in ps: - ps.remove(victim) - if len(ps) > 0: - target = random.choice(ps) - var.TARGETED[ass.nick] = target.nick - ass.send(messages["assassin_random"].format(target)) - message = [messages["sunrise"].format(min, sec)] # This needs to go down here since having them be their night value matters above @@ -3747,14 +3686,6 @@ def chk_nightdone(): nightroles = [p for p in nightroles if p.nick not in var.SILENCED] if var.PHASE == "night" and actedcount >= len(nightroles): - if not event.prevent_default: - # check for assassins that have not yet targeted - # must be handled separately because assassin only acts on nights when their target is dead - # and silenced assassin shouldn't add to actedcount - for ass in var.ROLES["assassin"]: - if ass.nick not in var.TARGETED.keys() | var.SILENCED: # FIXME - return - for x, t in var.TIMERS.items(): t[0].cancel() @@ -4584,31 +4515,6 @@ def choose(cli, nick, chan, rest, sendmsg=True): # XXX: transition_day also need debuglog("{0} ({1}) MATCH: {2} ({3}) + {4} ({5})".format(nick, get_role(nick), victim, get_role(victim), victim2, get_role(victim2))) -@cmd("target", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("assassin",)) -def target(cli, nick, chan, rest): - """Pick a player as your target, killing them if you die.""" - if var.TARGETED.get(nick) is not None: - pm(cli, nick, messages["assassin_already_targeted"]) - return - victim = get_victim(cli, nick, re.split(" +",rest)[0], False) - if not victim: - return - - if nick == victim: - pm(cli, nick, messages["no_target_self"]) - return - - if is_safe(nick, victim): - pm(cli, nick, messages["no_acting_on_succubus"].format("target")) - return - - victim = choose_target(nick, victim) - # assassin is a template so it will never get swapped, so don't check for exchanges with it - var.TARGETED[nick] = victim - pm(cli, nick, messages["assassin_target_success"].format(victim)) - - debuglog("{0} (assassin) TARGET: {1} ({2})".format(nick, victim, get_role(victim))) - @cmd("hex", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("hag",)) def hex_target(cli, nick, chan, rest): """Hex someone, preventing them from acting the next day and night.""" @@ -5022,25 +4928,6 @@ def transition_night(): else: lycan.send(messages["lycan_notify"]) - for ass in get_all_players(("assassin",)): - if ass.nick in var.TARGETED and var.TARGETED[ass.nick] is not None: - continue # someone already targeted - pl = ps[:] - random.shuffle(pl) - pl.remove(ass) - if ass in get_all_players(("village drunk",)): - var.TARGETED[ass.nick] = random.choice(pl) - message = messages["drunken_assassin_notification"].format(var.TARGETED[ass.nick]) - if not ass.prefers_simple(): - message += messages["assassin_info"] - ass.send(message) - else: - if ass.prefers_simple(): - ass.send(messages["assassin_simple"]) - else: - ass.send(messages["assassin_notify"]) - ass.send("Players: " + ", ".join(p.nick for p in pl)) - for turncoat in get_all_players(("turncoat",)): # they start out as unsided, but can change n1 if turncoat.nick not in var.TURNCOATS: @@ -5334,7 +5221,6 @@ def start(cli, nick, chan, forced = False, restart = ""): var.GUNNERS.clear() var.OBSERVED = {} var.CLONED = {} - var.TARGETED = {} var.LASTHEXED = {} var.MATCHMAKERS = set() var.SILENCED = set() @@ -6203,10 +6089,6 @@ def myrole(var, wrapper, message): # FIXME: Need to fix !swap once this gets con role = "sharpshooter" wrapper.pm(messages["gunner_simple"].format(role, var.GUNNERS[wrapper.source], "" if var.GUNNERS[wrapper.source] == 1 else "s")) - # Check assassin - if wrapper.source in var.ROLES["assassin"] and wrapper.source not in var.ROLES["amnesiac"]: - wrapper.pm(messages["assassin_role_info"].format(messages["assassin_targeting"].format(var.TARGETED[wrapper.source.nick]) if wrapper.source.nick in var.TARGETED else "")) - # Remind prophet of their role, in sleepy mode only where it is hacked into a template instead of a role if "prophet" in var.TEMPLATE_RESTRICTIONS and wrapper.source in var.ROLES["prophet"]: wrapper.pm(messages["prophet_simple"]) @@ -6684,9 +6566,7 @@ def revealroles(var, wrapper, message): # go through each nickname, adding extra info if necessary for user in users: special_case = [] - if role == "assassin" and user.nick in var.TARGETED: - special_case.append("targeting {0}".format(var.TARGETED[user.nick])) - elif role == "clone" and user.nick in var.CLONED: + if role == "clone" and user.nick in var.CLONED: special_case.append("cloning {0}".format(var.CLONED[user.nick])) # print how many bullets normal gunners have elif (role == "gunner" or role == "sharpshooter") and user in var.GUNNERS: