from copy import copy, deepcopy
from DnD_5e.tactics import Tactic, ThresholdTactic, ConditionTactic, MaxTactic
[docs]
class AttackTactic(Tactic):
"""
Subclass of :py:class:`Tactic` used for selecting :py:class:`Attack` s
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
class DamageTypeTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has the given damage type
"""
def __init__(self, **kwargs):
"""
Validate the input and set the instance variables
:param kwargs: keyword arguments. Same as in superclasses, plus these:
:param damage_type: the damage type that selected Attacks must have
:type damage_type: str
:raise: ValueError if input is invalid
"""
super().__init__(**kwargs)
self._damage_type = kwargs.get("damage_type")
if not isinstance(self._damage_type, str):
raise ValueError("Damage type must be a string")
def __eq__(self, other):
"""
:param other:
:return:
"""
return super().__eq__(other) and self.get_damage_type() == other.get_damage_type()
[docs]
def get_damage_type(self) -> str:
"""
:return: damage_type
"""
return self._damage_type
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.has_damage_type(self.get_damage_type())
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class DamageVulnerabilityTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has a damage type that the target is vulnerable to
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs) -> bool:
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
vulnerabilities = target.get_vulnerabilities()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
for vulnerability in vulnerabilities:
if item.has_damage_type(vulnerability):
return True
return False
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class DamageNoImmuneTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` such that none of that attack's damage is a type that the target is immune to
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs) -> bool:
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param kwargs:
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
immunities = target.get_immunities()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
for immunity in immunities:
if item.has_damage_type(immunity):
return False
return True
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class DamageNoResistanceTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` such that none of that attack's damage is a type that the target is resistant to
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs) -> bool:
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
resistances = target.get_resistances()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
for resistance in resistances:
if item.has_damage_type(resistance):
return False
return True
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class LowRangeTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has nonzero range lower than the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return 0 < item.get_range() < self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
except TypeError: # if range is not a number, e.g., "self"
return False
[docs]
class HighRangeTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has range higher than the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_range() > self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
except TypeError: # if range is not a number, e.g., "self"
return False
[docs]
class LowMeleeRangeTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has nonzero melee range lower than the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return 0 < item.get_melee_range() < self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
except TypeError: # if range is not a number, e.g., "self"
return False
[docs]
class HighMeleeRangeTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has range higher than the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_melee_range() > self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
except TypeError: # if range is not a number, e.g., "self"
return False
[docs]
class HasAdvTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has advantage
"""
def __init__(self, **kwargs):
"""
:param kwargs:
"""
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_adv() > 0
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HasDisAdvTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that has disadvantage. I don't know why you would do this, but now you can.
"""
def __init__(self, **kwargs):
"""
:param kwargs:
"""
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_adv() < 0
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HasNoDisadvTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` that does not have disadvantage
"""
def __init__(self, **kwargs):
"""
:param kwargs:
"""
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_adv() > -1
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighestMaxHitTactic(MaxTactic, AttackTactic):
"""
Select the :py:class:`Attack` with the highest maximum to-hit value (e.g., with an attack modifier of 10)
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def get_value(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_max_hit()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighMaxHitTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with a maximum to-hit value higher than the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_max_hit() > self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class MaxHitACTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with a maximum to-hit value greater than or equal to the target's AC
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_ac = target.get_ac()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_max_hit() >= target_ac
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighestAvgHitTactic(MaxTactic, AttackTactic):
"""
Select the :py:class:`Attack` with the highest average to-hit value
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def get_value(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_average_hit()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighAvgHitTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with an average to-hit value higher than threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_average_hit() > self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class AvgHitACHighTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with an average to-hit value greater than or equal to the target's AC
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_ac = target.get_ac()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_average_hit() >= target_ac
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighestMaxDamageTactic(MaxTactic, AttackTactic):
"""
Select the :py:class:`Attack` with the highest maximum damage
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def get_value(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_max_damage()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighMaxDamageTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with maximum damage above the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_max_damage() > self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class MaxDamageHPHighTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with a maximum damage value greater than or equal to the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_max_damage() >= target_hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class LowMaxDamageTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with maximum damage below the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_max_damage() < self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class MaxDamageHPLowTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with a maximum damage value lower than the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_max_damage() < target_hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighestMinDamageTactic(MaxTactic, AttackTactic):
"""
Select the :py:class:`Attack` with the highest minimum damage
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def get_value(self, item, **kwargs):
"""
:param item:
:return:
"""
return item.get_min_damage()
[docs]
class HighMinDamageTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with minimum damage above the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_min_damage() > self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class MinDamageHPHighTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with a minimum damage value greater than or equal to the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_min_damage() >= target_hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class LowMinDamageTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with minimum damage below the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_min_damage() < self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class MinDamageHPLowTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with a maximum damage value lower than or the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_min_damage() < target_hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class HighestAvgDamageTactic(MaxTactic, AttackTactic):
"""
Select the :py:class:`Attack` with the highest average damage
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def get_value(self, item, **kwargs):
"""
:param item:
:return:
"""
return item.get_average_damage()
[docs]
class HighAvgDamageTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with average damage above the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_average_damage() > self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class AvgDamageHPHighTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with a maximum damage value greater than or equal to the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_average_damage() >= target_hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class LowAvgDamageTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with average damage below the threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item:
:return:
"""
try:
return item.get_average_damage() < self.get_threshold()
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class AvgDamageHPLowTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with an average damage value lower than the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs):
"""
:param item: the Attack to look at
:type item: :py:class:`Attack`
:param target: the Combatant the attack will be made against
:type target: :py:class:`Combatant`
:return: True if the condition is met, False otherwise
"""
target = kwargs.get("target")
try:
target_hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_average_damage() < target_hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class DprMaxTactic(MaxTactic, AttackTactic):
"""
Select the :py:class:`Attack` with the highest dpr for the target (dpr is probability to hit * average damage)
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def get_value(self, item, **kwargs):
"""
Get the dpr for this Attack for the given target
:param item: the Attack we are looking at
:type item: :py:class:`Attack`
:param target: the Combatant we are trying to hit
:type target: :py:class:`Combatant`
:return:
"""
try:
return item.get_dpr(target=kwargs.get("target"))
except AttributeError:
raise ValueError("target must be a Combatant and items in choices must be Attacks")
[docs]
class DprHighTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with dpr (probability to hit the target * average damage) higher than threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def calculate_threshold(self, **kwargs) -> float: # pylint: disable=unused-argument
"""
Calculate threshold using keyword arguments.
:param kwargs: passed on from constructor
:return: threshold
"""
raise ValueError("Threshold must be a number")
[docs]
def validate_threshold(self):
"""
:return:
:raise: ValueError if threshold is invalid
"""
if not isinstance(self._threshold, (int, float)) or self._threshold < 0:
raise ValueError("Must provide non-negative number for threshold")
[docs]
def check_condition(self, item, **kwargs) -> bool:
"""
Check whether the given Attack has a dpr (probability to hit the target's ac * average damage) higher than threshold
:param item: the Attack we are looking at
:type item: :py:class:`Attack`
:return: True if the condition is met, False otherwise
"""
try:
return item.get_dpr(target=kwargs.get("target")) > self.get_threshold()
except AttributeError:
raise ValueError("target must be a Combatant and items in choices must be Attacks")
[docs]
class DprHpHighTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with dpr (probability to hit the target * average damage) higher than the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs) -> bool:
"""
Check whether the given Attack has a dpr (probability to hit the target's ac * average damage) greater than or equal to the target's current hp
:param item: the Attack we are looking at
:type item: :py:class:`Attack`
:return: True if the condition is met, False otherwise
"""
try:
target = kwargs.get("target")
hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_dpr(target=target) >= hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")
[docs]
class DprLowTactic(ThresholdTactic, ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with dpr (probability to hit the target * average damage) lower than threshold
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def calculate_threshold(self, **kwargs) -> float: # pylint: disable=unused-argument
"""
Calculate threshold using keyword arguments.
:param kwargs: passed on from constructor
:return: threshold
"""
raise ValueError("Threshold must be a number")
[docs]
def validate_threshold(self):
"""
:return:
:raise: ValueError if threshold is invalid
"""
if not isinstance(self._threshold, (int, float)) or self._threshold < 0:
raise ValueError("Must provide non-negative number for threshold")
[docs]
def check_condition(self, item, **kwargs) -> bool:
"""
Check whether the given Attack has a dpr (probability to hit the target's ac * average damage) higher than threshold
:param item: the Attack we are looking at
:type item: :py:class:`Attack`
:return: True if the condition is met, False otherwise
"""
try:
return item.get_dpr(target=kwargs.get("target")) < self.get_threshold()
except AttributeError:
raise ValueError("target must be a Combatant and items in choices must be Attacks")
[docs]
class DprHpLowTactic(ConditionTactic, AttackTactic):
"""
Select the :py:class:`Attack` with dpr (probability to hit the target * average damage) lower than the target's current hp
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
[docs]
def check_condition(self, item, **kwargs) -> bool:
"""
Check whether the given Attack has a dpr (probability to hit the target's ac * average damage) lower than the target's current hp
:param item: the Attack we are looking at
:type item: :py:class:`Attack`
:return: True if the condition is met, False otherwise
"""
try:
target = kwargs.get("target")
hp = target.get_current_hp()
except AttributeError:
raise ValueError("target must be a Combatant")
try:
return item.get_dpr(target=target) < hp
except AttributeError:
raise ValueError("Items in choices must be Attacks")