Project:Protection

Goals
The protection system must:
 * Allow stacking multiple protections on the same person, including the same protection multiple time
 * Apply for the night the protection was given as well as the subsequent day
 * Allow for removal of one protection for each kill
 * Allow for bypassing all protections on kill
 * Have protections only apply against certain roles
 * Allow for protections to apply against non-kill events (deducting accordingly)

Things that are explicitly out of scope for protection system:
 * Having protections conditionally apply on game state other than killer role
 * Having permanent protections that are not removed once they block a kill
 * Removing more than one protection per kill or non-kill event
 * Ordering certain protections to apply before other ones

Note: "out of scope" means that the system itself does not handle anything listed there, but roles and game modes could implement that logic themselves via event listeners.

Design
The module  contains a method-based API to interact with the protection status on a player. Consuming code must call these methods instead of attempting to directly interact with any state variables. For shorthand and convenience, the base  package exports the relevant public API for protections.

All protections are cleared at the beginning of the night transition.

Type aliases
To make the following code easier to understand, the following type aliases are employed:

Where in...
 * ProtectionEntry
 * Category is the scope of the protection
 * str is the role performing the protection

UserProtectionEntry is as ProtectionEntry, except the Optional[User] is the source of the protection.

Unless specified otherwise (i.e. with ), all values must be valid values for their type of thing and never.

add_protection method
Adds a protection onto the target.
 * var is the game state for DI purposes.
 * target is the user being protected.
 * protector is the user performing the protection. If the protection is not caused directly by a user, pass  instead.
 * protector_role is the role performing the protection.
 * scope is the set of roles that this protection should be applied against. By default, it applies to all roles. Normal wolf kills are under the "wolf" role, even if special wolves are in play.

try_protection method
Tries to protect the target, returning information about the protection or  if no protections on the target apply against the given source.
 * var is the game state for DI purposes.
 * target is the user being attacked that we are applying protections for.
 * attacker the user performing the attack. If None, it indicates the attack is not the direct action of a user (e.g. bot kills in villagergame or nightmare deaths in sleepy).
 * attacker_role is the role that is performing the attack. For generic wolf kills, the "wolf" role is used even if source is a special type of wolf.

If there are no protections on the target which can apply to this attack,  is returned. This alerts the caller to continue with the attack as successful. Otherwise, an iterable of strings is returned which are messages that should be displayed to the channel regarding the protection.

Internally, this method fires the  event, then calls the   event if a protection was successful, and then returns the messages to be printed or   if no protections apply.

remove_all_protections method
Removes all protections from the target which would apply against the given source. This will typically notify the target that they are no longer being protected.
 * var is the game state for DI purposes.
 * target is the user to remove protections from.
 * attacker is the user causing the removal, or None if this removal is not caused by a user. In the case of removals caused by role swaps, the remover is the same as the original protector.
 * attacker_role is the role causing the removal. In the case of removals caused by role swaps, the removal role is the same as the original protector role.
 * scope defines which protections to remove. All protections whose scopes contain at least one element of scope are removed.

Internally, this fires the  event for each protection that is in scope. Unless a listener confirms the removal, the protection will be kept.

try_protection event
Fired before any protections are applied.

Event data dict


 * evt is the Event object. Setting  causes no protections to work against the target, making the overall   call return   if   is empty or return   if it has items in the list.
 * var is the game state for DI.
 * target is the user we are trying to protect.
 * attacker is the user attacking target. If None, indicates that the attack is not caused by a particular user.
 * attacker_role is the role attacking target. "wolf" is used for generic wolf kills.
 * The protections key in the data dict is the list of all protections in scope for attacker_role. Listeners can manipulate this list to add, remove, or re-order protections. The list is randomly shuffled before the event is dispatched, such that there is no particular ordering to the list.
 * The messages key in the data dict are messages that should be output to the channel in the event of a successful protection. Listeners can add to this to create custom messages.

If the protections list has values at the end of the event, the first value in the list will be chosen as the protection to use.

player_protected event
Fired after the target has been successfully protected.

Event data dict


 * evt is the Event object. Setting  has no effect.
 * var is the game state for DI.
 * target is the user that was protected.
 * attacker is the user that attacked target, if any.
 * attacker_role is the role that attacked target.
 * protector is the user whose protection saved target, if any.
 * protector_role is the role whose protection saved target.
 * The messages key is filled with the messages from the  event.

The messages list from this event is what is ultimately returned from. This event is not fired if no protections apply to the target.

remove_protection event
Event data dict


 * evt is the Event object. Setting  has no effect.
 * var is the game state for DI.
 * target is the user the protection is being removed from.
 * attacker is the user causing the removal, if any.
 * attacker_role is the role causing the removal.
 * protector is the user whose protection is being removed, if any.
 * protector_role is the role whose protection is being removed.
 * By default,  is , meaning that the protection will remain in-place. A listener should set this to   and append an appropriate message to   if it wishes to remove the protection.
 * After the event is complete and only if the protection is being removed,  is sent all messages in   in private.

Role considerations

 * Bodyguard can listen to player_protected in order to kill itself should it successfully guard someone.
 * Bodyguard can also listen to try_protection to shunt itself to last in the list, so it doesn't apply before GA, prot totem, etc.
 * remove_all_protections is useful for exchange (role swaps) as well as fallen angel. Exchange sets attacker/attacker_role to be the same as protector/protector_role to indicate that the protection is going away because the user is retracting it. Fallen angel can listen for itself as the attacker_role and then kill protector as necessary. Shaman can listen for protector_role being a shaman role and therefore emit the broken totem message at end of night.
 * Blessed villager can re-add itself as a protection every night in one of the transition_night events.
 * Monster can add itself every night as a protection scoped only to "wolf" to block wolf kills while not blocking other kill types.