diff --git a/messages/en.json b/messages/en.json index 078a1b0..032decf 100644 --- a/messages/en.json +++ b/messages/en.json @@ -363,6 +363,7 @@ "investigator_reveal": "Someone accidentally drops a paper. The paper reveals that \u0002{0}\u0002 is the detective!", "harlot_already_visited": "You are already spending the night with \u0002{0}\u0002.", "harlot_success": "You are spending the night with \u0002{0}\u0002. Have a good time!", + "harlot_not_self": "You may not visit yourself. Use \"pass\" to choose to not visit anyone tonight.", "seer_fail": "You may only have one vision per round.", "no_see_self": "Seeing yourself would be a waste.", "seer_success": "You have a vision; in this vision, you see that \u0002{0}\u0002 is a \u0002{1}\u0002!", diff --git a/src/roles/angel.py b/src/roles/angel.py index 3b35644..5516f31 100644 --- a/src/roles/angel.py +++ b/src/roles/angel.py @@ -178,13 +178,6 @@ def on_fagb(evt, cli, var, victim, killer): @event_listener("transition_day_resolve", priority=2) def on_transition_day_resolve(evt, cli, var, victim): - # TODO: remove these checks once everything is split - # right now they're needed because otherwise protection may fire off even if the person isn't home - # that will not be an issue once everything is using the event - if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]: - return - # END checks to remove - if evt.data["protected"].get(victim) == "angel": evt.data["message"].append(messages["angel_protection"].format(victim)) evt.data["novictmsg"] = False diff --git a/src/roles/blessed.py b/src/roles/blessed.py index 02d7ae3..d54fc11 100644 --- a/src/roles/blessed.py +++ b/src/roles/blessed.py @@ -37,13 +37,6 @@ def on_transition_day(evt, cli, var): @event_listener("transition_day_resolve", priority=2) def on_transition_day_resolve(evt, cli, var, victim): - # TODO: remove these checks once everything is split - # right now they're needed because otherwise protection may fire off even if the person isn't home - # that will not be an issue once everything is using the event - if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]: - return - # END checks to remove - if evt.data["protected"].get(victim) == "blessing": # 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 diff --git a/src/roles/harlot.py b/src/roles/harlot.py new file mode 100644 index 0000000..f47c55f --- /dev/null +++ b/src/roles/harlot.py @@ -0,0 +1,165 @@ +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.decorators import cmd, event_listener +from src.messages import messages +from src.events import Event + +VISITED = {} + +@cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("harlot",)) +def hvisit(cli, nick, chan, rest): + """Visit a player. You will die if you visit a wolf or a target of the wolves.""" + + if VISITED.get(nick): + pm(cli, nick, messages["harlot_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["harlot_not_self"]) + return + + evt = Event("targeted_command", {"target": victim, "misdirection": True, "exchange": True}) + evt.dispatch(cli, var, "visit", nick, victim, frozenset({"immediate"})) + if evt.prevent_default: + return + victim = evt.data["target"] + vrole = get_role(victim) + + VISITED[nick] = victim + pm(cli, nick, messages["harlot_success"].format(victim)) + if nick != victim: + pm(cli, victim, messages["harlot_success"].format(nick)) + revt = Event("harlot_visit", {}) + revt.dispatch(cli, var, nick, victim) + + debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, get_role(nick), victim, vrole)) + chk_nightdone(cli) + +@cmd("pass", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("harlot",)) +def pass_cmd(cli, nick, chan, rest): + """Do not visit someone tonight.""" + if VISITED.get(nick): + pm(cli, nick, messages["harlot_already_visited"].format(VISITED[nick])) + return + VISITED[nick] = None + pm(cli, nick, messages["no_visit"]) + debuglog("{0} ({1}) PASS".format(nick, get_role(nick))) + chk_nightdone(cli) + +@event_listener("bite") +def on_bite(evt, cli, var, alpha, target): + if target not in var.ROLES["harlot"]: + return + hvisit = VISITED.get(target) + if hvisit and get_role(hvisit) not in var.WOLFCHAT_ROLES and (hvisit not in evt.param.bywolves or hvisit in evt.param.protected): + evt.data["can_bite"] = False + +@event_listener("transition_day_resolve", priority=1) +def on_transition_day_resolve(evt, cli, var, victim): + if victim in var.ROLES["harlot"] and VISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]: + 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, cli, var, victims): + for victim in victims + evt.data["bitten"]: + if victim in evt.data["dead"] and victim in VISITED.values() and (victim in evt.data["bywolves"] or victim in evt.data["bitten"]): + for hlt in VISITED: + if VISITED[hlt] == victim and hlt not in evt.data["bitten"] and hlt not in evt.data["dead"]: + if var.ROLE_REVEAL in ("on", "team"): + evt.data["message"].append(messages["visited_victim"].format(hlt, get_reveal_role(hlt))) + else: + evt.data["message"].append(messages["visited_victim_noreveal"].format(hlt)) + evt.data["bywolves"].add(hlt) + evt.data["onlybywolves"].add(hlt) + evt.data["dead"].append(hlt) + +@event_listener("transition_day_resolve_end", priority=3) +def on_transition_day_resolve_end3(evt, cli, var, victims): + for harlot in var.ROLES["harlot"]: + if VISITED.get(harlot) in list_players(var.WOLF_ROLES) and harlot not in evt.data["dead"] and harlot not in evt.data["bitten"]: + evt.data["message"].append(messages["harlot_visited_wolf"].format(harlot)) + evt.data["bywolves"].add(harlot) + evt.data["onlybywolves"].add(harlot) + evt.data["dead"].append(harlot) + +@event_listener("night_acted") +def on_night_acted(evt, cli, var, nick, sender): + if VISITED.get(nick): + evt.data["acted"] = True + +@event_listener("chk_nightdone") +def on_chk_nightdone(evt, cli, var): + evt.data["actedcount"] += len(VISITED) + evt.data["nightroles"].extend(var.ROLES["harlot"]) + +@event_listener("exchange_roles") +def on_exchange_roles(evt, cli, var, actor, nick, actor_role, nick_role): + if actor_role == "harlot": + if actor in VISITED: + if VISITED[actor] is not None: + pm(cli, VISITED[actor], messages["harlot_disappeared"].format(actor)) + del VISITED[actor] + if nick_role == "harlot": + if nick in VISITED: + if VISITED[nick] is not None: + pm(cli, VISITED[nick], messages["harlot_disappeared"].format(nick)) + del VISITED[nick] + +@event_listener("transition_night_end", priority=2) +def on_transition_night_end(evt, cli, var): + for harlot in var.ROLES["harlot"]: + pl = list_players() + random.shuffle(pl) + pl.remove(harlot) + if harlot in var.PLAYERS and not is_user_simple(harlot): + pm(cli, harlot, messages["harlot_info"]) + else: + pm(cli, harlot, messages["harlot_simple"]) + pm(cli, harlot, "Players: " + ", ".join(pl)) + +@event_listener("begin_day") +def on_begin_day(evt, cli, var): + VISITED.clear() + +@event_listener("get_special") +def on_get_special(evt, cli, var): + evt.data["special"].update(var.ROLES["harlot"]) + +@event_listener("del_player") +def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers): + if nickrole != "harlot": + return + if nick in VISITED: + del VISITED[nick] + +@event_listener("rename_player") +def on_rename(evt, cli, var, prefix, 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): + VISITED.clear() + +# vim: set sw=4 expandtab: diff --git a/src/roles/shaman.py b/src/roles/shaman.py index cd9352d..6c89374 100644 --- a/src/roles/shaman.py +++ b/src/roles/shaman.py @@ -434,13 +434,6 @@ def on_fagb(evt, cli, var, victim, killer): @event_listener("transition_day_resolve", priority=2) def on_transition_day_resolve2(evt, cli, var, victim): - # TODO: remove these checks once everything is split - # right now they're needed because otherwise protection may fire off even if the person isn't home - # that will not be an issue once everything is using the event - if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]: - return - # END checks to remove - if evt.data["protected"].get(victim) == "totem": evt.data["message"].append(messages["totem_protection"].format(victim)) evt.data["novictmsg"] = False @@ -452,8 +445,6 @@ def on_transition_day_resolve6(evt, cli, var, victim): # TODO: remove these checks once everything is split # right now they're needed because otherwise retribution may fire off when the target isn't actually dying # that will not be an issue once everything is using the event - if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in evt.data["dead"] and victim in evt.data["onlybywolves"]: - return if evt.data["protected"].get(victim): return if victim in var.ROLES["lycan"] and victim in evt.data["onlybywolves"] and victim not in var.IMMUNIZED: diff --git a/src/roles/succubus.py b/src/roles/succubus.py index 699ac7b..0277c63 100644 --- a/src/roles/succubus.py +++ b/src/roles/succubus.py @@ -71,17 +71,24 @@ def hvisit(cli, nick, chan, rest): debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, get_role(nick), victim, vrole)) chk_nightdone(cli) -@cmd("pass", chan=False, pm=True, playing=True, phases=("night",), roles=("succubus",)) +@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 + VISITED[nick] = None pm(cli, nick, messages["succubus_pass"]) debuglog("{0} ({1}) PASS".format(nick, get_role(nick))) chk_nightdone(cli) +@event_listener("harlot_visit") +def on_harlot_visit(evt, cli, var, nick, victim): + if get_role(victim) == "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, cli, var, shaman): if shaman in ENTRANCED: @@ -150,6 +157,12 @@ def on_chk_win(evt, cli, var, rolemap, lpl, lwolves, lrealwolves): 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, actor, nick): + if actor in var.ROLES["succubus"] or nick in var.ROLES["succubus"]: + evt.prevent_default = True + evt.stop_processing = True + @event_listener("del_player") def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers): global ALL_SUCC_IDLE diff --git a/src/wolfgame.py b/src/wolfgame.py index c7ca107..9a991d1 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -3338,7 +3338,6 @@ def begin_day(cli): var.KILLER = "" # nickname of who chose the victim var.HEXED = set() # set of hags that have silenced others var.OBSERVED = {} # those whom werecrows/sorcerers have observed - var.HVISITED = {} # those whom harlots have visited var.PASSED = set() # set of certain roles that have opted not to act var.STARTED_DAY_PLAYERS = len(list_players()) var.SILENCED = copy.copy(var.TOBESILENCED) @@ -3555,18 +3554,19 @@ def transition_day(cli, gameid=0): # note that we cannot bite visiting harlots unless they are visiting a wolf, # and lycans/immunized people turn/die instead of being bitten, so keep the kills valid on those got_bit = False - hvisit = var.HVISITED.get(target) bite_evt = Event("bite", { "can_bite": True, "kill": target in var.ROLES["lycan"] or target in var.LYCANTHROPES or target in var.IMMUNIZED - }) + }, + victims=victims, + killers=killers, + bywolves=bywolves, + onlybywolves=onlybywolves, + protected=protected, + bitten=bitten, + numkills=numkills) bite_evt.dispatch(cli, var, alpha, target) - if ((target not in var.ROLES["harlot"] - or not hvisit - or get_role(hvisit) in var.WOLFCHAT_ROLES - or (hvisit in bywolves and hvisit not in protected)) - and bite_evt.data["can_bite"] - and not bite_evt.data["kill"]): + if bite_evt.data["can_bite"] and not bite_evt.data["kill"]: # mark them as bitten got_bit = True # if they were also being killed by wolves, undo that @@ -3617,13 +3617,14 @@ 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 + # TODO: also needs to be split off into harlot.py + from src.roles import angel, harlot for v in victims_set: if v in var.DYING: victims.append(v) elif v in var.ROLES["bodyguard"] and angel.GUARDED.get(v) in victims_set: vappend.append(v) - elif v in var.ROLES["harlot"] and var.HVISITED.get(v) in victims_set: + elif v in var.ROLES["harlot"] and harlot.VISITED.get(v) in victims_set: vappend.append(v) else: victims.append(v) @@ -3641,7 +3642,7 @@ def transition_day(cli, gameid=0): if v in var.ROLES["bodyguard"] and angel.GUARDED.get(v) not in vappend: vappend.remove(v) victims.append(v) - elif v in var.ROLES["harlot"] and var.HVISITED.get(v) not in vappend: + elif v in var.ROLES["harlot"] and harlot.VISITED.get(v) not in vappend: vappend.remove(v) victims.append(v) @@ -3694,13 +3695,7 @@ def transition_day(cli, gameid=0): for victim in vlist: if not revt.dispatch(cli, var, victim): continue - if victim in var.ROLES["harlot"] and var.HVISITED.get(victim) and victim not in revt.data["dead"] and victim in revt.data["onlybywolves"]: - # alpha wolf can bite a harlot visiting another wolf, don't play a message in that case - # kept as a nested if so that the other victim logic does not run - if victim not in revt.data["bitten"]: - revt.data["message"].append(messages["target_not_home"]) - revt.data["novictmsg"] = False - elif (victim in var.ROLES["lycan"] or victim in var.LYCANTHROPES) and victim in revt.data["onlybywolves"] and victim not in var.IMMUNIZED: + if (victim in var.ROLES["lycan"] or victim in var.LYCANTHROPES) and victim in revt.data["onlybywolves"] and victim not in var.IMMUNIZED: vrole = get_role(victim) if vrole not in var.WOLFCHAT_ROLES: revt.data["message"].append(messages["new_wolf"]) @@ -3785,28 +3780,6 @@ def transition_day(cli, gameid=0): killers = revt2.data["killers"] protected = revt2.data["protected"] bitten = revt2.data["bitten"] - # handle separately so it always happens no matter how victim dies, and so that we can account for bitten victims as well - for victim in victims + bitten: - if victim in dead and victim in var.HVISITED.values() and (victim in bywolves or victim in bitten): # victim was visited by some harlot and victim was attacked by wolves - for hlt in var.HVISITED.keys(): - if var.HVISITED[hlt] == victim and hlt not in bitten and hlt not in dead: - if var.ROLE_REVEAL in ("on", "team"): - message.append(messages["visited_victim"].format(hlt, get_role(hlt))) - else: - message.append(messages["visited_victim_noreveal"].format(hlt)) - bywolves.add(hlt) - onlybywolves.add(hlt) - dead.append(hlt) - - if novictmsg and len(dead) == 0: - message.append(random.choice(messages["no_victims"]) + messages["no_victims_append"]) - - for harlot in var.ROLES["harlot"]: - if var.HVISITED.get(harlot) in list_players(var.WOLF_ROLES) and harlot not in dead and harlot not in bitten: - message.append(messages["harlot_visited_wolf"].format(harlot)) - bywolves.add(harlot) - onlybywolves.add(harlot) - dead.append(harlot) for victim in list(dead): if victim in var.GUNNERS.keys() and var.GUNNERS[victim] > 0 and victim in bywolves: @@ -3911,6 +3884,11 @@ def transition_day(cli, gameid=0): event_end.data["begin_day"](cli) +@event_listener("transition_day_resolve_end", priority=2) +def on_transition_day_resolve_end(evt, cli, var, victims): + if evt.data["novictmsg"] and len(evt.data["dead"]) == 0: + evt.data["message"].append(random.choice(messages["no_victims"]) + messages["no_victims_append"]) + @proxy.impl def chk_nightdone(cli): if var.PHASE != "night": @@ -3918,10 +3896,10 @@ def chk_nightdone(cli): pl = list_players() spl = set(pl) - actedcount = sum(map(len, (var.HVISITED, var.PASSED, var.OBSERVED, + actedcount = sum(map(len, (var.PASSED, var.OBSERVED, var.HEXED, var.CURSED, var.CHARMERS))) - nightroles = get_roles("harlot", "sorcerer", "hag", "warlock", "werecrow", "piper", "prophet") + nightroles = get_roles("sorcerer", "hag", "warlock", "werecrow", "piper", "prophet") for nick, info in var.PRAYED.items(): if info[0] > 0: @@ -4119,8 +4097,9 @@ def check_exchange(cli, actor, nick): #some roles can act on themselves, ignore this if actor == nick: return False - if nick in var.ROLES["succubus"]: - return False # succubus cannot be affected by exchange totem, at least for now + event = Event("can_exchange", {}) + if not event.dispatch(var, actor, nick): + return False # some roles such as succubus cannot be affected by exchange totem if nick in var.EXCHANGED: var.EXCHANGED.remove(nick) actor_role = get_role(actor) @@ -4143,11 +4122,6 @@ def check_exchange(cli, actor, nick): elif actor_role in ("werecrow", "sorcerer"): if actor in var.OBSERVED: del var.OBSERVED[actor] - elif actor_role == "harlot": - if actor in var.HVISITED: - if var.HVISITED[actor] is not None: - pm(cli, var.HVISITED[actor], messages["harlot_disappeared"].format(actor)) - del var.HVISITED[actor] elif actor_role == "hag": if actor in var.LASTHEXED: if var.LASTHEXED[actor] in var.TOBESILENCED and actor in var.HEXED: @@ -4183,11 +4157,6 @@ def check_exchange(cli, actor, nick): elif nick_role in ("werecrow", "sorcerer"): if nick in var.OBSERVED: del var.OBSERVED[nick] - elif nick_role == "harlot": - if nick in var.HVISITED: - if var.HVISITED[nick] is not None: - pm(cli, var.HVISITED[nick], messages["harlot_disappeared"].format(nick)) - del var.HVISITED[nick] elif nick_role == "hag": if nick in var.LASTHEXED: if var.LASTHEXED[nick] in var.TOBESILENCED and nick in var.HEXED: @@ -4645,39 +4614,6 @@ def pray(cli, nick, chan, rest): chk_nightdone(cli) -@cmd("visit", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("harlot",)) -def hvisit(cli, nick, chan, rest): - """Visit a player. You will die if you visit a wolf or a target of the wolves.""" - role = get_role(nick) - - if var.HVISITED.get(nick): - pm(cli, nick, messages["harlot_already_visited"].format(var.HVISITED[nick])) - return - victim = get_victim(cli, nick, re.split(" +",rest)[0], False, True) - if not victim: - return - - if nick == victim: # Staying home (same as calling pass, so call pass) - pass_cmd.func(cli, nick, chan, "") # XXX: Old API - return - else: - victim = choose_target(nick, victim) - if check_exchange(cli, nick, victim): - return - var.HVISITED[nick] = victim - pm(cli, nick, messages["harlot_success"].format(victim)) - if nick != victim: #prevent luck/misdirection totem weirdness - pm(cli, victim, messages["harlot_success"].format(nick)) - # TODO: turn this into an event once harlot is split off - if get_role(victim) == "succubus": - pm(cli, nick, messages["notify_succubus_target"].format(victim)) - pm(cli, victim, messages["succubus_harlot_success"].format(nick)) - from src.roles import succubus - succubus.ENTRANCED.add(nick) - - debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, role, victim, get_role(victim))) - chk_nightdone(cli) - @cmd("give", chan=False, pm=True, playing=True, silenced=True, phases=("day",), roles=("doctor",)) @cmd("immunize", "immunise", chan=False, pm=True, playing=True, silenced=True, phases=("day",), roles=("doctor",)) def immunize(cli, nick, chan, rest): @@ -4752,8 +4688,8 @@ def bite_cmd(cli, nick, chan, rest): chk_nightdone(cli) @cmd("pass", chan=False, pm=True, playing=True, phases=("night",), - roles=("harlot", "turncoat", "warlock")) -def pass_cmd(cli, nick, chan, rest): # XXX: hvisit (3 functions above this one) also needs updating alongside this + roles=("turncoat", "warlock")) +def pass_cmd(cli, nick, chan, rest): """Decline to use your special power for that night.""" nickrole = get_role(nick) @@ -4765,13 +4701,7 @@ def pass_cmd(cli, nick, chan, rest): # XXX: hvisit (3 functions above this one) cli.notice(nick, messages["silenced"]) return - if nickrole == "harlot": - if var.HVISITED.get(nick): - pm(cli, nick, (messages["harlot_already_visited"]).format(var.HVISITED[nick])) - return - var.HVISITED[nick] = None - pm(cli, nick, messages["no_visit"]) - elif nickrole == "turncoat": + if nickrole == "turncoat": if var.TURNCOATS[nick][1] == var.NIGHT_COUNT: # theoretically passing would revert them to how they were before, but # we aren't tracking that, so just tell them to change it back themselves. @@ -5348,16 +5278,6 @@ def transition_night(cli): # send PMs ps = list_players() - for harlot in var.ROLES["harlot"]: - pl = ps[:] - random.shuffle(pl) - pl.remove(harlot) - if harlot in var.PLAYERS and not is_user_simple(harlot): - pm(cli, harlot, messages["harlot_info"]) - else: - pm(cli, harlot, messages["harlot_simple"]) # !simple - pm(cli, harlot, "Players: " + ", ".join(pl)) - for pht in var.ROLES["prophet"]: chance1 = math.floor(var.PROPHET_REVEALED_CHANCE[0] * 100) chance2 = math.floor(var.PROPHET_REVEALED_CHANCE[1] * 100)