Source code for DnD_5e.encounter

import logging
from copy import deepcopy, copy

from DnD_5e.utility_methods_dnd import NullLogger

[docs] class Encounter: """ This class is used for running encounters """ def __init__(self, **kwargs): """ Set up the encounter. :param kwargs: keyword arguments. Valid arguments are as follows: :param name: what *self* is called. A unique name is recommended but not required :param teams: a list of :py:class:`Team` s :param max_rounds: the maximum number of rounds the encounter should last (default 50). To prevent infinite loops. """ self._name = kwargs.get("name", "Encounter") if not isinstance(self._name, str): raise ValueError("The name of the Encounter must be a string") self._logger = kwargs.get("logger", logging.getLogger("DnD_5e.encounter")) self._teams = [] teams = kwargs.get("teams") if teams: try: for the_team in teams: self.add_team(the_team) except TypeError: # trying to iterate over a non-iterable raise ValueError("Must provide a non-empty list (or set or tuple) of Teams") else: raise ValueError("Must provide a non-empty list (or set or tuple) of Teams") self._combatants = [] for the_team in self.get_teams(): self._combatants.extend(the_team.get_combatants()) # according to https://rpg.stackexchange.com/questions/93183/how-many-rounds-does-the-average-combat # -encounter-last, maximum number of rounds for a typical combat encounter is 5. So I figure if normal combat # takes more than 50 rounds, something is wrong. self._max_rounds = kwargs.get("max_rounds", 50) self._round = 1 self._initial_state = self.get_copy_of_teams_and_combatants()
[docs] def get_copy_of_teams_and_combatants(self): """ Make a copy of the teams (and the combatants within them), so we have a state to reset to :return: """ copied_teams = deepcopy(self.get_teams()) copied_combatants = [] for the_team in copied_teams: copied_combatants.extend(the_team.get_combatants()) return { 'teams': copied_teams, 'combatants': copied_combatants }
[docs] def get_initial_teams(self): """ :return: the copy of `self._teams` that is stored in `self._initial_state` """ return self._initial_state['teams']
[docs] def get_initial_combatants(self): """ :return: the copy of `self._combatants` that is stored in `self._initial_state` """ return self._initial_state['combatants']
def __eq__(self, other) -> bool: """ Compare *self* and *other* to determine if they are equal based on the following characteristics: :py:attr:`hit_dice`, :py:attr:`max_hp`, ability scores, :py:attr:`proficiencies`, :py:attr:`features`, :py:attr:`fighting_styles` :param other: the Encounter to be compared :type other: :py:class:`Encounter` :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_teams()) != len(other.get_teams()): return False if self.get_max_rounds() != other.get_max_rounds(): return False for i, the_team in enumerate(self.get_teams()): if not the_team == other.get_teams()[i]: return False return True
[docs] def get_name(self) -> str: """ :return: name """ return self._name
[docs] def set_name(self, name: str): self._name = name
[docs] def get_logger(self) -> logging.Logger: """ :return: logger """ return self._logger
[docs] def get_teams(self) -> list: """ :return: teams """ return self._teams
[docs] def get_combatants(self) -> list: """ :return: list of :py:class:`Combatant` s """ return self._combatants
[docs] def get_current_combatant(self): """ :return: the Combatant who should take a turn now :rtype: :py:class:`Combatant` """ combatants = self.get_combatants() return combatants[self.get_round() % len(combatants)]
[docs] def get_max_rounds(self) -> int: """ :return: maximum number of rounds before aborting this encounter """ return self._max_rounds
[docs] def get_round(self) -> int: """ :return: round number """ return self._round
[docs] def get_end_condition(self) -> bool: """ Check to see if it is time to end this encounter :return: False if at least two teams have some alive members, True otherwise """ num_conscious_teams = 0 for the_team in self.get_teams(): if the_team.has_any_conscious(): num_conscious_teams += 1 if num_conscious_teams > 1: return False return True
[docs] def get_team_stats(self) -> dict: """ Get stats for every Team :return: """ return {t.get_name(): t.get_stats() for t in self.get_teams()}
[docs] def get_combatant_stats(self) -> dict: """ Get stats for every Combatant :return: """ result = {} for comb in self.get_combatants(): result[comb.get_name()] = {"damage_dealt": comb.get_damage_dealt(), "damage_taken": comb.get_damage_taken(), "times_gone_unconscious": comb.get_times_unconscious(), "times_end_conscious": 0, "times_end_unconscious": 0, "times_end_dead": 0, "end_hp": comb.get_current_hp()} if comb.has_condition("unconscious"): result[comb.get_name()]["times_end_unconscious"] += 1 elif comb.has_condition("dead"): result[comb.get_name()]["times_end_dead"] += 1 else: result[comb.get_name()]["times_end_conscious"] += 1 return result
[docs] def get_encounter_statnames(self): """ Return the stats (besides the ones for Combatant and Team) that this Encounter tracks. Used by a Simulation to know which stats to keep track of. TODO: should this be a static method or a class method? :return: """ return ["rounds"]
[docs] def get_stats(self) -> dict: """ Get various stats about how the Encounter went :return: a dict containing the stats """ result = {} result["team"] = self.get_team_stats() result["combatants"] = self.get_combatant_stats() result["rounds"] = self.get_round() return result
[docs] def set_logger(self, logger: str): """ Set *self._logger* to the logger with the name specified in *logger* :param logger: the name of the Logger to use, or the logging.Logger object itself :return: None """ try: self._logger = logging.getLogger(logger) except TypeError: if isinstance(logger, (logging.Logger, NullLogger)): self._logger = logger else: self.get_logger().error("Tried to set logger to an invalid value", stack_info=True) raise ValueError("logger must be a string (the name of a logger), a Logger object, or NullLogger")
[docs] def add_team(self, the_team): """ Add a given Team to the encounter :param the_team: the :py:class:`Team` to add :type the_team: :py:class:`Team` :return: """ try: the_team.has_any_dead() except AttributeError: self.get_logger().error("Tried to add a team that is not of class Team", stack_info=True) raise ValueError("Tried to add a team that is not of class Team") self._teams.append(the_team)
# TODO: shouldn't I be adding the combatants here??
[docs] def roll_initiative(self): self.get_combatants().sort(key=lambda x: (x.roll_initiative(), x.get_dexterity())) self.get_logger().info("%s initiative order: %s", self.get_name(), str([c.get_name() for c in self.get_combatants()]))
[docs] def take_turn(self, comb): """ The specified :py:class:`Combatant` takes their turn in combat :param comb: the :py:class:`Combatant` whose turn it is :type comb: :py:class:`Combatant` :return: """ return comb.take_turn(self.get_teams())
[docs] def run(self): """ Run the encounter :return: """ self.get_logger().info("%s: starting the encounter", self.get_name()) self.roll_initiative() while True: self.get_logger().info("Round %d", self.get_round()) unconscious = [comb.get_name() for comb in self.get_combatants() if comb.has_condition("unconscious")] if unconscious: self.get_logger().info("Unconscious: %s", str(unconscious)) dead = [comb.get_name() for comb in self.get_combatants() if comb.has_condition("dead")] if dead: self.get_logger().info("Dead: %s", str(dead)) for i in range(len(self.get_combatants())): self.take_turn(self.get_combatants()[i]) if self.get_end_condition(): return 0 self._round += 1 if self.get_round() > self.get_max_rounds(): self.get_logger().warning("Encounter did not finish in maximum number of rounds") return -1
[docs] def reset(self): """ Reset attributes of *self* and of the Combatants in this Encounter. Do this to prepare to run the Encounter again. :return: None """ self._round = 1 self._teams = self.get_initial_teams() self._combatants = self.get_initial_combatants() self._initial_state = self.get_copy_of_teams_and_combatants()