diff --git a/messages/en.json b/messages/en.json index 7c7f456..4fa31d0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -651,12 +651,9 @@ "already_prayed": "You are exhausted and unable to receive any more visions tonight.", "specific_invalid_role": "\u0002{0}\u0002 is not a valid role.", "vision_only_role_self": "You receive a vision that you are the only \u0002{0}\u0002.", - "vision_no_more_role": "You receive a vision that there are no other \u0002{0}\u0002.", - "vision_players": "You receive a vision that at least one of these people is a \u0002{0}\u0002: ", - "vision_prophet": "You receive a vision that \u0002{0}\u0002 is the prophet!", - "vision_role": "You receive a vision that \u0002{0}\u0002 is a \u0002{1}\u0002.", + "vision_players": "You receive a vision that at least one of these people is a{1} \u0002{0}\u0002: {2}", + "vision_role": "You receive a vision that \u0002{2}\u0002 is a{1} \u0002{0}\u0002.", "vision_none": "You receive a vision that there are no \u0002{0}\u0002.", - "vision_recovering": "You are still recovering from your previous vision and are unable to receive any more visions tonight.", "succubus_already_visited": "You are already entrancing \u0002{0}\u0002 tonight.", "succubus_not_self": "You may not entrance yourself. Use \"pass\" to not entrance anyone tonight.", "notify_succubus_target": "You have become entranced by \u0002{0}\u0002. From this point on, you must vote along with them or risk dying. You \u0002cannot win with your own team\u0002, but you will win should all alive players become entranced.", @@ -680,9 +677,7 @@ "shaman_turn": "As you were out delivering your totem last night, a large werewolf overpowered and bit you. Shortly thereafter, you found yourself transforming into a wolf yourself! Your mind floods with new wicked ideas for totems.", "no_longer_entranced": "You are no longer entranced.", "doomsayer_notify": "You are a \u0002doomsayer\u0002. You can see how bad luck will befall someone at night by using \"see \" on them. You may also use \"kill \" to kill a villager.", - "prophet_notify_both": "You are a \u0002prophet\u0002. Each night you may pray up to twice to learn one player who has a particular role. The first time, you are given a list of players and have a{0} {1}% chance of revealing yourself to someone with that role. If you did not reveal yourself, you may pray again to obtain the exact player name with a{2} {3}% chance of revealing yourself. Use \"pray \" in PM to learn who has that role.", - "prophet_notify_second": "You are a \u0002prophet\u0002. Each night you may pray up to twice to learn one player who has a particular role. The first time, you are given a list of players with that role. You may pray again to obtain the exact player name, however this has a{0} {1}% chance of revealing yourself to that player. Use \"pray \" in PM to learn who has that role.", - "prophet_notify_none": "You are a \u0002prophet\u0002. Each night you may pray to learn one player who has a particular role. Use \"pray \" in PM to learn who has that role.", + "prophet_notify": "You are a \u0002prophet\u0002. Each night you may pray to learn one player who has a particular role. Use \"pray \" in PM to learn who has that role.", "prophet_simple": "You are a \u0002prophet\u0002.", "dullahan_targets_dead": "All your targets are already dead!", "dullahan_notify": "You are a \u0002dullahan\u0002. Every night, you may kill someone by using \"kill \". You win when all your targets are dead.", diff --git a/src/roles/_mystic_helper.py b/src/roles/_mystic_helper.py index 857423f..5cb2d28 100644 --- a/src/roles/_mystic_helper.py +++ b/src/roles/_mystic_helper.py @@ -21,7 +21,7 @@ def setup_variables(rolename, *, send_role, types): @event_listener("transition_night_end") def on_transition_night_end(evt, var): - villagers = set(get_players(("priest", "prophet", "doctor"))) + villagers = set(get_players(("priest", "doctor"))) win_stealers = set(get_players(("fool", "monster", "demoniac"))) neutrals = set(get_players(("turncoat", "clone", "jester"))) diff --git a/src/roles/prophet.py b/src/roles/prophet.py new file mode 100644 index 0000000..a2eb901 --- /dev/null +++ b/src/roles/prophet.py @@ -0,0 +1,115 @@ +import re +import random +import itertools +import math +from collections import defaultdict + +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 + +PRAYED = UserSet() # type: Set[users.User] + +@command("pray", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("prophet",)) +def pray(var, wrapper, message): + """Receive divine visions of who has a role.""" + if wrapper.source in PRAYED: + wrapper.pm(messages["already_prayed"]) + return + + what = re.split(" +", message)[0] + if not what: + wrapper.pm(messages["not_enough_parameters"]) + return + + # complete this as a match with other roles (so "cursed" can match "cursed villager" for instance) + role = complete_one_match(what.lower(), {p for p in var.ROLE_GUIDE if p not in var.TEMPLATE_RESTRICTIONS}) + if role is None and what.lower() in var.ROLE_ALIASES: + role = var.ROLE_ALIASES[what.lower()] + if role in var.TEMPLATE_RESTRICTIONS: # allow only main roles + role = None + if role is None: + # typo, let them fix it + wrapper.pm(messages["specific_invalid_role"].format(what)) + return + + # get a list of all roles actually in the game, including roles that amnesiacs will be turning into + # (amnesiacs are special since they're also listed as amnesiac; that way a prophet can see both who the + # amnesiacs themselves are as well as what they'll become) + pl = get_players() + from src.roles.amnesiac import ROLES as amn_roles + valid_roles = {r for p, r in amn_roles.items() if p in pl}.union(var.MAIN_ROLES.values()) + + PRAYED.add(wrapper.source) + + if role in valid_roles: + # this sees through amnesiac, so the amnesiac's final role counts as their role + # also, if we're the only person with that role, say so + people = set(get_all_players((role,))) | {p for p, r in amn_roles.items() if p in pl and r == role} + if len(people) == 1 and wrapper.source in people: + wrapper.pm(messages["vision_only_role_self"].format(role)) + PRAYED.add(wrapper.source) + debuglog("{0} (prophet) PRAY {1} - ONLY".format(wrapper.source, role)) + return + + target = random.choice(list(people)) + part = random.sample([p for p in pl if p is not wrapper.source], len(pl) // 3) + if target not in part: + part[0] = target + random.shuffle(part) + part = [p.nick for p in part] + + an = "" + if role.startswith(("a", "e", "i", "o", "u")): + an = "n" + + key = "vision_players" + if len(part) == 1: + key = "vision_role" + + if len(part) > 2: + msg = "{0}, and {1}".format(", ".join(part[:-1]), part[-1]) + else: + msg = " and ".join(part) + + wrapper.pm(messages[key].format(role, an, msg)) + debuglog("{0} (prophet) PRAY {1} ({2})".format(wrapper.source, role, target)) + + else: + # role is not in this game, this still counts as a successful activation of the power! + wrapper.pm(messages["vision_none"].format(plural(role))) + debuglog("{0} (prophet) PRAY {1} - NONE".format(wrapper.source, role)) + +@event_listener("transition_night_end") +def on_transition_night_end(evt, var): + for pht in get_all_players(("prophet",)): + if pht.prefers_simple(): + pht.send(messages["prophet_simple"]) + else: + pht.send(messages["prophet_notify"]) + +@event_listener("chk_nightdone") +def on_chk_nightdone(evt, var): + evt.data["nightroles"].extend(get_all_players(("prophet",))) + evt.data["actedcount"] += len(PRAYED) + +@event_listener("night_acted") +def on_night_acted(evt, var, spy, user): + if user in PRAYED: + evt.data["acted"] = True + +@event_listener("get_special") +def on_get_special(evt, var): + evt.data["villagers"].update(get_players(("prophet",))) + +@event_listener("begin_day") +def on_begin_day(evt, var): + PRAYED.clear() + +@event_listener("reset") +def on_reset(evt, var): + PRAYED.clear() diff --git a/src/settings.py b/src/settings.py index 0420a0f..1ef53c2 100644 --- a/src/settings.py +++ b/src/settings.py @@ -158,9 +158,6 @@ DETECTIVE_REVEALED_CHANCE = 2/5 SHARPSHOOTER_CHANCE = 1/5 # if sharpshooter is enabled, chance that a gunner will become a sharpshooter instead FALLEN_ANGEL_KILLS_GUARDIAN_ANGEL_CHANCE = 1/2 - # HALF FULL -PROPHET_REVEALED_CHANCE = ( 2/5 , 4/5 ) - AMNESIAC_NIGHTS = 3 # amnesiac gets to know their actual role on this night DOCTOR_IMMUNIZATION_MULTIPLIER = 0.135 # ceil(num_players * multiplier) = number of immunizations diff --git a/src/wolfgame.py b/src/wolfgame.py index e96f125..edd9919 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -614,7 +614,7 @@ def replace(var, wrapper, message): channels.Main.mode(("-v", target), ("+v", wrapper.source)) channels.Main.send(messages["player_swap"].format(wrapper.source, target)) - myrole.caller(wrapper.source.client, wrapper.source.nick, wrapper.target.name, "") # FIXME: Old API + myrole.func(var, wrapper, "") @command("pingif", "pingme", "pingat", "pingpref", pm=True) @@ -2793,24 +2793,6 @@ def rename_player(var, user, prefix): if prefix == k: var.PLAYERS[nick] = var.PLAYERS.pop(k) - kvp = [] - # Looks like {'nick': [_, 'nick1', _, {'nick2': [_]}]} - for a,b in var.PRAYED.items(): - kvp2 = [] - if a == prefix: - a = nick - if b[1] == prefix: - b[1] = nick - for c,d in b[3].items(): - if c == prefix: - c = nick - kvp2.append((c,d)) - b[3].update(kvp2) - kvp.append((a,b)) - var.PRAYED.update(kvp) - if prefix in var.PRAYED.keys(): - del var.PRAYED[prefix] - for dictvar in (var.OBSERVED, var.CLONED, var.LASTHEXED, var.BITE_PREFERENCES): kvp = [] for a,b in dictvar.items(): @@ -3147,8 +3129,7 @@ def transition_day(gameid=0): user = users._get(target) # FIXME evt = Event("night_acted", {"acted": False}) evt.dispatch(var, user, actor) - if ((target in var.PRAYED and var.PRAYED[target][0] > 0) or - target in var.OBSERVED or target in var.HEXED or target in var.CURSED or evt.data["acted"]): + if target in var.OBSERVED or target in var.HEXED or target in var.CURSED or evt.data["acted"]: actor.send(messages["werecrow_success"].format(user)) else: actor.send(messages["werecrow_failure"].format(user)) @@ -3532,11 +3513,7 @@ def chk_nightdone(): spl = set(pl) actedcount = sum(map(len, (var.PASSED, var.OBSERVED, var.HEXED, var.CURSED))) - nightroles = list(get_all_players(("sorcerer", "hag", "warlock", "werecrow", "prophet"))) - - for nick, info in var.PRAYED.items(): - if info[0] > 0: - actedcount += 1 + nightroles = list(get_all_players(("sorcerer", "hag", "warlock", "werecrow"))) if var.FIRST_NIGHT: actedcount += len(var.CLONED.keys()) @@ -4090,109 +4067,6 @@ def observe(cli, nick, chan, rest): debuglog("{0} ({1}) OBSERVE: {2} ({3})".format(nick, role, victim, get_role(victim))) -@cmd("pray", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("prophet",)) -def pray(cli, nick, chan, rest): - """Receive divine visions of who has a role.""" - # this command may be used multiple times in the course of the night, however it only needs - # to be used once to count towards ending night (additional uses don't count extra) - if nick in var.PRAYED and var.PRAYED[nick][0] == 2: - pm(cli, nick, messages["already_prayed"]) - return - elif nick not in var.PRAYED: - # [number of times prayed tonight, current target, target role, {target: [roles]}] - var.PRAYED[nick] = [0, None, None, defaultdict(set)] - - if var.PRAYED[nick][0] == 0: - what = re.split(" +", rest)[0] - if not what: - pm(cli, nick, messages["not_enough_parameters"]) - return - # complete this as a match with other roles (so "cursed" can match "cursed villager" for instance) - role = complete_one_match(what.lower(), var.ROLE_GUIDE.keys()) - if role is None: - if what.lower() in var.ROLE_ALIASES: - role = var.ROLE_ALIASES[what.lower()] - else: - # typo, let them fix it - pm(cli, nick, messages["specific_invalid_role"].format(what)) - return - - # get a list of all roles actually in the game, including roles that amnesiacs will be turning into - # (amnesiacs are special since they're also listed as amnesiac; that way a prophet can see both who the - # amnesiacs themselves are as well as what they'll become) - pl = list_players() - from src.roles.amnesiac import ROLES - valid_roles = {r for r, p in var.ROLES.items() if p} | {r for p, r in ROLES.items() if p.nick in pl} # FIXME - - if role in valid_roles: - # this sees through amnesiac, so the amnesiac's final role counts as their role - # also, if we're the only person with that role, say so and don't allow a second vision - people = set(get_roles(role)) | {p.nick for p, r in ROLES.items() if p.nick in pl and r == role} # FIXME - if len(people) == 1 and nick in people: - pm(cli, nick, messages["vision_only_role_self"].format(role)) - var.PRAYED[nick][0] = 2 - debuglog("{0} ({1}) PRAY {2} - ONLY".format(nick, get_role(nick), role)) - return - # select someone with the role that we haven't looked at before for this particular role - prevlist = (p for p, rl in var.PRAYED[nick][3].items() if role in rl) - for p in prevlist: - people.discard(p) - if len(people) == 0 or (len(people) == 1 and nick in people): - pm(cli, nick, messages["vision_no_more_role"].format(plural(role))) - var.PRAYED[nick][0] = 2 - debuglog("{0} ({1}) PRAY {2} - NO OTHER".format(nick, get_role(nick), role)) - return - target = random.choice(list(people)) - var.PRAYED[nick][0] = 1 - var.PRAYED[nick][1] = target - var.PRAYED[nick][2] = role - var.PRAYED[nick][3][target].add(role) - half = random.sample(pl, math.ceil(len(pl) / 2)) - if target not in half: - half[0] = target - random.shuffle(half) - # if prophet never reveals, there is no point making them pray twice, - # so just give them the player the first time around - if len(half) > 1 and (var.PROPHET_REVEALED_CHANCE[0] > 0 or var.PROPHET_REVEALED_CHANCE[1] > 0): - msg = messages["vision_players"].format(role) - if len(half) > 2: - msg += "{0}, and {1}.".format(", ".join(half[:-1]), half[-1]) - else: - msg += "{0} and {1}.".format(half[0], half[1]) - pm(cli, nick, msg) - debuglog("{0} ({1}) PRAY {2} ({3}) - HALF".format(nick, get_role(nick), role, target)) - if random.random() < var.PROPHET_REVEALED_CHANCE[0]: - pm(cli, target, messages["vision_prophet"].format(nick)) - debuglog("{0} ({1}) PRAY REVEAL".format(nick, get_role(nick), role)) - var.PRAYED[nick][0] = 2 - else: - # only one, go straight to second chance - var.PRAYED[nick][0] = 2 - pm(cli, nick, messages["vision_role"].format(target, role)) - debuglog("{0} ({1}) PRAY {2} ({3}) - FULL".format(nick, get_role(nick), role, target)) - if random.random() < var.PROPHET_REVEALED_CHANCE[1]: - pm(cli, target, messages["vision_prophet"].format(nick)) - debuglog("{0} ({1}) PRAY REVEAL".format(nick, get_role(nick))) - else: - # role is not in this game, this still counts as a successful activation of the power! - pm(cli, nick, messages["vision_none"].format(plural(role))) - debuglog("{0} ({1}) PRAY {2} - NONE".format(nick, get_role(nick), role)) - var.PRAYED[nick][0] = 2 - elif var.PRAYED[nick][1] is None: - # the previous vision revealed the prophet, so they cannot receive any more visions tonight - pm(cli, nick, messages["vision_recovering"]) - return - else: - # continuing a praying session from this night to obtain more information, give them the actual person - var.PRAYED[nick][0] = 2 - target = var.PRAYED[nick][1] - role = var.PRAYED[nick][2] - pm(cli, nick, messages["vision_role"].format(target, role)) - debuglog("{0} ({1}) PRAY {2} ({3}) - FULL".format(nick, get_role(nick), role, target)) - if random.random() < var.PROPHET_REVEALED_CHANCE[1]: - pm(cli, target, messages["vision_prophet"].format(nick)) - debuglog("{0} ({1}) PRAY REVEAL".format(nick, get_role(nick))) - @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): @@ -4618,10 +4492,6 @@ def transition_night(): var.OBSERVED = {} # those whom werecrows have observed var.TOBESILENCED = set() var.CONSECRATING.clear() - for nick in var.PRAYED: - var.PRAYED[nick][0] = 0 - var.PRAYED[nick][1] = None - var.PRAYED[nick][2] = None daydur_msg = "" @@ -4656,21 +4526,6 @@ def transition_night(): # send PMs ps = get_players() - for pht in get_all_players(("prophet",)): - chance1 = math.floor(var.PROPHET_REVEALED_CHANCE[0] * 100) - chance2 = math.floor(var.PROPHET_REVEALED_CHANCE[1] * 100) - an1 = "n" if chance1 >= 80 and chance1 < 90 else "" - an2 = "n" if chance2 >= 80 and chance2 < 90 else "" - if pht.prefers_simple(): - pht.send(messages["prophet_simple"]) - else: - if chance1 > 0: - pht.send(messages["prophet_notify_both"].format(an1, chance1, an2, chance2)) - elif chance2 > 0: - pht.send(messages["prophet_notify_second"].format(an2, chance2)) - else: - pht.send(messages["prophet_notify_none"]) - for drunk in get_all_players(("village drunk",)): if drunk.prefers_simple(): drunk.send(messages["drunk_simple"]) @@ -5024,7 +4879,6 @@ def start(cli, nick, chan, forced = False, restart = ""): var.PRIESTS = set() var.CONSECRATING.clear() var.DYING.clear() - var.PRAYED = {} var.DEADCHAT_PLAYERS.clear() var.SPECTATING_WOLFCHAT.clear() @@ -5824,7 +5678,7 @@ def listroles(cli, nick, chan, rest): reply(cli, nick, chan, " ".join(msg)) @command("myrole", pm=True, phases=("day", "night")) -def myrole(var, wrapper, message): # FIXME: Need to fix !swap once this gets converted +def myrole(var, wrapper, message): """Reminds you of your current role.""" ps = get_participants() @@ -5863,10 +5717,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")) - # 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"]) - @command("aftergame", "faftergame", flag="D", pm=True) def aftergame(var, wrapper, message): """Schedule a command to be run after the current game."""