From 745a1dc68a55d0f11ff883803a0a1ad34c8cd043 Mon Sep 17 00:00:00 2001 From: Em Barry Date: Mon, 23 Apr 2018 13:25:38 -0400 Subject: [PATCH] Convert chk_decision (#317) Convert chk_decision, chk_nightdone, transition_day, transition_night, doomsayer, mayor, and convert+split shamans in three files with a shared helper. Fixes and updates for the User containers, and some other tweaks and fixes. --- messages/en.json | 4 +- src/containers.py | 49 +- src/decorators.py | 8 +- src/functions.py | 15 +- src/gamemodes.py | 57 +- src/handler.py | 8 +- src/roles/__init__.py | 2 +- src/roles/_shaman_helper.py | 537 ++++++++++++++++++ src/roles/angel.py | 2 +- src/roles/blessed.py | 5 +- src/roles/crazed_shaman.py | 98 ++++ src/roles/cursed.py | 3 +- src/roles/detective.py | 2 +- src/roles/doomsayer.py | 116 ++-- src/roles/dullahan.py | 7 +- src/roles/fallenangel.py | 3 +- src/roles/harlot.py | 3 +- src/roles/hunter.py | 3 +- src/roles/investigator.py | 3 +- src/roles/madscientist.py | 8 +- src/roles/mayor.py | 17 +- src/roles/mystic.py | 3 +- src/roles/piper.py | 3 +- src/roles/seer.py | 2 +- src/roles/shaman.py | 645 ++-------------------- src/roles/skel.py | 3 +- src/roles/succubus.py | 34 +- src/roles/traitor.py | 3 +- src/roles/vengefulghost.py | 3 +- src/roles/vigilante.py | 3 +- src/roles/villager.py | 3 +- src/roles/wildchild.py | 8 +- src/roles/wolf.py | 2 +- src/roles/wolf_shaman.py | 102 ++++ src/roles/wolfcub.py | 3 +- src/settings.py | 2 +- src/wolfgame.py | 1040 +++++++++++++++++------------------ 37 files changed, 1522 insertions(+), 1287 deletions(-) create mode 100644 src/roles/_shaman_helper.py create mode 100644 src/roles/crazed_shaman.py create mode 100644 src/roles/wolf_shaman.py diff --git a/messages/en.json b/messages/en.json index 0bc3924..1e11519 100644 --- a/messages/en.json +++ b/messages/en.json @@ -502,7 +502,6 @@ "retribution_totem": "If the player who is given this totem will die tonight, they also kill anyone who killed them.", "misdirection_totem": "If the player who is given this totem attempts to use a power the following day or night, they will target a player adjacent to their intended target instead of the player they targeted.", "deceit_totem": "If the player who is given this totem is a seer or an oracle, or is seen by a seer or an oracle, the vision will be shifted. If the person would be seen as wolf, they are instead seen as a villager. Otherwise, they are seen as a wolf.", - "generic_bug_totem": "No description for this totem is available. This is a bug, so please report this to the admins.", "shaman_simple": "You are a \u0002{0}\u0002.", "totem_simple": "You have the \u0002{0}\u0002 totem.", "hunter_notify": "You are a \u0002hunter\u0002. Once per game, you may kill another player with \"kill \". If you do not wish to kill anyone tonight, use \"pass\" instead.", @@ -562,6 +561,8 @@ "need_one_wolf": "There has to be at least one wolf!", "too_many_wolves": "Too many wolves.", "error_role_players_count": "Error: Not all roles have defined player counts.", + "error_frole_too_many": "There are too many users assigned to role {0}. Please try again.", + "too_many_roles": "There are not enough players for the number of preset roles. Please try again.", "default_reset": "The default settings have been restored. Please {0}start again.", "command_disabled_admin": "This command has been disabled by an admin.", "not_enough_targets": "Not enough valid targets for the {0} template.", @@ -621,6 +622,7 @@ "invalid_target": "This can only be done on players in the channel or fake nicks.", "admin_only_force": "Only full admins can force an admin-only command.", "operation_successful": "Operation successful.", + "frole_incorrect": "Invalid arguments for {0}frole: {1}", "not_owner": "You are not the owner.", "invalid_permissions": "You do not have permission to use that command.", "player_joined_deadchat": "\u0002{0}\u0002 has joined the deadchat.", diff --git a/src/containers.py b/src/containers.py index 476458f..bd397d5 100644 --- a/src/containers.py +++ b/src/containers.py @@ -1,6 +1,8 @@ +import copy + from src.users import User -__all__ = ["UserList", "UserSet", "UserDict"] +__all__ = ["UserList", "UserSet", "UserDict", "DefaultUserDict"] """ * Important * @@ -45,12 +47,27 @@ class UserList(list): def __exit__(self, exc_type, exc_value, tb): self.clear() + def __eq__(self, other): + return self is other + + def __copy__(self): + return type(self)(self) + + def __deepcopy__(self, memo): + return type(self)(copy.deepcopy(x, memo) for x in self) + def __add__(self, other): if not isinstance(other, list): return NotImplemented self.extend(other) + def __getitem__(self, item): + new = super().__getitem__(item) + if isinstance(item, slice): + new = type(self)(new) + return new + def __setitem__(self, index, value): if not isinstance(value, User): raise TypeError("UserList may only contain User instances") @@ -135,6 +152,12 @@ class UserSet(set): def __exit__(self, exc_type, exc_value, tb): self.clear() + def __copy__(self): + return type(self)(self) + + def __deepcopy__(self, memo): + return type(self)(copy.deepcopy(x, memo) for x in self) + # Comparing UserSet instances for equality doesn't make much sense in our context # However, if there are identical instances in a list, we only want to remove ourselves @@ -263,6 +286,18 @@ class UserDict(dict): def __exit__(self, exc_type, exc_value, tb): self.clear() + def __eq__(self, other): + return self is other + + def __copy__(self): + return type(self)(self) + + def __deepcopy__(self, memo): + new = type(self)() + for key, value in self.items(): + new[key] = copy.deepcopy(value, memo) + return new + def __setitem__(self, item, value): old = self.get(item) super().__setitem__(item, value) @@ -288,6 +323,9 @@ class UserDict(dict): if value not in self.values(): value.dict_values.remove(self) + if isinstance(value, (UserSet, UserList, UserDict)): + value.clear() + def clear(self): for key, value in self.items(): if isinstance(key, User): @@ -339,3 +377,12 @@ class UserDict(dict): iterable = iterable.items() for key, value in iterable: self[key] = value + +class DefaultUserDict(UserDict): + def __init__(_self, _factory, _it=(), **kwargs): + _self.factory = _factory + super().__init__(_it, **kwargs) + + def __missing__(self, key): + self[key] = self.factory() + return self[key] diff --git a/src/decorators.py b/src/decorators.py index 5d44a5c..815f9b1 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -248,6 +248,10 @@ class command: self.aliases.append(name) alias = True + if playing: # Don't restrict to owners or allow in alt channels + self.owner_only = False + self.alt_allowed = False + def __call__(self, func): if isinstance(func, command): func = func.func @@ -303,7 +307,7 @@ class command: # Role commands might end the night if it's nighttime if var.PHASE == "night": from src.wolfgame import chk_nightdone - chk_nightdone(cli) + chk_nightdone() return if self.owner_only: @@ -461,7 +465,7 @@ class cmd: # Role commands might end the night if it's nighttime if var.PHASE == "night": from src.wolfgame import chk_nightdone - chk_nightdone(cli) + chk_nightdone() return forced_owner_only = False diff --git a/src/functions.py b/src/functions.py index 99a8df3..368bb5a 100644 --- a/src/functions.py +++ b/src/functions.py @@ -6,7 +6,8 @@ from src import users __all__ = [ "get_players", "get_all_players", "get_participants", "get_target", - "get_main_role", "get_all_roles", "get_reveal_role" + "get_main_role", "get_all_roles", "get_reveal_role", + "is_known_wolf_ally", ] def get_players(roles=None, *, mainroles=None): @@ -104,5 +105,17 @@ def get_reveal_role(user): else: return "village member" +def is_known_wolf_ally(actor, target): + actor_role = get_main_role(actor) + target_role = get_main_role(target) + + wolves = var.WOLFCHAT_ROLES + if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES: + if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF: + wolves = var.WOLF_ROLES + else: + wolves = var.WOLF_ROLES | {"traitor"} + + return actor_role in wolves and target_role in wolves # vim: set sw=4 expandtab: diff --git a/src/gamemodes.py b/src/gamemodes.py index d87b615..515c8f9 100644 --- a/src/gamemodes.py +++ b/src/gamemodes.py @@ -12,7 +12,7 @@ from src.utilities import * from src.messages import messages from src.functions import get_players, get_all_players, get_main_role from src.decorators import handle_error, command -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src import events, channels, users def game_mode(name, minp, maxp, likelihood = 0): @@ -172,6 +172,23 @@ class DefaultMode(GameMode): self.ROLE_INDEX = role_index self.ROLE_GUIDE = role_guide + def startup(self): + events.add_listener("chk_decision", self.chk_decision, priority=20) + + def teardown(self): + events.remove_listener("chk_decision", self.chk_decision, priority=20) + + def chk_decision(self, evt, var, force): + if len(var.ALL_PLAYERS) <= 9 and var.VILLAGERGAME_CHANCE > 0: + if users.Bot in evt.data["votelist"]: + if len(evt.data["votelist"][users.Bot]) == len(set(evt.params.voters) - evt.data["not_lynching"]): + channels.Main.send(messages["villagergame_nope"]) + from src.wolfgame import stop_game + stop_game("wolves") + evt.prevent_default = True + else: + del evt.data["votelist"][users.Bot] + @game_mode("villagergame", minp = 4, maxp = 9, likelihood = 0) class VillagergameMode(GameMode): """This mode definitely does not exist, now please go away.""" @@ -194,12 +211,14 @@ class VillagergameMode(GameMode): events.add_listener("chk_nightdone", self.chk_nightdone) events.add_listener("transition_day_begin", self.transition_day) events.add_listener("retribution_kill", self.on_retribution_kill, priority=4) + events.add_listener("chk_decision", self.chk_decision, priority=20) def teardown(self): events.remove_listener("chk_win", self.chk_win) events.remove_listener("chk_nightdone", self.chk_nightdone) events.remove_listener("transition_day_begin", self.transition_day) events.remove_listener("retribution_kill", self.on_retribution_kill, priority=4) + events.remove_listener("chk_decision", self.chk_decision, priority=20) def chk_win(self, evt, var, rolemap, mainroles, lpl, lwolves, lrealwolves): # village can only win via unanimous vote on the bot nick @@ -214,15 +233,15 @@ class VillagergameMode(GameMode): def chk_nightdone(self, evt, var): transition_day = evt.data["transition_day"] - evt.data["transition_day"] = lambda cli, gameid=0: self.prolong_night(cli, var, gameid, transition_day) + evt.data["transition_day"] = lambda gameid=0: self.prolong_night(var, gameid, transition_day) - def prolong_night(self, cli, var, gameid, transition_day): + def prolong_night(self, var, gameid, transition_day): nspecials = len(get_all_players(("seer", "harlot", "shaman", "crazed shaman"))) rand = random.gauss(5, 1.5) if rand <= 0 and nspecials > 0: - transition_day(cli, gameid=gameid) + transition_day(gameid=gameid) else: - t = threading.Timer(abs(rand), transition_day, args=(cli,), kwargs={"gameid": gameid}) + t = threading.Timer(abs(rand), transition_day, kwargs={"gameid": gameid}) t.start() def transition_day(self, evt, var): @@ -265,6 +284,16 @@ class VillagergameMode(GameMode): evt.data["target"] = None evt.stop_processing = True + def chk_decision(self, evt, var, force): + if users.Bot in evt.data["votelist"]: + if len(evt.data["votelist"][users.Bot]) == len(set(evt.params.voters) - evt.data["not_lynching"]): + channels.Main.send(messages["villagergame_win"]) + from src.wolfgame import stop_game + stop_game("everyone") + evt.prevent_default = True + else: + del evt.data["votelist"][users.Bot] + @game_mode("foolish", minp = 8, maxp = 24, likelihood = 8) class FoolishMode(GameMode): """Contains the fool, be careful not to lynch them!""" @@ -1315,7 +1344,7 @@ class MudkipMode(GameMode): events.remove_listener("daylight_warning", self.daylight_warning) events.remove_listener("transition_night_begin", self.transition_night_begin) - def chk_decision(self, evt, cli, var, force): + def chk_decision(self, evt, var, force): # If everyone is voting, end day here with the person with plurality being voted. If there's a tie, # kill all tied players rather than hanging. The intent of this is to benefit village team in the event # of a stalemate, as they could use the extra help (especially in 5p). @@ -1323,13 +1352,15 @@ class MudkipMode(GameMode): # in here, this means we're in a child chk_decision event called from this one # we need to ensure we don't turn into nighttime prematurely or try to vote # anyone other than the person we're forcing the lynch on - evt.data["transition_night"] = lambda cli: None + evt.data["transition_night"] = lambda: None if force: - evt.data["votelist"] = {force: set()} - evt.data["numvotes"] = {force: 0} + evt.data["votelist"].clear() + evt.data["votelist"][force] = set() + evt.data["numvotes"].clear() + evt.data["numvotes"][force] = 0 else: - evt.data["votelist"] = {} - evt.data["numvotes"] = {} + evt.data["votelist"].clear() + evt.data["numvotes"].clear() return avail = len(evt.params.voters) @@ -1357,12 +1388,12 @@ class MudkipMode(GameMode): for p in tovote: deadlist = tovote[:] deadlist.remove(p) - chk_decision(cli, force=p, deadlist=deadlist, end_game=p is last) + chk_decision(force=p, deadlist=deadlist, end_game=p is last) self.recursion_guard = False # gameid changes if game stops due to us voting someone if var.GAME_ID == gameid: - evt.data["transition_night"](cli) + evt.data["transition_night"]() # make original chk_decision that called us no-op evt.prevent_default = True diff --git a/src/handler.py b/src/handler.py index 0b3ef44..71e004b 100644 --- a/src/handler.py +++ b/src/handler.py @@ -24,10 +24,12 @@ def on_privmsg(cli, rawnick, chan, msg, *, notice=False, force_role=None): user = users._get(rawnick, allow_none=True) # FIXME + ch = chan.lstrip("".join(hooks.Features["PREFIX"])) + if users.equals(chan, users.Bot.nick): # PM target = users.Bot else: - target = channels.get(chan, allow_none=True) + target = channels.get(ch, allow_none=True) if user is None or target is None: return @@ -43,7 +45,7 @@ def on_privmsg(cli, rawnick, chan, msg, *, notice=False, force_role=None): if force_role is None: # if force_role isn't None, that indicates recursion; don't fire these off twice for fn in decorators.COMMANDS[""]: - fn.caller(cli, rawnick, chan, msg) + fn.caller(cli, rawnick, ch, msg) parts = msg.split(sep=" ", maxsplit=1) key = parts[0].lower() @@ -120,7 +122,7 @@ def on_privmsg(cli, rawnick, chan, msg, *, notice=False, force_role=None): for fn in cmds: if phase == var.PHASE: # FIXME: pass in var, wrapper, message instead of cli, rawnick, chan, message - fn.caller(cli, rawnick, chan, message) + fn.caller(cli, rawnick, ch, message) def unhandled(cli, prefix, cmd, *args): for fn in decorators.HOOKS.get(cmd, []): diff --git a/src/roles/__init__.py b/src/roles/__init__.py index f898c77..35a498c 100644 --- a/src/roles/__init__.py +++ b/src/roles/__init__.py @@ -9,7 +9,7 @@ search = os.path.join(path, "*.py") for f in glob.iglob(search): f = os.path.basename(f) n, _ = os.path.splitext(f) - if f == "__init__.py": + if f.startswith("_"): continue importlib.import_module("." + n, package="src.roles") diff --git a/src/roles/_shaman_helper.py b/src/roles/_shaman_helper.py new file mode 100644 index 0000000..79b96c5 --- /dev/null +++ b/src/roles/_shaman_helper.py @@ -0,0 +1,537 @@ +import itertools +import random +import re +from collections import deque + +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 + +DEATH = UserDict() # type: Dict[users.User, List[users.User]] +PROTECTION = UserList() # type: List[users.User] +REVEALING = UserSet() # type: Set[users.User] +NARCOLEPSY = UserSet() # type: Set[users.User] +SILENCE = UserSet() # type: Set[users.User] +DESPERATION = UserSet() # type: Set[users.User] +IMPATIENCE = UserList() # type: List[users.User] +PACIFISM = UserList() # type: List[users.User] +INFLUENCE = UserSet() # type: Set[users.User] +EXCHANGE = UserSet() # type: Set[users.User] +LYCANTHROPY = UserSet() # type: Set[users.User] +LUCK = UserSet() # type: Set[users.User] +PESTILENCE = UserSet() # type: Set[users.User] +RETRIBUTION = UserSet() # type: Set[users.User] +MISDIRECTION = UserSet() # type: Set[users.User] +DECEIT = UserSet() # type: Set[users.User] + +# holding vars that don't persist long enough to need special attention in +# reset/exchange/nickchange +havetotem = [] # type: List[users.User] +brokentotem = set() # type: Set[users.User] + +# To add new totem types in your custom roles/whatever.py file: +# 1. Add a key to var.TOTEM_CHANCES with the totem name +# 2. Add a message totemname_totem to your custom messages.json describing +# the totem (this is displayed at night if !simple is off) +# 3. Add events as necessary to implement the totem's functionality +# +# To add new shaman roles in your custom roles/whatever.py file: +# 1. Expand var.TOTEM_ORDER and upate var.TOTEM_CHANCES to account for the new width +# 2. Add the role to var.ROLE_GUIDE +# 3. Add the role to whatever other holding vars are necessary based on what it does +# 4. Setup initial variables and events with setup_variables(rolename, knows_totem, get_tags) +# knows_totem is a bool and keyword-only. get_tags is a function in the form get_tags(var, totem) +# and should return a set +# 5. Implement custom events if the role does anything else beyond giving totems. +# +# Modifying this file to add new totems or new shaman roles is generally never required + +def setup_variables(rolename, *, knows_totem, get_tags): + """Setup role variables and shared events.""" + TOTEMS = UserDict() # type: Dict[users.User, str] + LASTGIVEN = UserDict() # type: Dict[users.User, users.User] + SHAMANS = UserDict() # type: Dict[users.User, List[users.User]] + + @event_listener("reset") + def on_reset(evt, var): + TOTEMS.clear() + LASTGIVEN.clear() + SHAMANS.clear() + + @event_listener("begin_day") + def on_begin_day(evt, var): + SHAMANS.clear() + + @event_listener("revealroles_role") + def on_revealroles(evt, var, wrapper, user, role): + if role == rolename and user in TOTEMS: + if user in SHAMANS: + evt.data["special_case"].append("giving {0} totem to {1}".format(TOTEMS[user], SHAMANS[user][0])) + elif var.PHASE == "night": + evt.data["special_case"].append("has {0} totem".format(TOTEMS[user])) + elif user in LASTGIVEN and LASTGIVEN[user]: + evt.data["special_case"].append("gave {0} totem to {1}".format(TOTEMS[user], LASTGIVEN[user])) + + @event_listener("transition_day_begin", priority=7) + def on_transition_day_begin2(evt, var): + for shaman, (victim, target) in SHAMANS.items(): + totem = TOTEMS[shaman] + if totem == "death": # this totem stacks + if shaman not in DEATH: + DEATH[shaman] = UserList() + DEATH[shaman].append(victim) + elif totem == "protection": # this totem stacks + PROTECTION.append(victim) + elif totem == "revealing": + REVEALING.add(victim) + elif totem == "narcolepsy": + NARCOLEPSY.add(victim) + elif totem == "silence": + SILENCE.add(victim) + elif totem == "desperation": + DESPERATION.add(victim) + elif totem == "impatience": # this totem stacks + IMPATIENCE.append(victim) + elif totem == "pacifism": # this totem stacks + PACIFISM.append(victim) + elif totem == "influence": + INFLUENCE.add(victim) + elif totem == "exchange": + EXCHANGE.add(victim) + elif totem == "lycanthropy": + LYCANTHROPY.add(victim) + elif totem == "luck": + LUCK.add(victim) + elif totem == "pestilence": + PESTILENCE.add(victim) + elif totem == "retribution": + RETRIBUTION.add(victim) + elif totem == "misdirection": + MISDIRECTION.add(victim) + elif totem == "deceit": + DECEIT.add(victim) + # other totem types possibly handled in an earlier event, + # as such there is no else: clause here + + if target is not victim: + shaman.send(messages["totem_retarget"].format(victim)) + LASTGIVEN[shaman] = victim + + havetotem.extend(sorted(filter(None, LASTGIVEN.values()))) + + @event_listener("del_player") + def on_del_player(evt, var, user, mainrole, allroles, death_triggers): + for a,(b,c) in list(SHAMANS.items()): + if user in (a, b, c): + del SHAMANS[a] + + @event_listener("night_acted") + def on_acted(evt, var, user, actor): + if user in SHAMANS: + evt.data["acted"] = True + + @event_listener("get_special") + def on_get_special(evt, var): + evt.data["special"].update(get_players((rolename,))) + + @event_listener("chk_nightdone") + def on_chk_nightdone(evt, var): + evt.data["actedcount"] += len(SHAMANS) + evt.data["nightroles"].extend(get_players((rolename,))) + + @event_listener("get_role_metadata") + def on_get_role_metadata(evt, var, kind): + if kind == "night_kills": + # only add shamans here if they were given a death totem + # even though retribution kills, it is given a special kill message + evt.data[rolename] = list(TOTEMS.values()).count("death") + + @event_listener("exchange_roles") + def on_exchange(evt, var, actor, target, actor_role, target_role): + actor_totem = None + target_totem = None + if actor_role == rolename: + actor_totem = TOTEMS.pop(actor) + if actor in SHAMANS: + del SHAMANS[actor] + if actor in LASTGIVEN: + del LASTGIVEN[actor] + + if target_role == rolename: + target_totem = TOTEMS.pop(target) + if target in SHAMANS: + del SHAMANS[target] + if target in LASTGIVEN: + del LASTGIVEN[target] + + if target_totem: + if knows_totem: + evt.data["actor_messages"].append(messages["shaman_totem"].format(target_totem)) + TOTEMS[actor] = target_totem + if actor_totem: + if knows_totem: + evt.data["target_messages"].append(messages["shaman_totem"].format(actor_totem)) + TOTEMS[target] = actor_totem + + @event_listener("succubus_visit") + def on_succubus_visit(evt, var, succubus, target): + if target in SHAMANS and SHAMANS[target][1] in get_all_players(("succubus",)): + tags = get_tags(var, TOTEMS[target]) + if "beneficial" not in tags: + target.send(messages["retract_totem_succubus"].format(SHAMANS[target][1])) + del SHAMANS[target] + + if knows_totem: + @event_listener("myrole") + def on_myrole(evt, var, user): + if evt.data["role"] == rolename and var.PHASE == "night" and user not in SHAMANS: + evt.data["messages"].append(messages["totem_simple"].format(TOTEMS[user])) + + return (TOTEMS, LASTGIVEN, SHAMANS) + +def get_totem_target(var, wrapper, message, lastgiven): + """Get the totem target.""" + target = get_target(var, wrapper, re.split(" +", message)[0], allow_self=True) + if not target: + return + + if lastgiven.get(wrapper.source) is target: + wrapper.send(messages["shaman_no_target_twice"].format(target)) + return + + return target + +def give_totem(var, wrapper, target, prefix, tags, role, msg): + """Give a totem to a player. Return the value of SHAMANS[user].""" + + orig_target = target + orig_role = get_main_role(orig_target) + + evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True}, + action="give a totem{0} to".format(msg)) + + if not evt.dispatch(var, "totem", wrapper.source, target, frozenset(tags)): + return + + target = evt.data["target"] + targrole = get_main_role(target) + + wrapper.send(messages["shaman_success"].format(prefix, msg, orig_target)) + debuglog("{0} ({1}) TOTEM: {2} ({3}) as {4} ({5})".format(wrapper.source, role, target, targrole, orig_target, orig_role)) + + return UserList((target, orig_target)) + +@event_listener("see", priority=10) +def on_see(evt, var, nick, victim): + if (users._get(victim) in DECEIT) ^ (users._get(nick) in DECEIT): # FIXME + if evt.data["role"] in var.SEEN_WOLF and evt.data["role"] not in var.SEEN_DEFAULT: + evt.data["role"] = "villager" + else: + evt.data["role"] = "wolf" + +@event_listener("get_voters") +def on_get_voters(evt, var): + evt.data["voters"] -= NARCOLEPSY + +@event_listener("chk_decision", priority=1) +def on_chk_decision(evt, var, force): + nl = [] + for p in PACIFISM: + if p in evt.params.voters: + nl.append(p) + # .remove() will only remove the first instance, which means this plays nicely with pacifism countering this + for p in IMPATIENCE: + if p in nl: + nl.remove(p) + evt.data["not_lynching"].update(nl) + + for votee, voters in evt.data["votelist"].items(): + numvotes = 0 + random.shuffle(IMPATIENCE) + for v in IMPATIENCE: + if v in evt.params.voters and v not in voters and v is not votee: + # don't add them in if they have the same number or more of pacifism totems + # this matters for desperation totem on the votee + imp_count = IMPATIENCE.count(v) + pac_count = PACIFISM.count(v) + if pac_count >= imp_count: + continue + + # yes, this means that one of the impatient people will get desperation totem'ed if they didn't + # already !vote earlier. sucks to suck. >:) + voters.append(v) + + for v in voters: + weight = 1 + imp_count = IMPATIENCE.count(v) + pac_count = PACIFISM.count(v) + if pac_count > imp_count: + weight = 0 # more pacifists than impatience totems + elif imp_count == pac_count and v not in var.VOTES[votee]: + weight = 0 # impatience and pacifist cancel each other out, so don't count impatience + if v in INFLUENCE: + weight *= 2 + numvotes += weight + if votee not in evt.data["weights"]: + evt.data["weights"][votee] = {} + evt.data["weights"][votee][v] = weight + evt.data["numvotes"][votee] = numvotes + +@event_listener("chk_decision_abstain") +def on_chk_decision_abstain(evt, var, not_lynching): + for p in not_lynching: + if p in PACIFISM and p not in var.NO_LYNCH: + channels.Main.send(messages["player_meek_abstain"].format(p)) + +@event_listener("chk_decision_lynch", priority=1) +def on_chk_decision_lynch1(evt, var, voters): + votee = evt.data["votee"] + for p in voters: + if p in IMPATIENCE and p not in var.VOTES[votee]: + channels.Main.send(messages["impatient_vote"].format(p, votee)) + +# mayor is at exactly 3, so we want that to always happen before revealing totem +@event_listener("chk_decision_lynch", priority=3.1) +def on_chk_decision_lynch3(evt, var, voters): + votee = evt.data["votee"] + if votee in REVEALING: + role = get_main_role(votee) + rev_evt = Event("revealing_totem", {"role": role}) + rev_evt.dispatch(var, votee) + role = rev_evt.data["role"] + # TODO: once amnesiac is split, roll this into the revealing_totem event + if role == "amnesiac": + role = var.AMNESIAC_ROLES[votee.nick] + change_role(votee, "amnesiac", role) + var.AMNESIACS.add(votee.nick) + votee.send(messages["totem_amnesia_clear"]) + # 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 + # If turncoat, set their initial starting side to "none" just in case game ends before they can set it themselves + if role == "turncoat": + var.TURNCOATS[votee.nick] = ("none", -1) + + an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" + channels.Main.send(messages["totem_reveal"].format(votee, an, role)) + evt.data["votee"] = None + evt.prevent_default = True + evt.stop_processing = True + +@event_listener("chk_decision_lynch", priority=5) +def on_chk_decision_lynch5(evt, var, voters): + votee = evt.data["votee"] + if votee in DESPERATION: + # Also kill the very last person to vote them, unless they voted themselves last in which case nobody else dies + target = voters[-1] + if target is not votee: + prots = deque(var.ACTIVE_PROTECTIONS[target]) + while len(prots) > 0: + # an event can read the current active protection and cancel the totem + # if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS + # so that it cannot be used again (if the protection is meant to be usable once-only) + desp_evt = Event("desperation_totem", {}) + if not desp_evt.dispatch(var, votee, target, prots[0]): + return + prots.popleft() + if var.ROLE_REVEAL in ("on", "team"): + r1 = get_reveal_role(target) + an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" + tmsg = messages["totem_desperation"].format(votee, target, an1, r1) + else: + tmsg = messages["totem_desperation_no_reveal"].format(votee, target) + channels.Main.send(tmsg) + # we lie to this function so it doesn't devoice the player yet. instead, we'll let the call further down do it + evt.data["deadlist"].append(target) + evt.params.del_player(target, end_game=False, killer_role="shaman", deadlist=evt.data["deadlist"], ismain=False) + +@event_listener("transition_day", priority=2) +def on_transition_day2(evt, var): + for shaman, targets in DEATH.items(): + for target in targets: + evt.data["victims"].append(target) + evt.data["onlybywolves"].discard(target) + evt.data["killers"][target].append(shaman) + +@event_listener("transition_day", priority=4.1) +def on_transition_day3(evt, var): + # protection totems are applied first in default logic, however + # we set priority=4.1 to allow other modes of protection + # to pre-empt us if desired + pl = get_players() + vs = set(evt.data["victims"]) + for v in pl: + numtotems = PROTECTION.count(v) + if v in vs: + if v in var.DYING: + continue + numkills = evt.data["numkills"][v] + for i in range(0, numtotems): + numkills -= 1 + if numkills >= 0: + evt.data["killers"][v].pop(0) + if numkills <= 0 and v not in evt.data["protected"]: + evt.data["protected"][v] = "totem" + elif numkills <= 0: + var.ACTIVE_PROTECTIONS[v.nick].append("totem") + evt.data["numkills"][v] = numkills + else: + for i in range(0, numtotems): + var.ACTIVE_PROTECTIONS[v.nick].append("totem") + +@event_listener("fallen_angel_guard_break") +def on_fagb(evt, var, victim, killer): + # we'll never end up killing a shaman who gave out protection, but delete the totem since + # story-wise it gets demolished at night by the FA + while victim in havetotem: + havetotem.remove(victim) + brokentotem.add(victim) + +@event_listener("transition_day_begin", priority=6) +def on_transition_day_begin(evt, var): + # Reset totem variables + DEATH.clear() + PROTECTION.clear() + REVEALING.clear() + NARCOLEPSY.clear() + SILENCE.clear() + DESPERATION.clear() + IMPATIENCE.clear() + PACIFISM.clear() + INFLUENCE.clear() + EXCHANGE.clear() + LYCANTHROPY.clear() + LUCK.clear() + PESTILENCE.clear() + RETRIBUTION.clear() + MISDIRECTION.clear() + DECEIT.clear() + + # In transition_day_end we report who was given totems based on havetotem. + # Fallen angel messes with this list, hence why it is separated from LASTGIVEN + # and calculated here (updated in the separate role files) + brokentotem.clear() + havetotem.clear() + +@event_listener("transition_day_resolve", priority=2) +def on_transition_day_resolve2(evt, var, victim): + if evt.data["protected"].get(victim) == "totem": + evt.data["message"].append(messages["totem_protection"].format(victim)) + evt.data["novictmsg"] = False + evt.stop_processing = True + evt.prevent_default = True + +@event_listener("transition_day_resolve", priority=6) +def on_transition_day_resolve6(evt, 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 evt.data["protected"].get(victim): + return + if victim in var.ROLES["lycan"] and victim in evt.data["onlybywolves"] and victim.nick not in var.IMMUNIZED: + return + # END checks to remove + + if victim in RETRIBUTION: + killers = list(evt.data["killers"].get(victim, [])) + loser = None + while killers: + loser = random.choice(killers) + if loser in evt.data["dead"] or victim is loser: + killers.remove(loser) + continue + break + if loser in evt.data["dead"] or victim is loser: + loser = None + ret_evt = Event("retribution_kill", {"target": loser, "message": []}) + ret_evt.dispatch(var, victim, loser) + loser = ret_evt.data["target"] + evt.data["message"].extend(ret_evt.data["message"]) + if loser in evt.data["dead"] or victim is loser: + loser = None + if loser is not None: + prots = deque(var.ACTIVE_PROTECTIONS[loser.nick]) + while len(prots) > 0: + # an event can read the current active protection and cancel the totem + # if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS + # so that it cannot be used again (if the protection is meant to be usable once-only) + ret_evt = Event("retribution_totem", {"message": []}) + if not ret_evt.dispatch(var, victim, loser, prots[0]): + evt.data["message"].extend(ret_evt.data["message"]) + return + prots.popleft() + evt.data["dead"].append(loser) + if var.ROLE_REVEAL in ("on", "team"): + role = get_reveal_role(loser) + an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" + evt.data["message"].append(messages["totem_death"].format(victim, loser, an, role)) + else: + evt.data["message"].append(messages["totem_death_no_reveal"].format(victim, loser)) + +@event_listener("transition_day_end", priority=1) +def on_transition_day_end(evt, var): + message = [] + for player, tlist in itertools.groupby(havetotem): + ntotems = len(list(tlist)) + message.append(messages["totem_posession"].format( + player, "ed" if player not in get_players() else "s", "a" if ntotems == 1 else "\u0002{0}\u0002".format(ntotems), "s" if ntotems > 1 else "")) + for player in brokentotem: + message.append(messages["totem_broken"].format(player)) + channels.Main.send("\n".join(message)) + +@event_listener("begin_day") +def on_begin_day(evt, var): + # Apply totem effects that need to begin on day proper + var.EXCHANGED.update(p.nick for p in EXCHANGE) + var.SILENCED.update(p.nick for p in SILENCE) + var.LYCANTHROPES.update(p.nick for p in LYCANTHROPY) + # pestilence doesn't take effect on immunized players + var.DISEASED.update({p.nick for p in PESTILENCE} - var.IMMUNIZED) + var.LUCKY.update(p.nick for p in LUCK) + var.MISDIRECTED.update(p.nick for p in MISDIRECTION) + +@event_listener("abstain") +def on_abstain(evt, var, user): + if user in NARCOLEPSY: + user.send(messages["totem_narcolepsy"]) + evt.prevent_default = True + +@event_listener("lynch") +def on_lynch(evt, var, user): + if user in NARCOLEPSY: + user.send(messages["totem_narcolepsy"]) + evt.prevent_default = True + +@event_listener("assassinate") +def on_assassinate(evt, var, killer, target, prot): + if prot == "totem": + var.ACTIVE_PROTECTIONS[target.nick].remove("totem") + evt.prevent_default = True + evt.stop_processing = True + channels.Main.send(messages[evt.params.message_prefix + "totem"].format(killer, target)) + +@event_listener("reset") +def on_reset(evt, var): + DEATH.clear() + PROTECTION.clear() + REVEALING.clear() + NARCOLEPSY.clear() + SILENCE.clear() + DESPERATION.clear() + IMPATIENCE.clear() + PACIFISM.clear() + INFLUENCE.clear() + EXCHANGE.clear() + LYCANTHROPY.clear() + LUCK.clear() + PESTILENCE.clear() + RETRIBUTION.clear() + MISDIRECTION.clear() + DECEIT.clear() + + brokentotem.clear() + havetotem.clear() + +# vim: set sw=4 expandtab: diff --git a/src/roles/angel.py b/src/roles/angel.py index 4258883..735f170 100644 --- a/src/roles/angel.py +++ b/src/roles/angel.py @@ -10,7 +10,7 @@ from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players, get_all_players from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/blessed.py b/src/roles/blessed.py index 2344031..5e1f3b1 100644 --- a/src/roles/blessed.py +++ b/src/roles/blessed.py @@ -5,12 +5,11 @@ import math from collections import defaultdict import botconfig -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players, get_all_players from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event @@ -54,7 +53,7 @@ def on_transition_night_end(evt, var): @event_listener("desperation_totem") def on_desperation(evt, var, votee, target, prot): if prot == "blessing": - var.ACTIVE_PROTECTIONS[target].remove("blessing") + var.ACTIVE_PROTECTIONS[target.nick].remove("blessing") evt.prevent_default = True evt.stop_processing = True diff --git a/src/roles/crazed_shaman.py b/src/roles/crazed_shaman.py new file mode 100644 index 0000000..93dfab8 --- /dev/null +++ b/src/roles/crazed_shaman.py @@ -0,0 +1,98 @@ +import re +import random +import itertools +from collections import defaultdict, deque + +import botconfig +from src.utilities import * +from src import debuglog, errlog, plog, users, channels +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.dispatcher import MessageDispatcher +from src.messages import messages +from src.events import Event + +from src.roles._shaman_helper import setup_variables, get_totem_target, give_totem + +def get_tags(var, totem): + return set() + +TOTEMS, LASTGIVEN, SHAMANS = setup_variables("crazed shaman", knows_totem=False, get_tags=get_tags) + +@command("give", "totem", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("crazed shaman",)) +def crazed_shaman_totem(var, wrapper, message): + """Give a random totem to a player.""" + + target = get_totem_target(var, wrapper, message, LASTGIVEN) + if not target: + return + + totem = TOTEMS[wrapper.source] + + SHAMANS[wrapper.source] = give_totem(var, wrapper, target, prefix="You", tags=get_tags(var, totem), role="crazed shaman", msg="") + +@event_listener("player_win") +def on_player_win(evt, var, user, role, winner, survived): + if role == "crazed shaman" and survived and not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES: + evt.data["iwon"] = True + +@event_listener("transition_day_begin", priority=4) +def on_transition_day_begin(evt, var): + # Select random totem recipients if shamans didn't act + pl = get_players() + for shaman in get_players(("crazed shaman",)): + if shaman not in SHAMANS and shaman.nick not in var.SILENCED: + ps = pl[:] + if shaman in LASTGIVEN: + if LASTGIVEN[shaman] in ps: + ps.remove(LASTGIVEN[shaman]) + levt = Event("get_random_totem_targets", {"targets": ps}) + levt.dispatch(var, shaman) + ps = levt.data["targets"] + if ps: + target = random.choice(ps) + dispatcher = MessageDispatcher(shaman, shaman) + + tags = get_tags(var, TOTEMS[shaman]) + + SHAMANS[shaman] = give_totem(var, dispatcher, target, prefix=messages["random_totem_prefix"], tags=tags, role="crazed shaman", msg="") + else: + LASTGIVEN[shaman] = None + elif shaman not in SHAMANS: + LASTGIVEN[shaman] = None + +@event_listener("transition_night_end", priority=2.01) +def on_transition_night_end(evt, var): + max_totems = 0 + ps = get_players() + shamans = get_players(("crazed shaman",)) + index = var.TOTEM_ORDER.index("crazed shaman") + for c in var.TOTEM_CHANCES.values(): + max_totems += c[index] + + for s in list(LASTGIVEN): + if s not in shamans: + del LASTGIVEN[s] + + for shaman in shamans: + pl = ps[:] + random.shuffle(pl) + if LASTGIVEN.get(shaman): + if LASTGIVEN[shaman] in pl: + pl.remove(LASTGIVEN[shaman]) + + target = 0 + rand = random.random() * max_totems + for t in var.TOTEM_CHANCES.keys(): + target += var.TOTEM_CHANCES[t][index] + if rand <= target: + TOTEMS[shaman] = t + break + if shaman.prefers_simple(): + shaman.send(messages["shaman_simple"].format("crazed shaman")) + else: + shaman.send(messages["shaman_notify"].format("crazed shaman", "random ")) + shaman.send("Players: " + ", ".join(p.nick for p in pl)) + +# vim: set sw=4 expandtab: diff --git a/src/roles/cursed.py b/src/roles/cursed.py index 55c090d..544668e 100644 --- a/src/roles/cursed.py +++ b/src/roles/cursed.py @@ -5,11 +5,10 @@ import math from collections import defaultdict import botconfig -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/detective.py b/src/roles/detective.py index 976b282..6daed9e 100644 --- a/src/roles/detective.py +++ b/src/roles/detective.py @@ -7,7 +7,7 @@ from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players, get_all_players from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/doomsayer.py b/src/roles/doomsayer.py index be84981..f23f407 100644 --- a/src/roles/doomsayer.py +++ b/src/roles/doomsayer.py @@ -1,107 +1,84 @@ import re import random -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog -from src.functions import get_players, get_all_players -from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.functions import get_players, get_all_players, get_main_role, get_target, is_known_wolf_ally +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 -SEEN = set() -KILLS = {} -SICK = {} -LYCANS = {} +SEEN = UserSet() +KILLS = UserDict() +SICK = UserDict() +LYCANS = UserDict() _mappings = ("death", KILLS), ("lycan", LYCANS), ("sick", SICK) -@cmd("see", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("doomsayer",)) -def see(cli, nick, chan, rest): +@command("see", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("doomsayer",)) +def see(var, wrapper, message): """Use your paranormal senses to determine a player's doom.""" - role = get_role(nick) - if nick in SEEN: - pm(cli, nick, messages["seer_fail"]) + if wrapper.source in SEEN: + wrapper.send(messages["seer_fail"]) return - victim = get_victim(cli, nick, re.split(" +",rest)[0], False) - if not victim: + target = get_target(var, wrapper, re.split(" +", message)[0], not_self_message="no_see_self") + if not target: return - if victim == nick: - pm(cli, nick, messages["no_see_self"]) + if is_known_wolf_ally(wrapper.source, target): + wrapper.send(messages["no_see_wolf"]) return - if in_wolflist(nick, victim): - pm(cli, nick, messages["no_see_wolf"]) - return - - doomsayer = users._get(nick) # FIXME - target = users._get(victim) # FIXME evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True}) - evt.dispatch(var, "see", doomsayer, target, frozenset({"detrimental", "immediate"})) + evt.dispatch(var, "see", wrapper.source, target, frozenset({"detrimental", "immediate"})) if evt.prevent_default: return - victim = evt.data["target"].nick - victimrole = get_role(victim) + + target = evt.data["target"] + targrole = get_main_role(target) mode, mapping = random.choice(_mappings) - pm(cli, nick, messages["doomsayer_{0}".format(mode)].format(victim)) - if mode != "sick" or nick not in var.IMMUNIZED: - mapping[nick] = victim + wrapper.send(messages["doomsayer_{0}".format(mode)].format(target)) + if mode != "sick" or wrapper.source.nick not in var.IMMUNIZED: + mapping[wrapper.source] = target - debuglog("{0} ({1}) SEE: {2} ({3}) - {4}".format(nick, role, victim, victimrole, mode.upper())) - relay_wolfchat_command(cli, nick, messages["doomsayer_wolfchat"].format(nick, victim), ("doomsayer",), is_wolf_command=True) + debuglog("{0} (doomsayer) SEE: {1} ({2}) - {3}".format(wrapper.source, target, targrole, mode.upper())) + relay_wolfchat_command(wrapper.client, wrapper.source.nick, messages["doomsayer_wolfchat"].format(wrapper.source, target), ("doomsayer",), is_wolf_command=True) - SEEN.add(nick) - -@event_listener("rename_player") -def on_rename(evt, var, prefix, nick): - if prefix in SEEN: - SEEN.remove(prefix) - SEEN.add(nick) - for name, dictvar in _mappings: - kvp = [] - for a, b in dictvar.items(): - if a == prefix: - a = nick - if b == prefix: - b = nick - kvp.append((a, b)) - dictvar.update(kvp) - if prefix in dictvar: - del dictvar[prefix] + SEEN.add(wrapper.source) @event_listener("night_acted") def on_acted(evt, var, user, actor): - if user.nick in SEEN: + if user in SEEN: evt.data["acted"] = True @event_listener("exchange_roles") def on_exchange(evt, var, actor, target, actor_role, target_role): if actor_role == "doomsayer" and target_role != "doomsayer": - SEEN.discard(actor.nick) + SEEN.discard(actor) for name, mapping in _mappings: - mapping.pop(actor.nick, None) + mapping.pop(actor, None) elif target_role == "doomsayer" and actor_role != "doomsayer": - SEEN.discard(target.nick) + SEEN.discard(target) for name, mapping in _mappings: - mapping.pop(target.nick, None) + mapping.pop(target, None) @event_listener("del_player") def on_del_player(evt, var, user, mainrole, allroles, death_triggers): - SEEN.discard(user.nick) + SEEN.discard(user) for name, dictvar in _mappings: for k, v in list(dictvar.items()): - if user.nick in (k, v): + if user in (k, v): del dictvar[k] @event_listener("doctor_immunize") def on_doctor_immunize(evt, var, doctor, target): - if target in SICK.values(): + user = users._get(target) # FIXME + if user in SICK.values(): for n, v in list(SICK.items()): - if v == target: + if v is user: del SICK[n] evt.data["message"] = "not_sick" @@ -115,15 +92,15 @@ def on_chk_nightdone(evt, var): evt.data["nightroles"].extend(get_all_players(("doomsayer",))) @event_listener("abstain") -def on_abstain(evt, cli, var, nick): - if nick in SICK.values(): - pm(cli, nick, messages["illness_no_vote"]) +def on_abstain(evt, var, user): + if user in SICK.values(): + user.send(messages["illness_no_vote"]) evt.prevent_default = True @event_listener("lynch") -def on_lynch(evt, cli, var, nick): - if nick in SICK.values(): - pm(cli, nick, messages["illness_no_vote"]) +def on_lynch(evt, var, target): + if target in SICK.values(): + target.send(messages["illness_no_vote"]) evt.prevent_default = True @event_listener("get_voters") @@ -132,17 +109,14 @@ def on_get_voters(evt, var): @event_listener("transition_day_begin") def on_transition_day_begin(evt, var): - for victim in SICK.values(): - user = users._get(victim) - user.queue_message(messages["player_sick"]) + for target in SICK.values(): + target.queue_message(messages["player_sick"]) if SICK: - user.send_messages() + target.send_messages() @event_listener("transition_day", priority=2) def on_transition_day(evt, var): - for k, v in list(KILLS.items()): - killer = users._get(k) # FIXME - victim = users._get(v) # FIXME + for killer, victim in list(KILLS.items()): evt.data["victims"].append(victim) # even though doomsayer is a wolf, remove from onlybywolves since # that particular item indicates that they were the target of a wolf !kill. diff --git a/src/roles/dullahan.py b/src/roles/dullahan.py index 101b0d6..fc7f4be 100644 --- a/src/roles/dullahan.py +++ b/src/roles/dullahan.py @@ -7,7 +7,7 @@ from src.utilities import * from src.functions import get_players, get_all_players, get_target, get_main_role, get_reveal_role from src import users, channels, debuglog, errlog, plog from src.decorators import command, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event import botconfig @@ -199,9 +199,8 @@ def on_myrole(evt, var, user): evt.data["messages"].append(messages["dullahan_targets_dead"]) @event_listener("revealroles_role") -def on_revealroles_role(evt, var, wrapper, nickname, role): - user = users._get(nickname) # FIXME - if role == "dullahan" and user in TARGETS: # FIXME +def on_revealroles_role(evt, var, wrapper, user, role): + if role == "dullahan" and user in TARGETS: targets = set(TARGETS[user]) for target in TARGETS[user]: if target.nick in var.DEAD: diff --git a/src/roles/fallenangel.py b/src/roles/fallenangel.py index 315d3a8..4a9400b 100644 --- a/src/roles/fallenangel.py +++ b/src/roles/fallenangel.py @@ -5,11 +5,10 @@ import math from collections import defaultdict import botconfig -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.functions import get_players, get_all_players from src.messages import messages from src.events import Event diff --git a/src/roles/harlot.py b/src/roles/harlot.py index cba82c2..c9d33cc 100644 --- a/src/roles/harlot.py +++ b/src/roles/harlot.py @@ -5,12 +5,11 @@ 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.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 +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/hunter.py b/src/roles/hunter.py index e660d59..05ee034 100644 --- a/src/roles/hunter.py +++ b/src/roles/hunter.py @@ -2,12 +2,11 @@ import re import random from collections import defaultdict -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players, get_all_players, get_target, get_main_role from src.decorators import command, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/investigator.py b/src/roles/investigator.py index e05216e..8610620 100644 --- a/src/roles/investigator.py +++ b/src/roles/investigator.py @@ -5,12 +5,11 @@ 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.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 +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/madscientist.py b/src/roles/madscientist.py index 81daec1..7f14be0 100644 --- a/src/roles/madscientist.py +++ b/src/roles/madscientist.py @@ -5,12 +5,11 @@ import math from collections import defaultdict, deque import botconfig -import src.settings as var 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 from src.decorators import command, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event @@ -170,11 +169,10 @@ def on_myrole(evt, var, user): evt.data["messages"].append(messages["mad_scientist_myrole_targets"].format(target1, target2)) @event_listener("revealroles_role") -def on_revealroles(evt, var, wrapper, nickname, role): +def on_revealroles(evt, var, wrapper, user, role): if role == "mad scientist": pl = get_players() - target1, target2 = _get_targets(var, pl, users._get(nickname)) # FIXME + target1, target2 = _get_targets(var, pl, user) evt.data["special_case"].append(messages["mad_scientist_revealroles_targets"].format(target1, target2)) - # vim: set sw=4 expandtab: diff --git a/src/roles/mayor.py b/src/roles/mayor.py index 2725cfe..25538ed 100644 --- a/src/roles/mayor.py +++ b/src/roles/mayor.py @@ -5,27 +5,20 @@ import math from collections import defaultdict import botconfig -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event -REVEALED_MAYORS = set() - -@event_listener("rename_player") -def on_rename_player(evt, var, prefix, nick): - if prefix in REVEALED_MAYORS: - REVEALED_MAYORS.remove(prefix) - REVEALED_MAYORS.add(nick) +REVEALED_MAYORS = UserSet() @event_listener("chk_decision_lynch", priority=3) -def on_chk_decision_lynch(evt, cli, var, voters): +def on_chk_decision_lynch(evt, var, voters): votee = evt.data["votee"] - if users._get(votee) in var.ROLES["mayor"] and votee not in REVEALED_MAYORS: # FIXME - cli.msg(botconfig.CHANNEL, messages["mayor_reveal"].format(votee)) + if votee in var.ROLES["mayor"] and votee not in REVEALED_MAYORS: + channels.Main.send(messages["mayor_reveal"].format(votee)) REVEALED_MAYORS.add(votee) evt.data["votee"] = None evt.prevent_default = True diff --git a/src/roles/mystic.py b/src/roles/mystic.py index d87d867..37df9ad 100644 --- a/src/roles/mystic.py +++ b/src/roles/mystic.py @@ -1,12 +1,11 @@ import re import random -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players, get_all_players from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/piper.py b/src/roles/piper.py index 2aa2042..944148b 100644 --- a/src/roles/piper.py +++ b/src/roles/piper.py @@ -5,12 +5,11 @@ import math from collections import defaultdict import botconfig -import src.settings as var from src.utilities import * from src.functions import get_players, get_all_players, get_target, get_main_role from src import channels, users, debuglog, errlog, plog from src.decorators import command, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/seer.py b/src/roles/seer.py index a94ca37..4b10914 100644 --- a/src/roles/seer.py +++ b/src/roles/seer.py @@ -5,7 +5,7 @@ import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.functions import get_players, get_all_players, get_main_role from src.messages import messages from src.events import Event diff --git a/src/roles/shaman.py b/src/roles/shaman.py index c77720f..1b6ae2b 100644 --- a/src/roles/shaman.py +++ b/src/roles/shaman.py @@ -4,637 +4,98 @@ import itertools from collections import defaultdict, deque import botconfig -import src.settings as var from src.utilities import * from src import debuglog, errlog, plog, users, channels -from src.functions import get_players, get_all_players, get_main_role, get_reveal_role -from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +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.dispatcher import MessageDispatcher from src.messages import messages from src.events import Event -# To add new totem types in your custom roles/whatever.py file: -# 1. Add a key to var.TOTEM_CHANCES with the totem name -# 2. Add a message totemname_totem to your custom messages.json describing -# the totem (this is displayed at night if !simple is off) -# 3. Add events as necessary to implement the totem's functionality -# -# To add new shaman roles in your custom roles/whatever.py file: -# 1. Expand var.TOTEM_ORDER and upate var.TOTEM_CHANCES to account for the new width -# 2. Add the role to var.ROLE_GUIDE -# 3. Add the role to whatever other holding vars are necessary based on what it does -# 4. Implement custom events if the role does anything else beyond giving totems. -# -# Modifying this file to add new totems or new shaman roles is generally never required - -TOTEMS = {} # type: Dict[str, str] -LASTGIVEN = {} # type: Dict[str, str] -SHAMANS = {} # type: Dict[str, Tuple[str, str]] - -DEATH = {} # type: Dict[str, str] -PROTECTION = [] # type: List[str] -REVEALING = set() # type: Set[str] -NARCOLEPSY = set() # type: Set[str] -SILENCE = set() # type: Set[str] -DESPERATION = set() # type: Set[str] -IMPATIENCE = [] # type: List[str] -PACIFISM = [] # type: List[str] -INFLUENCE = set() # type: Set[str] -EXCHANGE = set() # type: Set[str] -LYCANTHROPY = set() # type: Set[str] -LUCK = set() # type: Set[str] -PESTILENCE = set() # type: Set[str] -RETRIBUTION = set() # type: Set[str] -MISDIRECTION = set() # type: Set[str] -DECEIT = set() # type: Set[str] - -# holding vars that don't persist long enough to need special attention in -# reset/exchange/nickchange -havetotem = [] # type: List[str] -brokentotem = set() # type: Set[str] - -# FIXME: this needs to be split into shaman.py, wolfshaman.py, and crazedshaman.py -@cmd("give", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=var.TOTEM_ORDER) -@cmd("totem", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=var.TOTEM_ORDER) -def totem(cli, nick, chan, rest, prefix="You"): # XXX: The transition_day_begin event needs updating alongside this - """Give a totem to a player.""" - victim = get_victim(cli, nick, re.split(" +",rest)[0], False, True) - if not victim: - return - if LASTGIVEN.get(nick) == victim: - pm(cli, nick, messages["shaman_no_target_twice"].format(victim)) - return - - original_victim = victim - role = get_role(nick) # FIXME: this is bad, check if user is in var.ROLES[thingy] instead once converted - totem = "" - if role != "crazed shaman": - totem = " of " + TOTEMS[nick] +from src.roles._shaman_helper import setup_variables, get_totem_target, give_totem +def get_tags(var, totem): tags = set() - if role != "crazed shaman" and TOTEMS[nick] in var.BENEFICIAL_TOTEMS: + if totem in var.BENEFICIAL_TOTEMS: tags.add("beneficial") + return tags - shaman = users._get(nick) # FIXME - target = users._get(victim) # FIXME +TOTEMS, LASTGIVEN, SHAMANS = setup_variables("shaman", knows_totem=True, get_tags=get_tags) - evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True}, - action="give a totem{0} to".format(totem)) - evt.dispatch(var, "totem", shaman, target, frozenset(tags)) - if evt.prevent_default: +@command("give", "totem", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("shaman",)) +def shaman_totem(var, wrapper, message): + """Give a totem to a player.""" + + target = get_totem_target(var, wrapper, message, LASTGIVEN) + if not target: return - victim = evt.data["target"].nick - victimrole = get_role(victim) - pm(cli, nick, messages["shaman_success"].format(prefix, totem, original_victim)) - if role == "wolf shaman": - relay_wolfchat_command(cli, nick, messages["shaman_wolfchat"].format(nick, original_victim), ("wolf shaman",), is_wolf_command=True) - SHAMANS[nick] = (victim, original_victim) - debuglog("{0} ({1}) TOTEM: {2} ({3})".format(nick, role, victim, TOTEMS[nick])) + totem = TOTEMS[wrapper.source] -@event_listener("rename_player") -def on_rename(evt, var, prefix, nick): - if prefix in TOTEMS: - TOTEMS[nick] = TOTEMS.pop(prefix) - - for dictvar in (LASTGIVEN, DEATH): - kvp = {} - for a,b in dictvar.items(): - s = nick if a == prefix else a - t = nick if b == prefix else b - kvp[s] = t - dictvar.update(kvp) - if prefix in dictvar: - del dictvar[prefix] - - kvp = {} - for a,(b,c) in SHAMANS.items(): - s = nick if a == prefix else a - t1 = nick if b == prefix else b - t2 = nick if c == prefix else c - kvp[s] = (t1, t2) - SHAMANS.update(kvp) - if prefix in SHAMANS: - del SHAMANS[prefix] - - for listvar in (PROTECTION, IMPATIENCE, PACIFISM): - for i,a in enumerate(listvar): - if a == prefix: - listvar[i] = nick - - for setvar in (REVEALING, NARCOLEPSY, SILENCE, DESPERATION, - INFLUENCE, EXCHANGE, LYCANTHROPY, LUCK, PESTILENCE, - RETRIBUTION, MISDIRECTION, DECEIT): - for a in list(setvar): - if a == prefix: - setvar.discard(a) - setvar.add(nick) - -@event_listener("see", priority=10) -def on_see(evt, var, nick, victim): - if (victim in DECEIT) ^ (nick in DECEIT): - if evt.data["role"] in var.SEEN_WOLF and evt.data["role"] not in var.SEEN_DEFAULT: - evt.data["role"] = "villager" - else: - evt.data["role"] = "wolf" - -@event_listener("del_player") -def on_del_player(evt, var, user, mainrole, allroles, death_triggers): - for a,(b,c) in list(SHAMANS.items()): - if user.nick in (a, b, c): - del SHAMANS[a] - -@event_listener("night_acted") -def on_acted(evt, var, user, actor): - if user.nick in SHAMANS: - evt.data["acted"] = True - -@event_listener("get_special") -def on_get_special(evt, var): - evt.data["special"].update(get_players(("shaman", "crazed shaman", "wolf shaman"))) - -@event_listener("exchange_roles") -def on_exchange(evt, var, actor, target, actor_role, target_role): - actor_totem = None - target_totem = None - if actor_role in var.TOTEM_ORDER: - actor_totem = TOTEMS.pop(actor.nick) - if actor.nick in SHAMANS: - del SHAMANS[actor.nick] - if actor.nick in LASTGIVEN: - del LASTGIVEN[actor.nick] - if target_role in var.TOTEM_ORDER: - target_totem = TOTEMS.pop(target.nick) - if target.nick in SHAMANS: - del SHAMANS[target.nick] - if target.nick in LASTGIVEN: - del LASTGIVEN[target.nick] - if target_totem: - if target_role != "crazed shaman": - evt.data["actor_messages"].append(messages["shaman_totem"].format(target_totem)) - TOTEMS[actor.nick] = target_totem - if actor_totem: - if actor_role != "crazed shaman": - evt.data["target_messages"].append(messages["shaman_totem"].format(actor_totem)) - TOTEMS[target.nick] = actor_totem - -@event_listener("chk_nightdone") -def on_chk_nightdone(evt, var): - evt.data["actedcount"] += len(SHAMANS) - evt.data["nightroles"].extend(get_players(var.TOTEM_ORDER)) - -@event_listener("get_voters") -def on_get_voters(evt, var): - evt.data["voters"] -= NARCOLEPSY - -@event_listener("chk_decision", priority=1) -def on_chk_decision(evt, cli, var, force): - nl = [] - for p in PACIFISM: - if p in evt.params.voters: - nl.append(p) - # .remove() will only remove the first instance, which means this plays nicely with pacifism countering this - for p in IMPATIENCE: - if p in nl: - nl.remove(p) - evt.data["not_lynching"] |= set(nl) - - for votee, voters in evt.data["votelist"].items(): - numvotes = 0 - random.shuffle(IMPATIENCE) - for v in IMPATIENCE: - if v in evt.params.voters and v not in voters and v != votee: - # don't add them in if they have the same number or more of pacifism totems - # this matters for desperation totem on the votee - imp_count = IMPATIENCE.count(v) - pac_count = PACIFISM.count(v) - if pac_count >= imp_count: - continue - - # yes, this means that one of the impatient people will get desperation totem'ed if they didn't - # already !vote earlier. sucks to suck. >:) - voters.append(v) - for v in voters: - weight = 1 - imp_count = IMPATIENCE.count(v) - pac_count = PACIFISM.count(v) - if pac_count > imp_count: - weight = 0 # more pacifists than impatience totems - elif imp_count == pac_count and v not in var.VOTES[votee]: - weight = 0 # impatience and pacifist cancel each other out, so don't count impatience - if v in INFLUENCE: - weight *= 2 - numvotes += weight - if votee not in evt.data["weights"]: - evt.data["weights"][votee] = {} - evt.data["weights"][votee][v] = weight - evt.data["numvotes"][votee] = numvotes - -@event_listener("chk_decision_abstain") -def on_chk_decision_abstain(evt, cli, var, nl): - for p in nl: - if p in PACIFISM and p not in var.NO_LYNCH: - cli.msg(botconfig.CHANNEL, messages["player_meek_abstain"].format(p)) - -@event_listener("chk_decision_lynch", priority=1) -def on_chk_decision_lynch1(evt, cli, var, voters): - votee = evt.data["votee"] - for p in voters: - if p in IMPATIENCE and p not in var.VOTES[votee]: - cli.msg(botconfig.CHANNEL, messages["impatient_vote"].format(p, votee)) - -# mayor is at exactly 3, so we want that to always happen before revealing totem -@event_listener("chk_decision_lynch", priority=3.1) -def on_chk_decision_lynch3(evt, cli, var, voters): - votee = evt.data["votee"] - if votee in REVEALING: - role = get_role(votee) - rev_evt = Event("revealing_totem", {"role": role}) - rev_evt.dispatch(cli, var, votee) - role = rev_evt.data["role"] - # TODO: once amnesiac is split, roll this into the revealing_totem event - if role == "amnesiac": - role = var.AMNESIAC_ROLES[votee] - change_role(users._get(votee), "amnesiac", role) # FIXME - var.AMNESIACS.add(votee) - pm(cli, votee, messages["totem_amnesia_clear"]) - # 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 - # If turncoat, set their initial starting side to "none" just in case game ends before they can set it themselves - if role == "turncoat": - var.TURNCOATS[votee] = ("none", -1) - - an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" - cli.msg(botconfig.CHANNEL, messages["totem_reveal"].format(votee, an, role)) - evt.data["votee"] = None - evt.prevent_default = True - evt.stop_processing = True - -@event_listener("chk_decision_lynch", priority=5) -def on_chk_decision_lynch5(evt, cli, var, voters): - votee = evt.data["votee"] - if votee in DESPERATION: - # Also kill the very last person to vote them, unless they voted themselves last in which case nobody else dies - target = voters[-1] - if target != votee: - prots = deque(var.ACTIVE_PROTECTIONS[target]) - while len(prots) > 0: - # an event can read the current active protection and cancel the totem - # if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS - # so that it cannot be used again (if the protection is meant to be usable once-only) - desp_evt = Event("desperation_totem", {}) - if not desp_evt.dispatch(var, votee, target, prots[0]): - return - prots.popleft() - if var.ROLE_REVEAL in ("on", "team"): - r1 = get_reveal_role(users._get(target)) # FIXME - an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" - tmsg = messages["totem_desperation"].format(votee, target, an1, r1) - else: - tmsg = messages["totem_desperation_no_reveal"].format(votee, target) - cli.msg(botconfig.CHANNEL, tmsg) - # we lie to this function so it doesn't devoice the player yet. instead, we'll let the call further down do it - evt.data["deadlist"].append(target) - better_deadlist = [users._get(p) for p in evt.data["deadlist"]] # FIXME - target_user = users._get(target) # FIXME - evt.params.del_player(target_user, end_game=False, killer_role="shaman", deadlist=better_deadlist, ismain=False) - -@event_listener("player_win") -def on_player_win(evt, var, user, rol, winner, survived): - if rol == "crazed shaman" and survived and not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES: - evt.data["iwon"] = True + SHAMANS[wrapper.source] = give_totem(var, wrapper, target, prefix="You", tags=get_tags(var, totem), role="shaman", msg=" of {0}".format(totem)) @event_listener("transition_day_begin", priority=4) def on_transition_day_begin(evt, var): # Select random totem recipients if shamans didn't act pl = get_players() - for shaman in get_players(var.TOTEM_ORDER): - if shaman.nick not in SHAMANS and shaman.nick not in var.SILENCED: + for shaman in get_players(("shaman",)): + if shaman not in SHAMANS and shaman.nick not in var.SILENCED: ps = pl[:] - if shaman.nick in LASTGIVEN: - user = users._get(LASTGIVEN[shaman.nick]) # FIXME - if user in ps: - ps.remove(user) + if shaman in LASTGIVEN: + if LASTGIVEN[shaman] in ps: + ps.remove(LASTGIVEN[shaman]) levt = Event("get_random_totem_targets", {"targets": ps}) levt.dispatch(var, shaman) ps = levt.data["targets"] if ps: target = random.choice(ps) - totem.func(target.client, shaman.nick, shaman.nick, target.nick, messages["random_totem_prefix"]) # XXX: Old API + dispatcher = MessageDispatcher(shaman, shaman) + + tags = get_tags(var, TOTEMS[shaman]) + + SHAMANS[shaman] = give_totem(var, dispatcher, target, prefix=messages["random_totem_prefix"], tags=tags, role="shaman", msg=" of {0}".format(TOTEMS[shaman])) else: - LASTGIVEN[shaman.nick] = None + LASTGIVEN[shaman] = None elif shaman not in SHAMANS: - LASTGIVEN[shaman.nick] = None - -@event_listener("transition_day_begin", priority=6) -def on_transition_day_begin2(evt, var): - # Reset totem variables - DEATH.clear() - PROTECTION.clear() - REVEALING.clear() - NARCOLEPSY.clear() - SILENCE.clear() - DESPERATION.clear() - IMPATIENCE.clear() - PACIFISM.clear() - INFLUENCE.clear() - EXCHANGE.clear() - LYCANTHROPY.clear() - LUCK.clear() - PESTILENCE.clear() - RETRIBUTION.clear() - MISDIRECTION.clear() - DECEIT.clear() - - # Give out totems here - for shaman, (victim, target) in SHAMANS.items(): - totemname = TOTEMS[shaman] - if totemname == "death": # this totem stacks - DEATH[shaman] = victim - elif totemname == "protection": # this totem stacks - PROTECTION.append(victim) - elif totemname == "revealing": - REVEALING.add(victim) - elif totemname == "narcolepsy": - NARCOLEPSY.add(victim) - elif totemname == "silence": - SILENCE.add(victim) - elif totemname == "desperation": - DESPERATION.add(victim) - elif totemname == "impatience": # this totem stacks - IMPATIENCE.append(victim) - elif totemname == "pacifism": # this totem stacks - PACIFISM.append(victim) - elif totemname == "influence": - INFLUENCE.add(victim) - elif totemname == "exchange": - EXCHANGE.add(victim) - elif totemname == "lycanthropy": - LYCANTHROPY.add(victim) - elif totemname == "luck": - LUCK.add(victim) - elif totemname == "pestilence": - PESTILENCE.add(victim) - elif totemname == "retribution": - RETRIBUTION.add(victim) - elif totemname == "misdirection": - MISDIRECTION.add(victim) - elif totemname == "deceit": - DECEIT.add(victim) - # other totem types possibly handled in an earlier event, - # as such there is no else: clause here - if target != victim: - user = users._get(shaman) # FIXME - user.send(messages["totem_retarget"].format(victim)) - LASTGIVEN[shaman] = victim - - # In transition_day_end we report who was given totems based on havetotem. - # Fallen angel messes with this list, hence why it is separated from LASTGIVEN - # and calculated here. - brokentotem.clear() - havetotem.clear() - havetotem.extend(sorted(filter(None, LASTGIVEN.values()))) - -@event_listener("transition_day", priority=2) -def on_transition_day2(evt, var): - for k, d in DEATH.items(): - shaman = users._get(k) # FIXME - target = users._get(d) # FIXME - evt.data["victims"].append(target) - evt.data["onlybywolves"].discard(target) - evt.data["killers"][target].append(shaman) - -@event_listener("transition_day", priority=4.1) -def on_transition_day3(evt, var): - # protection totems are applied first in default logic, however - # we set priority=4.1 to allow other modes of protection - # to pre-empt us if desired - pl = get_players() - vs = set(evt.data["victims"]) - for v in pl: - numtotems = PROTECTION.count(v.nick) - if v in vs: - if v in var.DYING: - continue - numkills = evt.data["numkills"][v] - for i in range(0, numtotems): - numkills -= 1 - if numkills >= 0: - evt.data["killers"][v].pop(0) - if numkills <= 0 and v not in evt.data["protected"]: - evt.data["protected"][v] = "totem" - elif numkills <= 0: - var.ACTIVE_PROTECTIONS[v.nick].append("totem") - evt.data["numkills"][v] = numkills - else: - for i in range(0, numtotems): - var.ACTIVE_PROTECTIONS[v.nick].append("totem") - -@event_listener("fallen_angel_guard_break") -def on_fagb(evt, var, victim, killer): - # we'll never end up killing a shaman who gave out protection, but delete the totem since - # story-wise it gets demolished at night by the FA - while victim.nick in havetotem: - havetotem.remove(victim.nick) - brokentotem.add(victim.nick) - -@event_listener("transition_day_resolve", priority=2) -def on_transition_day_resolve2(evt, var, victim): - if evt.data["protected"].get(victim) == "totem": - evt.data["message"].append(messages["totem_protection"].format(victim)) - evt.data["novictmsg"] = False - evt.stop_processing = True - evt.prevent_default = True - -@event_listener("transition_day_resolve", priority=6) -def on_transition_day_resolve6(evt, 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 evt.data["protected"].get(victim): - return - if victim in var.ROLES["lycan"] and victim in evt.data["onlybywolves"] and victim.nick not in var.IMMUNIZED: - return - # END checks to remove - - if victim.nick in RETRIBUTION: - killers = list(evt.data["killers"].get(victim, [])) - loser = None - while killers: - loser = random.choice(killers) - if loser in evt.data["dead"] or victim is loser: - killers.remove(loser) - continue - break - if loser in evt.data["dead"] or victim is loser: - loser = None - ret_evt = Event("retribution_kill", {"target": loser, "message": []}) - ret_evt.dispatch(var, victim, loser) - loser = ret_evt.data["target"] - evt.data["message"].extend(ret_evt.data["message"]) - if loser in evt.data["dead"] or victim is loser: - loser = None - if loser is not None: - prots = deque(var.ACTIVE_PROTECTIONS[loser.nick]) - while len(prots) > 0: - # an event can read the current active protection and cancel the totem - # if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS - # so that it cannot be used again (if the protection is meant to be usable once-only) - ret_evt = Event("retribution_totem", {"message": []}) - if not ret_evt.dispatch(var, victim, loser, prots[0]): - evt.data["message"].extend(ret_evt.data["message"]) - return - prots.popleft() - evt.data["dead"].append(loser) - if var.ROLE_REVEAL in ("on", "team"): - role = get_reveal_role(loser) - an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" - evt.data["message"].append(messages["totem_death"].format(victim, loser, an, role)) - else: - evt.data["message"].append(messages["totem_death_no_reveal"].format(victim, loser)) - -@event_listener("transition_day_end", priority=1) -def on_transition_day_end(evt, var): - message = [] - for player, tlist in itertools.groupby(havetotem): - ntotems = len(list(tlist)) - message.append(messages["totem_posession"].format( - player, "ed" if player not in list_players() else "s", "a" if ntotems == 1 else "\u0002{0}\u0002".format(ntotems), "s" if ntotems > 1 else "")) - for player in brokentotem: - message.append(messages["totem_broken"].format(player)) - channels.Main.send("\n".join(message)) + LASTGIVEN[shaman] = None @event_listener("transition_night_end", priority=2.01) def on_transition_night_end(evt, var): - max_totems = defaultdict(int) + max_totems = 0 ps = get_players() - shamans = list_players(var.TOTEM_ORDER) # FIXME: Need to convert alongside the entire role - for ix in range(len(var.TOTEM_ORDER)): - for c in var.TOTEM_CHANCES.values(): - max_totems[var.TOTEM_ORDER[ix]] += c[ix] - for s in list(LASTGIVEN.keys()): + shamans = get_players(("shaman",)) + index = var.TOTEM_ORDER.index("shaman") + for c in var.TOTEM_CHANCES.values(): + max_totems += c[index] + + for s in list(LASTGIVEN): if s not in shamans: del LASTGIVEN[s] - for shaman in get_players(var.TOTEM_ORDER): + + for shaman in shamans: pl = ps[:] random.shuffle(pl) - if LASTGIVEN.get(shaman.nick): - user = users._get(LASTGIVEN[shaman.nick]) # FIXME - if user in pl: - pl.remove(user) - role = get_main_role(shaman) # FIXME: don't use get_main_role here once split into one file per role - indx = var.TOTEM_ORDER.index(role) + if LASTGIVEN.get(shaman): + if LASTGIVEN[shaman] in pl: + pl.remove(LASTGIVEN[shaman]) + target = 0 - rand = random.random() * max_totems[var.TOTEM_ORDER[indx]] + rand = random.random() * max_totems for t in var.TOTEM_CHANCES.keys(): - target += var.TOTEM_CHANCES[t][indx] + target += var.TOTEM_CHANCES[t][index] if rand <= target: - TOTEMS[shaman.nick] = t # FIXME: Fix once shaman is converted + TOTEMS[shaman] = t break if shaman.prefers_simple(): - if role not in var.WOLFCHAT_ROLES: - shaman.send(messages["shaman_simple"].format(role)) - if role != "crazed shaman": - shaman.send(messages["totem_simple"].format(TOTEMS[shaman.nick])) # FIXME + shaman.send(messages["shaman_simple"].format("shaman")) + shaman.send(messages["totem_simple"].format(TOTEMS[shaman])) else: - if role not in var.WOLFCHAT_ROLES: - shaman.send(messages["shaman_notify"].format(role, "random " if shaman in var.ROLES["crazed shaman"] else "")) - if role != "crazed shaman": - totem = TOTEMS[shaman.nick] # FIXME - tmsg = messages["shaman_totem"].format(totem) - try: - tmsg += messages[totem + "_totem"] - except KeyError: - tmsg += messages["generic_bug_totem"] - shaman.send(tmsg) - if role not in var.WOLFCHAT_ROLES: - shaman.send("Players: " + ", ".join(p.nick for p in pl)) - -@event_listener("begin_day") -def on_begin_day(evt, var): - # Apply totem effects that need to begin on day proper - var.EXCHANGED.update(EXCHANGE) - var.SILENCED.update(SILENCE) - var.LYCANTHROPES.update(LYCANTHROPY) - # pestilence doesn't take effect on immunized players - var.DISEASED.update(PESTILENCE - var.IMMUNIZED) - var.LUCKY.update(LUCK) - var.MISDIRECTED.update(MISDIRECTION) - - SHAMANS.clear() - -@event_listener("abstain") -def on_abstain(evt, cli, var, nick): - if nick in NARCOLEPSY: - pm(cli, nick, messages["totem_narcolepsy"]) - evt.prevent_default = True - -@event_listener("lynch") -def on_lynch(evt, cli, var, nick): - if nick in NARCOLEPSY: - pm(cli, nick, messages["totem_narcolepsy"]) - evt.prevent_default = True - -@event_listener("assassinate") -def on_assassinate(evt, var, killer, target, prot): - if prot == "totem": - var.ACTIVE_PROTECTIONS[target.nick].remove("totem") - evt.prevent_default = True - evt.stop_processing = True - channels.Main.send(messages[evt.params.message_prefix + "totem"].format(killer, target)) - -@event_listener("succubus_visit") -def on_succubus_visit(evt, var, succubus, target): - if (users._get(SHAMANS.get(target.nick, (None, None))[1], allow_none=True) in get_all_players(("succubus",)) and # FIXME - (get_main_role(target) == "crazed shaman" or TOTEMS[target.nick] not in var.BENEFICIAL_TOTEMS)): - target.send(messages["retract_totem_succubus"].format(SHAMANS[target.nick][1])) - del SHAMANS[target.nick] - -@event_listener("myrole") -def on_myrole(evt, var, user): - role = evt.data["role"] - if role in var.TOTEM_ORDER and role != "crazed shaman" and var.PHASE == "night" and user.nick not in SHAMANS: - evt.data["messages"].append(messages["totem_simple"].format(TOTEMS[user.nick])) - -@event_listener("revealroles_role") -def on_revealroles(evt, var, wrapper, nickname, role): - if role in var.TOTEM_ORDER and nickname in TOTEMS: - if nickname in SHAMANS: - evt.data["special_case"].append("giving {0} totem to {1}".format(TOTEMS[nickname], SHAMANS[nickname][0])) - elif var.PHASE == "night": - evt.data["special_case"].append("has {0} totem".format(TOTEMS[nickname])) - elif nickname in LASTGIVEN and LASTGIVEN[nickname]: - evt.data["special_case"].append("gave {0} totem to {1}".format(TOTEMS[nickname], LASTGIVEN[nickname])) - -@event_listener("reset") -def on_reset(evt, var): - TOTEMS.clear() - LASTGIVEN.clear() - SHAMANS.clear() - DEATH.clear() - PROTECTION.clear() - REVEALING.clear() - NARCOLEPSY.clear() - SILENCE.clear() - DESPERATION.clear() - IMPATIENCE.clear() - PACIFISM.clear() - INFLUENCE.clear() - EXCHANGE.clear() - LYCANTHROPY.clear() - LUCK.clear() - PESTILENCE.clear() - RETRIBUTION.clear() - MISDIRECTION.clear() - DECEIT.clear() - -@event_listener("get_role_metadata") -def on_get_role_metadata(evt, var, kind): - if kind == "night_kills": - # only add shamans here if they were given a death totem - # even though retribution kills, it is given a special kill message - # note that all shaman types (shaman/CS/wolf shaman) are lumped under the "shaman" key (for now), - # this will change so they all get their own key in the future (once this is split into 3 files) - evt.data["shaman"] = list(TOTEMS.values()).count("death") + shaman.send(messages["shaman_notify"].format("shaman", "")) + totem = TOTEMS[shaman] + tmsg = messages["shaman_totem"].format(totem) + tmsg += messages[totem + "_totem"] + shaman.send(tmsg) + shaman.send("Players: " + ", ".join(p.nick for p in pl)) # vim: set sw=4 expandtab: diff --git a/src/roles/skel.py b/src/roles/skel.py index 174859d..ec6c0e9 100644 --- a/src/roles/skel.py +++ b/src/roles/skel.py @@ -5,12 +5,11 @@ 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.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 +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/succubus.py b/src/roles/succubus.py index 48d3eef..b45b961 100644 --- a/src/roles/succubus.py +++ b/src/roles/succubus.py @@ -5,12 +5,11 @@ 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.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 +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event @@ -103,16 +102,17 @@ def on_get_random_totem_targets(evt, var, shaman): evt.data["targets"].remove(succubus) @event_listener("chk_decision") -def on_chk_decision(evt, cli, var, force): +def on_chk_decision(evt, var, force): for votee, voters in evt.data["votelist"].items(): - if users._get(votee) in get_all_players(("succubus",)): # FIXME + if votee in get_all_players(("succubus",)): for vtr in ENTRANCED: - if vtr.nick in voters: - evt.data["numvotes"][votee] -= evt.data["weights"][votee][vtr.nick] - evt.data["weights"][votee][vtr.nick] = 0 + if vtr in voters: + evt.data["numvotes"][votee] -= evt.data["weights"][votee][vtr] + evt.data["weights"][votee][vtr] = 0 def _kill_entranced_voters(var, votelist, not_lynching, votee): - if not {p.nick for p in get_all_players(("succubus",))} & (set(itertools.chain(*votelist.values())) | not_lynching): # FIXME + voters = set(itertools.chain(*votelist.values())) + if not get_all_players(("succubus",)) & (voters | not_lynching): # none of the succubi voted (or there aren't any succubi), so short-circuit return # kill off everyone entranced that did not follow one of the succubi's votes or abstain @@ -122,32 +122,28 @@ def _kill_entranced_voters(var, votelist, not_lynching, votee): ENTRANCED_DYING.add(x) for other_votee, other_voters in votelist.items(): - if {p.nick for p in get_all_players(("succubus",))} & set(other_voters): # FIXME - if votee == other_votee: + if get_all_players(("succubus",)) & set(other_voters): + if votee is other_votee: ENTRANCED_DYING.clear() return - for x in set(ENTRANCED_DYING): - if x.nick in other_voters: - ENTRANCED_DYING.remove(x) + ENTRANCED_DYING.difference_update(other_voters) - if {p.nick for p in get_all_players(("succubus",))} & not_lynching: # FIXME + if get_all_players(("succubus",)) & not_lynching: if votee is None: ENTRANCED_DYING.clear() return - for x in set(ENTRANCED_DYING): - if x.nick in not_lynching: - ENTRANCED_DYING.remove(x) + ENTRANCED_DYING.difference_update(not_lynching) @event_listener("chk_decision_lynch", priority=5) -def on_chk_decision_lynch(evt, cli, var, voters): +def on_chk_decision_lynch(evt, var, voters): # a different event may override the original votee, but people voting along with succubus # won't necessarily know that, so base whether or not they risk death on the person originally voted _kill_entranced_voters(var, evt.params.votelist, evt.params.not_lynching, evt.params.original_votee) @event_listener("chk_decision_abstain") -def on_chk_decision_abstain(evt, cli, var, not_lynching): +def on_chk_decision_abstain(evt, var, not_lynching): _kill_entranced_voters(var, evt.params.votelist, not_lynching, None) # entranced logic should run after team wins have already been determined (aka run last) diff --git a/src/roles/traitor.py b/src/roles/traitor.py index da1af87..2e504ee 100644 --- a/src/roles/traitor.py +++ b/src/roles/traitor.py @@ -5,11 +5,10 @@ import math from collections import defaultdict import botconfig -import src.settings as var from src.utilities import * from src import debuglog, errlog, plog, users, channels from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/vengefulghost.py b/src/roles/vengefulghost.py index 6929a8a..48fdf21 100644 --- a/src/roles/vengefulghost.py +++ b/src/roles/vengefulghost.py @@ -2,12 +2,11 @@ import re import random from collections import defaultdict -import src.settings as var from src.utilities import * from src import channels, users, debuglog, errlog, plog from src.functions import get_players, get_target, get_main_role from src.decorators import command, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/vigilante.py b/src/roles/vigilante.py index adeb315..9cffa7e 100644 --- a/src/roles/vigilante.py +++ b/src/roles/vigilante.py @@ -2,12 +2,11 @@ import re import random from collections import defaultdict -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players, get_all_players, get_main_role, get_target from src.decorators import command, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/villager.py b/src/roles/villager.py index 5f43239..15dad86 100644 --- a/src/roles/villager.py +++ b/src/roles/villager.py @@ -1,9 +1,8 @@ -import src.settings as var from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/wildchild.py b/src/roles/wildchild.py index 45bb18f..0cfa16d 100644 --- a/src/roles/wildchild.py +++ b/src/roles/wildchild.py @@ -6,7 +6,7 @@ from src.utilities import * from src import users, channels, debuglog, errlog, plog from src.functions import get_players, get_all_players, get_main_role from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event @@ -148,10 +148,10 @@ def on_transition_night_end(evt, var): child.send(messages[to_send]) @event_listener("revealroles_role") -def on_revealroles_role(evt, var, wrapper, nick, role): +def on_revealroles_role(evt, var, wrapper, user, role): if role == "wild child": - if nick in IDOLS: - evt.data["special_case"].append("picked {0} as idol".format(IDOLS[nick])) + if user.nick in IDOLS: + evt.data["special_case"].append("picked {0} as idol".format(IDOLS[user.nick])) else: evt.data["special_case"].append("no idol picked yet") diff --git a/src/roles/wolf.py b/src/roles/wolf.py index 369d38f..7aa5097 100644 --- a/src/roles/wolf.py +++ b/src/roles/wolf.py @@ -7,7 +7,7 @@ from src.utilities import * from src.functions import get_players, get_all_players, get_main_role, get_all_roles from src import debuglog, errlog, plog, users, channels from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event diff --git a/src/roles/wolf_shaman.py b/src/roles/wolf_shaman.py new file mode 100644 index 0000000..7c11866 --- /dev/null +++ b/src/roles/wolf_shaman.py @@ -0,0 +1,102 @@ +import re +import random +import itertools +from collections import defaultdict, deque + +import botconfig +from src.utilities import * +from src import debuglog, errlog, plog, users, channels +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.dispatcher import MessageDispatcher +from src.messages import messages +from src.events import Event + +from src.roles._shaman_helper import setup_variables, get_totem_target, give_totem + +def get_tags(var, totem): + tags = set() + if totem in var.BENEFICIAL_TOTEMS: + tags.add("beneficial") + return tags + +TOTEMS, LASTGIVEN, SHAMANS = setup_variables("wolf shaman", knows_totem=True, get_tags=get_tags) + +@command("give", "totem", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("wolf shaman",)) +def wolf_shaman_totem(var, wrapper, message): + """Give a totem to a player.""" + + target = get_totem_target(var, wrapper, message, LASTGIVEN) + if not target: + return + + totem = TOTEMS[wrapper.source] + + SHAMANS[wrapper.source] = give_totem(var, wrapper, target, prefix="You", tags=get_tags(var, totem), role="wolf shaman", msg=" of {0}".format(totem)) + + relay_wolfchat_command(wrapper.client, wrapper.source.nick, messages["shaman_wolfchat"].format(wrapper.source, target), ("wolf shaman",), is_wolf_command=True) + +@event_listener("transition_day_begin", priority=4) +def on_transition_day_begin(evt, var): + # Select random totem recipients if shamans didn't act + pl = get_players() + for shaman in get_players(("wolf shaman",)): + if shaman not in SHAMANS and shaman.nick not in var.SILENCED: + ps = pl[:] + if shaman in LASTGIVEN: + if LASTGIVEN[shaman] in ps: + ps.remove(LASTGIVEN[shaman]) + levt = Event("get_random_totem_targets", {"targets": ps}) + levt.dispatch(var, shaman) + ps = levt.data["targets"] + if ps: + target = random.choice(ps) + dispatcher = MessageDispatcher(shaman, shaman) + + tags = get_tags(var, TOTEMS[shaman]) + + SHAMANS[shaman] = give_totem(var, dispatcher, target, prefix=messages["random_totem_prefix"], tags=tags, role="wolf shaman", msg=" of {0}".format(TOTEMS[shaman])) + relay_wolfchat_command(shaman.client, shaman.nick, messages["shaman_wolfchat"].format(shaman, target), ("wolf shaman",), is_wolf_command=True) + else: + LASTGIVEN[shaman] = None + elif shaman not in SHAMANS: + LASTGIVEN[shaman] = None + +@event_listener("transition_night_end", priority=2.01) +def on_transition_night_end(evt, var): + max_totems = 0 + ps = get_players() + shamans = get_players(("wolf shaman",)) + index = var.TOTEM_ORDER.index("wolf shaman") + for c in var.TOTEM_CHANCES.values(): + max_totems += c[index] + + for s in list(LASTGIVEN): + if s not in shamans: + del LASTGIVEN[s] + + for shaman in shamans: + pl = ps[:] + random.shuffle(pl) + if LASTGIVEN.get(shaman): + if LASTGIVEN[shaman] in pl: + pl.remove(LASTGIVEN[shaman]) + + target = 0 + rand = random.random() * max_totems + for t in var.TOTEM_CHANCES.keys(): + target += var.TOTEM_CHANCES[t][index] + if rand <= target: + TOTEMS[shaman] = t + break + if shaman.prefers_simple(): + # Message about role was sent with wolfchat + shaman.send(messages["totem_simple"].format(TOTEMS[shaman])) + else: + totem = TOTEMS[shaman] + tmsg = messages["shaman_totem"].format(totem) + tmsg += messages[totem + "_totem"] + shaman.send(tmsg) + +# vim: set sw=4 expandtab: diff --git a/src/roles/wolfcub.py b/src/roles/wolfcub.py index d905df6..89365d3 100644 --- a/src/roles/wolfcub.py +++ b/src/roles/wolfcub.py @@ -2,12 +2,11 @@ import re import random from collections import defaultdict -import src.settings as var from src.utilities import * from src.functions import get_players from src import debuglog, errlog, plog, users, channels from src.decorators import cmd, event_listener -from src.containers import UserList, UserSet, UserDict +from src.containers import UserList, UserSet, UserDict, DefaultUserDict from src.messages import messages from src.events import Event from src.roles import wolf diff --git a/src/settings.py b/src/settings.py index 0ec0029..1eeae7f 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,7 +1,7 @@ import fnmatch import re import threading -from collections import defaultdict, OrderedDict +from collections import OrderedDict LANGUAGE = 'en' diff --git a/src/wolfgame.py b/src/wolfgame.py index d6c71dd..65eb961 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -49,8 +49,8 @@ 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 -from src.functions import get_players, get_all_players, get_participants, get_main_role, get_all_roles, get_reveal_role +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.messages import messages from src.warnings import * from src.context import IRCContext @@ -86,8 +86,16 @@ var.OLD_MODES = defaultdict(set) var.ROLES = UserDict() # type: Dict[str, Set[users.User]] var.MAIN_ROLES = UserDict() # type: Dict[users.User, str] var.ALL_PLAYERS = UserList() +var.FORCE_ROLES = DefaultUserDict(UserSet) var.DYING = UserSet() +var.WOUNDED = UserSet() +var.CONSECRATING = UserSet() +var.GUNNERS = UserDict() + +var.NO_LYNCH = UserSet() +var.VOTES = UserDict() + var.DEADCHAT_PLAYERS = UserSet() var.SPECTATING_WOLFCHAT = UserSet() @@ -108,7 +116,7 @@ var.RESTARTING = False var.BITTEN_ROLES = {} var.LYCAN_ROLES = {} -var.START_VOTES = set() +var.START_VOTES = UserSet() if botconfig.DEBUG_MODE and var.DISABLE_DEBUG_MODE_TIMERS: var.NIGHT_TIME_LIMIT = 0 # 120 @@ -309,13 +317,14 @@ def reset(): var.JOINED_THIS_GAME_ACCS = set() # same, except accounts var.PINGED_ALREADY = set() var.PINGED_ALREADY_ACCS = set() - var.NO_LYNCH = set() + var.NO_LYNCH.clear() var.FGAMED = False var.GAMEMODE_VOTES = {} #list of players who have used !game var.START_VOTES.clear() # list of players who have voted to !start var.LOVERS = {} # need to be here for purposes of random var.ROLE_STATS = frozenset() # type: FrozenSet[FrozenSet[Tuple[str, int]]] var.ROLE_SETS = [] # type: List[Tuple[Counter[str], int]] + var.VOTES.clear() reset_settings() @@ -331,6 +340,7 @@ def reset(): var.ORIGINAL_ROLES.clear() var.ROLES["person"] = UserSet() var.MAIN_ROLES.clear() + var.FORCE_ROLES.clear() evt = Event("reset", {}) evt.dispatch(var) @@ -1720,65 +1730,58 @@ def stats(cli, nick, chan, rest): reply(cli, nick, chan, stats_mssg) @handle_error -def hurry_up(cli, gameid, change): +def hurry_up(gameid, change): if var.PHASE != "day": return if gameid: if gameid != var.DAY_ID: return - chan = botconfig.CHANNEL - if not change: event = Event("daylight_warning", {"message": "daylight_warning"}) event.dispatch(var) - cli.msg(chan, messages[event.data["message"]]) + channels.Main.send(messages[event.data["message"]]) return var.DAY_ID = 0 - pl = set(list_players()) - (var.WOUNDED | var.CONSECRATING) + pl = set(get_players()) - (var.WOUNDED | var.CONSECRATING) evt = Event("get_voters", {"voters": pl}) evt.dispatch(var) pl = evt.data["voters"] - - avail = len(pl) - votesneeded = avail // 2 + 1 - not_lynching = len(var.NO_LYNCH) - - avail = len(pl) - votesneeded = avail // 2 + 1 not_lynching = set(var.NO_LYNCH) - votelist = copy.deepcopy(var.VOTES) - # Note: this event can be differentiated between regular chk_decision - # by checking evt.params.timeout. - event = Event("chk_decision", { - "not_lynching": not_lynching, - "votelist": votelist, - "numvotes": {}, # filled as part of a priority 1 event - "weights": {}, # filled as part of a priority 1 event - "transition_night": transition_night - }, voters=pl, timeout=True) - if not event.dispatch(cli, var, ""): - return - not_lynching = event.data["not_lynching"] - votelist = event.data["votelist"] - numvotes = event.data["numvotes"] + avail = len(pl) + votesneeded = avail // 2 + 1 + + with copy.deepcopy(var.VOTES) as votelist: + # Note: this event can be differentiated between regular chk_decision + # by checking evt.params.timeout. + event = Event("chk_decision", { + "not_lynching": not_lynching, + "votelist": votelist, + "numvotes": {}, # filled as part of a priority 1 event + "weights": {}, # filled as part of a priority 1 event + "transition_night": transition_night + }, voters=pl, timeout=True) + if not event.dispatch(var, None): + return + numvotes = event.data["numvotes"] + + found_dup = False + maxfound = (0, "") + for votee, voters in votelist.items(): + if numvotes[votee] > maxfound[0]: + maxfound = (numvotes[votee], votee) + found_dup = False + elif numvotes[votee] == maxfound[0]: + found_dup = True - found_dup = False - maxfound = (0, "") - for votee, voters in votelist.items(): - if numvotes[votee] > maxfound[0]: - maxfound = (numvotes[votee], votee) - found_dup = False - elif numvotes[votee] == maxfound[0]: - found_dup = True if maxfound[0] > 0 and not found_dup: - cli.msg(chan, messages["sunset_lynch"]) - chk_decision(cli, force=maxfound[1]) # Induce a lynch + channels.Main.send(messages["sunset_lynch"]) + chk_decision(force=maxfound[1]) # Induce a lynch else: - cli.msg(chan, messages["sunset"]) - event.data["transition_night"](cli) + channels.Main.send(messages["sunset"]) + event.data["transition_night"]() @cmd("fnight", flag="N") def fnight(cli, nick, chan, rest): @@ -1786,7 +1789,7 @@ def fnight(cli, nick, chan, rest): if var.PHASE != "day": cli.notice(nick, messages["not_daytime"]) else: - hurry_up(cli, 0, True) + hurry_up(0, True) @cmd("fday", flag="N") @@ -1795,108 +1798,91 @@ def fday(cli, nick, chan, rest): if var.PHASE != "night": cli.notice(nick, messages["not_nighttime"]) else: - transition_day(cli) + transition_day() -# Specify force = "nick" to force nick to be lynched -def chk_decision(cli, force="", end_game=True, deadlist=[]): +# Specify force = user to force user to be lynched +def chk_decision(force=None, end_game=True, deadlist=None): + if deadlist is None: + deadlist = [] with var.GRAVEYARD_LOCK: if var.PHASE != "day": return # Even if the lynch fails, we want to go to night phase if we are forcing a lynch (day timeout) do_night_transision = True if force else False - chan = botconfig.CHANNEL - pl = set(list_players()) - (var.WOUNDED | var.CONSECRATING) + pl = set(get_players()) - (var.WOUNDED | var.CONSECRATING) evt = Event("get_voters", {"voters": pl}) evt.dispatch(var) pl = evt.data["voters"] + not_lynching = set(var.NO_LYNCH) avail = len(pl) votesneeded = avail // 2 + 1 - not_lynching = set(var.NO_LYNCH) - deadlist = deadlist[:] # make a copy as events may mutate it - votelist = copy.deepcopy(var.VOTES) - event = Event("chk_decision", { - "not_lynching": not_lynching, - "votelist": votelist, - "numvotes": {}, # filled as part of a priority 1 event - "weights": {}, # filled as part of a priority 1 event - "transition_night": transition_night - }, voters=pl, timeout=False) - if not event.dispatch(cli, var, force): - return - not_lynching = event.data["not_lynching"] - votelist = event.data["votelist"] - numvotes = event.data["numvotes"] + with copy.deepcopy(var.VOTES) as votelist: - gm = var.CURRENT_GAMEMODE.name - if (gm == "default" or gm == "villagergame") and len(var.ALL_PLAYERS) <= 9 and var.VILLAGERGAME_CHANCE > 0: - if users.Bot.nick in votelist: - if len(votelist[users.Bot.nick]) == avail: - if gm == "default": - cli.msg(botconfig.CHANNEL, messages["villagergame_nope"]) - stop_game("wolves") - return - else: - cli.msg(botconfig.CHANNEL, messages["villagergame_win"]) - stop_game("everyone") - return - else: - del votelist[users.Bot.nick] + event = Event("chk_decision", { + "not_lynching": not_lynching, + "votelist": votelist, + "numvotes": {}, # filled as part of a priority 1 event + "weights": {}, # filled as part of a priority 1 event + "transition_night": transition_night + }, voters=pl, timeout=False) + if not event.dispatch(var, force): + return - # we only need 50%+ to not lynch, instead of an actual majority, because a tie would time out day anyway - # don't check for ABSTAIN_ENABLED here since we may have a case where the majority of people have pacifism totems or something - if len(not_lynching) >= math.ceil(avail / 2): - abs_evt = Event("chk_decision_abstain", {}, votelist=votelist, numvotes=numvotes) - abs_evt.dispatch(cli, var, not_lynching) - cli.msg(botconfig.CHANNEL, messages["village_abstain"]) - var.ABSTAINED = True - event.data["transition_night"](cli) - return - for votee, voters in votelist.items(): - if numvotes[votee] >= votesneeded or votee == force: - # priorities: - # 1 = displaying impatience totem messages - # 3 = mayor/revealing totem - # 4 = fool - # 5 = desperation totem, other things that happen on generic lynch - vote_evt = Event("chk_decision_lynch", {"votee": votee, "deadlist": deadlist}, - del_player=del_player, - original_votee=votee, - force=(votee == force), - votelist=votelist, - not_lynching=not_lynching) - if vote_evt.dispatch(cli, var, voters): - votee = vote_evt.data["votee"] - deadlist = vote_evt.data["deadlist"] - # roles that end the game upon being lynched - if votee in get_roles("fool"): # FIXME - # ends game immediately, with fool as only winner - # hardcode "fool" as the role since game is ending due to them being lynched, - # so we want to show "fool" even if it's a template - lmsg = random.choice(messages["lynch_reveal"]).format(votee, "", "fool") - cli.msg(botconfig.CHANNEL, lmsg) - if chk_win(winner="@" + votee): + numvotes = event.data["numvotes"] + + # we only need 50%+ to not lynch, instead of an actual majority, because a tie would time out day anyway + # don't check for ABSTAIN_ENABLED here since we may have a case where the majority of people have pacifism totems or something + if len(not_lynching) >= math.ceil(avail / 2): + abs_evt = Event("chk_decision_abstain", {}, votelist=votelist, numvotes=numvotes) + abs_evt.dispatch(var, not_lynching) + channels.Main.send(messages["village_abstain"]) + var.ABSTAINED = True + event.data["transition_night"]() + return + for votee, voters in votelist.items(): + if numvotes[votee] >= votesneeded or votee is force: + # priorities: + # 1 = displaying impatience totem messages + # 3 = mayor/revealing totem + # 4 = fool + # 5 = desperation totem, other things that happen on generic lynch + vote_evt = Event("chk_decision_lynch", {"votee": votee, "deadlist": deadlist}, + del_player=del_player, + original_votee=votee, + force=(votee is force), + votelist=votelist, + not_lynching=not_lynching) + if vote_evt.dispatch(var, voters): + votee = vote_evt.data["votee"] + # roles that end the game upon being lynched + if votee in get_all_players(("fool",)): + # ends game immediately, with fool as only winner + # hardcode "fool" as the role since game is ending due to them being lynched, + # so we want to show "fool" even if it's a template + lmsg = random.choice(messages["lynch_reveal"]).format(votee, "", "fool") + channels.Main.send(lmsg) + if chk_win(winner="@" + votee.nick): + return + deadlist.append(votee) + # Other + if votee in get_all_players(("jester",)): + var.JESTERS.add(votee.nick) + + if var.ROLE_REVEAL in ("on", "team"): + rrole = get_reveal_role(votee) + an = "n" if rrole.startswith(("a", "e", "i", "o", "u")) else "" + lmsg = random.choice(messages["lynch_reveal"]).format(votee, an, rrole) + else: + lmsg = random.choice(messages["lynch_no_reveal"]).format(votee) + channels.Main.send(lmsg) + if not del_player(votee, killer_role="villager", deadlist=deadlist, end_game=end_game): return - deadlist.append(votee) - # Other - if votee in get_roles("jester"): # FIXME - var.JESTERS.add(votee) - - if var.ROLE_REVEAL in ("on", "team"): - rrole = get_reveal_role(users._get(votee)) # FIXME - an = "n" if rrole.startswith(("a", "e", "i", "o", "u")) else "" - lmsg = random.choice(messages["lynch_reveal"]).format(votee, an, rrole) - else: - lmsg = random.choice(messages["lynch_no_reveal"]).format(votee) - cli.msg(botconfig.CHANNEL, lmsg) - better_deadlist = [users._get(p) for p in deadlist] # FIXME -- convert chk_decision_lynch to be user-aware - if not del_player(users._get(votee), killer_role="villager", deadlist=better_deadlist, end_game=end_game): # FIXME - return - do_night_transision = True - break - if do_night_transision: - event.data["transition_night"](cli) + do_night_transision = True + break + if do_night_transision: + event.data["transition_night"]() @cmd("votes", pm=True, phases=("join", "day", "night")) def show_votes(cli, nick, chan, rest): @@ -1926,7 +1912,7 @@ def show_votes(cli, nick, chan, rest): with var.WARNING_LOCK: if var.START_VOTES: - the_message += messages["start_votes"].format(len(var.START_VOTES), ', '.join(var.START_VOTES)) + the_message += messages["start_votes"].format(len(var.START_VOTES), ", ".join(p.nick for p in var.START_VOTES)) elif var.PHASE == "night": cli.notice(nick, messages["voting_daytime_only"]) @@ -1953,13 +1939,13 @@ def show_votes(cli, nick, chan, rest): else: votelist = ["{0}: {1} ({2})".format(votee, len(var.VOTES[votee]), - " ".join(var.VOTES[votee])) + " ".join(p.nick for p in var.VOTES[votee])) for votee in var.VOTES.keys()] msg = "{0}{1}".format(_nick, ", ".join(votelist)) reply(cli, nick, chan, msg) - pl = set(list_players()) - (var.WOUNDED | var.CONSECRATING) + pl = set(get_players()) - (var.WOUNDED | var.CONSECRATING) evt = Event("get_voters", {"voters": pl}) evt.dispatch(var) pl = evt.data["voters"] @@ -2275,13 +2261,13 @@ def chk_win_conditions(rolemap, mainroles, end_game=True, winner=None): """Internal handler for the chk_win function.""" with var.GRAVEYARD_LOCK: if var.PHASE == "day": - pl = set(list_players()) - (var.WOUNDED | var.CONSECRATING) # TODO: Convert to users + pl = set(get_players()) - (var.WOUNDED | var.CONSECRATING) evt = Event("get_voters", {"voters": pl}) evt.dispatch(var) pl = evt.data["voters"] lpl = len(pl) else: - pl = set(list_players(mainroles=mainroles)) + pl = set(get_players(mainroles=mainroles)) lpl = len(pl) if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES: @@ -2292,10 +2278,10 @@ def chk_win_conditions(rolemap, mainroles, end_game=True, winner=None): else: wcroles = var.WOLFCHAT_ROLES - wolves = set(list_players(wcroles, mainroles=mainroles)) + wolves = set(get_players(wcroles, mainroles=mainroles)) lwolves = len(wolves & pl) lcubs = len(rolemap.get("wolf cub", ())) - lrealwolves = len(list_players(var.WOLF_ROLES - {"wolf cub"}, mainroles=mainroles)) + lrealwolves = len(get_players(var.WOLF_ROLES - {"wolf cub"}, mainroles=mainroles)) lmonsters = len(rolemap.get("monster", ())) ldemoniacs = len(rolemap.get("demoniac", ())) ltraitors = len(rolemap.get("traitor", ())) @@ -2542,28 +2528,28 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill if var.GAMEPHASE == "day" and timeleft_internal("day") > var.DAY_TIME_LIMIT and var.DAY_TIME_LIMIT > 0: if "day" in var.TIMERS: var.TIMERS["day"][0].cancel() - t = threading.Timer(var.DAY_TIME_LIMIT, hurry_up, [channels.Main.client, var.DAY_ID, True]) + t = threading.Timer(var.DAY_TIME_LIMIT, hurry_up, [var.DAY_ID, True]) var.TIMERS["day"] = (t, time.time(), var.DAY_TIME_LIMIT) t.daemon = True t.start() # Don't duplicate warnings, i.e. only set the warn timer if a warning was not already given if "day_warn" in var.TIMERS and var.TIMERS["day_warn"][0].isAlive(): var.TIMERS["day_warn"][0].cancel() - t = threading.Timer(var.DAY_TIME_WARN, hurry_up, [channels.Main.client, var.DAY_ID, False]) + t = threading.Timer(var.DAY_TIME_WARN, hurry_up, [var.DAY_ID, False]) var.TIMERS["day_warn"] = (t, time.time(), var.DAY_TIME_WARN) t.daemon = True t.start() elif var.GAMEPHASE == "night" and timeleft_internal("night") > var.NIGHT_TIME_LIMIT and var.NIGHT_TIME_LIMIT > 0: if "night" in var.TIMERS: var.TIMERS["night"][0].cancel() - t = threading.Timer(var.NIGHT_TIME_LIMIT, hurry_up, [channels.Main.client, var.NIGHT_ID, True]) + t = threading.Timer(var.NIGHT_TIME_LIMIT, hurry_up, [var.NIGHT_ID, True]) var.TIMERS["night"] = (t, time.time(), var.NIGHT_TIME_LIMIT) t.daemon = True t.start() # Don't duplicate warnings, e.g. only set the warn timer if a warning was not already given if "night_warn" in var.TIMERS and var.TIMERS["night_warn"][0].isAlive(): var.TIMERS["night_warn"][0].cancel() - t = threading.Timer(var.NIGHT_TIME_WARN, hurry_up, [channels.Main.client, var.NIGHT_ID, False]) + t = threading.Timer(var.NIGHT_TIME_WARN, hurry_up, [var.NIGHT_ID, False]) var.TIMERS["night_warn"] = (t, time.time(), var.NIGHT_TIME_WARN) t.daemon = True t.start() @@ -2627,7 +2613,7 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill del var.GAMEMODE_VOTES[player.nick] with var.WARNING_LOCK: - var.START_VOTES.discard(player.nick) + var.START_VOTES.discard(player) # Cancel the start vote timer if there are no votes left if not var.START_VOTES and "start_votes" in var.TIMERS: @@ -2666,23 +2652,24 @@ def del_player(player, *, devoice=True, end_game=True, death_triggers=True, kill for x in (var.PASSED, var.HEXED, var.MATCHMAKERS, var.CURSED): x.discard(player.nick) if var.PHASE == "day" and ret: - var.VOTES.pop(player.nick, None) # Delete other people's votes on the player + if player in var.VOTES: + del var.VOTES[player] # Delete other people's votes on the player for k in list(var.VOTES.keys()): - if player.nick in var.VOTES[k]: - var.VOTES[k].remove(player.nick) + if player in var.VOTES[k]: + var.VOTES[k].remove(player) if not var.VOTES[k]: # no more votes on that person del var.VOTES[k] break # can only vote once - var.NO_LYNCH.discard(player.nick) - var.WOUNDED.discard(player.nick) - var.CONSECRATING.discard(player.nick) + var.NO_LYNCH.discard(player) + var.WOUNDED.discard(player) + var.CONSECRATING.discard(player) # note: PHASE = "day" and GAMEPHASE = "night" during transition_day; # we only want to induce a lynch if it's actually day and we aren't in a chained death if var.GAMEPHASE == "day" and ismain and not end_game: - chk_decision(channels.Main.client) + chk_decision() elif var.PHASE == "night" and ret and ismain: - chk_nightdone(channels.Main.client) + chk_nightdone() return ret @@ -2761,7 +2748,7 @@ def reaper(cli, gameid): del_player(user, end_game=False, death_triggers=False) win = chk_win() if not win and var.PHASE == "day" and var.GAMEPHASE == "day": - chk_decision(cli) + chk_decision() pl = list_players() x = [a for a in to_warn if a in pl] if x: @@ -2982,7 +2969,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.GUNNERS, var.TURNCOATS, + for dictvar in (var.FINAL_ROLES, var.TURNCOATS, var.DOCTORS, var.BITTEN_ROLES, var.LYCAN_ROLES, var.AMNESIAC_ROLES): if prefix in dictvar.keys(): dictvar[nick] = dictvar.pop(prefix) @@ -3014,7 +3001,7 @@ def rename_player(var, user, prefix): for setvar in (var.HEXED, var.SILENCED, var.MATCHMAKERS, var.PASSED, var.JESTERS, var.AMNESIACS, var.LYCANTHROPES, var.LUCKY, var.DISEASED, var.MISDIRECTED, var.EXCHANGED, var.IMMUNIZED, var.CURED_LYCANS, - var.ALPHA_WOLVES, var.CURSED, var.PRIESTS, var.CONSECRATING): + var.ALPHA_WOLVES, var.CURSED, var.PRIESTS): if prefix in setvar: setvar.remove(prefix) setvar.add(nick) @@ -3028,29 +3015,9 @@ def rename_player(var, user, prefix): var.IDLE_WARNED_PM.remove(prefix) var.IDLE_WARNED_PM.add(nick) - if var.PHASE == "day": - for setvar in (var.WOUNDED, var.INVESTIGATED): - if prefix in setvar: - setvar.remove(prefix) - setvar.add(nick) - if prefix in var.VOTES: - var.VOTES[nick] = var.VOTES.pop(prefix) - for v in var.VOTES.values(): - if prefix in v: - v.remove(prefix) - v.append(nick) - if var.PHASE == "join": if prefix in var.GAMEMODE_VOTES: var.GAMEMODE_VOTES[nick] = var.GAMEMODE_VOTES.pop(prefix) - with var.WARNING_LOCK: - if prefix in var.START_VOTES: - var.START_VOTES.discard(prefix) - var.START_VOTES.add(nick) - - if prefix in var.NO_LYNCH: - var.NO_LYNCH.remove(prefix) - var.NO_LYNCH.add(nick) @event_listener("account_change") def account_change(evt, var, user): @@ -3226,9 +3193,7 @@ def leave_game(var, wrapper, message): del_player(wrapper.source, death_triggers=False) -def begin_day(cli): - chan = botconfig.CHANNEL - +def begin_day(): # Reset nighttime variables var.GAMEPHASE = "day" var.KILLER = "" # nickname of who chose the victim @@ -3245,15 +3210,15 @@ def begin_day(cli): var.DYING.clear() var.LAST_GOAT.clear() msg = messages["villagers_lynch"].format(botconfig.CMD_CHAR, len(list_players()) // 2 + 1) - cli.msg(chan, msg) + channels.Main.send(msg) var.DAY_ID = time.time() if var.DAY_TIME_WARN > 0: if var.STARTED_DAY_PLAYERS <= var.SHORT_DAY_PLAYERS: - t1 = threading.Timer(var.SHORT_DAY_WARN, hurry_up, [cli, var.DAY_ID, False]) + t1 = threading.Timer(var.SHORT_DAY_WARN, hurry_up, [var.DAY_ID, False]) l = var.SHORT_DAY_WARN else: - t1 = threading.Timer(var.DAY_TIME_WARN, hurry_up, [cli, var.DAY_ID, False]) + t1 = threading.Timer(var.DAY_TIME_WARN, hurry_up, [var.DAY_ID, False]) l = var.DAY_TIME_WARN var.TIMERS["day_warn"] = (t1, var.DAY_ID, l) t1.daemon = True @@ -3261,10 +3226,10 @@ def begin_day(cli): if var.DAY_TIME_LIMIT > 0: # Time limit enabled if var.STARTED_DAY_PLAYERS <= var.SHORT_DAY_PLAYERS: - t2 = threading.Timer(var.SHORT_DAY_LIMIT, hurry_up, [cli, var.DAY_ID, True]) + t2 = threading.Timer(var.SHORT_DAY_LIMIT, hurry_up, [var.DAY_ID, True]) l = var.SHORT_DAY_LIMIT else: - t2 = threading.Timer(var.DAY_TIME_LIMIT, hurry_up, [cli, var.DAY_ID, True]) + t2 = threading.Timer(var.DAY_TIME_LIMIT, hurry_up, [var.DAY_ID, True]) l = var.DAY_TIME_LIMIT var.TIMERS["day"] = (t2, var.DAY_ID, l) t2.daemon = True @@ -3275,12 +3240,12 @@ def begin_day(cli): for player in get_players(): if not player.is_fake: modes.append(("+v", player.nick)) - mass_mode(cli, modes, []) + channels.Main.mode(*modes) event = Event("begin_day", {}) event.dispatch(var) # induce a lynch if we need to (due to lots of pacifism/impatience totems or whatever) - chk_decision(cli) + chk_decision() @handle_error def night_warn(gameid): @@ -3293,7 +3258,7 @@ def night_warn(gameid): channels.Main.send(messages["twilight_warning"]) @handle_error -def transition_day(cli, gameid=0): +def transition_day(gameid=0): if gameid: if gameid != var.NIGHT_ID: return @@ -3306,9 +3271,7 @@ def transition_day(cli, gameid=0): var.DAY_COUNT += 1 var.FIRST_DAY = (var.DAY_COUNT == 1) var.DAY_START_TIME = datetime.now() - var.VOTES = {} - - chan = botconfig.CHANNEL + var.VOTES.clear() event_begin = Event("transition_day_begin", {}) event_begin.dispatch(var) @@ -3342,9 +3305,8 @@ def transition_day(cli, gameid=0): mm.send(messages["random_matchmaker"]) # Reset daytime variables - var.INVESTIGATED = set() - var.WOUNDED = set() - var.NO_LYNCH = set() + var.WOUNDED.clear() + var.NO_LYNCH.clear() for crow, target in iter(var.OBSERVED.items()): if crow not in get_roles("werecrow"): # FIXME @@ -3362,7 +3324,7 @@ def transition_day(cli, gameid=0): if var.START_WITH_DAY and var.FIRST_DAY: # TODO: need to message everyone their roles and give a short thing saying "it's daytime" # but this is good enough for now to prevent it from crashing - begin_day(cli) + begin_day() return td = var.DAY_START_TIME - var.NIGHT_START_TIME @@ -3383,23 +3345,27 @@ def transition_day(cli, gameid=0): # 6 = rearranging victim list (ensure bodyguard/harlot messages plays), # fixing killers dict priority again (in case step 4 or 5 added to it) # Actually killing off the victims happens in transition_day_resolve + # We set the variables here first; listeners should mutate, not replace + # We don't need to use User containers here, as these don't persist long enough + # This removes the burden of having to clear them at the end or should an error happen + victims = [] + killers = defaultdict(list) + bywolves = set() + onlybywolves = set() + protected = {} + bitten = [] + numkills = {} + evt = Event("transition_day", { - "victims": [], - "killers": defaultdict(list), - "bywolves": set(), - "onlybywolves": set(), - "protected": {}, - "bitten": [], - "numkills": {} # populated at priority 3 + "victims": victims, + "killers": killers, + "bywolves": bywolves, + "onlybywolves": onlybywolves, + "protected": protected, + "bitten": bitten, + "numkills": numkills, # populated at priority 3 }) evt.dispatch(var) - victims = evt.data["victims"] - killers = evt.data["killers"] - bywolves = evt.data["bywolves"] - onlybywolves = evt.data["onlybywolves"] - protected = evt.data["protected"] - bitten = evt.data["bitten"] - numkills = evt.data["numkills"] for player in var.DYING: victims.append(player) @@ -3476,7 +3442,7 @@ def transition_day(cli, gameid=0): var.BITE_PREFERENCES = {} - victims = [] + victims.clear() # Ensures that special events play for bodyguard and harlot-visiting-victim so that kill can # be correctly attributed to wolves (for vengeful ghost lover), and that any gunner events # can play. Harlot visiting wolf doesn't play special events if they die via other means since @@ -3621,25 +3587,19 @@ def transition_day(cli, gameid=0): revt2 = Event("transition_day_resolve_end", { "message": revt.data["message"], "novictmsg": revt.data["novictmsg"], - "dead": revt.data["dead"], - "bywolves": revt.data["bywolves"], - "onlybywolves": revt.data["onlybywolves"], - "killers": revt.data["killers"], - "protected": revt.data["protected"], - "bitten": revt.data["bitten"] + "dead": dead, + "bywolves": bywolves, + "onlybywolves": onlybywolves, + "killers": killers, + "protected": protected, + "bitten": bitten }) revt2.dispatch(var, victims) message = revt2.data["message"] novictmsg = revt2.data["novictmsg"] - dead = revt2.data["dead"] - bywolves = revt2.data["bywolves"] - onlybywolves = revt2.data["onlybywolves"] - killers = revt2.data["killers"] - protected = revt2.data["protected"] - bitten = revt2.data["bitten"] for victim in list(dead): - if victim.nick in var.GUNNERS and var.GUNNERS[victim.nick] > 0 and victim in bywolves: + if victim in var.GUNNERS and var.GUNNERS[victim] > 0 and victim in bywolves: if random.random() < var.GUNNER_KILLS_WOLF_AT_NIGHT_CHANCE: # pick a random wofl to be shot woflset = {wolf for wolf in get_players(var.WOLF_ROLES) if wolf not in dead} @@ -3655,12 +3615,12 @@ def transition_day(cli, gameid=0): else: message.append(messages["gunner_killed_wolf_overnight_no_reveal"].format(victim, deadwolf)) dead.append(deadwolf) - var.GUNNERS[victim.nick] -= 1 # deduct the used bullet + var.GUNNERS[victim] -= 1 # deduct the used bullet for victim in dead: - if victim in bywolves and victim.nick in var.DISEASED: + if victim in bywolves and victim in var.DISEASED: var.DISEASED_WOLVES = True - if var.WOLF_STEALS_GUN and victim in bywolves and victim.nick in var.GUNNERS and var.GUNNERS[victim.nick] > 0: + if var.WOLF_STEALS_GUN and victim in bywolves and victim in var.GUNNERS and var.GUNNERS[victim] > 0: # victim has bullets try: looters = get_players(var.WOLFCHAT_ROLES) @@ -3671,16 +3631,16 @@ def transition_day(cli, gameid=0): else: looters.remove(guntaker) if guntaker not in dead: - numbullets = var.GUNNERS[victim.nick] + numbullets = var.GUNNERS[victim] if guntaker.nick not in var.GUNNERS: - var.GUNNERS[guntaker.nick] = 0 + var.GUNNERS[guntaker] = 0 if guntaker not in get_all_players(("gunner", "sharpshooter")): var.ROLES["gunner"].add(guntaker) - var.GUNNERS[guntaker.nick] += 1 # only transfer one bullet + var.GUNNERS[guntaker] += 1 # only transfer one bullet guntaker.send(messages["wolf_gunner"].format(victim)) except IndexError: pass # no wolves to give gun to (they were all killed during night or something) - var.GUNNERS[victim.nick] = 0 # just in case + var.GUNNERS[victim] = 0 # just in case channels.Main.send("\n".join(message)) @@ -3714,7 +3674,7 @@ def transition_day(cli, gameid=0): debuglog("{0} ({1}) TURNED WOLF".format(chump, chumprole)) var.BITTEN_ROLES[chump.nick] = chumprole change_role(chump, chumprole, newrole) - relay_wolfchat_command(cli, chump.nick, messages["wolfchat_new_member"].format(chump, newrole), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) + 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) killer_role = {} for deadperson in dead: @@ -3740,14 +3700,14 @@ def transition_day(cli, gameid=0): if chk_win(): # if after the last person is killed, one side wins, then actually end the game here return - event_end.data["begin_day"](cli) + event_end.data["begin_day"]() @event_listener("transition_day_resolve_end", priority=2) def on_transition_day_resolve_end(evt, 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"]) -def chk_nightdone(cli): +def chk_nightdone(): if var.PHASE != "night": return @@ -3804,103 +3764,93 @@ def chk_nightdone(cli): var.TIMERS = {} if var.PHASE == "night": # Double check - event.data["transition_day"](cli) + event.data["transition_day"]() -@cmd("nolynch", "nl", "novote", "nv", "abstain", "abs", playing=True, phases=("day",)) -def no_lynch(cli, nick, chan, rest): +@command("nolynch", "nl", "novote", "nv", "abstain", "abs", playing=True, phases=("day",)) +def no_lynch(var, wrapper, message): """Allows you to abstain from voting for the day.""" - if chan == botconfig.CHANNEL: - evt = Event("abstain", {}) - if not var.ABSTAIN_ENABLED: - cli.notice(nick, messages["command_disabled"]) - return - elif var.LIMIT_ABSTAIN and var.ABSTAINED: - cli.notice(nick, messages["exhausted_abstain"]) - return - elif var.LIMIT_ABSTAIN and var.FIRST_DAY: - cli.notice(nick, messages["no_abstain_day_one"]) - return - elif not evt.dispatch(cli, var, nick): - return - elif nick in var.WOUNDED: - cli.msg(chan, messages["wounded_no_vote"].format(nick)) - return - elif nick in var.CONSECRATING: - pm(cli, nick, messages["consecrating_no_vote"]) - return - candidates = var.VOTES.keys() - for voter in list(candidates): - if nick in var.VOTES[voter]: - var.VOTES[voter].remove(nick) - if not var.VOTES[voter]: - del var.VOTES[voter] - var.NO_LYNCH.add(nick) - cli.msg(chan, messages["player_abstain"].format(nick)) - - chk_decision(cli) + evt = Event("abstain", {}) + if not var.ABSTAIN_ENABLED: + wrapper.pm(messages["command_disabled"]) return + elif var.LIMIT_ABSTAIN and var.ABSTAINED: + wrapper.pm(messages["exhausted_abstain"]) + return + elif var.LIMIT_ABSTAIN and var.FIRST_DAY: + wrapper.pm(messages["no_abstain_day_one"]) + return + elif not evt.dispatch(var, wrapper.source): + return + elif wrapper.source in var.WOUNDED: + channels.Main.send(messages["wounded_no_vote"].format(wrapper.source)) + return + elif wrapper.source in var.CONSECRATING: + wrapper.pm(messages["consecrating_no_vote"]) + return + for voter in list(var.VOTES): + if wrapper.source in var.VOTES[voter]: + var.VOTES[voter].remove(wrapper.source) + if not var.VOTES[voter]: + del var.VOTES[voter] + var.NO_LYNCH.add(wrapper.source) + channels.Main.send(messages["player_abstain"].format(wrapper.source)) -@cmd("lynch", playing=True, pm=True, phases=("day",)) -def lynch(cli, nick, chan, rest): + chk_decision() + +@command("lynch", playing=True, pm=True, phases=("day",)) +def lynch(var, wrapper, message): """Use this to vote for a candidate to be lynched.""" - if not rest: - show_votes.caller(cli, nick, chan, rest) + if not message: + show_votes.caller(wrapper.client, wrapper.source.nick, wrapper.target.name, message) return - if chan != botconfig.CHANNEL: + if wrapper.source in var.WOUNDED: + wrapper.send(messages["wounded_no_vote"].format(wrapper.source)) + return + if wrapper.source in var.CONSECRATING: + wrapper.pm(messages["consecrating_no_vote"]) return - rest = re.split(" +",rest)[0].strip() + msg = re.split(" +", message)[0].strip() troll = False if ((var.CURRENT_GAMEMODE.name == "default" or var.CURRENT_GAMEMODE.name == "villagergame") and var.VILLAGERGAME_CHANCE > 0 and len(var.ALL_PLAYERS) <= 9): troll = True - voted = get_victim(cli, nick, rest, True, var.SELF_LYNCH_ALLOWED, bot_in_list=troll) + no_vote_self = "save_self" + if wrapper.source in get_all_players(("fool", "jester")): + no_vote_self = "no_self_lynch" + + voted = get_target(var, wrapper, msg, allow_self=var.SELF_LYNCH_ALLOWED, allow_bot=troll, not_self_message=no_vote_self) if not voted: return evt = Event("lynch", {"target": voted}) - if not evt.dispatch(cli, var, nick): + if not evt.dispatch(var, wrapper.source): return voted = evt.data["target"] - if not var.SELF_LYNCH_ALLOWED: - if nick == voted: - if nick in get_roles("fool", "jester"): # FIXME - cli.notice(nick, messages["no_self_lynch"]) - else: - cli.notice(nick, messages["save_self"]) - return - if nick in var.WOUNDED: - cli.msg(chan, (messages["wounded_no_vote"]).format(nick)) - return - if nick in var.CONSECRATING: - pm(cli, nick, messages["consecrating_no_vote"]) - return - - var.NO_LYNCH.discard(nick) + var.NO_LYNCH.discard(wrapper.source) lcandidates = list(var.VOTES.keys()) for voters in lcandidates: # remove previous vote - if voters == voted and nick in var.VOTES[voters]: + if voters is voted and wrapper.source in var.VOTES[voters]: break - if nick in var.VOTES[voters]: - var.VOTES[voters].remove(nick) - if not var.VOTES.get(voters) and voters != voted: + if wrapper.source in var.VOTES[voters]: + var.VOTES[voters].remove(wrapper.source) + if not var.VOTES.get(voters) and voters is not voted: del var.VOTES[voters] break - if voted not in var.VOTES.keys(): - var.VOTES[voted] = [] - if nick not in var.VOTES[voted]: - var.VOTES[voted].append(nick) - cli.msg(chan, (messages["player_vote"]).format(nick, voted)) + if voted not in var.VOTES: + var.VOTES[voted] = UserList() + if wrapper.source not in var.VOTES[voted]: + var.VOTES[voted].append(wrapper.source) + wrapper.send(messages["player_vote"].format(wrapper.source, voted)) var.LAST_VOTES = None # reset - chk_decision(cli) - + chk_decision() # chooses a target given nick, taking luck totem/misdirection totem into effect # returns the actual target @@ -4126,73 +4076,66 @@ def check_exchange(cli, actor, nick): return True return False -@cmd("retract", "r", pm=True, phases=("day", "join")) -def retract(cli, nick, chan, rest): +@command("retract", "r", phases=("day", "join")) +def retract(var, wrapper, message): """Takes back your vote during the day (for whom to lynch).""" - - if chan != botconfig.CHANNEL: - return - user = users._get(nick) # FIXME - if user not in get_players() or user in var.DISCONNECTED: + if wrapper.source not in get_players() or wrapper.source in var.DISCONNECTED: return with var.GRAVEYARD_LOCK, var.WARNING_LOCK: if var.PHASE == "join": - if chan == botconfig.CHANNEL: - if not nick in var.START_VOTES: - cli.notice(nick, messages["start_novote"]) - else: - var.START_VOTES.discard(nick) - cli.msg(chan, messages["start_retract"].format(nick)) + if not wrapper.source in var.START_VOTES: + wrapper.pm(messages["start_novote"]) + else: + var.START_VOTES.discard(wrapper.source) + wrapper.send(messages["start_retract"].format(wrapper.source)) - if len(var.START_VOTES) < 1: - var.TIMERS['start_votes'][0].cancel() - del var.TIMERS['start_votes'] - return + if len(var.START_VOTES) < 1: + var.TIMERS["start_votes"][0].cancel() + del var.TIMERS["start_votes"] if var.PHASE != "day": return - if nick in var.NO_LYNCH: - var.NO_LYNCH.remove(nick) - cli.msg(chan, messages["retracted_vote"].format(nick)) + if wrapper.source in var.NO_LYNCH: + var.NO_LYNCH.remove(wrapper.source) + wrapper.send(messages["retracted_vote"].format(wrapper.source)) var.LAST_VOTES = None # reset return - candidates = var.VOTES.keys() - for voter in list(candidates): - if nick in var.VOTES[voter]: - var.VOTES[voter].remove(nick) + for voter in list(var.VOTES): + if wrapper.source in var.VOTES[voter]: + var.VOTES[voter].remove(wrapper.source) if not var.VOTES[voter]: del var.VOTES[voter] - cli.msg(chan, messages["retracted_vote"].format(nick)) + wrapper.send(messages["retracted_vote"].format(wrapper.source)) var.LAST_VOTES = None # reset break else: - cli.notice(nick, messages["pending_vote"]) + wrapper.pm(messages["pending_vote"]) @command("shoot", playing=True, silenced=True, phases=("day",)) def shoot(var, wrapper, message): """Use this to fire off a bullet at someone in the day if you have bullets.""" - if wrapper.target is not channels.Main: - return - - if wrapper.source.nick not in var.GUNNERS.keys(): + if wrapper.source not in var.GUNNERS.keys(): wrapper.pm(messages["no_gun"]) return - elif not var.GUNNERS.get(wrapper.source.nick): + elif not var.GUNNERS.get(wrapper.source): wrapper.pm(messages["no_bullets"]) return - victim = get_victim(wrapper.source.client, wrapper.source.nick, re.split(" +", message)[0], True) - if not victim: - return - if victim == wrapper.source.nick: - wrapper.pm(messages["gunner_target_self"]) - return - # get actual victim - victim = choose_target(wrapper.source.nick, victim) - wolfshooter = wrapper.source.nick in list_players(var.WOLFCHAT_ROLES) - var.GUNNERS[wrapper.source.nick] -= 1 + target = get_target(var, wrapper, re.split(" +", message)[0], not_self_message="gunner_target_self") + if not target: + return + + # get actual victim + evt = Event("targeted_command", {"target": target, "misdirection": True, "exchange": True}) + if not evt.dispatch(var, "shoot", wrapper.source, target, frozenset({"detrimental"})): + return + + target = evt.data["target"] + + wolfshooter = wrapper.source in get_players(var.WOLFCHAT_ROLES) + var.GUNNERS[wrapper.source] -= 1 rand = random.random() if wrapper.source in var.ROLES["village drunk"]: @@ -4203,61 +4146,63 @@ def shoot(var, wrapper, message): chances = var.GUN_CHANCES # TODO: make this into an event once we split off gunner - if victim in get_roles("succubus"): # FIXME + if target in get_all_players(("succubus",)): chances = chances[:3] + (0,) - wolfvictim = victim in list_players(var.WOLF_ROLES) - realrole = get_role(victim) - victimrole = get_reveal_role(users._get(victim)) # FIXME + wolfvictim = target in get_players(var.WOLF_ROLES) + realrole = get_main_role(target) + targrole = get_reveal_role(target) alwaysmiss = (realrole == "werekitten") if rand <= chances[0] and not (wolfshooter and wolfvictim) and not alwaysmiss: # didn't miss or suicide and it's not a wolf shooting another wolf - wrapper.send(messages["shoot_success"].format(wrapper.source.nick, victim)) - an = "n" if victimrole.startswith(("a", "e", "i", "o", "u")) else "" + wrapper.send(messages["shoot_success"].format(wrapper.source, target)) + an = "n" if targrole.startswith(("a", "e", "i", "o", "u")) else "" if realrole in var.WOLF_ROLES: if var.ROLE_REVEAL == "on": - wrapper.send(messages["gunner_victim_wolf_death"].format(victim,an, victimrole)) + wrapper.send(messages["gunner_victim_wolf_death"].format(target, an, targrole)) else: # off and team - wrapper.send(messages["gunner_victim_wolf_death_no_reveal"].format(victim)) - if not del_player(users._get(victim), killer_role=get_main_role(wrapper.source)): # FIXME + wrapper.send(messages["gunner_victim_wolf_death_no_reveal"].format(target)) + if not del_player(target, killer_role=get_main_role(wrapper.source)): return elif random.random() <= chances[3]: accident = "accidentally " if wrapper.source in var.ROLES["sharpshooter"]: accident = "" # it's an accident if the sharpshooter DOESN'T headshot :P - wrapper.send(messages["gunner_victim_villager_death"].format(victim, accident)) + wrapper.send(messages["gunner_victim_villager_death"].format(target, accident)) if var.ROLE_REVEAL in ("on", "team"): - wrapper.send(messages["gunner_victim_role"].format(an, victimrole)) - if not del_player(users._get(victim), killer_role=get_main_role(wrapper.source)): # FIXME + wrapper.send(messages["gunner_victim_role"].format(an, targrole)) + if not del_player(target, killer_role=get_main_role(wrapper.source)): return else: - wrapper.send(messages["gunner_victim_injured"].format(victim)) - var.WOUNDED.add(victim) + wrapper.send(messages["gunner_victim_injured"].format(target)) + var.WOUNDED.add(target) lcandidates = list(var.VOTES.keys()) for cand in lcandidates: # remove previous vote - if victim in var.VOTES[cand]: - var.VOTES[cand].remove(victim) + if target in var.VOTES[cand]: + var.VOTES[cand].remove(target) if not var.VOTES.get(cand): del var.VOTES[cand] break - chk_decision(wrapper.source.client) + chk_decision() chk_win() + elif rand <= chances[0] + chances[1]: - wrapper.send(messages["gunner_miss"].format(wrapper.source.nick)) + wrapper.send(messages["gunner_miss"].format(wrapper.source)) else: if var.ROLE_REVEAL in ("on", "team"): - wrapper.send(messages["gunner_suicide"].format(wrapper.source.nick, get_reveal_role(wrapper.source))) + wrapper.send(messages["gunner_suicide"].format(wrapper.source, get_reveal_role(wrapper.source))) else: - wrapper.send(messages["gunner_suicide_no_reveal"].format(wrapper.source.nick)) - if not del_player(wrapper.source, killer_role="villager"): # blame explosion on villager's shoddy gun construction or something - return # Someone won. + wrapper.send(messages["gunner_suicide_no_reveal"].format(wrapper.source)) + del_player(wrapper.source, killer_role="villager") # blame explosion on villager's shoddy gun construction or something def is_safe(nick, victim): # replace calls to this with targeted_command event when splitting roles from src.roles import succubus - return nick in succubus.ENTRANCED and victim in get_roles("succubus") # FIXME + user = users._get(nick) # FIXME + target = users._get(victim) # FIXME + return user in succubus.ENTRANCED and target in get_all_players(("succubus",)) @cmd("bless", chan=False, pm=True, playing=True, silenced=True, phases=("day",), roles=("priest",)) def bless(cli, nick, chan, rest): @@ -4309,7 +4254,7 @@ def consecrate(cli, nick, chan, rest): if users._get(victim) in vengefulghost.GHOSTS: var.SILENCED.add(victim) - var.CONSECRATING.add(nick) + var.CONSECRATING.add(users._get(nick)) # FIXME pm(cli, nick, messages["consecrate_success"].format(victim)) debuglog("{0} ({1}) CONSECRATE: {2}".format(nick, get_role(nick), victim)) # consecrating can possibly cause game to end, so check for that @@ -4959,7 +4904,7 @@ def relay(var, wrapper, message): player.send_messages() @handle_error -def transition_night(cli): +def transition_night(): if var.PHASE == "night": return var.PHASE = "night" @@ -4976,8 +4921,8 @@ def transition_night(cli): modes = [] for player in get_players(): if not player.is_fake: - modes.append(("-v", player.nick)) - mass_mode(cli, modes, []) + modes.append(("-v", player)) + channels.Main.mode(*modes) for x, tmr in var.TIMERS.items(): # cancel daytime timer tmr[0].cancel() @@ -4990,7 +4935,7 @@ def transition_night(cli): var.PASSED = set() var.OBSERVED = {} # those whom werecrows have observed var.TOBESILENCED = set() - var.CONSECRATING = set() + var.CONSECRATING.clear() for nick in var.PRAYED: var.PRAYED[nick][0] = 0 var.PRAYED[nick][1] = None @@ -5005,11 +4950,9 @@ def transition_night(cli): min, sec = td.seconds // 60, td.seconds % 60 daydur_msg = messages["day_lasted"].format(min,sec) - chan = botconfig.CHANNEL - var.NIGHT_ID = time.time() if var.NIGHT_TIME_LIMIT > 0: - t = threading.Timer(var.NIGHT_TIME_LIMIT, transition_day, [cli, var.NIGHT_ID]) + t = threading.Timer(var.NIGHT_TIME_LIMIT, transition_day, [var.NIGHT_ID]) var.TIMERS["night"] = (t, var.NIGHT_ID, var.NIGHT_TIME_LIMIT) t.daemon = True t.start() @@ -5035,7 +4978,7 @@ def transition_night(cli): from src.roles import succubus if amnrole == "succubus" and amnuser in succubus.ENTRANCED: succubus.ENTRANCED.remove(amnuser) - pm(cli, amn, messages["no_longer_entranced"]) + amnuser.send(messages["no_longer_entranced"]) if var.FIRST_NIGHT: # we don't need to tell them twice if they remember right away continue showrole = amnrole @@ -5046,18 +4989,18 @@ def transition_night(cli): n = "" if showrole.startswith(("a", "e", "i", "o", "u")): n = "n" - pm(cli, amn, messages["amnesia_clear"].format(n, showrole)) + amnuser.send(messages["amnesia_clear"].format(n, showrole)) if in_wolflist(amn, amn): if amnrole in var.WOLF_ROLES: - relay_wolfchat_command(cli, amn, messages["amnesia_wolfchat"].format(amn, showrole), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) + relay_wolfchat_command(amnuser.client, amn, messages["amnesia_wolfchat"].format(amn, showrole), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) else: - relay_wolfchat_command(cli, amn, messages["amnesia_wolfchat"].format(amn, showrole), var.WOLFCHAT_ROLES) + relay_wolfchat_command(amnuser.client, amn, messages["amnesia_wolfchat"].format(amn, showrole), var.WOLFCHAT_ROLES) elif amnrole == "turncoat": var.TURNCOATS[amn] = ("none", -1) debuglog("{0} REMEMBER: {1} as {2}".format(amn, amnrole, showrole)) if var.FIRST_NIGHT and chk_win(end_game=False): # prevent game from ending as soon as it begins (useful for the random game mode) - start(cli, users.Bot.nick, botconfig.CHANNEL, restart=var.CURRENT_GAMEMODE.name) + start(channels.Main.client, users.Bot.nick, channels.Main.name, restart=var.CURRENT_GAMEMODE.name) return # game ended from bitten / amnesiac turning, narcolepsy totem expiring, or other weirdness @@ -5065,160 +5008,158 @@ def transition_night(cli): return # send PMs - ps = list_players() + ps = get_players() - for pht in get_roles("prophet"): # FIXME + 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 in var.PLAYERS and not is_user_simple(pht): + if pht.prefers_simple(): + pht.send(messages["prophet_simple"]) + else: if chance1 > 0: - pm(cli, pht, messages["prophet_notify_both"].format(an1, chance1, an2, chance2)) + pht.send(messages["prophet_notify_both"].format(an1, chance1, an2, chance2)) elif chance2 > 0: - pm(cli, pht, messages["prophet_notify_second"].format(an2, chance2)) + pht.send(messages["prophet_notify_second"].format(an2, chance2)) else: - pm(cli, pht, messages["prophet_notify_none"]) - else: - pm(cli, pht, messages["prophet_simple"]) + pht.send(messages["prophet_notify_none"]) - for drunk in get_roles("village drunk"): # FIXME - if drunk in var.PLAYERS and not is_user_simple(drunk): - pm(cli, drunk, messages["drunk_notification"]) + for drunk in get_all_players(("village drunk",)): + if drunk.prefers_simple(): + drunk.send(messages["drunk_simple"]) else: - pm(cli, drunk, messages["drunk_simple"]) + drunk.send(messages["drunk_notification"]) - for doctor in get_roles("doctor"): # FIXME - if doctor in var.DOCTORS and var.DOCTORS[doctor] > 0: # has immunizations remaining + for doctor in get_all_players(("doctor",)): + if doctor.nick in var.DOCTORS and var.DOCTORS[doctor.nick] > 0: # has immunizations remaining pl = ps[:] random.shuffle(pl) - if doctor in var.PLAYERS and not is_user_simple(doctor): - pm(cli, doctor, messages["doctor_notify"]) + if doctor.prefers_simple(): + doctor.send(messages["doctor_simple"]) else: - pm(cli, doctor, messages["doctor_simple"]) - pm(cli, doctor, messages["doctor_immunizations"].format(var.DOCTORS[doctor], 's' if var.DOCTORS[doctor] > 1 else '')) + doctor.send(messages["doctor_notify"]) + doctor.send(messages["doctor_immunizations"].format(var.DOCTORS[doctor.nick], 's' if var.DOCTORS[doctor.nick] > 1 else '')) - for fool in get_roles("fool"): # FIXME - if fool in var.PLAYERS and not is_user_simple(fool): - pm(cli, fool, messages["fool_notify"]) + for fool in get_all_players(("fool",)): + if fool.prefers_simple(): + fool.send(messages["fool_simple"]) else: - pm(cli, fool, messages["fool_simple"]) + fool.send(messages["fool_notify"]) - for jester in get_roles("jester"): # FIXME - if jester in var.PLAYERS and not is_user_simple(jester): - pm(cli, jester, messages["jester_notify"]) + for jester in get_all_players(("jester",)): + if jester.prefers_simple(): + jester.send(messages["jester_simple"]) else: - pm(cli, jester, messages["jester_simple"]) + jester.send(messages["jester_notify"]) - for monster in get_roles("monster"): # FIXME - if monster in var.PLAYERS and not is_user_simple(monster): - pm(cli, monster, messages["monster_notify"]) + for monster in get_all_players(("monster",)): + if monster.prefers_simple(): + monster.send(messages["monster_simple"]) else: - pm(cli, monster, messages["monster_simple"]) + monster.send(messages["monster_notify"]) - for demoniac in get_roles("demoniac"): # FIXME - if demoniac in var.PLAYERS and not is_user_simple(demoniac): - pm(cli, demoniac, messages["demoniac_notify"]) + for demoniac in get_all_players(("demoniac",)): + if demoniac.prefers_simple(): + demoniac.send(messages["demoniac_simple"]) else: - pm(cli, demoniac, messages["demoniac_simple"]) + demoniac.send(messages["demoniac_notify"]) - for lycan in get_roles("lycan"): # FIXME - if lycan in var.PLAYERS and not is_user_simple(lycan): - pm(cli, lycan, messages["lycan_notify"]) + for lycan in get_all_players(("lycan",)): + if lycan.prefers_simple(): + lycan.send(messages["lycan_simple"]) else: - pm(cli, lycan, messages["lycan_simple"]) + lycan.send(messages["lycan_notify"]) - for ass in get_roles("assassin"): # FIXME - if ass in var.TARGETED and var.TARGETED[ass] != None: + for ass in get_all_players(("assassin",)): + if ass.nick in var.TARGETED and var.TARGETED[ass.nick] is not None: continue # someone already targeted pl = ps[:] random.shuffle(pl) pl.remove(ass) - role = get_role(ass) - if role == "village drunk": - var.TARGETED[ass] = random.choice(pl) - message = messages["drunken_assassin_notification"].format(var.TARGETED[ass]) - if ass in var.PLAYERS and not is_user_simple(ass): + if ass in get_all_players(("village drunk",)): + var.TARGETED[ass.nick] = random.choice(pl) + message = messages["drunken_assassin_notification"].format(var.TARGETED[ass.nick]) + if not ass.prefers_simple(): message += messages["assassin_info"] - pm(cli, ass, message) + ass.send(message) else: - if ass in var.PLAYERS and not is_user_simple(ass): - pm(cli, ass, (messages["assassin_notify"])) + if ass.prefers_simple(): + ass.send(messages["assassin_simple"]) else: - pm(cli, ass, messages["assassin_simple"]) - pm(cli, ass, "Players: " + ", ".join(pl)) + ass.send(messages["assassin_notify"]) + ass.send("Players: " + ", ".join(p.nick for p in pl)) - for turncoat in get_roles("turncoat"): # FIXME + for turncoat in get_all_players(("turncoat",)): # they start out as unsided, but can change n1 - if turncoat not in var.TURNCOATS: - var.TURNCOATS[turncoat] = ("none", -1) + if turncoat.nick not in var.TURNCOATS: + var.TURNCOATS[turncoat.nick] = ("none", -1) - if turncoat in var.PLAYERS and not is_user_simple(turncoat): + if turncoat.prefers_simple(): + turncoat.send(messages["turncoat_simple"].format(var.TURNCOATS[turncoat.nick][0])) + else: message = messages["turncoat_notify"] - if var.TURNCOATS[turncoat][0] != "none": - message += messages["turncoat_current_team"].format(var.TURNCOATS[turncoat][0]) + if var.TURNCOATS[turncoat.nick][0] != "none": + message += messages["turncoat_current_team"].format(var.TURNCOATS[turncoat.nick][0]) else: message += messages["turncoat_no_team"] - pm(cli, turncoat, message) - else: - pm(cli, turncoat, messages["turncoat_simple"].format(var.TURNCOATS[turncoat][0])) + turncoat.send(message) - for priest in get_roles("priest"): # FIXME - if priest in var.PLAYERS and not is_user_simple(priest): - pm(cli, priest, messages["priest_notify"]) + for priest in get_all_players(("priest",)): + if priest.prefers_simple(): + priest.send(messages["priest_simple"]) else: - pm(cli, priest, messages["priest_simple"]) + priest.send(messages["priest_notify"]) if var.FIRST_NIGHT or var.ALWAYS_PM_ROLE: - for mm in get_roles("matchmaker"): # FIXME + for mm in get_all_players(("matchmaker",)): pl = ps[:] random.shuffle(pl) - if mm in var.PLAYERS and not is_user_simple(mm): - pm(cli, mm, messages["matchmaker_notify"]) + if mm.prefers_simple(): + mm.send(messages["matchmaker_simple"]) else: - pm(cli, mm, messages["matchmaker_simple"]) - pm(cli, mm, "Players: " + ", ".join(pl)) + mm.send(messages["matchmaker_notify"]) + mm.send("Players: " + ", ".join(p.nick for p in pl)) - for clone in get_roles("clone"): # FIXME + for clone in get_all_players(("clone",)): pl = ps[:] random.shuffle(pl) pl.remove(clone) - if clone in var.PLAYERS and not is_user_simple(clone): - pm(cli, clone, messages["clone_notify"]) + if clone.prefers_simple(): + clone.send(messages["clone_simple"]) else: - pm(cli, clone, messages["clone_simple"]) - pm(cli, clone, "Players: "+", ".join(pl)) + clone.send(messages["clone_notify"]) + clone.send("Players: "+", ".join(p.nick for p in pl)) - for minion in get_roles("minion"): # FIXME - wolves = list_players(var.WOLF_ROLES) + for minion in get_all_players(("minion",)): + wolves = get_players(var.WOLF_ROLES) random.shuffle(wolves) - if minion in var.PLAYERS and not is_user_simple(minion): - pm(cli, minion, messages["minion_notify"]) + if minion.prefers_simple(): + minion.send(messages["minion_simple"]) else: - pm(cli, minion, messages["minion_simple"]) - pm(cli, minion, "Wolves: " + ", ".join(wolves)) + minion.send(messages["minion_notify"]) + minion.send("Wolves: " + ", ".join(p.nick for p in wolves)) - for g in var.GUNNERS.keys(): + for g in var.GUNNERS: if g not in ps: continue elif not var.GUNNERS[g]: continue elif var.GUNNERS[g] == 0: continue - norm_notify = g in var.PLAYERS and not is_user_simple(g) role = "gunner" - if g in get_roles("sharpshooter"): # FIXME + if g in get_all_players(("sharpshooter",)): role = "sharpshooter" - if norm_notify: + if g.prefers_simple(): + gun_msg = messages["gunner_simple"].format(role, str(var.GUNNERS[g]), "s" if var.GUNNERS[g] > 1 else "") + else: if role == "gunner": gun_msg = messages["gunner_notify"].format(role, botconfig.CMD_CHAR, str(var.GUNNERS[g]), "s" if var.GUNNERS[g] > 1 else "") elif role == "sharpshooter": gun_msg = messages["sharpshooter_notify"].format(role, botconfig.CMD_CHAR, str(var.GUNNERS[g]), "s" if var.GUNNERS[g] > 1 else "") - else: - gun_msg = messages["gunner_simple"].format(role, str(var.GUNNERS[g]), "s" if var.GUNNERS[g] > 1 else "") - pm(cli, g, gun_msg) + g.send(gun_msg) event_end = Event("transition_night_end", {}) event_end.dispatch(var) @@ -5227,13 +5168,12 @@ def transition_night(cli): if not var.FIRST_NIGHT: dmsg = (dmsg + messages["first_night_begin"]) - cli.msg(chan, dmsg) + channels.Main.send(dmsg) debuglog("BEGIN NIGHT") # If there are no nightroles that can act, immediately turn it to daytime - chk_nightdone(cli) + chk_nightdone() -def cgamemode(cli, arg): - chan = botconfig.CHANNEL +def cgamemode(arg): if var.ORIGINAL_SETTINGS: # needs reset reset_settings() @@ -5256,7 +5196,7 @@ def cgamemode(cli, arg): var.CURRENT_GAMEMODE = gm return True except InvalidModeException as e: - cli.msg(botconfig.CHANNEL, "Invalid mode: "+str(e)) + channels.Main.send("Invalid mode: "+str(e)) return False else: cli.msg(chan, messages["game_mode_not_found"].format(modeargs[0])) @@ -5297,6 +5237,7 @@ def start(cli, nick, chan, forced = False, restart = ""): return villagers = list_players() + vils = set(get_players()) pl = villagers[:] if not restart: @@ -5326,8 +5267,9 @@ def start(cli, nick, chan, forced = False, restart = ""): return with var.WARNING_LOCK: - if not forced and nick in var.START_VOTES: - cli.notice(nick, messages["start_already_voted"]) + user = users._get(nick) # FIXME + if not forced and user in var.START_VOTES: + user.send(messages["start_already_voted"], notice=True) return start_votes_required = min(math.ceil(len(villagers) * var.START_VOTES_SCALE), var.START_VOTES_MAX) @@ -5336,7 +5278,7 @@ def start(cli, nick, chan, forced = False, restart = ""): # Checked here to make sure that a player that has already voted can't # vote again for the final start. if len(var.START_VOTES) < start_votes_required - 1: - var.START_VOTES.add(nick) + var.START_VOTES.add(user) msg = messages["start_voted"] remaining_votes = start_votes_required - len(var.START_VOTES) @@ -5360,16 +5302,16 @@ def start(cli, nick, chan, forced = False, restart = ""): votes[gamemode] = votes.get(gamemode, 0) + 1 voted = [gamemode for gamemode in votes if votes[gamemode] == max(votes.values()) and votes[gamemode] >= len(villagers)/2] if len(voted): - cgamemode(cli, random.choice(voted)) + cgamemode(random.choice(voted)) else: possiblegamemodes = [] for gamemode in var.GAME_MODES.keys() - var.DISABLED_GAMEMODES: if len(villagers) >= var.GAME_MODES[gamemode][1] and len(villagers) <= var.GAME_MODES[gamemode][2] and var.GAME_MODES[gamemode][3] > 0: possiblegamemodes += [gamemode]*(var.GAME_MODES[gamemode][3]+votes.get(gamemode, 0)*15) - cgamemode(cli, random.choice(possiblegamemodes)) + cgamemode(random.choice(possiblegamemodes)) else: - cgamemode(cli, restart) + cgamemode(restart) var.GAME_ID = time.time() # restart reaper timer addroles = {} @@ -5380,12 +5322,16 @@ def start(cli, nick, chan, forced = False, restart = ""): for index in range(len(var.ROLE_INDEX) - 1, -1, -1): if var.ROLE_INDEX[index] <= len(villagers): for role, num in var.ROLE_GUIDE.items(): # allow event to override some roles - addroles[role] = addroles.get(role, num[index]) + addroles[role] = max(addroles.get(role, num[index]), len(var.FORCE_ROLES.get(role, ()))) break else: cli.msg(chan, messages["no_settings_defined"].format(nick, len(villagers))) return + if sum([addroles[r] for r in addroles if r not in var.TEMPLATE_RESTRICTIONS]) > len(villagers): + channels.Main.send(messages["too_many_roles"]) + return + possible_rolesets = [] roleset_roles = defaultdict(int) for rs, amt in var.ROLE_SETS: @@ -5435,7 +5381,7 @@ def start(cli, nick, chan, forced = False, restart = ""): var.ROLES.clear() var.ROLES[var.DEFAULT_ROLE] = UserSet() var.MAIN_ROLES.clear() - var.GUNNERS = {} + var.GUNNERS.clear() var.OBSERVED = {} var.CLONED = {} var.TARGETED = {} @@ -5472,7 +5418,7 @@ def start(cli, nick, chan, forced = False, restart = ""): var.EXCHANGED_ROLES = [] var.EXTRA_WOLVES = 0 var.PRIESTS = set() - var.CONSECRATING = set() + var.CONSECRATING.clear() var.DYING.clear() var.PRAYED = {} @@ -5480,26 +5426,41 @@ def start(cli, nick, chan, forced = False, restart = ""): var.SPECTATING_WOLFCHAT.clear() var.SPECTATING_DEADCHAT.clear() + for role, ps in var.FORCE_ROLES.items(): + vils.difference_update(ps) + for role, count in addroles.items(): if role in var.TEMPLATE_RESTRICTIONS.keys(): var.ROLES[role] = [None] * count continue # We deal with those later, see below - selected = random.sample(villagers, count) + to_add = set() + + if role in var.FORCE_ROLES: + if len(var.FORCE_ROLES[role]) > count: + channels.Main.send(messages["error_frole_too_many"].format(role)) + return + for user in var.FORCE_ROLES[role]: + var.MAIN_ROLES[user] = role + to_add.add(user) + count -= 1 + + selected = random.sample(vils, count) for x in selected: - var.MAIN_ROLES[users._get(x)] = role # FIXME - villagers.remove(x) - var.ROLES[role] = UserSet(users._get(x) for x in selected) # FIXME + var.MAIN_ROLES[x] = role + vils.remove(x) + var.ROLES[role] = UserSet(selected) + var.ROLES[role].update(to_add) fixed_count = count - roleset_roles[role] if fixed_count > 0: for pr in possible_rolesets: pr[role] += fixed_count - var.ROLES[var.DEFAULT_ROLE].update(users._get(x) for x in villagers) # FIXME - for x in villagers: - var.MAIN_ROLES[users._get(x)] = var.DEFAULT_ROLE # FIXME - if villagers: + var.ROLES[var.DEFAULT_ROLE].update(vils) + for x in vils: + var.MAIN_ROLES[x] = var.DEFAULT_ROLE + if vils: for pr in possible_rolesets: - pr[var.DEFAULT_ROLE] += len(villagers) + pr[var.DEFAULT_ROLE] += len(vils) # Collapse possible_rolesets into var.ROLE_STATS # which is a FrozenSet[FrozenSet[Tuple[str, int]]] @@ -5538,14 +5499,14 @@ def start(cli, nick, chan, forced = False, restart = ""): num_sharpshooters = 0 for gunner in gunner_list: if gunner in var.ROLES["village drunk"]: - var.GUNNERS[gunner.nick] = (var.DRUNK_SHOTS_MULTIPLIER * math.ceil(var.SHOTS_MULTIPLIER * len(pl))) + var.GUNNERS[gunner] = (var.DRUNK_SHOTS_MULTIPLIER * math.ceil(var.SHOTS_MULTIPLIER * len(pl))) elif num_sharpshooters < addroles["sharpshooter"] and gunner not in cannot_be_sharpshooter and random.random() <= var.SHARPSHOOTER_CHANCE: - var.GUNNERS[gunner.nick] = math.ceil(var.SHARPSHOOTER_MULTIPLIER * len(pl)) + var.GUNNERS[gunner] = math.ceil(var.SHARPSHOOTER_MULTIPLIER * len(pl)) var.ROLES["gunner"].remove(gunner) var.ROLES["sharpshooter"].append(gunner) num_sharpshooters += 1 else: - var.GUNNERS[gunner.nick] = math.ceil(var.SHOTS_MULTIPLIER * len(pl)) + var.GUNNERS[gunner] = math.ceil(var.SHOTS_MULTIPLIER * len(pl)) var.ROLES["sharpshooter"] = UserSet(p for p in var.ROLES["sharpshooter"] if p is not None) @@ -5650,11 +5611,11 @@ def start(cli, nick, chan, forced = False, restart = ""): var.FIRST_NIGHT = True if not var.START_WITH_DAY: var.GAMEPHASE = "night" - transition_night(cli) + transition_night() else: var.FIRST_DAY = True var.GAMEPHASE = "day" - transition_day(cli) + transition_day() decrement_stasis() @@ -6307,11 +6268,11 @@ def myrole(var, wrapper, message): # FIXME: Need to fix !swap once this gets con 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.nick in var.GUNNERS and var.GUNNERS[wrapper.source.nick]: + if wrapper.source not in var.ROLES["amnesiac"] and wrapper.source in var.GUNNERS and var.GUNNERS[wrapper.source]: role = "gunner" if wrapper.source in var.ROLES["sharpshooter"]: role = "sharpshooter" - wrapper.pm(messages["gunner_simple"].format(role, var.GUNNERS[wrapper.source.nick], "" if var.GUNNERS[wrapper.source.nick] == 1 else "s")) + wrapper.pm(messages["gunner_simple"].format(role, var.GUNNERS[wrapper.source], "" if var.GUNNERS[wrapper.source] == 1 else "s")) # Check assassin if wrapper.source in var.ROLES["assassin"] and wrapper.source not in var.ROLES["amnesiac"]: @@ -6789,37 +6750,39 @@ def revealroles(var, wrapper, message): for role in role_order(): if var.ROLES.get(role): # make a copy since this list is modified - nicks = [p.nick for p in var.ROLES[role]] # FIXME: convert to users + users = list(var.ROLES[role]) + out = [] # go through each nickname, adding extra info if necessary - for i in range(len(nicks)): + for user in users: special_case = [] - nickname = nicks[i] - if role == "assassin" and nickname in var.TARGETED: - special_case.append("targeting {0}".format(var.TARGETED[nickname])) - elif role == "clone" and nickname in var.CLONED: - special_case.append("cloning {0}".format(var.CLONED[nickname])) - elif role == "amnesiac" and nickname in var.AMNESIAC_ROLES: - special_case.append("will become {0}".format(var.AMNESIAC_ROLES[nickname])) + if role == "assassin" and user.nick in var.TARGETED: + special_case.append("targeting {0}".format(var.TARGETED[user.nick])) + elif role == "clone" and user.nick in var.CLONED: + special_case.append("cloning {0}".format(var.CLONED[user.nick])) + elif role == "amnesiac" and user.nick in var.AMNESIAC_ROLES: + special_case.append("will become {0}".format(var.AMNESIAC_ROLES[user.nick])) # print how many bullets normal gunners have - elif (role == "gunner" or role == "sharpshooter") and nickname in var.GUNNERS: - special_case.append("{0} bullet{1}".format(var.GUNNERS[nickname], "" if var.GUNNERS[nickname] == 1 else "s")) - elif role == "turncoat" and nickname in var.TURNCOATS: - special_case.append("currently with \u0002{0}\u0002".format(var.TURNCOATS[nickname][0]) - if var.TURNCOATS[nickname][0] != "none" else "not currently on any side") + elif (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, wrapper, nickname, role) + evt.dispatch(var, wrapper, user, role) special_case = evt.data["special_case"] - user = users._get(nickname) # FIXME if not evt.prevent_default and user not in var.ORIGINAL_ROLES[role] and role not in var.TEMPLATE_RESTRICTIONS: for old_role in role_order(): # order doesn't matter here, but oh well if user in var.ORIGINAL_ROLES[old_role] and user not in var.ROLES[old_role]: special_case.append("was {0}".format(old_role)) break if special_case: - nicks[i] = "".join((nicks[i], " (", ", ".join(special_case), ")")) - output.append("\u0002{0}\u0002: {1}".format(role, ", ".join(nicks))) + out.append("".join((user.nick, " (", ", ".join(special_case), ")"))) + else: + out.append(user.nick) + + output.append("\u0002{0}\u0002: {1}".format(role, ", ".join(out))) # print out lovers too done = {} @@ -6851,19 +6814,16 @@ def revealroles(var, wrapper, message): else: wrapper.pm(*output, sep=" | ") - -@cmd("fgame", flag="g", raw_nick=True, phases=("join",)) -def fgame(cli, nick, chan, rest): +@command("fgame", flag="g", phases=("join",)) +def fgame(var, wrapper, message): """Force a certain game mode to be picked. Disable voting for game modes upon use.""" - nick = parse_nick(nick)[0] + pl = get_players() - pl = list_players() - - if nick not in pl and not is_admin(nick): + if wrapper.source not in pl and not wrapper.source.is_admin(): return - if rest: - gamemode = rest.strip().lower() + if message: + gamemode = message.strip().lower() parts = gamemode.replace("=", " ", 1).split(None, 1) if len(parts) > 1: gamemode, modeargs = parts @@ -6875,15 +6835,50 @@ def fgame(cli, nick, chan, rest): gamemode = gamemode.split()[0] gamemode = complete_one_match(gamemode, var.GAME_MODES.keys() - var.DISABLED_GAMEMODES) if not gamemode: - cli.notice(nick, messages["invalid_mode"].format(rest.split()[0])) + wrapper.pm(messages["invalid_mode"].format(message.split()[0])) return parts[0] = gamemode - if cgamemode(cli, "=".join(parts)): - cli.msg(chan, messages["fgame_success"].format(nick)) + if cgamemode("=".join(parts)): + channels.Main.send(messages["fgame_success"].format(wrapper.source)) var.FGAMED = True else: - cli.notice(nick, fgame.__doc__()) + wrapper.pm(fgame.__doc__()) + +@command("frole", flag="d", phases=("join",)) +def frole(var, wrapper, message): + """Force a player into a certain role.""" + pl = get_players() + if wrapper.source not in pl and not wrapper.source.is_admin(): + return + + to_force = {} + + parts = message.strip().lower().replace(":", " ").replace(",", " ").replace("=", " ").split() + if len(parts) % 2: # odd number of arguments; invalid + wrapper.send(messages["frole_incorrect"].format(botconfig.CMD_CHAR, message)) + return + + for name, role in zip(parts[::2], parts[1::2]): + user, _ = users.complete_match(name, pl) + role = role.replace("_", " ") + if role in var.ROLE_ALIASES: + role = var.ROLE_ALIASES[role] + if user is None or role not in var.ROLE_GUIDE or role == var.DEFAULT_ROLE: + wrapper.send(messages["frole_incorrect"].format(botconfig.CMD_CHAR, message)) + return + to_force[user] = role + + for user, role in to_force.items(): + for key, ps in tuple(var.FORCE_ROLES.items()): + if user in ps: + ps.remove(user) + if not ps: + del var.FORCE_ROLES[key] + + var.FORCE_ROLES[role].add(user) + + wrapper.send(messages["operation_successful"]) def fgame_help(args=""): args = args.strip() @@ -6898,9 +6893,6 @@ def fgame_help(args=""): fgame.__doc__ = fgame_help - -before_debug_mode_commands = list(COMMANDS.keys()) - if botconfig.DEBUG_MODE: @command("eval", owner_only=True, pm=True) @@ -6974,13 +6966,13 @@ if botconfig.DEBUG_MODE: who = who.replace("_", " ") if who == "*": # wildcard match - tgt = list_players() + tgt = get_players() elif (who not in var.ROLES or not var.ROLES[who]) and (who != "gunner" or var.PHASE in ("none", "join")): cli.msg(chan, nick+": invalid role") return elif who == "gunner": - tgt = {users._get(g) for g in var.GUNNERS.keys()} # FIXME + tgt = set(var.GUNNERS) else: tgt = set(var.ROLES[who])