11
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ 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. \$\endgroup\$ – Skidsdev May 3 '17 at 15:48
  • \$\begingroup\$ Also are level and constitution passed as integers, or as strings saying "xth level" and "y CON"? \$\endgroup\$ – Skidsdev May 3 '17 at 15:48
  • 1
    \$\begingroup\$ Gosh, I'm old, I still remember this table: ancientscrossroads.com/adnd_tools/con_table.htm \$\endgroup\$ – Neil May 3 '17 at 15:58
  • \$\begingroup\$ @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. \$\endgroup\$ – undergroundmonorail May 3 '17 at 16:26
  • 3
    \$\begingroup\$ I found it pretty hard to parse out the relevant information from all the info being thrown at me. \$\endgroup\$ – Jonathan Allan May 3 '17 at 16:33
2
\$\begingroup\$

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
\$\endgroup\$
8
\$\begingroup\$

JavaScript (ES6), 81 78 76 74 bytes

Takes input as (class, level, constitution_ability_score). The class is case-insensitive.

(c,l,s,h=(d=parseInt(c,34)*59.9%97%77%6|4)+(s-10>>1))=>(h>0?h*l:h+l-1)+d-2

This is essentially using the same math as my initial version, but d is now computed without any lookup table.

Test cases

let f =

(c,l,s,h=(d=parseInt(c,34)*59.9%97%77%6|4)+(s-10>>1))=>(h>0?h*l:h+l-1)+d-2

console.log(f('Barbarian', 15, 13))  // 125
console.log(f('Rogue',     10, 18))  // 93
console.log(f('Wizard',    15, 18))  // 122
console.log(f('Wizard',    16,  1))  // 16
console.log(f('Barbarian', 15,  7))  // 80
console.log(f('Warlock',   15,  3))  // 18
console.log(f('Ranger',    14,  1))  // 18
console.log(f('Warlock',    3, 14))  // 24
console.log(f('Druid',      3,  4))  // 9
console.log(f('Cleric',    11,  5))  // 25
console.log(f('Bard',      20, 11))  // 103
console.log(f('Barbarian', 11, 13))  // 93
console.log(f('Bard',       8, 19))  // 75
console.log(f('Bard',      16, 17))  // 131
console.log(f('Fighter',   10,  6))  // 44
console.log(f('Monk',      10,  2))  // 13
console.log(f('Cleric',    14, 17))  // 115
console.log(f('Cleric',     6,  5))  // 15
console.log(f('Rogue',      7, 13))  // 45
console.log(f('Cleric',     4, 14))  // 31
console.log(f('Rogue',     19, 15))  // 136
console.log(f('Paladin',   13, 13))  // 95
console.log(f('Cleric',    13, 15))  // 94
console.log(f('Bard',       8,  5))  // 19
console.log(f('Monk',      20, 11))  // 103
console.log(f('Barbarian',  8, 20))  // 101
console.log(f('Monk',       1,  4))  // 5
console.log(f('Bard',       5, 17))  // 43
console.log(f('Monk',      18,  7))  // 57
console.log(f('Wizard',    17,  5))  // 19


Initial version, 87 84 bytes

(c,l,s,h=(d=+'55654607554506'[parseInt(c,35)%24%15])+(s-10>>1))=>(h>0?h*l:h+l-1)+d-2

How it works

The tricky part is to convert the class string c into the corresponding hit dice. More precisely, the value that we're going to store is d = dice / 2 + 1.

We use the formula parseInt(c, 35) % 24 % 15 which gives:

Class       | Base 35 -> decimal | MOD 24 | MOD 15 | d
------------+--------------------+--------+--------+---
"Sorcerer"  |      1847055419467 |     19 |      4 | 4
"Wizard"    |               1138 |     10 |     10 | 4
"Bard"      |             484833 |      9 |      9 | 5
"Cleric"    |          662409592 |     16 |      1 | 5
"Druid"     |           20703143 |     23 |      8 | 5
"Monk"      |             973475 |     11 |     11 | 5
"Rogue"     |           41566539 |      3 |      3 | 5
"Warlock"   |        59391165840 |      0 |      0 | 5
"Fighter"   |        28544153042 |      2 |      2 | 6
"Paladin"   |        46513817828 |     20 |      5 | 6
"Ranger"    |         1434103117 |     13 |     13 | 6
"Barbarian" |     25464249364423 |      7 |      7 | 7

By inserting the values of d at the corresponding positions into a string and padding unused slots with zeros, we get:

00000000001111
01234567890123
--------------
55654607554506

Hence the final formula:

d = +'55654607554506'[parseInt(c, 35) % 24 % 15]

