From bfc675e953485e7d4ab942a35d672f6ebbd73e9f Mon Sep 17 00:00:00 2001 From: skizzerz Date: Fri, 23 Sep 2016 20:10:04 -0500 Subject: [PATCH] Split fallen angel Also fixes some bugs with using stop_propagation instead of stop_processing in events (the former does absolutely nothing). Added a skeleton file to assist with adding new roles, contains the needed imports on top and vim modeline on the bottom. Yes, these are all related and need to go in the same commit, stop throwing things at me. --- src/roles/angel.py | 34 +++++++++++-- src/roles/blessed.py | 8 +-- src/roles/fallenangel.py | 54 ++++++++++++++++++++ src/roles/shaman.py | 20 ++++++-- src/roles/skel.py | 17 +++++++ src/roles/wolf.py | 1 + src/wolfgame.py | 104 +++------------------------------------ 7 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 src/roles/fallenangel.py create mode 100644 src/roles/skel.py diff --git a/src/roles/angel.py b/src/roles/angel.py index 6da32db..2b4c877 100644 --- a/src/roles/angel.py +++ b/src/roles/angel.py @@ -147,6 +147,32 @@ def on_transition_day(evt, cli, var): if GUARDED.get(g) == v: var.ACTIVE_PROTECTIONS[v].append("bodyguard") +@event_listener("fallen_angel_guard_break") +def on_fagb(evt, cli, var, victim, killer): + for g in var.ROLES["guardian angel"]: + if GUARDED.get(g) == victim: + if random.random() < var.FALLEN_ANGEL_KILLS_GUARDIAN_ANGEL_CHANCE: + if g in evt.data["protected"]: + del evt.data["protected"][g] + evt.data["bywolves"].add(g) + if g not in evt.data["victims"]: + evt.data["onlybywolves"].add(g) + evt.data["victims"].append(g) + evt.data["killers"][g].append(killer) + if g != victim: + pm(cli, g, messages["fallen_angel_success"].format(victim)) + for g in var.ROLES["bodyguard"]: + if GUARDED.get(g) == victim: + if g in evt.data["protected"]: + del evt.data["protected"][g] + evt.data["bywolves"].add(g) + if g not in evt.data["victims"]: + evt.data["onlybywolves"].add(g) + evt.data["victims"].append(g) + evt.data["killers"][g].append(killer) + if g != victim: + pm(cli, g, messages["fallen_angel_success"].format(victim)) + @event_listener("transition_day_resolve", priority=2) def on_transition_day_resolve(evt, cli, var, victim): # TODO: remove these checks once everything is split @@ -159,7 +185,7 @@ def on_transition_day_resolve(evt, cli, var, victim): if evt.data["protected"].get(victim) == "angel": evt.data["message"].append(messages["angel_protection"].format(victim)) evt.data["novictmsg"] = False - evt.stop_propagation = True + evt.stop_processing = True evt.prevent_default = True elif evt.data["protected"].get(victim) == "bodyguard": for bodyguard in var.ROLES["bodyguard"]: @@ -167,7 +193,7 @@ def on_transition_day_resolve(evt, cli, var, victim): evt.data["dead"].append(bodyguard) evt.data["message"].append(messages["bodyguard_protection"].format(bodyguard)) evt.data["novictmsg"] = False - evt.stop_propagation = True + evt.stop_processing = True evt.prevent_default = True break @@ -246,12 +272,12 @@ def on_assassinate(evt, cli, var, nick, target, prot): if prot == "angel" and var.GAMEPHASE == "night": var.ACTIVE_PROTECTIONS[target].remove("angel") evt.prevent_default = True - evt.stop_propagation = True + evt.stop_processing = True cli.msg(botconfig.CHANNEL, messages[evt.params.message_prefix + "angel"].format(nick, target)) elif prot == "bodyguard": var.ACTIVE_PROTECTIONS[target].remove("bodyguard") evt.prevent_default = True - evt.stop_propagation = True + evt.stop_processing = True for bg in var.ROLES["bodyguard"]: if GUARDED.get(bg) == target: cli.msg(botconfig.CHANNEL, messages[evt.params.message_prefix + "bodyguard"].format(nick, target, bg)) diff --git a/src/roles/blessed.py b/src/roles/blessed.py index 111e512..03f8845 100644 --- a/src/roles/blessed.py +++ b/src/roles/blessed.py @@ -48,7 +48,7 @@ def on_transition_day_resolve(evt, cli, var, victim): # don't play any special message for a blessed target, this means in a game with priest and monster it's not really possible # for wolves to tell which is which. May want to change that in the future to be more obvious to wolves since there's not really # any good reason to hide that info from them. In any case, we don't want to say the blessed person was attacked to the channel - evt.stop_propagation = True + evt.stop_processing = True evt.prevent_default = True @event_listener("transition_night_end", priority=5) @@ -65,21 +65,21 @@ def on_desperation(evt, cli, var, votee, target, prot): if prot == "blessing": var.ACTIVE_PROTECTIONS[target].remove("blessing") evt.prevent_default = True - evt.stop_propagation = True + evt.stop_processing = True @event_listener("retribution_totem") def on_retribution(evt, cli, var, victim, loser, prot): if prot == "blessing": var.ACTIVE_PROTECTIONS[target].remove("blessing") evt.prevent_default = True - evt.stop_propagation = True + evt.stop_processing = True @event_listener("assassinate") def on_assassinate(evt, cli, var, nick, target, prot): if prot == "blessing": var.ACTIVE_PROTECTIONS[target].remove("blessing") evt.prevent_default = True - evt.stop_propagation = True + evt.stop_processing = True # don't message the channel whenever a blessing blocks a kill, but *do* let the killer know so they don't try to report it as a bug pm(cli, nick, messages["assassin_fail_blessed"].format(target)) diff --git a/src/roles/fallenangel.py b/src/roles/fallenangel.py new file mode 100644 index 0000000..9693d91 --- /dev/null +++ b/src/roles/fallenangel.py @@ -0,0 +1,54 @@ +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 debuglog, errlog, plog +from src.decorators import cmd, event_listener +from src.messages import messages +from src.events import Event + +@event_listener("transition_day", priority=4.8) +def on_transition_day(evt, cli, var): + # now that all protections are finished, add people back to onlybywolves + # if they're down to 1 active kill and wolves were a valid killer + # TODO: split out var.ENTRANCED_DYING when succubus is split + # that should probably be a priority 4.7 listener + victims = set(list_players()) & set(evt.data["victims"]) - var.DYING - var.ENTRANCED_DYING + for v in victims: + if evt.data["numkills"][v] == 1 and v in evt.data["bywolves"]: + evt.data["onlybywolves"].add(v) + + if len(var.ROLES["fallen angel"]) > 0: + for p, t in list(evt.data["protected"].items()): + if p in evt.data["bywolves"]: + if p in evt.data["protected"]: + pm(cli, p, messages["fallen_angel_deprotect"]) + + # let other roles do special things when we bypass their guards + killer = random.choice(list(var.ROLES["fallen angel"])) + fevt = Event("fallen_angel_guard_break", evt.data) + fevt.dispatch(cli, var, p, killer) + + if p in evt.data["protected"]: + del evt.data["protected"][p] + if p in var.ACTIVE_PROTECTIONS: + del var.ACTIVE_PROTECTIONS[p] + # mark kill as performed by a random FA + # this is important as there may otherwise be no killers if every kill was blocked + evt.data["killers"][p].append(killer) + +@event_listener("assassinate", priority=1) +def on_assassinate(evt, cli, var, nick, target, prot): + # bypass all protection if FA is doing the killing + # we do this by stopping propagation, meaning future events won't fire + if evt.params.nickrole == "fallen angel": + evt.params.prots.clear() + evt.stop_processing = True + evt.prevent_default = True + +# vim: set sw=4 expandtab: diff --git a/src/roles/shaman.py b/src/roles/shaman.py index f82ecee..2f9b0e0 100644 --- a/src/roles/shaman.py +++ b/src/roles/shaman.py @@ -49,6 +49,7 @@ DECEIT = set() # type: Set[str] # holding vars that don't persist long enough to need special attention in # reset/exchange/nickchange havetotem = [] # type: List[str] +brokentotem = set() # type: Set[str] @cmd("give", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=var.TOTEM_ORDER) @cmd("totem", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=var.TOTEM_ORDER) @@ -227,7 +228,7 @@ def on_chk_decision(evt, cli, var, force): @event_listener("chk_decision", priority=1.1) def on_hurry_up(evt, cli, var, force): if evt.params.timeout: - evt.stop_propagation = True + evt.stop_processing = True @event_listener("chk_decision_abstain") def on_chk_decision_abstain(evt, cli, var, nl): @@ -267,7 +268,7 @@ def on_chk_decision_lynch3(evt, cli, var, voters): cli.msg(botconfig.CHANNEL, messages["totem_reveal"].format(votee, an, role)) evt.data["votee"] = None evt.prevent_default = True - evt.stop_propagation = True + evt.stop_processing = True @event_listener("chk_decision_lynch", priority=5) def on_chk_decision_lynch5(evt, cli, var, voters): @@ -388,6 +389,7 @@ def on_transition_day_begin2(evt, cli, var): # In transition_day_end we report who was given totems based on havetotem. # Fallen angel messes with this list, hence why it is separated from LASTGIVEN # and calculated here. + brokentotem.clear() havetotem.clear() havetotem.extend(sorted(filter(None, LASTGIVEN.values()))) @@ -424,6 +426,14 @@ def on_transition_day3(evt, cli, var): for i in range(0, numtotems): var.ACTIVE_PROTECTIONS[v].append("totem") +@event_listener("fallen_angel_guard_break") +def on_fagb(evt, cli, var, victim, killer): + # we'll never end up killing a shaman who gave out protection, but delete the totem since + # story-wise it gets demolished at night by the FA + while victim in havetotem: + havetotem.remove(victim) + brokentotem.add(victim) + @event_listener("transition_day_resolve", priority=2) def on_transition_day_resolve2(evt, cli, var, victim): # TODO: remove these checks once everything is split @@ -436,7 +446,7 @@ def on_transition_day_resolve2(evt, cli, var, victim): if evt.data["protected"].get(victim) == "totem": evt.data["message"].append(messages["totem_protection"].format(victim)) evt.data["novictmsg"] = False - evt.stop_propagation = True + evt.stop_processing = True evt.prevent_default = True @event_listener("transition_day_resolve", priority=6) @@ -495,6 +505,8 @@ def on_transition_day_end(evt, cli, var): ntotems = len(list(tlist)) message.append(messages["totem_posession"].format( player, "ed" if player not in list_players() else "s", "a" if ntotems == 1 else "\u0002{0}\u0002".format(ntotems), "s" if ntotems > 1 else "")) + for player in brokentotem: + message.append(messages["totem_broken"].format(player)) cli.msg(botconfig.CHANNEL, "\n".join(message)) @event_listener("transition_night_end", priority=2.01) @@ -570,7 +582,7 @@ def on_assassinate(evt, cli, var, nick, target, prot): if prot == "totem": var.ACTIVE_PROTECTIONS[target].remove("totem") evt.prevent_default = True - evt.stop_propagation = True + evt.stop_processing = True cli.msg(botconfig.CHANNEL, messages[evt.params.message_prefix + "totem"].format(nick, target)) @event_listener("myrole") diff --git a/src/roles/skel.py b/src/roles/skel.py new file mode 100644 index 0000000..a7cbc15 --- /dev/null +++ b/src/roles/skel.py @@ -0,0 +1,17 @@ +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 debuglog, errlog, plog +from src.decorators import cmd, event_listener +from src.messages import messages +from src.events import Event + +# Skeleton file for new roles, basically to get all the imports right and stuff + +# vim: set sw=4 expandtab: diff --git a/src/roles/wolf.py b/src/roles/wolf.py index 5c9657c..6124e91 100644 --- a/src/roles/wolf.py +++ b/src/roles/wolf.py @@ -171,6 +171,7 @@ def on_transition_day(evt, cli, var): # this should be moved to an event in kill, where monster prefixes their nick with ! # and fallen angel subsequently removes the ! prefix + # TODO: when monster is split off if len(var.ROLES["fallen angel"]) == 0: for monster in var.ROLES["monster"]: if monster in evt.data["victims"]: diff --git a/src/wolfgame.py b/src/wolfgame.py index f153fe7..c792318 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -2874,14 +2874,12 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death deadlist=deadlist, original=original, refresh_pl=refresh_pl, - message_prefix="assassin_fail_") + message_prefix="assassin_fail_", + nickrole=nickrole, + nicktpls=nicktpls, + prots=prots) while len(prots) > 0: - # FA bypasses all protection (TODO: split off) - # when split instead of setting prots to [] will need to stop_propagation but NOT prevent_default - if nickrole == "fallen angel": - prots = [] - break - # an event can read the current active protection and cancel the totem + # 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(cli, var, nick, target, prots[0]): @@ -3896,58 +3894,6 @@ def transition_day(cli, gameid=0): victims_set = set(victims) vappend = [] - # Logic out stacked kills and protections. If we get down to 1 kill remaining that is valid and the victim is in bywolves, - # we re-add them to onlybywolves to indicate that the other kill attempts were guarded against (and the wolf kill is what went through) - # If protections >= kills, we keep track of which protection message to show (prot totem > GA > bodyguard > blessing) - # TODO: split out adding people back to onlybywolves as part of splitting off FA - pl = list_players() - for v in pl: - if v in victims_set: - if v in var.DYING: - continue # dying by themselves, not killed by wolves - if numkills[v] == 1 and v in bywolves: - onlybywolves.add(v) - - fallenkills = set() - brokentotem = set() - from src.roles.shaman import havetotem - from src.roles import angel - if len(var.ROLES["fallen angel"]) > 0: - for p, t in list(protected.items()): - if p in bywolves: - for g in var.ROLES["guardian angel"]: - if angel.GUARDED.get(g) == p and random.random() < var.FALLEN_ANGEL_KILLS_GUARDIAN_ANGEL_CHANCE: - if g in protected: - del protected[g] - bywolves.add(g) - victims.append(g) - fallenkills.add(g) - if g not in victims_set: - victims_set.add(g) - onlybywolves.add(g) - for g in var.ROLES["bodyguard"]: - if angel.GUARDED.get(g) == p: - if g in protected: - del protected[g] - bywolves.add(g) - victims.append(g) - fallenkills.add(g) - if g not in victims_set: - victims_set.add(g) - onlybywolves.add(g) - # we'll never end up killing a shaman who gave out protection, but delete the totem since - # story-wise it gets demolished at night by the FA - while p in havetotem: - havetotem.remove(p) - brokentotem.add(p) - if p in protected: - del protected[p] - if p in var.ACTIVE_PROTECTIONS: - del var.ACTIVE_PROTECTIONS[p] - # mark kill as performed by a random FA - # this is important as there may otherwise be no killers if every kill was blocked - killers[p].append(random.choice(list(var.ROLES["fallen angel"]))) - # set to True if we play chilling howl message due to a bitten person turning new_wolf = False if var.ALPHA_ENABLED: # check for bites @@ -4019,6 +3965,7 @@ def transition_day(cli, gameid=0): # that assumes they die en route to the wolves (and thus don't shoot/give out gun/etc.) # TODO: this needs to be split off into angel.py, but all the stuff above it needs to be split off first # so even though angel.py exists we can't exactly do this now + from src.roles import angel for v in victims_set: if v in var.DYING: victims.append(v) @@ -4046,40 +3993,6 @@ def transition_day(cli, gameid=0): vappend.remove(v) victims.append(v) - # If FA is killing through a guard, let them as well as the victim know so they don't - # try to report the extra kills as a bug - fallenmsg = set() - if len(var.ROLES["fallen angel"]) > 0: - for v in fallenkills: - t = angel.GUARDED.get(v) - if v not in fallenmsg: - fallenmsg.add(v) - if v != t: - pm(cli, v, (messages["fallen_angel_success"]).format(t)) - else: - pm(cli, v, messages["fallen_angel_deprotect"]) - if v != t and t not in fallenmsg: - fallenmsg.add(t) - pm(cli, t, messages["fallen_angel_deprotect"]) - # Also message GAs that don't die and their victims - for g in var.ROLES["guardian angel"]: - v = angel.GUARDED.get(g) - if v in bywolves and g not in fallenkills: - if g not in fallenmsg: - fallenmsg.add(g) - if g != v: - pm(cli, g, messages["fallen_angel_success"].format(v)) - else: - pm(cli, g, messages["fallen_angel_deprotect"]) - if g != v and v not in fallenmsg: - fallenmsg.add(v) - pm(cli, v, messages["fallen_angel_deprotect"]) - # Finally, message blessed people that aren't otherwise being guarded by a GA or bodyguard - for v in bywolves: - if v not in fallenmsg and v in var.ROLES["blessed villager"]: - fallenmsg.add(v) - pm(cli, v, messages["fallen_angel_deprotect"]) - # Select a random target for assassin that isn't already going to die if they didn't target pl = list_players() for ass in var.ROLES["assassin"]: @@ -4329,11 +4242,6 @@ def transition_day(cli, gameid=0): if deadperson in list_players(): del_player(cli, deadperson, end_game=False, killer_role=killer_role[deadperson], deadlist=dead, original=deadperson) - message = [] - for brokentotem in brokentotem: - message.append(messages["totem_broken"].format(brokentotem)) - cli.msg(chan, "\n".join(message)) - event_end = Event("transition_day_end", {"begin_day": begin_day}) event_end.dispatch(cli, var)