diff --git a/messages/en.json b/messages/en.json index 4fa31d0..d48b8f4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -196,10 +196,9 @@ "single_winner": "The winner is \u0002{0}\u0002.", "two_winners": "The winners are \u0002{0}\u0002 and \u0002{1}\u0002.", "many_winners": "The winners are {0}, and \u0002{1}\u0002.", - "clone_turn": "You are now a{0} \u0002{1}\u0002.", + "new_role": "You are now a{0} \u0002{1}\u0002.", "forever_aclone": "It appears that \u0002{0}\u0002 was cloning you, so you are now stuck as a clone forever. How sad.", "clone_success": "You will now be cloning \u0002{0}\u0002 if they die.", - "clone_wolf": "\u0002{0}\u0002 cloned \u0002{1}\u0002 and has now become a wolf!", "no_other_wolves": "There are no other wolves.", "has_minions": "You have \u0002{0}\u0002 {1} at your command!", "lover_suicide": "Saddened by the loss of their lover, \u0002{0}\u0002, a{1} \u0002{2}\u0002, commits suicide.", @@ -223,7 +222,6 @@ "hunter_discard": "Your target has died, so you may now pick a new one.", "wild_child_already_picked": "You have already picked your idol for this game.", "wild_child_success": "You have picked {0} to be your idol for this game.", - "wild_child_as_wolf": "\u0002{0}\u0002's idol has died, and is now a \u0002wolf\u0002!", "wild_child_random_idol": "Upon waking up, the first person you see is \u0002{0}\u0002, and they become your idol.", "wild_child_revealroles_picked": "picked {0} as idol", "wild_child_revealroles_no_idol": "no idol picked yet", @@ -281,7 +279,6 @@ "angel_protection": "\u0002{0}\u0002 was attacked last night, but luckily, the guardian angel was on duty.", "bodyguard_protection": "\u0002{0}\u0002 sacrificed their life to guard that of another.", "lycan_turn": "HOOOOOOOOOWL. You have become... a wolf!", - "lycan_wc_notification": "\u0002{0}\u0002 is now a wolf!", "totem_banish": "\u0002{0}\u0002's totem emitted a brilliant flash of light last night. It appears that \u0002{1}\u0002's spirit was driven away by the flash.", "totem_death": "\u0002{0}\u0002's totem emitted a brilliant flash of light last night. The dead body of \u0002{1}\u0002, a{2} \u0002{3}\u0002, was found at the scene.", "totem_death_no_reveal": "\u0002{0}\u0002's totem emitted a brilliant flash of light last night. The dead body of \u0002{1}\u0002 was found at the scene.", @@ -453,9 +450,8 @@ "fallen_angel_turn": "While out last night, you were overpowered by a large werewolf and bitten. Shortly thereafter, you found your wings turning black as night and sadistic thoughts infiltrating your mind...", "harlot_turn": "While out visiting last night, you were overpowered by a large werewolf and bitten. Shortly thereafter, you found yourself turning into a werewolf yourself!", "bitten_turn": "You woke suddenly last night to a sharp pain, as you were bit by a large werewolf. Shortly thereafter, you found yourself turning into a werewolf yourself!", - "wolfchat_new_member": "\u0002{0}\u0002 is now a \u0002{1}\u0002!", + "wolfchat_new_member": "\u0002{0}\u0002 is now a{1} \u0002{2}\u0002!", "amnesia_clear": "Your amnesia clears and you now remember that you are {0} \u0002{1}\u0002!", - "amnesia_wolfchat": "\u0002{0}\u0002 is now a \u0002{1}\u0002!", "wolf_notify": "You are a \u0002{0}wolf\u0002. It is your job to kill all the villagers. Use \"kill \" to kill a villager.", "cursed_traitor_notify": "You are a \u0002{0}cursed traitor\u0002. Normally, you would be seen as a villager by the seer and oracle, but since you're cursed, you are seen as a wolf.", "traitor_notify": "You are a \u0002{0}traitor\u0002. You are exactly like a villager and not even a seer or oracle can see your true identity, only detectives and augurs can.", @@ -737,7 +733,6 @@ "sleepy_nightmare_death": "As the sun starts rising, your legs give out, causing the beast to descend upon you and snuff out your life.", "sleepy_priest_death": "The sky suddenly darkens as a thunderstorm appears from nowhere. The bell on the newly-abandoned church starts ringing in sinister tones before the building is struck repeatedly by lightning, setting it alight in a raging inferno...", "sleepy_doomsayer_turn": "You feel something rushing into you and taking control over your mind and body. It causes you to rapidly start transforming into a werewolf, and you realize your vision powers can now be used to inflict malady on the unwary. You are now a \u0002doomsayer\u0002.", - "sleepy_doomsayer_wolfchat": "\u0002{0}\u0002 is now a \u0002doomsayer\u0002.", "sleepy_succubus_turn": "You feel something rushing into you and taking control over your mind and body. You are now a \u0002succubus\u0002. Your job is to entrance the village, bringing them all under your absolute control.", "sleepy_demoniac_turn": "You feel something rushing into you and taking control over your mind and body, showing you your new purpose in life. There are far greater evils than the wolves lurking in the shadows, and by sacrificing all of the wolves, you can unleash those evils upon the world. You are now a \u0002demoniac\u0002.", "fquit_fail": "Forcing a live player to leave must be done in channel.", @@ -781,7 +776,7 @@ "vengeful_role": "You are a \u0002vengeful ghost\u0002 who is against the \u0002{0}\u0002.", "show_role": "You are a{0} \u0002{1}\u0002.", "original_wolves": "Original wolves: {0}", - "turncoat_side": "Current side: \u0002{0}\u0002.", + "turncoat_side": "You are {0}.", "assassin_role_info": "You are an \u0002assassin\u0002{0}.", "assassin_targeting": " and targeting {0}", "bitten_info": "You were bitten by an alpha wolf and have \u0002{0} night{1}\u0002 until your transformation.", diff --git a/src/functions.py b/src/functions.py index 70ad699..4f2261b 100644 --- a/src/functions.py +++ b/src/functions.py @@ -5,7 +5,7 @@ from src import users __all__ = [ "get_players", "get_all_players", "get_participants", - "get_target", + "get_target", "change_role" "get_main_role", "get_all_roles", "get_reveal_role", "is_known_wolf_ally", ] @@ -66,6 +66,34 @@ def get_target(var, wrapper, message, *, allow_self=False, allow_bot=False, not_ return match +def change_role(var, player, oldrole, newrole, *, inherit_from=None, message="new_role"): + # in_wolfchat is filled as part of priority 4 + # if you wish to modify evt.data["role"], do so in priority 3 or sooner + evt = Event("new_role", + {"role": newrole, "messages": [], "in_wolfchat": False}, + inherit_from=inherit_from) + evt.dispatch(var, player, oldrole) + newrole = evt.data["role"] + + var.ROLES[oldrole].remove(player) + var.ROLES[newrole].add(player) + # only adjust MAIN_ROLES/FINAL_ROLES if we're changing the player's actual role + if var.MAIN_ROLES[player] == oldrole: + var.MAIN_ROLES[player] = newrole + var.FINAL_ROLES[player.nick] = newrole + + sayrole = newrole + if sayrole in var.HIDDEN_VILLAGERS: + sayrole = "villager" + elif sayrole in var.HIDDEN_ROLES: + sayrole = var.DEFAULT_ROLE + an = "n" if sayrole.startswith(("a", "e", "i", "o", "u")) else "" + + player.send(messages[message].format(an, sayrole)) + player.send(*evt.data["messages"]) + + return newrole + def get_main_role(user): role = var.MAIN_ROLES.get(user) if role is not None: diff --git a/src/gamemodes.py b/src/gamemodes.py index cac119b..5d5d96e 100644 --- a/src/gamemodes.py +++ b/src/gamemodes.py @@ -10,7 +10,7 @@ import botconfig import src.settings as var from src.utilities import * from src.messages import messages -from src.functions import get_players, get_all_players, get_main_role +from src.functions import get_players, get_all_players, get_main_role, change_role from src.decorators import handle_error, command from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src import events, channels, users @@ -870,6 +870,7 @@ class SleepyMode(GameMode): self.TEMPLATE_RESTRICTIONS["prophet"] = frozenset(self.ROLE_GUIDE.keys()) - {"priest", "blessed villager", "prophet"} # this ensures that village drunk will always receive the gunner template self.TEMPLATE_RESTRICTIONS["gunner"] = frozenset(self.ROLE_GUIDE.keys()) - {"village drunk", "cursed villager", "gunner"} + self.cmd_params = dict(chan=False, pm=True, playing=True, phases=("night",)) # disable wolfchat #self.RESTRICT_WOLFCHAT = 0x0f @@ -879,13 +880,17 @@ class SleepyMode(GameMode): events.add_listener("chk_nightdone", self.prolong_night) events.add_listener("transition_day_begin", self.nightmare_kill) events.add_listener("del_player", self.happy_fun_times) - self.north_cmd = command("north", "n", chan=False, pm=True, playing=True, phases=("night",))(functools.partial(self.move, "n")) - self.east_cmd = command("east", "e", chan=False, pm=True, playing=True, phases=("night",))(functools.partial(self.move, "e")) - self.south_cmd = command("south", "s", chan=False, pm=True, playing=True, phases=("night",))(functools.partial(self.move, "s")) - self.west_cmd = command("west", "w", chan=False, pm=True, playing=True, phases=("night",))(functools.partial(self.move, "w")) + events.add_listener("revealroles", self.on_revealroles) self.having_nightmare = UserList() + cmd_params = dict(chan=False, pm=True, playing=True, phases=("night",), users=self.having_nightmare) + + self.north_cmd = command("north", "n", **cmd_params)(functools.partial(self.move, "n")) + self.east_cmd = command("east", "e", **cmd_params)(functools.partial(self.move, "e")) + self.south_cmd = command("south", "s", **cmd_params)(functools.partial(self.move, "s")) + self.west_cmd = command("west", "w", **cmd_params)(functools.partial(self.move, "w")) + def teardown(self): from src import decorators events.remove_listener("dullahan_targets", self.dullahan_targets) @@ -893,6 +898,8 @@ class SleepyMode(GameMode): events.remove_listener("chk_nightdone", self.prolong_night) events.remove_listener("transition_day_begin", self.nightmare_kill) events.remove_listener("del_player", self.happy_fun_times) + events.remove_listener("revealroles", self.on_revealroles) + def remove_command(name, command): if len(decorators.COMMANDS[name]) > 1: decorators.COMMANDS[name].remove(command) @@ -909,9 +916,8 @@ class SleepyMode(GameMode): self.having_nightmare.clear() - def dullahan_targets(self, evt, var, dullahans, max_targets): - for dull in dullahans: - evt.data["targets"][dull] = UserSet(var.ROLES["priest"]) + def dullahan_targets(self, evt, var, dullahan, max_targets): + evt.data["targets"].update(var.ROLES["priest"]) def setup_nightmares(self, evt, var): if random.random() < 1/5: @@ -991,8 +997,6 @@ class SleepyMode(GameMode): self.nightmare_step() def move(self, direction, var, wrapper, message): - if not self.having_nightmare or self.having_nightmare[0] is not wrapper.source: - return opposite = {"n": "s", "e": "w", "s": "n", "w": "e"} if self.prev_direction == opposite[direction]: wrapper.pm(messages["sleepy_nightmare_invalid_direction"]) @@ -1043,17 +1047,17 @@ class SleepyMode(GameMode): cultists = [p for p in get_players(("cultist",)) if p in pl and random.random() < turn_chance] channels.Main.send(messages["sleepy_priest_death"]) for seer in seers: - change_role(seer, "seer", "doomsayer") - seer.send(messages["sleepy_doomsayer_turn"]) - relay_wolfchat_command(seer.client, seer.nick, messages["sleepy_doomsayer_wolfchat"].format(seer), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) + change_role(var, seer, "seer", "doomsayer", message="sleepy_doomsayer_turn") for harlot in harlots: - change_role(harlot, "harlot", "succubus") - harlot.send(messages["sleepy_succubus_turn"]) + change_role(var, harlot, "harlot", "succubus", message="sleepy_succubus_turn") for cultist in cultists: - change_role(cultist, "cultist", "demoniac") - cultist.send(messages["sleepy_demoniac_turn"]) + change_role(var, cultist, "cultist", "demoniac", message="sleepy_demoniac_turn") # NOTE: chk_win is called by del_player, don't need to call it here even though this has a chance of ending game + def on_revealroles(self, evt, var, wrapper): + if self.having_nightmare: + evt.data["output"].append("\u0002having nightmare\u0002: {0}".format(self.having_nightmare[0])) + @game_mode("maelstrom", minp=8, maxp=24, likelihood=0) class MaelstromMode(GameMode): """Some people just want to watch the world burn.""" @@ -1148,7 +1152,7 @@ class MaelstromMode(GameMode): COMMANDS["myrole"][0].caller(wrapper.source.client, wrapper.source.nick, wrapper.target.name, "") # FIXME: New/old API # if they're a wolfchat role, alert the other wolves if role in var.WOLFCHAT_ROLES: - relay_wolfchat_command(wrapper.source.client, wrapper.source.nick, messages["wolfchat_new_member"].format(wrapper.source.nick, role), var.WOLFCHAT_ROLES, is_wolf_command=True, is_kill_command=True) + relay_wolfchat_command(wrapper.source.client, wrapper.source.nick, messages["wolfchat_new_member"].format(wrapper.source.nick, "", role), var.WOLFCHAT_ROLES, is_wolf_command=True, is_kill_command=True) # TODO: make this part of !myrole instead, no reason we can't give out wofllist in that wolves = list_players(var.WOLFCHAT_ROLES) pl = get_players() diff --git a/src/roles/_mystic_helper.py b/src/roles/_mystic_helper.py index 8fa834c..0c7435a 100644 --- a/src/roles/_mystic_helper.py +++ b/src/roles/_mystic_helper.py @@ -23,7 +23,7 @@ def setup_variables(rolename, *, send_role, types): def on_transition_night_end(evt, var): villagers = set(get_players(("priest", "doctor"))) win_stealers = set(get_players(("fool", "monster", "demoniac"))) - neutrals = set(get_players(("turncoat", "jester"))) + neutrals = set(get_players(("jester",))) special_evt = Event("get_special", {"villagers": villagers, "wolves": set(), "win_stealers": win_stealers, "neutrals": neutrals}) special_evt.dispatch(var) diff --git a/src/roles/_seer_helper.py b/src/roles/_seer_helper.py index 020f365..55409fd 100644 --- a/src/roles/_seer_helper.py +++ b/src/roles/_seer_helper.py @@ -26,12 +26,10 @@ def setup_variables(rolename): def on_get_special(evt, var): evt.data["villagers"].update(get_players((rolename,))) - @event_listener("exchange_roles") - def on_exchange(evt, var, actor, target, actor_role, target_role): - if actor_role == rolename and target_role != rolename: - SEEN.discard(actor) - elif target_role == rolename and actor_role != rolename: - SEEN.discard(target) + @event_listener("new_role") + def on_new_role(evt, var, user, old_role): + if old_role == rolename and evt.data["role"] != rolename: + SEEN.discard(user) @event_listener("chk_nightdone") def on_chk_nightdone(evt, var): diff --git a/src/roles/amnesiac.py b/src/roles/amnesiac.py index 55c16c0..c22970a 100644 --- a/src/roles/amnesiac.py +++ b/src/roles/amnesiac.py @@ -6,7 +6,7 @@ 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, is_known_wolf_ally +from src.functions import get_players, get_all_players, get_main_role, get_reveal_role, get_target, is_known_wolf_ally, change_role from src.decorators import command, event_listener from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages @@ -29,12 +29,6 @@ def _get_blacklist(var): blacklist.add("matchmaker") return blacklist -@event_listener("role_assignment") -def on_role_assignment(evt, var, gamemode, pl): - roles = var.ROLE_GUIDE.keys() - _get_blacklist(var) - for amnesiac in get_all_players(("amnesiac",)): - ROLES[amnesiac] = random.choice(list(roles)) - @event_listener("transition_night_begin") def on_transition_night_begin(evt, var): global STATS_FLAG @@ -44,34 +38,12 @@ def on_transition_night_begin(evt, var): STATS_FLAG = True for amn in amnesiacs: - role = ROLES[amn] - change_role(amn, "amnesiac", role) - evt = Event("new_role", {}) - evt.dispatch(var, amn, role) - if var.FIRST_NIGHT: # we don't need to tell them twice if they remember right away - continue - showrole = role - if showrole in var.HIDDEN_VILLAGERS: - showrole = "villager" - elif showrole in var.HIDDEN_ROLES: - showrole = var.DEFAULT_ROLE - a = "a" - if showrole.startswith(("a", "e", "i", "o", "u")): - a = "an" - amn.send(messages["amnesia_clear"].format(a, showrole)) - if is_known_wolf_ally(amn, amn): - if role in var.WOLF_ROLES: - relay_wolfchat_command(amn.client, amn.nick, messages["amnesia_wolfchat"].format(amn, showrole), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) - else: - relay_wolfchat_command(amn.client, amn.nick, messages["amnesia_wolfchat"].format(amn, showrole), var.WOLFCHAT_ROLES) - - debuglog("{0} REMEMBER: {1} as {2}".format(amn, role, showrole)) + role = change_role(var, amn, "amnesiac", ROLES[amn], message="amnesia_clear") + debuglog("{0} REMEMBER: {1}".format(amn, role)) @event_listener("new_role") -def on_new_role(evt, var, user, role): - if role == "turncoat": # FIXME: Need to split into turncoat.py when split - var.TURNCOATS[user.nick] = ("none", -1) - if role == "doctor": # FIXME: Need to split into doctor.py when split +def doctor_new_role(evt, var, user, old_role): + if evt.data["role"] == "doctor": # FIXME: Need to split into doctor.py when split var.DOCTORS[user.nick] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * len(get_players())) @event_listener("investigate") @@ -79,28 +51,17 @@ def on_investigate(evt, var, actor, target): if evt.data["role"] == "amnesiac": evt.data["role"] = ROLES[target] -@event_listener("exchange_roles") -def on_exchange_roles(evt, var, actor, target, actor_role, target_role): +@event_listener("new_role", priority=1) # Exchange, clone, etc. - assign the amnesiac's final role +def update_amnesiac(evt, var, user, old_role): # FIXME: exchange totem messes with var.HIDDEN_AMNESIAC (the new amnesiac is no longer hidden should they die) - if actor_role == "amnesiac": - actor_role = ROLES[actor] - if target in ROLES: - ROLES[actor] = ROLES[target] - ROLES[target] = actor_role - else: - del ROLES[actor] - ROLES[target] = actor_role + if evt.params.inherit_from is not None and evt.data["role"] == "amnesiac" and old_role != "amnesiac": + evt.data["role"] = ROLES[evt.params.inherit_from] - if target_role == "amnesiac": - if actor not in ROLES: - target_role = ROLES[target] - ROLES[actor] = target_role - del ROLES[target] - else: # we swapped amnesiac_roles earlier on, get our version back - target_role = ROLES[actor] - - evt.data["actor_role"] = actor_role - evt.data["target_role"] = target_role +@event_listener("new_role") +def on_new_role(evt, var, user, old_role): + if evt.params.inherit_from is None and evt.data["role"] == "amnesiac": + roles = var.ROLE_GUIDE.keys() - _get_blacklist(var) + ROLES[user] = random.choice(list(roles)) @event_listener("revealing_totem") def on_revealing_totem(evt, var, votee): @@ -108,14 +69,8 @@ def on_revealing_totem(evt, var, votee): global STATS_FLAG STATS_FLAG = True if evt.data["role"] == "amnesiac": - role = ROLES[votee] - change_role(votee, "amnesiac", role) votee.send(messages["totem_amnesia_clear"]) - nevt = Event("new_role", {}) - nevt.dispatch(var, votee, role) - evt.data["role"] = role - # If wolfteam, don't bother giving list of wolves since night is about to start anyway - # Existing wolves also know that someone just joined their team because revealing totem says what they are + change_role(var, votee, "amnesiac", ROLES[votee]) @event_listener("get_reveal_role") def on_reveal_role(evt, var, user): diff --git a/src/roles/clone.py b/src/roles/clone.py index 0e0ffb9..540a752 100644 --- a/src/roles/clone.py +++ b/src/roles/clone.py @@ -5,8 +5,8 @@ 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 import events, channels, users, debuglog, errlog, plog +from src.functions import get_players, get_all_players, get_main_role, get_reveal_role, get_target, change_role from src.decorators import command, event_listener from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages @@ -45,14 +45,15 @@ def clone(var, wrapper, message): debuglog("{0} (clone) CLONE: {1} ({2})".format(wrapper.source, target, get_main_role(target))) -@event_listener("init") -def on_init(evt): +def setup_clone(evt): # We need to add "clone" to the role command exceptions so there's no error # This is done here so that var isn't imported at the global scope # (when we implement proper game state this will be in a different event) from src import settings as var var.ROLE_COMMAND_EXCEPTIONS.add("clone") +events.add_listener("init", setup_clone) # no IRC connection, so no possible error handler yet + @event_listener("get_reveal_role") def on_get_reveal_role(evt, var, user): if var.HIDDEN_CLONE and user in var.ORIGINAL_ROLES["clone"]: @@ -72,19 +73,7 @@ def on_del_player(evt, var, player, mainrole, allroles, death_triggers): # clone is cloning target, so clone becomes target's role # clone does NOT get any of target's templates (gunner/assassin/etc.) del CLONED[clone] - if mainrole == "amnesiac": - from src.roles.amnesiac import ROLES as amn_roles - # clone gets the amnesiac's real role - mainrole = amn_roles[player] - change_role(clone, "clone", mainrole) - debuglog("{0} (clone) CLONE DEAD PLAYER: {1} ({2})".format(clone, target, mainrole)) - sayrole = mainrole - if sayrole in var.HIDDEN_VILLAGERS: - sayrole = "villager" - elif sayrole in var.HIDDEN_ROLES: - sayrole = var.DEFAULT_ROLE - an = "n" if sayrole.startswith(("a", "e", "i", "o", "u")) else "" - clone.send(messages["clone_turn"].format(an, sayrole)) + mainrole = change_role(var, clone, "clone", mainrole, inherit_from=target) # if a clone is cloning a clone, clone who the old clone cloned if mainrole == "clone" and player in CLONED: if CLONED[player] is clone: @@ -93,30 +82,8 @@ def on_del_player(evt, var, player, mainrole, allroles, death_triggers): CLONED[clone] = CLONED[player] clone.send(messages["clone_success"].format(CLONED[clone])) debuglog("{0} (clone) CLONE: {1} ({2})".format(clone, CLONED[clone], get_main_role(CLONED[clone]))) - elif mainrole in var.WOLFCHAT_ROLES: - wolves = get_players(var.WOLFCHAT_ROLES) - wolves.remove(clone) # remove self from list - for wolf in wolves: - wolf.queue_message(messages["clone_wolf"].format(clone, player)) - if wolves: - wolf.send_messages() - if var.PHASE == "day": - random.shuffle(wolves) - for i, wolf in enumerate(wolves): - wolfrole = get_main_role(wolf) - wevt = Event("wolflist", {"tags": set()}) - wevt.dispatch(var, wolf, clone) - tags = " ".join(wevt.data["tags"]) - if tags: - tags += " " - wolves[i] = "\u0002{0}\u0002 ({1}{2})".format(wolf, tags, wolfrole) - if wolves: - clone.send(messages["wolves_list"].format(wolves)) - else: - clone.send(messages["no_other_wolves"]) - elif mainrole == "turncoat": - var.TURNCOATS[clone.nick] = ("none", -1) # FIXME + debuglog("{0} (clone) CLONE DEAD PLAYER: {1} ({2})".format(clone, target, mainrole)) if mainrole == "clone" and player in CLONED: del CLONED[player] @@ -155,23 +122,12 @@ def on_transition_day_begin(evt, var): CLONED[clone] = target clone.send(messages["random_clone"].format(target)) -@event_listener("exchange_roles") -def on_exchange_roles(evt, var, actor, target, actor_role, target_role): - actor_target = None - target_target = None - if actor_role == "clone": - if actor in CLONED: - actor_target = CLONED.pop(actor) - evt.data["target_messages"].append(messages["clone_target"].format(actor_target)) - if target_role == "clone": - if target in CLONED: - target_target = CLONED.pop(target) - evt.data["actor_messages"].append(messages["clone_target"].format(target_target)) - - if actor_target is not None: - CLONED[target] = actor_target - if target_target is not None: - CLONED[actor] = target_target +@event_listener("swap_role_state") +def on_swap_role_state(evt, var, actor, target, role): + if role == "clone": + CLONED[target], CLONED[actor] = CLONED.pop(actor), CLONED.pop(target) + evt.data["target_messages"].append(messages["clone_target"].format(CLONED[target])) + evt.data["actor_messages"].append(messages["clone_target"].format(CLONED[actor])) @event_listener("player_win") def on_player_win(evt, var, player, role, winner, survived): @@ -179,17 +135,17 @@ def on_player_win(evt, var, player, role, winner, survived): if role == "clone" and survived and not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES: evt.data["iwon"] = True -@event_listener("del_player") +@event_listener("del_player", priority=1) def first_death_occured(evt, var, player, mainrole, allroles, death_triggers): global CLONE_ENABLED if CLONE_ENABLED: return - if var.PHASE in var.GAME_PHASES and (CLONED or get_all_players(("clone",))) and not var.FIRST_NIGHT: + if CLONED and var.PHASE in var.GAME_PHASES: CLONE_ENABLED = True @event_listener("update_stats") def on_update_stats(evt, var, player, mainrole, revealrole, allroles): - if CLONE_ENABLED: + if CLONE_ENABLED and not var.HIDDEN_CLONE: evt.data["possible"].add("clone") @event_listener("myrole") @@ -212,3 +168,5 @@ def on_reset(evt, var): global CLONE_ENABLED CLONE_ENABLED = False CLONED.clear() + +# vim: set sw=4 expandtab: diff --git a/src/roles/detective.py b/src/roles/detective.py index 56a8429..14783b7 100644 --- a/src/roles/detective.py +++ b/src/roles/detective.py @@ -64,12 +64,10 @@ def on_del_player(evt, var, user, mainrole, allroles, death_triggers): def on_get_special(evt, var): evt.data["villagers"].update(get_players(("detective",))) -@event_listener("exchange_roles") -def on_exchange(evt, var, actor, target, actor_role, target_role): - if actor_role == "detective" and target_role != "detective": - INVESTIGATED.discard(actor) - elif target_role == "detective" and actor_role != "detective": - INVESTIGATED.discard(target) +@event_listener("new_role") +def on_new_role(evt, var, user, old_role): + if old_role == "detective" and evt.data["role"] != "detective": + INVESTIGATED.discard(user) @event_listener("transition_night_end", priority=2) def on_transition_night_end(evt, var): diff --git a/src/roles/dullahan.py b/src/roles/dullahan.py index 684cf1a..abcfada 100644 --- a/src/roles/dullahan.py +++ b/src/roles/dullahan.py @@ -120,25 +120,40 @@ def on_transition_day(evt, var): evt.data["onlybywolves"].discard(d) evt.data["killers"][d].append(k) -@event_listener("exchange_roles") -def on_exchange(evt, var, actor, target, actor_role, target_role): - for k in set(KILLS): - if k is actor or k is target: - del KILLS[k] +@event_listener("new_role") +def on_new_role(evt, var, player, old_role): + if player in TARGETS and old_role == "dullahan" and evt.data["role"] != "dullahan": + del KILLS[:player:] + del TARGETS[player] - for k in set(TARGETS): - if actor_role == "dullahan" and target_role != "dullahan" and k is actor: - targets = TARGETS.pop(k) - if target in targets: - targets.remove(target) - targets.add(actor) - TARGETS[target] = targets - if target_role == "dullahan" and actor_role != "dullahan" and k is target: - targets = TARGETS.pop(k) - if actor in targets: - targets.remove(actor) - targets.add(target) - TARGETS[actor] = targets + if player not in TARGETS and evt.data["role"] == "dullahan": + ps = get_players() + max_targets = math.ceil(8.1 * math.log(len(ps), 10) - 5) + TARGETS[player] = UserSet() + + dull_targets = Event("dullahan_targets", {"targets": TARGETS[player]}) # support sleepy + dull_targets.dispatch(var, player, max_targets) + + ps.remove(player) + while len(TARGETS[player]) < max_targets: + target = random.choice(ps) + ps.remove(target) + TARGETS[player].add(target) + +@event_listener("swap_role_state") +def on_swap_role_state(evt, var, actor, target, role): + if role == "dullahan": + targ_targets = TARGETS.pop(target) + if actor in targ_targets: + targ_targets.remove(actor) + targ_targets.add(target) + act_targets = TARGETS.pop(actor) + if target in act_targets: + act_targets.remove(target) + act_targets.add(actor) + + TARGETS[actor] = targ_targets + TARGETS[target] = act_targets @event_listener("chk_nightdone") def on_chk_nightdone(evt, var): @@ -165,24 +180,6 @@ def on_transition_night_end(evt, var): t = messages["dullahan_targets"] if var.FIRST_NIGHT else messages["dullahan_remaining_targets"] dullahan.send(messages[to_send], t + ", ".join(t.nick for t in targets), sep="\n") -@event_listener("role_assignment") -def on_role_assignment(evt, var, gamemode, pl): - # 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] = UserSet() - dull_targets = Event("dullahan_targets", {"targets": TARGETS}) # support sleepy - dull_targets.dispatch(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("succubus_visit") def on_succubus_visit(evt, var, succubus, target): succubi = get_all_players(("succubus",)) diff --git a/src/roles/hunter.py b/src/roles/hunter.py index 63e7c93..39fef1a 100644 --- a/src/roles/hunter.py +++ b/src/roles/hunter.py @@ -96,14 +96,12 @@ def on_transition_day(evt, var): # important, otherwise our del_player listener lets hunter kill again del KILLS[k] -@event_listener("exchange_roles") -def on_exchange(evt, var, actor, target, actor_role, target_role): - del KILLS[:actor:] - del KILLS[:target:] - HUNTERS.discard(actor) - HUNTERS.discard(target) - PASSED.discard(actor) - PASSED.discard(target) +@event_listener("new_role") +def on_new_role(evt, var, user, old_role): + if old_role == "hunter": + del KILLS[:user:] + HUNTERS.discard(user) + PASSED.discard(user) @event_listener("chk_nightdone") def on_chk_nightdone(evt, var): diff --git a/src/roles/investigator.py b/src/roles/investigator.py index adad6b8..de077d0 100644 --- a/src/roles/investigator.py +++ b/src/roles/investigator.py @@ -97,12 +97,10 @@ def on_del_player(evt, var, user, mainrole, allroles, death_triggers): def on_get_special(evt, var): evt.data["villagers"].update(get_players(("investigator",))) -@event_listener("exchange_roles") -def on_exchange(evt, var, actor, target, actor_role, target_role): - if actor_role == "investigator" and target_role != "investigator": - INVESTIGATED.discard(actor) - elif target_role == "investigator" and actor_role != "investigator": - INVESTIGATED.discard(target) +@event_listener("new_role") +def on_new_role(evt, var, user, old_role): + if old_role == "investigator" and evt.data["role"] != "investigator": + INVESTIGATED.discard(user) @event_listener("transition_night_end", priority=2) def on_transition_night_end(evt, var): diff --git a/src/roles/prophet.py b/src/roles/prophet.py index cbf1aef..418e3e5 100644 --- a/src/roles/prophet.py +++ b/src/roles/prophet.py @@ -113,3 +113,5 @@ def on_begin_day(evt, var): @event_listener("reset") def on_reset(evt, var): PRAYED.clear() + +# vim: set sw=4 expandtab: diff --git a/src/roles/succubus.py b/src/roles/succubus.py index ebe1082..1df8da9 100644 --- a/src/roles/succubus.py +++ b/src/roles/succubus.py @@ -195,8 +195,8 @@ def on_get_special(evt, var): evt.data["win_stealers"].update(get_players(("succubus",))) @event_listener("new_role") -def on_new_role(evt, var, user, role): - if role == "succubus" and user in ENTRANCED: +def on_new_role(evt, var, user, old_role): + if evt.data["role"] == "succubus" and user in ENTRANCED: ENTRANCED.remove(user) user.send(messages["no_longer_entranced"]) diff --git a/src/roles/turncoat.py b/src/roles/turncoat.py new file mode 100644 index 0000000..969c37d --- /dev/null +++ b/src/roles/turncoat.py @@ -0,0 +1,134 @@ +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 + +TURNCOATS = UserDict() # type: Dict[users.User, Tuple[str, int]] +PASSED = UserSet() # type: Set[users.User] + +def _get_side(user): + side = "currently with \u0002{0}\u0002".format(TURNCOATS[user][0]) + if TURNCOATS[user][0] == "none": + side = "not currently on any side" + return side + +@command("side", chan=False, pm=True, playing=True, phases=("night",), roles=("turncoat",)) +def change_sides(var, wrapper, message, sendmsg=True): + if TURNCOATS[wrapper.source][1] == var.NIGHT_COUNT - 1: + wrapper.pm(messages["turncoat_already_turned"]) + return + + team = re.split(" +", message)[0] + team = complete_one_match(team, ("villagers", "wolves")) + if not team: + wrapper.pm(messages["turncoat_error"]) + return + + wrapper.pm(messages["turncoat_success"].format(team)) + TURNCOATS[wrapper.source] = (team, var.NIGHT_COUNT) + PASSED.discard(wrapper.source) + debuglog("{0} (turncoat) SIDE {1}".format(wrapper.source, team)) + +@command("pass", chan=False, pm=True, playing=True, phases=("night",), roles=("turncoat",)) +def pass_cmd(var, wrapper, message): + """Decline to use your special power for that night.""" + if TURNCOATS[wrapper.source][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. + wrapper.pm(messages["turncoat_fail"]) + return + + wrapper.pm(messages["turncoat_pass"]) + if TURNCOATS[wrapper.source][1] == var.NIGHT_COUNT - 1: + # don't add to PASSED since we aren't counting them anyway for nightdone + # let them still use !pass though to make them feel better or something + return + PASSED.add(wrapper.source) + + debuglog("{0} (turncoat) PASS".format(wrapper.source)) + +@event_listener("transition_night_end") +def on_transition_night_end(evt, var): + for turncoat in get_all_players(("turncoat",)): + # they start out as unsided, but can change n1 + if turncoat not in TURNCOATS: + TURNCOATS[turncoat] = ("none", -1) + + if turncoat.prefers_simple(): + turncoat.send(messages["turncoat_simple"].format(TURNCOATS[turncoat][0])) + else: + message = messages["turncoat_notify"] + if TURNCOATS[turncoat][0] != "none": + message += messages["turncoat_current_team"].format(TURNCOATS[turncoat][0]) + else: + message += messages["turncoat_no_team"] + turncoat.send(message) + +@event_listener("chk_nightdone") +def on_chk_nightdone(evt, var): + # add in turncoats who should be able to act or who passed + # but if they can act they're in TURNCOATS where the second tuple item is the current night + # (if said tuple item is the previous night, then they are not allowed to act tonight) + pl = get_players() + evt.data["actedcount"] += len(PASSED) + for turncoat, (team, night) in TURNCOATS.items(): + if turncoat not in pl: + continue + if night == var.NIGHT_COUNT: + evt.data["nightroles"].append(turncoat) + evt.data["actedcount"] += 1 + elif night < var.NIGHT_COUNT - 1: + evt.data["nightroles"].append(turncoat) + +@event_listener("player_win") +def on_player_win(evt, var, player, role, winner, survived): + if role == "turncoat" and player in TURNCOATS and TURNCOATS[player][0] != "none": + evt.data["won"] = (winner == TURNCOATS[player][0]) + +@event_listener("myrole") +def on_myrole(evt, var, user): + if evt.data["role"] == "turncoat" and user in TURNCOATS: + evt.data["messages"].append(messages["turncoat_side"].format(_get_side(user))) + +@event_listener("revealroles_role") +def on_revealroles_role(evt, var, user, role): + if role == "turncoat" and user in TURNCOATS: + evt.data["special_case"].append(_get_side(user)) + +@event_listener("get_special") +def on_get_special(evt, var): + evt.data["neutrals"].update(get_players(("turncoat",))) + +@event_listener("new_role") +def on_new_role(evt, var, player, old_role): + if old_role == "turncoat" and evt.data["role"] != "turncoat": + del TURNCOATS[player] + elif evt.data["role"] == "turncoat" and old_role != "turncoat": + TURNCOATS[player] = ("none", -1) + +@event_listener("swap_role_state") +def on_swap_role_state(evt, var, actor, target, role): + if role == "turncoat": + TURNCOATS[actor], TURNCOATS[target] = TURNCOATS.pop(target), TURNCOATS.pop(actor) + evt.data["actor_messages"].append(messages["turncoat_side"].format(_get_side(actor))) + evt.data["target_messages"].append(messages["turncoat_side"].format(_get_side(target))) + +@event_listener("begin_day") +def on_begin_day(evt, var): + PASSED.clear() + +@event_listener("reset") +def on_reset(evt, var): + PASSED.clear() + TURNCOATS.clear() + +# vim: set sw=4 expandtab: diff --git a/src/roles/vigilante.py b/src/roles/vigilante.py index 2be6721..055a0d3 100644 --- a/src/roles/vigilante.py +++ b/src/roles/vigilante.py @@ -83,12 +83,11 @@ def on_transition_day(evt, var): if get_main_role(target) not in var.WOLF_ROLES | var.WIN_STEALER_ROLES: var.DYING.add(vigilante) -@event_listener("exchange_roles") -def on_exchange(evt, var, actor, target, actor_role, target_role): - del KILLS[:actor:] - del KILLS[:target:] - PASSED.discard(actor) - PASSED.discard(target) +@event_listener("new_role") +def on_new_role(evt, var, user, old_role): + if old_role == "vigilante": + del KILLS[:user:] + PASSED.discard(user) @event_listener("chk_nightdone") def on_chk_nightdone(evt, var): diff --git a/src/roles/wildchild.py b/src/roles/wildchild.py index bbee209..f8f9f88 100644 --- a/src/roles/wildchild.py +++ b/src/roles/wildchild.py @@ -6,7 +6,7 @@ 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.functions import get_players, get_all_players, get_main_role, get_reveal_role, get_target, change_role from src.decorators import command, event_listener from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages @@ -73,39 +73,10 @@ def on_del_player(evt, var, user, mainrole, allroles, death_triggers): continue # Change their main role to wolf - child.send(messages["wild_child_idol_died"]) WILD_CHILDREN.add(child) - change_role(child, get_main_role(child), "wolf") + change_role(var, child, get_main_role(child), "wolf", message="wild_child_idol_died") var.ROLES["wild child"].discard(child) - if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES: - if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF: - wcroles = var.WOLF_ROLES - else: - wcroles = var.WOLF_ROLES | {"traitor"} - - wolves = get_players(wcroles) - wolves.remove(child) - if wolves: - for wolf in wolves: - wolf.queue_message(messages["wild_child_as_wolf"].format(child)) - wolf.send_messages() - - # Send wolf list - if var.PHASE == "day": - random.shuffle(wolves) - names = [] - cursed_list = get_all_players(("cursed villager",)) - for i, wolf in enumerate(wolves): - role = get_main_role(wolf) - cursed = "cursed " if wolf in cursed_list else "" - names.append("\u0002{0}\u0002 ({1}{2})".format(wolf, cursed, role)) - - if names: - child.send(messages["wolves_list"].format(", ".join(names))) - else: - child.send(messages["no_other_wolves"]) - @event_listener("chk_nightdone") def on_chk_nightdone(evt, var): if var.FIRST_NIGHT: diff --git a/src/roles/wolf.py b/src/roles/wolf.py index ab6dd29..980bcfe 100644 --- a/src/roles/wolf.py +++ b/src/roles/wolf.py @@ -226,8 +226,8 @@ def on_retribution_kill(evt, var, victim, orig_target): wolves = get_players(CAN_KILL) evt.data["target"] = random.choice(wolves) -@event_listener("exchange_roles", priority=2) -def on_exchange(evt, var, actor, target, actor_role, target_role): +@event_listener("new_role", priority=4) +def on_new_role(evt, var, player, old_role): wcroles = var.WOLFCHAT_ROLES if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES: if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF: @@ -235,77 +235,61 @@ def on_exchange(evt, var, actor, target, actor_role, target_role): else: wcroles = var.WOLF_ROLES | {"traitor"} - if target_role in wcroles and actor_role not in wcroles: + if old_role is None: + # initial role assignment; don't do all the logic below about notifying other wolves and such + if evt.data["role"] in wcroles: + evt.data["in_wolfchat"] = True + return + + sayrole = evt.data["role"] + if sayrole in var.HIDDEN_VILLAGERS: + sayrole = "villager" + elif sayrole in var.HIDDEN_ROLES: + sayrole = var.DEFAULT_ROLE + an = "n" if sayrole.startswith(("a", "e", "i", "o", "u")) else "" + + if player.nick in KILLS: # FIXME + del KILLS[player.nick] + + if old_role not in wcroles and evt.data["role"] in wcroles: + # a new wofl has joined the party, give them tummy rubs and the wolf list + # and let the other wolves know to break out the confetti and villager steaks + wofls = get_players(wcroles) + evt.data["in_wolfchat"] = True + if wofls: + new_wolves = [] + for wofl in wofls: + wofl.queue_message(messages["wolfchat_new_member"].format(player, an, sayrole)) + wofl.send_messages() + pl = get_players() + pl.remove(player) random.shuffle(pl) - pl.remove(actor) # remove self from list - notify = [] - to_send = [] - for player in pl: - prole = get_main_role(player) - if player is target: - prole = actor_role - wevt = Event("wolflist", {"tags": set()}) - wevt.dispatch(var, player, actor) + pt = [] + wevt = Event("wolflist", {"tags": set()}) + for p in pl: + prole = get_main_role(p) + wevt.data["tags"].clear() + wevt.dispatch(var, p, player) tags = " ".join(wevt.data["tags"]) if prole in wcroles: if tags: tags += " " - to_send.append("\u0002{0}\u0002 ({1}{2})".format(player, tags, prole)) - notify.append(player) + pt.append("\u0002{0}\u0002 ({1}{2})".format(p, tags, prole)) elif tags: - to_send.append("{0} ({1})".format(player, tags)) + pt.append("{0} ({1})".format(p, tags)) else: - to_send.append(player.nick) + pt.append(p.nick) - for player in notify: - player.queue_message(messages["players_exchanged_roles"].format(target, actor)) - if notify: - player.send_messages() + evt.data["messages"].append(messages["players_list"].format(", ".join(pt))) - evt.data["actor_messages"].append(messages["players_list"].format(", ".join(to_send))) - if target_role in CAN_KILL and var.DISEASED_WOLVES: - evt.data["actor_messages"].append(messages["ill_wolves"]) - if var.ALPHA_ENABLED and target_role == "alpha wolf" and actor.nick not in var.ALPHA_WOLVES: - evt.data["actor_messages"].append(messages["wolf_bite"]) - elif actor_role in wcroles and target_role not in wcroles: - pl = get_players() - random.shuffle(pl) - pl.remove(target) # remove self from list - notify = [] - to_send = [] - for player in pl: - prole = get_main_role(player) - if player is actor: - prole = target_role - wevt = Event("wolflist", {"tags": set()}) - wevt.dispatch(var, player, target) - tags = " ".join(wevt.data["tags"]) - if prole in wcroles: - if tags: - tags += " " - to_send.append("\u0002{0}\u0002 ({1}{2})".format(player, tags, prole)) - notify.append(player) - elif tags: - to_send.append("{0} ({1})".format(player, tags)) - else: - to_send.append(player.nick) - - for player in notify: - player.queue_message(messages["players_exchanged_roles"].format(actor, target)) - if notify: - player.send_messages() - - evt.data["target_messages"].append(messages["players_list"].format(", ".join(to_send))) - if actor_role in CAN_KILL and var.DISEASED_WOLVES: - evt.data["target_messages"].append(messages["ill_wolves"]) - if var.ALPHA_ENABLED and actor_role == "alpha wolf" and target.nick not in var.ALPHA_WOLVES: - evt.data["target_messages"].append(messages["wolf_bite"]) - - if actor.nick in KILLS: - del KILLS[actor.nick] - if target.nick in KILLS: - del KILLS[target.nick] + if var.PHASE == "night": + # inform the new wolf that they can kill and stuff + if evt.data["role"] in CAN_KILL and var.DISEASED_WOLVES: + evt.data["messages"].append(messages["ill_wolves"]) + # FIXME: split when alpha wolf is split + if var.ALPHA_ENABLED and evt.data["role"] == "alpha wolf" and player.nick not in var.ALPHA_WOLVES: + evt.data["messages"].append(messages["wolf_bite"]) @event_listener("chk_nightdone", priority=3) def on_chk_nightdone(evt, var): @@ -320,7 +304,7 @@ def on_chk_nightdone(evt, var): evt.data["actedcount"] += len(KILLS) evt.data["nightroles"].append(users.FakeUser.from_nick("@WolvesAgree@")) # check if wolves are actually agreeing or not; - # only count agreement_user if they are + # only add to count if they actually agree # (this is *slighty* less hacky than deducting 1 from actedcount as we did previously) kills = set() for ls in KILLS.values(): diff --git a/src/roles/wolfcub.py b/src/roles/wolfcub.py index 89365d3..f34ba2d 100644 --- a/src/roles/wolfcub.py +++ b/src/roles/wolfcub.py @@ -25,23 +25,10 @@ def on_del_player(evt, var, user, mainrole, allroles, death_triggers): global ANGRY_WOLVES ANGRY_WOLVES = True -# wolf fires on priority 2, so we can add our extra messages now (at default priority 5) -@event_listener("exchange_roles") -def on_exchange(evt, var, actor, target, actor_role, target_role): - if not ANGRY_WOLVES: - return - - wcroles = var.WOLFCHAT_ROLES - if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES: - if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF: - wcroles = var.WOLF_ROLES - else: - wcroles = var.WOLF_ROLES | {"traitor"} - - if target_role in wcroles and actor_role not in wcroles and wolf.wolf_can_kill(var, target): - evt.data["actor_messages"].append(messages["angry_wolves"]) - elif actor_role in wcroles and target_role not in wcroles and wolf.wolf_can_kill(var, actor): - evt.data["target_messages"].append(messages["angry_wolves"]) +@event_listener("new_role") +def on_new_role(evt, var, player, old_role): + if ANGRY_WOLVES and evt.data["in_wolfchat"] and wolf.wolf_can_kill(var, player): + evt.data["messages"].append(messages["angry_wolves"]) @event_listener("transition_night_end", priority=3) def on_transition_night_end(evt, var): diff --git a/src/utilities.py b/src/utilities.py index ecdad8d..baa5a63 100644 --- a/src/utilities.py +++ b/src/utilities.py @@ -12,7 +12,7 @@ __all__ = ["pm", "is_fake_nick", "mass_mode", "mass_privmsg", "reply", "is_user_simple", "is_user_notice", "in_wolflist", "relay_wolfchat_command", "irc_lower", "irc_equals", "match_hostmask", "is_owner", "is_admin", "plural", "singular", "list_players", - "get_role", "get_roles", "change_role", "role_order", "break_long_message", + "get_role", "get_roles", "role_order", "break_long_message", "complete_match", "complete_one_match", "get_victim", "InvalidModeException"] # message either privmsg or notice, depending on user settings def pm(cli, target, message): @@ -316,16 +316,6 @@ def get_roles(*roles, rolemap=None): all_roles.append(rolemap[role]) return [u.nick for u in itertools.chain(*all_roles)] -# TODO: move this to functions.py -def change_role(user, oldrole, newrole, set_final=True): - var.ROLES[oldrole].remove(user) - var.ROLES[newrole].add(user) - # only adjust MAIN_ROLES/FINAL_ROLES if we're changing the user's actual role - if var.MAIN_ROLES[user] == oldrole: - var.MAIN_ROLES[user] = newrole - if set_final: - var.FINAL_ROLES[user.nick] = newrole - role_order = lambda: var.ROLE_GUIDE def break_long_message(phrases, joinstr = " "): diff --git a/src/wolfgame.py b/src/wolfgame.py index ae00932..0220b36 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -21,8 +21,9 @@ import copy import fnmatch -import itertools import functools +import itertools +import json import math import os import platform @@ -37,8 +38,8 @@ import threading import time import traceback import urllib.request + from collections import defaultdict, deque, Counter -import json from datetime import datetime, timedelta from oyoyo.parse import parse_nick @@ -48,13 +49,19 @@ import src import src.settings as var from src.utilities import * from src import db, events, dispatcher, channels, users, hooks, logger, debuglog, errlog, plog -from src.decorators import command, cmd, hook, handle_error, event_listener, COMMANDS + from src.containers import UserList, UserSet, UserDict, DefaultUserDict -from src.functions import get_players, get_all_players, get_participants, get_main_role, get_all_roles, get_reveal_role, get_target +from src.decorators import command, cmd, hook, handle_error, event_listener, COMMANDS from src.messages import messages from src.warnings import * from src.context import IRCContext +from src.functions import ( + get_players, get_all_players, get_participants, + get_main_role, get_all_roles, get_reveal_role, + get_target, change_role, + ) + # done this way so that events is accessible in !eval (useful for debugging) Event = events.Event @@ -2109,8 +2116,6 @@ def stop_game(var, winner="", abort=False, additional_winners=None, log=True): teams = {"monster":"monsters", "demoniac":"demoniacs"} if rol in teams and winner == teams[rol]: won = True - elif rol == "turncoat" and splr in var.TURNCOATS and var.TURNCOATS[splr][0] != "none": - won = (winner == var.TURNCOATS[splr][0]) elif rol == "fool" and "@" + splr == winner: won = True @@ -2735,8 +2740,7 @@ def rename_player(var, user, prefix): dictvar.update(kvp) if prefix in dictvar.keys(): del dictvar[prefix] - for dictvar in (var.FINAL_ROLES, var.TURNCOATS, - var.DOCTORS, var.BITTEN_ROLES, var.LYCAN_ROLES): + for dictvar in (var.FINAL_ROLES, var.DOCTORS, var.BITTEN_ROLES, var.LYCAN_ROLES): if prefix in dictvar.keys(): dictvar[nick] = dictvar.pop(prefix) # defaultdict(list), where keys are nicks and items in list do not matter @@ -3246,28 +3250,9 @@ def transition_day(gameid=0): if vrole not in var.WOLFCHAT_ROLES: revt.data["message"].append(messages["new_wolf"]) var.EXTRA_WOLVES += 1 - victim.send(messages["lycan_turn"]) var.LYCAN_ROLES[victim.nick] = vrole - change_role(victim, vrole, "wolf") + change_role(var, victim, vrole, "wolf", message="lycan_turn") var.ROLES["lycan"].discard(victim) # in the event lycan was a template, we want to ensure it gets purged - wolves = get_players(var.WOLFCHAT_ROLES) - random.shuffle(wolves) - wolves.remove(victim) # remove self from list - to_send = [] - for wolf in wolves: - wolf.queue_message(messages["lycan_wc_notification"].format(victim)) - role = get_main_role(wolf) - wevt = Event("wolflist", {"tags": set()}) - wevt.dispatch(var, wolf, victim) - tags = " ".join(wevt.data["tags"]) - if tags: - tags += " " - to_send.append("\u0002{0}\u0002 ({1}{2})".format(wolf, tags, role)) - - if wolves: - wolf.send_messages() - - victim.send(messages["wolves_list"].format(", ".join(to_send))) revt.data["novictmsg"] = False elif victim not in revt.data["dead"]: # not already dead via some other means if var.ROLE_REVEAL in ("on", "team"): @@ -3368,29 +3353,28 @@ def transition_day(gameid=0): continue newrole = "wolf" + to_send = "bitten_turn" if chumprole == "guardian angel": - chump.send(messages["fallen_angel_turn"]) + to_send = "fallen_angel_turn" # fallen angels also automatically gain the assassin template if they don't already have it newrole = "fallen angel" var.ROLES["assassin"].add(chump) debuglog("{0} (guardian angel) TURNED FALLEN ANGEL".format(chump)) elif chumprole in ("seer", "oracle", "augur"): - chump.send(messages["seer_turn"]) + to_send = "seer_turn" newrole = "doomsayer" debuglog("{0} ({1}) TURNED DOOMSAYER".format(chump, chumprole)) elif chumprole in var.TOTEM_ORDER: - chump.send(messages["shaman_turn"]) + to_send = "shaman_turn" newrole = "wolf shaman" debuglog("{0} ({1}) TURNED WOLF SHAMAN".format(chump, chumprole)) elif chumprole == "harlot": - chump.send(messages["harlot_turn"]) + to_send = "harlot_turn" debuglog("{0} (harlot) TURNED WOLF".format(chump)) else: - chump.send(messages["bitten_turn"]) debuglog("{0} ({1}) TURNED WOLF".format(chump, chumprole)) var.BITTEN_ROLES[chump.nick] = chumprole - change_role(chump, chumprole, newrole) - relay_wolfchat_command(chump.client, chump.nick, messages["wolfchat_new_member"].format(chump, newrole), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) + change_role(var, chump, chumprole, newrole, message=to_send) killer_role = {} for deadperson in dead: @@ -3427,8 +3411,6 @@ def chk_nightdone(): if var.PHASE != "night": return - pl = get_players() - spl = set(pl) actedcount = sum(map(len, (var.PASSED, var.OBSERVED, var.HEXED, var.CURSED))) nightroles = list(get_all_players(("sorcerer", "hag", "warlock", "werecrow"))) @@ -3438,19 +3420,6 @@ def chk_nightdone(): nightroles.extend(get_all_players(("alpha wolf",))) actedcount += len([p for p in var.ALPHA_WOLVES if p in get_roles("alpha wolf")]) # FIXME - # add in turncoats who should be able to act -- if they passed they're already in var.PASSED - # but if they can act they're in var.TURNCOATS where the second tuple item is the current night - # (if said tuple item is the previous night, then they are not allowed to act tonight) - for tc, tu in var.TURNCOATS.items(): - user = users._get(tc) # FIXME - if user not in pl: - continue - if tu[1] == var.NIGHT_COUNT: - nightroles.append(user) - actedcount += 1 - elif tu[1] < var.NIGHT_COUNT - 1: - nightroles.append(user) - event = Event("chk_nightdone", {"actedcount": actedcount, "nightroles": nightroles, "transition_day": transition_day}) event.dispatch(var) actedcount = event.data["actedcount"] @@ -3603,6 +3572,11 @@ def choose_target(actor, nick): # returns true if a swap happened # check for that to short-circuit the nightrole def check_exchange(cli, actor, nick): + # July 2nd, 2018 - The exchanging mechanic has been updated and no longer handles + # some forms of exchanging properly. As a result, we are disabling exchanging until + # role classes are implemented, which needs all roles to be fully split first. + # Until then, this function is a no-op. -Vgr & woffle + return False #some roles can act on themselves, ignore this if actor == nick: return False @@ -3636,8 +3610,6 @@ def check_exchange(cli, actor, nick): var.ALPHA_WOLVES.discard(actor) elif actor_role == "warlock": var.CURSED.discard(actor) - elif actor_role == "turncoat": - del var.TURNCOATS[actor] # var.PASSED is used by many roles @@ -3660,17 +3632,20 @@ def check_exchange(cli, actor, nick): var.ALPHA_WOLVES.discard(nick) elif nick_role == "warlock": var.CURSED.discard(nick) - elif nick_role == "turncoat": - del var.TURNCOATS[nick] evt = Event("exchange_roles", {"actor_messages": [], "target_messages": [], "actor_role": actor_role, "target_role": nick_role}) - evt.dispatch(var, user, target, actor_role, nick_role) + evt.dispatch(var, user, target, actor_role, nick_role) # FIXME: Deprecated, change in favor of new_role and swap_role_state - actor_role = evt.data["actor_role"] - nick_role = evt.data["target_role"] + nick_role = change_role(var, user, actor_role, nick_role, inherit_from=target) + actor_role = change_role(var, target, nick_role, actor_role, inherit_from=user) + + if nick_role == actor_role: # make sure that two players with the same role exchange their role state properly (e.g. dullahan) + evt_same = Event("swap_role_state", {"actor_messages": [], "target_messages": []}) + evt_same.dispatch(var, user, target, actor_role) + + user.send(*evt_same.data["actor_messages"]) + target.send(*evt_same.data["target_messages"]) - change_role(user, actor_role, nick_role) - change_role(target, nick_role, actor_role) if actor in var.BITTEN_ROLES.keys(): if nick in var.BITTEN_ROLES.keys(): var.BITTEN_ROLES[actor], var.BITTEN_ROLES[nick] = var.BITTEN_ROLES[nick], var.BITTEN_ROLES[actor] @@ -3691,25 +3666,6 @@ def check_exchange(cli, actor, nick): var.LYCAN_ROLES[actor] = var.LYCAN_ROLES[nick] del var.LYCAN_ROLES[nick] - actor_rev_role = actor_role - if actor_role in var.HIDDEN_ROLES: - actor_rev_role = var.DEFAULT_ROLE - elif actor_role in var.HIDDEN_VILLAGERS: - actor_rev_role = "villager" - - nick_rev_role = nick_role - if nick_role in var.HIDDEN_ROLES: - nick_rev_role = var.DEFAULT_ROLE - elif actor_role in var.HIDDEN_VILLAGERS: - nick_rev_role = "villager" - - # don't say who, since misdirection/luck totem may have switched it - # and this makes life far more interesting - user.send(messages["role_swap"].format(nick_rev_role)) - target.send(messages["role_swap"].format(actor_rev_role)) - user.send(*evt.data["actor_messages"]) - target.send(*evt.data["target_messages"]) - wcroles = var.WOLFCHAT_ROLES if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES: if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF: @@ -3726,8 +3682,6 @@ def check_exchange(cli, actor, nick): if player in get_roles("cursed villager"): # FIXME pl[i] = player + " (cursed)" pm(cli, actor, messages["players_list"].format(", ".join(pl))) - elif nick_role == "turncoat": - var.TURNCOATS[actor] = ("none", -1) if actor_role not in wcroles and actor_role == "warlock": # this means warlock isn't in wolfchat, so only give cursed list @@ -3738,8 +3692,6 @@ def check_exchange(cli, actor, nick): if player in get_roles("cursed villager"): # FIXME pl[i] = player + " (cursed)" pm(cli, nick, messages["players_list"].format(", ".join(pl))) - elif actor_role == "turncoat": - var.TURNCOATS[nick] = ("none", -1) var.EXCHANGED_ROLES.append((actor, nick)) return True @@ -3994,15 +3946,14 @@ def immunize(cli, nick, chan, rest): var.DISEASED.remove(victim) if victim in get_roles("lycan"): # FIXME lycan = True - lycan_message = (messages["lycan_cured"]) if get_role(victim) == "lycan": - change_role(users._get(victim), "lycan", "villager") # FIXME + change_role(var, users._get(victim), "lycan", "villager", message="lycan_cured") # FIXME else: var.ROLES["lycan"].remove(users._get(victim)) # FIXME var.CURED_LYCANS.add(victim) else: lycan_message = messages[evt.data["message"]] - pm(cli, victim, (messages["immunization_success"]).format(lycan_message)) + pm(cli, victim, (messages["immunization_success"]).format(lycan_message)) if evt.data["success"]: var.IMMUNIZED.add(victim) var.DOCTORS[nick] -= 1 @@ -4040,57 +3991,18 @@ def bite_cmd(cli, nick, chan, rest): relay_wolfchat_command(cli, nick, messages["alpha_bite_wolfchat"].format(nick, victim), ("alpha wolf",), is_wolf_command=True) debuglog("{0} ({1}) BITE: {2} ({3})".format(nick, get_role(nick), actual, get_role(actual))) -@cmd("pass", chan=False, pm=True, playing=True, phases=("night",), roles=("turncoat", "warlock")) +@cmd("pass", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("warlock",)) def pass_cmd(cli, nick, chan, rest): """Decline to use your special power for that night.""" - nickrole = get_role(nick) - - # turncoats can change roles and pass even if silenced - if nickrole != "turncoat" and nick in var.SILENCED: - if chan == nick: - pm(cli, nick, messages["silenced"]) - else: - cli.notice(nick, messages["silenced"]) + if nick in var.CURSED: + pm(cli, nick, messages["already_cursed"]) return - - 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. - pm(cli, nick, messages["turncoat_fail"]) - return - pm(cli, nick, messages["turncoat_pass"]) - if var.TURNCOATS[nick][1] == var.NIGHT_COUNT - 1: - # don't add to var.PASSED since we aren't counting them anyway for nightdone - # let them still use !pass though to make them feel better or something - return - var.PASSED.add(nick) - elif nickrole == "warlock": - if nick in var.CURSED: - pm(cli, nick, messages["already_cursed"]) - return - pm(cli, nick, messages["warlock_pass"]) - relay_wolfchat_command(cli, nick, messages["warlock_pass_wolfchat"].format(nick), ("warlock",)) - var.PASSED.add(nick) + pm(cli, nick, messages["warlock_pass"]) + relay_wolfchat_command(cli, nick, messages["warlock_pass_wolfchat"].format(nick), ("warlock",)) + var.PASSED.add(nick) debuglog("{0} ({1}) PASS".format(nick, get_role(nick))) -@cmd("side", chan=False, pm=True, playing=True, phases=("night",), roles=("turncoat",)) -def change_sides(cli, nick, chan, rest, sendmsg=True): - if var.TURNCOATS[nick][1] == var.NIGHT_COUNT - 1: - pm(cli, nick, messages["turncoat_already_turned"]) - return - - team = re.split(" +", rest)[0] - team = complete_one_match(team, ("villagers", "wolves")) - if not team: - pm(cli, nick, messages["turncoat_error"]) - return - - pm(cli, nick, messages["turncoat_success"].format(team)) - var.TURNCOATS[nick] = (team, var.NIGHT_COUNT) - debuglog("{0} ({1}) SIDE {2}".format(nick, get_role(nick), team)) - @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.""" @@ -4437,21 +4349,6 @@ def transition_night(): else: lycan.send(messages["lycan_notify"]) - for turncoat in get_all_players(("turncoat",)): - # they start out as unsided, but can change n1 - if turncoat.nick not in var.TURNCOATS: - var.TURNCOATS[turncoat.nick] = ("none", -1) - - if turncoat.prefers_simple(): - turncoat.send(messages["turncoat_simple"].format(var.TURNCOATS[turncoat.nick][0])) - else: - message = messages["turncoat_notify"] - if var.TURNCOATS[turncoat.nick][0] != "none": - message += messages["turncoat_current_team"].format(var.TURNCOATS[turncoat.nick][0]) - else: - message += messages["turncoat_no_team"] - turncoat.send(message) - for priest in get_all_players(("priest",)): if priest.prefers_simple(): priest.send(messages["priest_simple"]) @@ -4738,7 +4635,6 @@ def start(cli, nick, chan, forced = False, restart = ""): var.BITTEN_ROLES = {} var.LYCAN_ROLES = {} var.ACTIVE_PROTECTIONS = defaultdict(list) - var.TURNCOATS = {} var.EXCHANGED_ROLES = [] var.EXTRA_WOLVES = 0 var.PRIESTS = set() @@ -4857,8 +4753,10 @@ def start(cli, nick, chan, forced = False, restart = ""): var.LAST_TIME = None var.LAST_VOTES = None - event = Event("role_assignment", {}) - event.dispatch(var, var.CURRENT_GAMEMODE.name, get_players()) + for role, players in var.ROLES.items(): + for player in players: + evt = Event("new_role", {"messages": [], "role": role}, inherit_from=None) + evt.dispatch(var, player, None) if not restart: gamemode = var.CURRENT_GAMEMODE.name @@ -5567,10 +5465,6 @@ def myrole(var, wrapper, message): for msg in evt.data["messages"]: wrapper.pm(msg) - # Remind turncoats of their side - if role == "turncoat": - wrapper.pm(messages["turncoat_side"].format(var.TURNCOATS.get(wrapper.source.nick, "none")[0])) - # Check for gun/bullets if wrapper.source not in var.ROLES["amnesiac"] and wrapper.source in var.GUNNERS and var.GUNNERS[wrapper.source]: role = "gunner" @@ -6041,9 +5935,6 @@ def revealroles(var, wrapper, message): # print how many bullets normal gunners have if (role == "gunner" or role == "sharpshooter") and user in var.GUNNERS: special_case.append("{0} bullet{1}".format(var.GUNNERS[user], "" if var.GUNNERS[user] == 1 else "s")) - elif role == "turncoat" and user.nick in var.TURNCOATS: - special_case.append("currently with \u0002{0}\u0002".format(var.TURNCOATS[user.nick][0]) - if var.TURNCOATS[user.nick][0] != "none" else "not currently on any side") evt = Event("revealroles_role", {"special_case": special_case}) evt.dispatch(var, user, role)