Once we have d, we compute:

h = d + ((s - 10) >> 1))

which is the theoretical number of points that are gained at each level-up.

If h is positive, we simply compute:

h * l

If not, we need to take into account the fact that at least 1 point is gained at each level-up. So we compute instead:

h + l - 1

In both cases, we adjust the result by adding d - 2, so that the initial number of points is correctly integrated.

Test cases

let f =

(c,l,s,h=(d=+'55654607554506'[parseInt(c,35)%24%15])+(s-10>>1))=>(h>0?h*l:h+l-1)+d-2

console.log(f('Barbarian', 15, 13))  // 125
console.log(f('Rogue',     10, 18))  // 93
console.log(f('Wizard',    15, 18))  // 122
console.log(f('Wizard',    16,  1))  // 16
console.log(f('Barbarian', 15,  7))  // 80
console.log(f('Warlock',   15,  3))  // 18
console.log(f('Ranger',    14,  1))  // 18
console.log(f('Warlock',    3, 14))  // 24
console.log(f('Druid',      3,  4))  // 9
console.log(f('Cleric',    11,  5))  // 25
console.log(f('Bard',      20, 11))  // 103
console.log(f('Barbarian', 11, 13))  // 93
console.log(f('Bard',       8, 19))  // 75
console.log(f('Bard',      16, 17))  // 131
console.log(f('Fighter',   10,  6))  // 44
console.log(f('Monk',      10,  2))  // 13
console.log(f('Cleric',    14, 17))  // 115
console.log(f('Cleric',     6,  5))  // 15
console.log(f('Rogue',      7, 13))  // 45
console.log(f('Cleric',     4, 14))  // 31
console.log(f('Rogue',     19, 15))  // 136
console.log(f('Paladin',   13, 13))  // 95
console.log(f('Cleric',    13, 15))  // 94
console.log(f('Bard',       8,  5))  // 19
console.log(f('Monk',      20, 11))  // 103
console.log(f('Barbarian',  8, 20))  // 101
console.log(f('Monk',       1,  4))  // 5
console.log(f('Bard',       5, 17))  // 43
console.log(f('Monk',      18,  7))  // 57
console.log(f('Wizard',    17,  5))  // 19

\$\endgroup\$
  • \$\begingroup\$ Something seems to be wrong with your code; Bards with a CON of 1 or Wizards with a CON of 2 or 3 get the same number of hit points whatever their level. \$\endgroup\$ – Neil May 3 '17 at 16:35
  • 1
    \$\begingroup\$ @Neil Thanks for noticing. I think this is fixed. \$\endgroup\$ – Arnauld May 3 '17 at 16:59
3
\$\begingroup\$

Batch, 172 bytes

@set/aa=1-%3/2,h=4-a
@for %%c in (-1.Sorcerer -1.Wizard 1.Fighter 1.Paladin 1.Ranger 2.Barbarian)do @if %%~xc==.%1 set/aa-=c=%%~nc,h+=c*2
@cmd/cset/a"a*=(a>>9),-~a*~-%2+h

Takes class, level, and constitution as command-line arguments. Explanation: The HP can be calculated as (HP at level 1) + (level - 1) + min(further HP per level, 0) * (level - 1). The further HP per level is half the hit die plus the constitution modifier. Most classes use d8 so this becomes one less than half the constitution (%3/2-1), while the HP at level 1 is 3 more than that. The further HP per level and HP at level 1 are then adjusted for the six classes that don't use d8. The further HP per level is then limited to 0 (this actually uses the negative value as it's slightly golfier this way.)

\$\endgroup\$
2
\$\begingroup\$

R, 181 163 bytes

function(s,n,t){a=Hmisc::Cs(rc,za,rd,er,ui,mk,gu,rl,gh,la,ng,rb)
b=c(6,6,rep(8,6),rep(10,3),12)
d=b[a == substr(s,3,4)]
m=floor((t-10)/2)
d+m+(n-1)*max(d/2+1+m,1)}

Anonymous function. Runs as f(class, level, CON).

Explanation: Creates vectors for class s to dice max d, using the 3rd and 4th letters in the class name (smallest unique mapping I found).

CON mod m is straight from the spec, and HP = first level (d + m) + the rest of the levels ((n-1) * max(average_die + m, 1).

> f("Barbarian", 15, 13)
[1] 125
> f("Rogue", 10, 18)
[1] 93
> f("Wizard", 15, 18)
[1] 122
> f("Wizard", 16, 1)
[1] 16
> f("Barbarian", 15, 7)
[1] 80
> f("Warlock", 15, 3)
[1] 18
> f("Ranger", 14, 1)
[1] 18
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.