Example #1
0
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)
Example #2
0
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]))
Example #3
0
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))
Example #4
0
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))
Example #5
0
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")
Example #6
0
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)
Example #7
0
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)
Example #8
0
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)
Example #9
0
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")
Example #10
0
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]))
Example #11
0
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)
Example #12
0
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
Example #13
0
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
Example #14
0
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)])
Example #15
0
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)
Example #16
0
def on_gun_shoot(evt, var, user, target, role):
    if "werekitten" in get_all_roles(target):
        evt.data["hit"] = False
        evt.data["kill"] = False
Example #17
0
def on_gun_shoot(evt, var, user, target, role):
    if "tough wolf" in get_all_roles(target):
        evt.data["kill"] = False
Example #18
0
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)
Example #19
0
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
Example #20
0
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)
Example #21
0
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