D&D 5e HP Calculator
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
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.)
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