Source code for DnD_5e.utility_methods_dnd

"""
A module containing useful functions for other modules in this DnD 5e combat project
"""

import random
import warnings
from typing import Tuple, Union, List

TYPE_ROLL_RESULT = Tuple[int, int]
TYPE_DICE_TUPLE = Tuple[int, int]

# table from https://5etools.com/crcalculator.html
CR_XP_TBL = {0: 10, 0.125: 25, 0.25: 50, 0.5: 100, 1: 200, 2: 450, 3: 700, 4: 1100, 5: 1800, 6: 2300, 7: 2900,
             8: 3900, 9: 5000, 10: 5900, 11: 7200, 12: 8400, 13: 10000, 14: 11500, 15: 13000, 16: 15000, 17: 18000,
             18: 20000, 19: 22000, 20: 25000, 21: 33000, 22: 41000, 23: 50000, 24: 62000, 25: 75000, 26: 90000,
             27: 105000, 28: 120000, 29: 135000, 30: 155000}

[docs] def ability_to_mod(score: int) -> int: """ Convert an ability score to a modifier :param score: the ability score to convert :type score: integer between 1 and 30 (inclusive) :return: the ability modifier :rtype: int :raise: ValueError if *score* is invalid """ if not isinstance(score, int): raise ValueError("Ability score must be an integer") if score < 1: raise ValueError("Ability score too low (did you input a modifier?)") if score > 30: raise ValueError("Ability score too high (did you add an extra digit?)") return (score - 10) // 2
[docs] def validate_dice(dice) -> TYPE_DICE_TUPLE: """ Determine whether a given dice representation is valid. If it is, return the dice in tuple form :param dice: the dice to validate :type dice: a tuple of dice number and dice type (e.g., (1, 6)) or a string of format dicenum + "d" + dicetype (e.g., "1d6") :return: the dice representation :rtype: :py:const:`TYPE_DICE_TUPLE` :raise: ValueError if *dice* is invalid """ if isinstance(dice, tuple): # lists are mutable and that could be a problem if len(dice) != 2: raise ValueError("Must provide exactly two values: number of dice and type of dice (1, 6)") if not isinstance(dice[0], int) or not isinstance(dice[1], int) or dice[0] < 1 or dice[1] < 1: raise ValueError("Must provide exactly two positive integer values: number of dice and type of dice (1, 6)") elif isinstance(dice, str): try: dice = tuple(int(x) for x in dice.split("d")) except ValueError: raise ValueError("Must provide damage dice in tuple of two ints (1, 6) or string format 1d6") if len(dice) != 2: raise ValueError("Must provide damage dice in tuple of two ints (1, 6) or string format 1d6") else: raise ValueError("Must provide damage dice in tuple of two ints (1, 6) or string format 1d6") return dice
# TODO: remove if unused
[docs] def roll_dice(dice_type: int, num: int = 1, modifier: int = 0, adv: int = 0, critable: bool = False) -> TYPE_ROLL_RESULT: """ Roll dice (applying advantage as specified), add the modifier (but don't go below 0), and return a result that includes the total number and an integer that indicates whether or not the roll was a crit :param dice_type: the type of dice to roll (e.g., a d20) :type dice_type: positive integer :param num: the number of dice to roll :type num: positive integer :param modifier: the modifier to add after all dice have been rolled :type modifier: int :param adv: indicates whether this roll has advantage :type adv: one of these integers: -1, 0, 1 :param critable: whether critical success and critical fail are possible :type critable: bool :return: a tuple that contains the total roll value as well as an indicator of whether the roll was a crit or not :rtype: :py:const:`TYPE_ROLL_RESULT` """ roll_val = 0 crit = 0 if num > 1 and critable: warnings.warn("Rolling multiple critable dice in one go is currently not supported. " "Crit information will match the last die rolled.") for i in range(num): # pylint: disable=unused-variable nat_roll = random.randint(1, dice_type) if adv: roll_2 = random.randint(1, dice_type) if adv > 0: nat_roll = max(nat_roll, roll_2) else: nat_roll = min(nat_roll, roll_2) crit = 0 if critable: if nat_roll == dice_type: crit = 1 # crit success elif nat_roll == 1: crit = -1 # crit fail. WARNING: -1 evaluates to true. Use this method appropriately. roll_val += nat_roll total_val = roll_val + modifier total_val = max(total_val, 0) return total_val, crit
[docs] def cr_to_xp(cr: Union[float, int]) -> int: """ Convert a challenge rating to an xp value :param cr: challenge rating :type cr: float or int :return: the xp value that corresponds to *cr* :rtype: positive integer :raise: ValueError if *cr* is not a valid challenge rating """ try: return CR_XP_TBL[cr] except KeyError: raise ValueError("Invalid cr value")
[docs] def calc_advantage(advs: Union[List[int], Tuple[int]]) -> int: """ Calculate advantage or disadvantage based on a list of advantage values .. warning: does not sanitize input (i.e., assumes you gave a sequence of valid values). User must use appropriately :param advs: a list or tuple of advantage values :type advs: a list or tuple of integers :return: a value to indicate advantage :rtype: one of these integers: -1, 0, 1 """ sum_val = 0 for adv in advs: sum_val += adv if sum_val == 0: return 0 if sum_val > 0: return 1 return -1
[docs] def time_to_rounds(time_var) -> int: # pylint: disable=inconsistent-return-statements """ Convert a given time to a number of rounds :param time_var: a variable that contains some kind of time :type time_var: string, or tuple containing a positive integer and a string :return: the number of rounds that corresponds to *time_var* :rtype: non-negative integer :raise: ValueError if *time_var* is invalid """ if isinstance(time_var, str): if time_var == "instantaneous": return 0 time_list = time_var.split(" ") elif isinstance(time_var, (tuple, list)): time_list = time_var else: raise ValueError("Must provide string (\"1 minute\") or tuple/list (1, \"minute\")") try: time = int(time_list[0]) if time <= 0: raise ValueError() except ValueError: raise ValueError("Time must be a positive integer") try: if time_list[1] not in ["minute", "minutes", "hour", "hours", "round", "rounds"]: raise ValueError("Unit must be minutes, hours, or rounds; e.g., 1 minute") except IndexError: raise ValueError("Must provide string (\"1 minute\") or tuple/list (1, \"minute\")") if "round" in time_list[1]: return time if "minute" in time_list[1]: return time * 10 if "hour" in time_list[1]: return time * 600 raise ValueError("Something else went wrong")
[docs] def proficiency_bonus_per_level(level: int) -> int: """ Calculate proficiency bonus based on level :param level: a Character level :type level: integer between 1 and 20 (inclusive) :return: the proficiency bonus that corresponds to *level* :rtype: positive integer """ if not isinstance(level, int) or level <= 0 or level > 20: raise ValueError("Level must be an integer between 1 and 20") return (level - 1) // 4 + 2
[docs] def proficency_bonus_by_cr(cr: int) -> int: """ Calculate proficiency bonus based on challenge rating :param cr: challenge rating :type cr: non-negative number :return: the proficiency bonus that corresponds to *cr* :rtype: positive integer """ if not isinstance(cr, (int, float)) or cr < 0: raise ValueError("cr must be a non-negative number") if cr < 5: return 2 return 3 + (cr - 5) // 4
[docs] def ability_from_abbreviation(name: str) -> str: # pragma: no cover """ Get the full ability score name given an abbreviation :param name: the abbreviated ability score name :type name: one of these strings: "str", "dex", "con", "int", "wis", "cha" :return: the full ability score name that corresponds to *name* :rtype: one of these strings: "strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma" :raise: ValueError if *name* is invalid """ if name == "str": return "strength" if name == "dex": return "dexterity" if name == "con": return "constitution" if name == "int": return "intelligence" if name == "wis": return "wisdom" if name == "cha": return "charisma" raise ValueError("Unknown abbreviation")
[docs] class NullLogger: """ For when you don't care about the logging results and you don't want multiprocessing runs to crash and burn """ def __init__(self): pass
[docs] def debug(self, msg, *args, **kwargs): pass
[docs] def info(self, msg, *args, **kwargs): pass
[docs] def warning(self, msg, *args, **kwargs): pass
[docs] def error(self, msg, *args, **kwargs): pass
[docs] def critical(self, msg, *args, **kwargs): pass
[docs] def log(self, lvl, msg, *args, **kwargs): pass
[docs] def exception(self, msg, *args, **kwargs): pass