D&D 5e HP Calculator

  • I have trouble remembering everything I have to do when leveling up a D&D character. For whatever reason, one of the things that gives me trouble is figuring out what their new maximum HP value should be. For this challenge, you will write a program or function to calculate the correct value automatically.


    Terminology


    The first thing you need to know about to calculate max HP is the "Constitution modifier". Each DND character has six integer ability scores, including one for Constitution. The only relevant knowledge required for this challenge is how the Constitution ability score affects another stat, which is the Constitution modifier. In short, the modifier is equal to floor( (ability_score - 10) / 2 ). Adventurers can only have ability scores from 1 to 20, inclusive. Your code will never have to handle scores outside that range, which also means it will never have to handle a modifier lower than -5 or greater than +5. Though the Constitution modifier can change as a character levels up, its effects on HP are applied retroactively, so only its current value is needed to calculate current max HP.



    (This is entirely irrelevant to the challenge, but if you're curious about how it affects maximum HP: You can assume the "Tough" feat adds 2 to a character's Constitution modifier for the purposes of HP calculation, since that's effectively what it does. That's not the text of the feat but the math works out to be exactly the same. You don't have to handle this feat in your answer.)



    Next, every class has an assigned "hit die" type, which is involved in calculating HP. The following table lists the hit dice for each class.


    Sorcerer:  d6
    Wizard: d6
    Bard: d8
    Cleric: d8
    Druid: d8
    Monk: d8
    Rogue: d8
    Warlock: d8
    Fighter: d10
    Paladin: d10
    Ranger: d10
    Barbarian: d12

    Finally, the character's level. All that this affects is how many times to add a value to the running total in the following section. A character's level is an integer from 1 to 20, inclusive1. Your code will never have to handle a level outside that range. To reach level n, a character starts at level 1 and levels up n-1 times. For example, a level 3 character got to where they are by being a level 1 character and levelling up twice.


    How to Calculate Max HP


    A character's maximum HP is equal to their HP at level 1 plus the sum of the increase they received at each level.


    At level 1


    At level 1, a character's HP is equal to the highest possible roll on their hit die (the number in the name of the die, for those of you unfamiliar with dice that have more than 6 sides) plus their Constitution modifier. Remember that when calculating HP at a later level, you may assume a character's Constitution has always been the same, as this part of the calculation is re-done every time Constitution changes.


    When levelling up


    Every time a character levels up, they have two options. They may either roll one of their hit dice or take the average roll of that die (rounded up). Whichever they choose, their Constitution modifier is added to the result. This total is the amount that their HP increases. For this challenge, the average roll is always taken, so output is deterministic. (Again, if you're not familiar with >6 sided dice, you can calculate the rounded-up average roll as (highest_possible_roll / 2) + 1.)


    There is one notable exception. A character's maximum HP always increases by at least 1 each time they level up2. If the instructions in the above paragraph would result in an increase of 0 or less when leveling up, it increases by 1 instead.


    The Challenge


    Your program or function will take three inputs:



    • The character's class, as a string

    • The character's level

    • The character's Constitution ability score (not modifier)


    It will output only one thing: The character's current maximum HP.


    Examples


    Every possible combination of inputs and their associated outputs can be found at this link. For the sake of having something to look at on this page, here are 30 test cases chosen at random:


    Barbarian, 15th level, 13 CON: 125
    Rogue, 10th level, 18 CON: 93
    Wizard, 15th level, 18 CON: 122
    Wizard, 16th level, 1 CON: 16
    Barbarian, 15th level, 7 CON: 80
    Warlock, 15th level, 3 CON: 18
    Ranger, 14th level, 1 CON: 18
    Warlock, 3rd level, 14 CON: 24
    Druid, 3rd level, 4 CON: 9
    Cleric, 11th level, 5 CON: 25
    Bard, 20th level, 11 CON: 103
    Barbarian, 11th level, 13 CON: 93
    Bard, 8th level, 19 CON: 75
    Bard, 16th level, 17 CON: 131
    Fighter, 10th level, 6 CON: 44
    Monk, 10th level, 2 CON: 13
    Cleric, 14th level, 17 CON: 115
    Cleric, 6th level, 5 CON: 15
    Rogue, 7th level, 13 CON: 45
    Cleric, 4th level, 14 CON: 31
    Rogue, 19th level, 15 CON: 136
    Paladin, 13th level, 13 CON: 95
    Cleric, 13th level, 15 CON: 94
    Bard, 8th level, 5 CON: 19
    Monk, 20th level, 11 CON: 103
    Barbarian, 8th level, 20 CON: 101
    Monk, 1st level, 4 CON: 5
    Bard, 5th level, 17 CON: 43
    Monk, 18th level, 7 CON: 57
    Wizard, 17th level, 5 CON: 19




    1. Strictly speaking, I don't think there's a rule that says 20 is the maximum level. However, 21 is the point where there stop being tables in the book to tell you what some of the various numbers in the rules should be, including the amount of experience you need to obtain to reach it. That's a good enough level cap for me.


    2. I actually don't think this is true with RAW. I asked on rpg.se and such a thing doesn't appear to be written down anywhere. However, Mike Mearls, lead designer of D&D, tweeted it in March 2015. This is not authoritative the way that you could argue a tweet from lead rules developer Jeremy Crawford would be, but it is evidence that it's what they intended, so I'll use it for this challenge.


    Does class have to be given as a string, or can it be given as the number of the hit die, given that's the only relevant information for a class. Otherwise people will just need a generic table of "If these classes then this die, if these classes then this die" etc.

    Also are level and constitution passed as integers, or as strings saying "xth level" and "y CON"?

    @Mayube Probably shouldn't have asked a question and immediately gone out for pizza, huh? :P The class has to be a string, because I think there's enough data in those strings to find patterns to shorten the table (which appears to be the case, based on the answers that have come in so far). Level and constitution are ints.

    I don't think you have enough edge cases in your sample test cases.

    I found it pretty hard to parse out the relevant information from all the info being thrown at me.

    @undergroundmonorail In that case I'm gunna post a non-competing answer, because my language can't handle string parsing :(

    Here is a verbose (i.e. non-golfed) reference implementation with the listed test cases. (It's a tinyurl as the TIO link itself was too long for a comment).

    Oh wait, he's still alive. It was *the rest of the party that died a horrific death.*

  • Jelly, 34 bytes



    OSị“#®Ʋ?[’ṃ6¤ð+‘»1×⁵’¤+⁸Ḥ¤+ð⁹_10:2


    Full program taking three command line arguments: class, score, level.



    Try it online!



    How?



    The middle of the code, separated by ðs is a dyadic link which calculates the result from some previously calculated values:



    +‘»1×⁵’¤+⁸Ḥ¤+ - maxHitPoints(halfDieValue, modifier)
    + - add: halfDieValue + modifier
    ‘ - increment
    »1 - maximum of that and 1: this is the level-up delta
    ¤ - nilad followed by links as a nilad:
    ⁵ - program's 3rd argument, level (5th command line argument)
    ’ - decrement: this is the number of level-ups
    × - multiply: level-ups * level-up delta
    ¤ - nilad followed by links as a nilad:
    ⁸ - link's left argument: halfDieValue
    Ḥ - double: dieValue
    + - add: level-ups * level-up delta + dieValue
    + - add: level-ups * level-up delta + dieValue + modifier


    The modifier is calculated at the right hand side:



    ⁹_10:2 - getModifier(class, score)
    ⁹ - link's right argument, the 2nd argument, the score
    _10 - minus 10
    :2 - integer divide by 2


    Half the die value is calculated at the left hand side:



    OSị“#®Ʋ?[’ṃ6¤ - getHalfDieValue(class)
    O - cast to ordinals
    S - sum
    ¤ - nilad followed by link(s) as a nilad:
    “#®Ʋ?[’ - base 250 literal 140775266092
    ṃ6 - convert to base 6 but with 6s in place of 0s
    ị - index into (1-indexed and modular)


    Considering the ordinal sums of the class names modulo m such that m is minimal while keeping the classifications (by die) from colliding yields m=15. Placing the required values (half-die roll) at those indexes in a list of length 15 allows lookup using Jelly's modular indexing with . Compressing the list as a base 6 number with the lone 6 replaced by a 0 is a byte shorter than the alternatives of base-7 compression or base-4 compression & increasing the values (with the byte overhead associated with using an extra nilad in the chain). The base 6, rather than 7, decompression is achieved by using the fact that base decompression, (rather than base conversion, b), has implicit range construction when it's right argument, r, is an integer, which means is is like converting to base r and then changing any 0 to an r all in one go.



    That is:



             class: Sorcerer,Wizard,Bard,Cleric,Druid,Monk,Rogue,Warlock,Fighter,Paladin,Ranger,Barbarian
    Ordinal sum: 837, 625, 377, 594, 504, 405, 514, 723, 713, 697, 607, 898
    mod 15: 12, 10, 2, 9, 9, 0, 4, 3, 8, 7, 7, 13
    required value: 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 6


    Rearranging to the list, converting the 6 at index 13 to a zero and making it minimal in base 6:



    mod 15:    2   3   4           7   8   9  10      12  13      0  
    value: 1, 4, 4, 4, 0, 0, 5, 5, 4, 3, 0, 3, 0, 0, 4


    Making the code



                    list: [1,4,4,4,0,0,5,5,4,3,0,3,0,0,4]
    from base 6: 140775266092
    to base 250: [36,9,154,64,92]
    code page characters: # ® Ʋ ? [
    final code: “#®Ʋ?[’ṃ6

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM

Tags used