import warnings
from copy import copy, deepcopy
from DnD_5e import combatant, tactics
[docs]
class Team:
"""
A container class for :py:class:`Combatant` s, to be used by :py:class:`Encounter`
"""
def __init__(self, **kwargs):
"""
Validate the input and set the instance variables
:param kwargs: keyword arguments. Valid arguments are as follows:
:param name: what *self* is called
:type name: str
:param combatant_list: list or tuple of team members
:type combatant_list: list or tuple of :py:class:`Combatant` s
:param attack_tactic: Tactic to decide who/what to attack
:type attack_tactic: :py:class:`Tactic`
:raise: ValueError if input is invalid
"""
self._name = kwargs.get("name", "team")
if not isinstance(self._name, str):
raise ValueError("Name must be a string")
# a tactic for selecting who/what to attack. If none provided, use default (random) tactic
self._enemy_tactic = kwargs.get("enemy_tactic", tactics.Tactic())
if not isinstance(self._enemy_tactic, tactics.Tactic):
raise ValueError("Enemy tactic must be a Tactic")
combatant_list = kwargs.get("combatant_list")
if not combatant_list or not isinstance(combatant_list, (list, tuple)):
raise ValueError("Combatant list must be a non-empty list of Combatants")
self._combatants = [] # copy the list so the user doesn't accidentally mess with it later
for item in combatant_list:
self.add_combatant(item)
def __eq__(self, other) -> bool:
"""
Compare *self* and *other* to determine if they are equal based on :py:attr:`combatant_list`
.. Note::
Does not consider :py:attr:`attack_tactic` when determining equality
:param other: the Team to be compared
:type other: :py:class:`Team`
:return: True if *self* equals *other*, False otherwise
:rtype: bool
"""
if other is self:
return True
if type(other) != type(self): # pylint: disable=unidiomatic-typecheck
return False
if len(self.get_combatants()) != len(other.get_combatants()):
return False
for i, the_combatant in enumerate(self.get_combatants()):
if not the_combatant == other.get_combatants()[i]:
return False
return True
[docs]
def get_name(self) -> str:
"""
:return: self._name
:rtype: str
"""
return self._name
[docs]
def set_name(self, name: str):
if not isinstance(name, str):
raise ValueError("name must be a string")
self._name = name
[docs]
def get_combatants(self) -> list:
"""
:return: self._combatants
:rtype: list of :py:class:`Combatant` s
"""
return self._combatants
[docs]
def get_enemy_tactic(self):
"""
:return: self._enemy_tactic
:rtype: :py:class:`Tactic`
"""
return self._enemy_tactic
[docs]
def has_member(self, member: combatant.Combatant) -> bool:
"""
:param member: the member who may or may not be on the team
:type member: Combatant
:return: True if *member* is in the list of combatants, False otherwise
"""
return member in self.get_combatants()
[docs]
def get_stats(self):
"""
Calculate and return num_conscious, num_unconscious, num_dead
:return:
"""
result = {"num_conscious": 0, "num_unconscious": 0, "num_dead": 0}
for comb in self.get_combatants():
if comb.has_condition("unconscious"):
result["num_unconscious"] += 1
elif comb.has_condition("dead"):
result["num_dead"] += 1
else:
result["num_conscious"] += 1
return result
[docs]
def has_all_with_condition(self, condition: str):
"""
:return: True if all combatants have the given condition, False otherwise
"""
for item in self.get_combatants():
if not item.has_condition(condition):
return False
return True
[docs]
def has_any_with_condition(self, condition: str):
"""
:return: True if at least one combatant has the given condition, False otherwise
"""
for item in self.get_combatants():
if item.has_condition(condition):
return True
return False
[docs]
def has_some_not_all_with_condition(self, condition: str):
"""
:return: True if some combatants have the condition and other combatants don't, False otherwise
"""
found_true = False
found_false = False
for item in self.get_combatants():
if item.has_condition(condition):
found_true = True
else:
found_false = True
return found_true and found_false
[docs]
def has_all_unconscious(self) -> bool:
"""
:return: True if all combatants are unconscious, False otherwise
"""
return self.has_all_with_condition("unconscious")
[docs]
def has_all_dead(self) -> bool:
"""
:return: True if all combatants are dead, False otherwise
"""
return self.has_all_with_condition("dead")
[docs]
def has_all_alive(self) -> bool:
"""
:return: True if all combatants are alive, False otherwise
"""
return not self.has_any_with_condition("dead")
[docs]
def has_all_conscious(self) -> bool:
"""
:return: True if all combatants are conscious, False otherwise
"""
for item in self.get_combatants():
if not item.is_conscious():
return False
return True
[docs]
def has_any_unconscious(self) -> bool:
"""
:return: True if at least 1 combatant is unconscious, False otherwise
"""
return self.has_any_with_condition("unconscious")
[docs]
def has_any_dead(self) -> bool:
"""
:return: True if at least 1 combatant is dead, False otherwise
"""
return self.has_any_with_condition("dead")
[docs]
def has_some_alive(self) -> bool:
"""
:return: True if at least 1 combatant is alive, False otherwise
"""
return not self.has_all_with_condition("dead")
[docs]
def has_some_not_all_unconscious(self):
"""
:return: True if some but not all combatants are unconscious
"""
return self.has_some_not_all_with_condition("unconscious")
[docs]
def has_some_not_all_dead(self):
"""
:return: True if some but not all combatants are dead
"""
return self.has_some_not_all_with_condition("dead")
[docs]
def has_any_conscious(self) -> bool:
"""
:return: True if at least 1 combatant is conscious, False otherwise
"""
for item in self.get_combatants():
if item.is_conscious():
return True
return False
[docs]
def add_combatant(self, new_combatant: combatant.Combatant):
"""
Add the specified :py:class:`Combatant` to the list of combatants.
Does not check to see if *new_combatant* is already on the team
:param new_combatant: the Combatant to add
:type new_combatant: :py:class:`Combatant`
:return: None
:raise: ValueError if input is invalid
"""
if not isinstance(new_combatant, combatant.Combatant):
raise ValueError("New combatant to add must be a Combatant")
self._combatants.append(new_combatant)
new_combatant.set_team(self)
[docs]
def remove_combatant(self, old_combatant: combatant.Combatant):
"""
Remove the specified :py:class:`Combatant` from the list of combatants.
Checks first to see if *old_combatant* is on the team
:param old_combatant: the Combatant to remove
:type old_combatant: :py:class:`Combatant`
:return: None
"""
if self.has_member(old_combatant):
self.get_combatants().remove(old_combatant)
old_combatant.set_team(None)
else:
warnings.warn(f"Tried to remove combatant that is not on team {self.get_name()}")