def kill_players(var, *, end_game: bool = True) -> bool: """ Kill all players marked as dying. This function is not re-entrant; do not call it inside of a del_player or kill_players event listener. This function does not print anything to the channel; code which calls add_dying should print things as appropriate. :param var: The game state :param end_game: Whether or not to check for win conditions and perform state transitions (temporary) :returns: True if the game is ending (temporary) """ t = time.time() with var.GRAVEYARD_LOCK: # FIXME if not var.GAME_ID or var.GAME_ID > t: # either game ended, or a new game has started return dead = set() while DYING: player, (killer_role, reason, death_triggers) = DYING.popitem() main_role = get_main_role(player) reveal_role = get_reveal_role(player) all_roles = get_all_roles(player) # kill them off del var.MAIN_ROLES[player] for role in all_roles: var.ROLES[role].remove(player) dead.add(player) # Don't track players that quit before the game started if var.PHASE != "join": var.DEAD.add(player) # notify listeners that the player died for possibility of chained deaths evt = Event("del_player", {}, killer_role=killer_role, main_role=main_role, reveal_role=reveal_role, reason=reason) evt_death_triggers = death_triggers and var.PHASE in var.GAME_PHASES evt.dispatch(var, player, all_roles, evt_death_triggers) # give roles/modes an opportunity to adjust !stats now that all deaths have resolved evt = Event("reconfigure_stats", {"new": []}) newstats = set() for rs in var.ROLE_STATS: d = Counter(dict(rs)) evt.data["new"] = [d] evt.dispatch(var, d, "del_player") for v in evt.data["new"]: if min(v.values()) >= 0: newstats.add(frozenset(v.items())) var.ROLE_STATS = frozenset(newstats) # notify listeners that all deaths have resolved # FIXME: end_game is a temporary hack until we move state transitions into the event loop # (priority 10 listener sets prevent_default if end_game=True and game is ending; that's another temporary hack) # Once hacks are removed, this function will not have any return value and the end_game kwarg will go away evt = Event("kill_players", {}, end_game=end_game) return not evt.dispatch(var, dead)
def on_new_role(evt, var, user, old_role): if evt.data["role"] == "wolf" and old_role == "wild child" and evt.params.inherit_from and "wild child" in get_all_roles(evt.params.inherit_from): evt.data["role"] = "wild child" if evt.params.inherit_from in IDOLS and "wild child" not in get_all_roles(user): IDOLS[user] = IDOLS.pop(evt.params.inherit_from) evt.data["messages"].append(messages["wild_child_idol"].format(IDOLS[user]))
def on_transition_day_begin(evt, var): pl = get_players() for insomniac in get_all_players(("insomniac", )): p1, p2 = _get_targets(var, pl, insomniac) p1_roles = get_all_roles(p1) p2_roles = get_all_roles(p2) if p1_roles & Nocturnal and p2_roles & Nocturnal: # both of the players next to the insomniac were awake last night insomniac.send(messages["insomniac_both_awake"].format(p1, p2)) elif p1_roles & Nocturnal: insomniac.send(messages["insomniac_awake"].format(p1)) elif p2_roles & Nocturnal: insomniac.send(messages["insomniac_awake"].format(p2)) else: # both players next to the insomniac were asleep all night insomniac.send(messages["insomniac_asleep"].format(p1, p2))
def on_transition_day_begin(evt, var): for crow, target in OBSERVED.items(): # if any of target's roles (primary or secondary) are Nocturnal, we see them as awake roles = get_all_roles(target) if roles & Nocturnal: crow.send(messages["werecrow_success"].format(target)) else: crow.send(messages["werecrow_failure"].format(target))
def wolf_retract(var, wrapper, message): """Removes a wolf's kill selection.""" if not get_all_roles(wrapper.source) & Wolf & Killer: return if wrapper.source in KILLS: del KILLS[wrapper.source] wrapper.pm(messages["retracted_kill"]) send_wolfchat_message(var, wrapper.source, messages["wolfchat_retracted_kill"].format(wrapper.source), Wolf, role="wolf", command="retract")
def wolf_can_kill(var, wolf): # a wolf can kill if wolves in general can kill, and the wolf belongs to a role in CAN_KILL # this is a utility function meant to be used by other wolf role modules nevt = Event("wolf_numkills", {"numkills": 1}) nevt.dispatch(var) num_kills = nevt.data["numkills"] if num_kills == 0: return False wolfroles = get_all_roles(wolf) return bool(CAN_KILL & wolfroles)
def wolf_can_kill(var, wolf): # a wolf can kill if wolves in general can kill, and the wolf is a Killer # this is a utility function meant to be used by other wolf role modules nevt = Event("wolf_numkills", {"numkills": 1, "message": ""}) nevt.dispatch(var) num_kills = nevt.data["numkills"] if num_kills == 0: return False wolfroles = get_all_roles(wolf) return bool(Wolf & Killer & wolfroles)
def wolf_kill(var, wrapper, message): """Kill one or more players as a wolf.""" # verify this user can actually kill if not get_all_roles(wrapper.source) & Wolf & Killer: return pieces = re.split(" +", message) targets = [] orig = [] nevt = Event("wolf_numkills", {"numkills": 1, "message": ""}) nevt.dispatch(var) num_kills = nevt.data["numkills"] if not num_kills: if nevt.data["message"]: wrapper.pm(messages[nevt.data["message"]]) return if len(pieces) < num_kills: wrapper.pm(messages["wolf_must_target_multiple"]) return for targ in pieces[:num_kills]: target = get_target(var, wrapper, targ, not_self_message="no_suicide") if target is None: return if is_known_wolf_ally(var, wrapper.source, target): wrapper.pm(messages["wolf_no_target_wolf"]) return if target in orig: wrapper.pm(messages["wolf_must_target_multiple"]) return orig.append(target) target = try_misdirection(var, wrapper.source, target) if try_exchange(var, wrapper.source, target): return targets.append(target) KILLS[wrapper.source] = UserList(targets) if len(orig) > 1: wrapper.pm(messages["player_kill_multiple"].format(orig)) msg = messages["wolfchat_kill_multiple"].format(wrapper.source, orig) debuglog("{0} KILL: {1} ({3}) and {2} ({4})".format(wrapper.source, targets[0], targets[1], get_main_role(targets[0]), get_main_role(targets[1]))) else: wrapper.pm(messages["player_kill"].format(orig[0])) msg = messages["wolfchat_kill"].format(wrapper.source, orig[0]) debuglog("{0} KILL: {1} ({2})".format(wrapper.source, targets[0], get_main_role(targets[0]))) send_wolfchat_message(var, wrapper.source, msg, Wolf, role="wolf", command="kill")
def on_new_role(evt, var, user, old_role): if evt.data[ "role"] == "wolf" and old_role == "wild child" and evt.params.inherit_from and "wild child" in get_all_roles( evt.params.inherit_from): evt.data["role"] = "wild child" if evt.params.inherit_from in IDOLS and "wild child" not in get_all_roles( user): IDOLS[user] = IDOLS.pop(evt.params.inherit_from) evt.data["messages"].append(messages["wild_child_idol"].format( IDOLS[user]))
def kill_players(var, *, end_game: bool = True) -> bool: """ Kill all players marked as dying. This function is not re-entrant; do not call it inside of a del_player or kill_players event listener. This function does not print anything to the channel; code which calls add_dying should print things as appropriate. :param var: The game state :param end_game: Whether or not to check for win conditions and perform state transitions (temporary) :returns: True if the game is ending (temporary) """ t = time.time() with var.GRAVEYARD_LOCK: # FIXME if not var.GAME_ID or var.GAME_ID > t: # either game ended, or a new game has started return dead = set() while DYING: player, (killer_role, reason, death_triggers) = DYING.popitem() main_role = get_main_role(player) reveal_role = get_reveal_role(player) all_roles = get_all_roles(player) # kill them off del var.MAIN_ROLES[player] for role in all_roles: var.ROLES[role].remove(player) dead.add(player) var.DEAD.add(player) # notify listeners that the player died for possibility of chained deaths evt = Event("del_player", {}, killer_role=killer_role, main_role=main_role, reveal_role=reveal_role, reason=reason) evt_death_triggers = death_triggers and var.PHASE in var.GAME_PHASES evt.dispatch(var, player, all_roles, evt_death_triggers) # notify listeners that all deaths have resolved # FIXME: end_game is a temporary hack until we move state transitions into the event loop # (priority 10 listener sets prevent_default if end_game=True and game is ending; that's another temporary hack) # Once hacks are removed, this function will not have any return value and the end_game kwarg will go away evt = Event("kill_players", {}, end_game=end_game) return not evt.dispatch(var, dead)
def change_totem(var, player, totem, roles=None): """Change the player's totem to the specified totem. If roles is specified, only operates if the player has one of those roles. Otherwise, changes the totem for all shaman roles the player has. If the player previously gave out totems, they are retracted. """ player_roles = get_all_roles(player) shaman_roles = set(player_roles & _rolestate.keys()) if roles is not None: shaman_roles.intersection_update(roles) for role in shaman_roles: del _rolestate[role]["SHAMANS"][:player:] del _rolestate[role]["LASTGIVEN"][:player:] if isinstance(totem, str): if "," in totem: totemdict = {} tlist = totem.split(",") for t in tlist: if ":" not in t: # FIXME: localize raise ValueError("Expected format totem:count,totem:count,...") tval, count = t.split(":") tval = tval.strip() count = int(count.strip()) match = match_totem(var, tval, scope=var.CURRENT_GAMEMODE.TOTEM_CHANCES) if not match: # FIXME: localize raise ValueError("{0} is not a valid totem type.".format(tval)) tval = match.get().key if count < 1: # FIXME: localize raise ValueError("Totem count for {0} cannot be less than 1.".format(tval)) totemdict[tval] = count else: match = match_totem(var, totem, scope=var.CURRENT_GAMEMODE.TOTEM_CHANCES) if not match: # FIXME: localize raise ValueError("{0} is not a valid totem type.".format(totem)) totemdict = {match.get().key: 1} else: totemdict = totem _rolestate[role]["TOTEMS"][player] = totemdict
def change_totem(var, player, totem, roles=None): """Change the player's totem to the specified totem. If roles is specified, only operates if the player has one of those roles. Otherwise, changes the totem for all shaman roles the player has. If the player previously gave out totems, they are retracted. """ if totem not in var.CURRENT_GAMEMODE.TOTEM_CHANCES: raise ValueError("{0} is not a valid totem type.".format(totem)) player_roles = get_all_roles(player) shaman_roles = set(player_roles & _rolestate.keys()) if roles is not None: shaman_roles.intersection_update(roles) for role in shaman_roles: del _rolestate[role]["SHAMANS"][:player:] del _rolestate[role]["LASTGIVEN"][:player:] _rolestate[role]["TOTEMS"][player] = totem
def on_transition_night_end(evt, var): wolves = get_all_players(Wolfchat) # roles allowed to talk in wolfchat talkroles = get_talking_roles(var) # condition imposed on talking in wolfchat (only during day/night, or no talking) # 0 = no talking # 1 = normal # 2 = only during day # 3 = only during night wccond = 1 if var.RESTRICT_WOLFCHAT & var.RW_DISABLE_NIGHT: if var.RESTRICT_WOLFCHAT & var.RW_DISABLE_DAY: wccond = 0 else: wccond = 2 elif var.RESTRICT_WOLFCHAT & var.RW_DISABLE_DAY: wccond = 3 for wolf in wolves: can_talk = len(get_all_roles(wolf) & talkroles) > 0 if len(wolves) > 1 and can_talk and wccond > 0: wolf.send(messages["wolfchat_notify_{0}".format(wccond)])
def on_privmsg(cli, rawnick, chan, msg, *, notice=False, force_role=None): if notice and "!" not in rawnick or not rawnick: # server notice; we don't care about those return user = users._get(rawnick, allow_none=True) # FIXME if users.equals(chan, users.Bot.nick): # PM target = users.Bot else: target = channels.get(chan, allow_none=True) if user is None or target is None: return wrapper = MessageDispatcher(user, target) if wrapper.public and botconfig.IGNORE_HIDDEN_COMMANDS and not chan.startswith(tuple(hooks.Features["CHANTYPES"])): return if (notice and ((wrapper.public and not botconfig.ALLOW_NOTICE_COMMANDS) or (wrapper.private and not botconfig.ALLOW_PRIVATE_NOTICE_COMMANDS))): return # not allowed in settings 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) parts = msg.split(sep=" ", maxsplit=1) key = parts[0].lower() if len(parts) > 1: message = parts[1].lstrip() else: message = "" if wrapper.public and not key.startswith(botconfig.CMD_CHAR): return # channel message but no prefix; ignore if key.startswith(botconfig.CMD_CHAR): key = key[len(botconfig.CMD_CHAR):] if not key: # empty key ("") already handled above return # Don't change this into decorators.COMMANDS[key] even though it's a defaultdict, # as we don't want to insert bogus command keys into the dict. cmds = [] phase = var.PHASE if user in get_participants(): roles = get_all_roles(user) # A user can be a participant but not have a role, for example, dead vengeful ghost has_roles = len(roles) != 0 if force_role is not None: roles &= {force_role} # only fire off role commands for the forced role common_roles = set(roles) # roles shared by every eligible role command have_role_cmd = False for fn in decorators.COMMANDS.get(key, []): if not fn.roles: cmds.append(fn) continue if roles.intersection(fn.roles): have_role_cmd = True cmds.append(fn) common_roles.intersection_update(fn.roles) if force_role is not None and not have_role_cmd: # Trying to force a non-role command with a role. # We allow non-role commands to execute if a role is forced if a role # command is also executed, as this would allow (for example) a bot admin # to add extra effects to all "kill" commands without needing to continually # update the list of roles which can use "kill". However, we don't want to # allow things like "wolf pstats" because that just doesn't make sense. return if has_roles and not common_roles: # getting here means that at least one of the role_cmds is disjoint # from the others. For example, augur see vs seer see when a bare see # is executed. In this event, display a helpful error message instructing # the user to resolve the ambiguity. common_roles = set(roles) info = [0,0] for fn in cmds: fn_roles = roles.intersection(fn.roles) if not fn_roles: continue for role1 in common_roles: info[0] = role1 break for role2 in fn_roles: info[1] = role2 break common_roles &= fn_roles if not common_roles: break wrapper.pm(messages["ambiguous_command"].format(key, info[0], info[1])) return elif force_role is None: cmds = decorators.COMMANDS.get(key, []) 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)
def on_gun_shoot(evt, var, user, target, role): if "werekitten" in get_all_roles(target): evt.data["hit"] = False evt.data["kill"] = False
def on_gun_shoot(evt, var, user, target, role): if "tough wolf" in get_all_roles(target): evt.data["kill"] = False
def parse_and_dispatch(var, wrapper: MessageDispatcher, key: str, message: str, role: Optional[str] = None, force: Optional[User] = None) -> None: """ Parses a command key and dispatches it should it match a valid command. :param var: Game state :param wrapper: Information about who is executing command and where command is being executed :param key: Command name. May be prefixed with command character and role name. :param message: Parameters to the command. :param role: Only dispatch a role command for the specified role. Even if a role name is specified in key, this will take precedence if set. :param force: Force the command to execute as the specified user instead of the current user. Admin and owner commands cannot be forced this way. When forcing a command, we set the appropriate context (channel vs PM) automatically as well. :return: """ _ignore_locals_ = True if key.startswith(botconfig.CMD_CHAR): key = key[len(botconfig.CMD_CHAR):] # check for role prefix parts = key.split(sep=":", maxsplit=1) if len(parts) > 1 and len(parts[0]): key = parts[1] role_prefix = parts[0] else: key = parts[0] role_prefix = None if role: role_prefix = role if not key: return if force: context = MessageDispatcher(force, wrapper.target) else: context = wrapper if role_prefix is not None: # match a role prefix to a role. Multi-word roles are supported by stripping the spaces matches = complete_role(var, role_prefix, remove_spaces=True) if len(matches) == 1: role_prefix = matches[0] elif len(matches) > 1: wrapper.pm(messages["ambiguous_role"].format(matches)) return else: wrapper.pm(messages["no_such_role"].format(role_prefix)) return # Don't change this into decorators.COMMANDS[key] even though it's a defaultdict, # as we don't want to insert bogus command keys into the dict. cmds = [] phase = var.PHASE if context.source in get_participants(): roles = get_all_roles(context.source) common_roles = set( roles) # roles shared by every eligible role command # A user can be a participant but not have a role, for example, dead vengeful ghost has_roles = len(roles) != 0 if role_prefix is not None: roles &= { role_prefix } # only fire off role commands for the user-specified role else: roles = set() common_roles = set() has_roles = False for fn in decorators.COMMANDS.get(key, []): if not fn.roles: cmds.append(fn) elif roles.intersection(fn.roles): cmds.append(fn) common_roles.intersection_update(fn.roles) if has_roles and not common_roles: # getting here means that at least one of the role_cmds is disjoint # from the others. For example, augur see vs seer see when a bare see # is executed. In this event, display a helpful error message instructing # the user to resolve the ambiguity. common_roles = set(roles) info = [0, 0] role_map = messages.get_role_mapping() for fn in cmds: fn_roles = roles.intersection(fn.roles) if not fn_roles: continue for role1 in common_roles: info[0] = role_map[role1].replace(" ", "") break for role2 in fn_roles: info[1] = role_map[role2].replace(" ", "") break common_roles &= fn_roles if not common_roles: break wrapper.pm(messages["ambiguous_command"].format(key, info[0], info[1])) return for fn in cmds: if force: if fn.owner_only or fn.flag: wrapper.pm(messages["no_force_admin"]) return if fn.chan: context.target = channels.Main else: context.target = users.Bot if phase == var.PHASE: # don't call any more commands if one we just called executed a phase transition fn.caller(var, context, message)
def on_gun_shoot(evt, var, user, target, role): if evt.data["hit"] and "vengeful ghost" in get_all_roles(target): # VGs automatically die if hit by a gun to make gunner a bit more dangerous in some modes evt.data["kill"] = True
def on_privmsg(cli, rawnick, chan, msg, *, notice=False, force_role=None): if notice and "!" not in rawnick or not rawnick: # server notice; we don't care about those return 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(ch, allow_none=True) if user is None or target is None: return wrapper = MessageDispatcher(user, target) if wrapper.public and botconfig.IGNORE_HIDDEN_COMMANDS and not chan.startswith(tuple(hooks.Features["CHANTYPES"])): return if (notice and ((wrapper.public and not botconfig.ALLOW_NOTICE_COMMANDS) or (wrapper.private and not botconfig.ALLOW_PRIVATE_NOTICE_COMMANDS))): return # not allowed in settings 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, ch, msg) parts = msg.split(sep=" ", maxsplit=1) key = parts[0].lower() if len(parts) > 1: message = parts[1].lstrip() else: message = "" if wrapper.public and not key.startswith(botconfig.CMD_CHAR): return # channel message but no prefix; ignore if key.startswith(botconfig.CMD_CHAR): key = key[len(botconfig.CMD_CHAR):] if not key: # empty key ("") already handled above return # Don't change this into decorators.COMMANDS[key] even though it's a defaultdict, # as we don't want to insert bogus command keys into the dict. cmds = [] phase = var.PHASE if user in get_participants(): roles = get_all_roles(user) # A user can be a participant but not have a role, for example, dead vengeful ghost has_roles = len(roles) != 0 if force_role is not None: roles &= {force_role} # only fire off role commands for the forced role common_roles = set(roles) # roles shared by every eligible role command have_role_cmd = False for fn in decorators.COMMANDS.get(key, []): if not fn.roles: cmds.append(fn) continue if roles.intersection(fn.roles): have_role_cmd = True cmds.append(fn) common_roles.intersection_update(fn.roles) if force_role is not None and not have_role_cmd: # Trying to force a non-role command with a role. # We allow non-role commands to execute if a role is forced if a role # command is also executed, as this would allow (for example) a bot admin # to add extra effects to all "kill" commands without needing to continually # update the list of roles which can use "kill". However, we don't want to # allow things like "wolf pstats" because that just doesn't make sense. return if has_roles and not common_roles: # getting here means that at least one of the role_cmds is disjoint # from the others. For example, augur see vs seer see when a bare see # is executed. In this event, display a helpful error message instructing # the user to resolve the ambiguity. common_roles = set(roles) info = [0,0] for fn in cmds: fn_roles = roles.intersection(fn.roles) if not fn_roles: continue for role1 in common_roles: info[0] = role1 break for role2 in fn_roles: info[1] = role2 break common_roles &= fn_roles if not common_roles: break wrapper.pm(messages["ambiguous_command"].format(key, info[0], info[1])) return elif force_role is None: cmds = decorators.COMMANDS.get(key, []) for fn in cmds: if phase == var.PHASE: # FIXME: pass in var, wrapper, message instead of cli, rawnick, chan, message fn.caller(cli, rawnick, ch, message)
def is_known_wolf_ally(var, actor, target): actor_roles = get_all_roles(actor) target_roles = get_all_roles(target) wolves = get_wolfchat_roles(var) return actor_roles & wolves and target_roles & wolves