Wednesday, April 1, 2015

D&D Battle simulator (part 1)



Who would win in a battle of 15 peasants against a hill giant? In Dungeons and Dragons it is quite hard to predict the difficulty of an encounter. Unlike previous editions, in fifth edition the challenge is based on XP, while the challenge rating (CR) limits how nasty the monster can get. However, it isn't quite right.
In order to find out what the victory probabilities are as various parameters are changed I wrote a Python script to simulate D&D battles. The script can be found in my GitHub repository: https://github.com/matteoferla/tangents/blob/master/DnD_Battler.py



Details
It doesn't operate on a grid and instead simply assumed everyone is next to everyone. Adding a grid would be easy, but making the various creatures abide by a reasonable strategy would be very hard. There already is a fair amount of strategy already in the code adding more may require Bayesian tricks.
One such strategic choice is who to target. D&D battles have a peculiarity in that one isn't really supposed to know the game stats of other PCs and monsters, yet one does —prior knowledge or guesswork during the battle. This simulator can either target randomly or, by default, target the creature with the lowest HP. This results in "squishies" getting mercilessly owned in the first few rounds, which is what may happen for squishy monsters, but not squishy PCs, because they generally cower in the rear, while the DM kindly focuses on the tanks in melee range.
In terms of strategy, the code determine if a team has a better turn economy and therefore makes the weakest character (target) dodge and characters with a net to use that —both actions do no damage, but stop the monster's action.
The script does not do spellcasting, except for power word heal and barkskin, nor does it implement several class features as that would require a lot more code. Currently, barbarians can't lose range, for example. Nevertheless, it is very useful for testing hypotheses.

For example let's see how much of a challenge stuff is for 4 level 3 heroes.
For them hard is above .9k and deadly above 1.6k corrected xp.
4 generic heroes vs. 1 ankylosaurus (700 xp): Medium encounter
 

hero = Creature("hero", "good",
                    ac=16, hp=18, #bog standard shielded leather-clad level 3.                    attack_parameters=[['longsword', 4, 2, 8]])
 
anky=Creature("Ankylosaurus",
              ac=15, hp=68, alignment='evil',
              attack_parameters=[['tail',7,4,6,6,6,6]],
              log="CR 3 700 XP") 
 
This has a 61% survival.
The generic "hero" above is so generic, he is truly underpowered. Instead, 2 lore bards and 2 generic tanks have a 97% win chance. The characters could be optimised better, but this is a safe lineup:
 
bard = Creature("Doppelbard", "good",
                      hp=18, ac=16,
                      healing_spells=6, healing_bonus=3, healing_dice=4,
                      initiative_bonus=2,
                      attack_parameters=[['rapier', 4, 2, 8]])
 
generic_tank = Creature("generic tank", "good",
                        hp=20, ac=17,
                        initiative_bonus=2,
                        attack_parameters=[['great sword', 5, 3, 6, 6]])
 
polar= Creature("polar bear",'evil',
                ac=12, hp=42,
                attack_parameters=[['bite',7,5,8],['claw',7,5,6,6]])
 

2 polar bears are a Hard encounter (1.3k) and the heroes will have a 52% chance. 3 polar bears (2.7k)? 5% chance. However, when I use the guestimated stats of the party I currently play with I get a whopping 80% (druid, barbarian, bard, generic_tank).
druid = Creature("Twice Brown Bear Druid",
                 hp=86, ac=11, alignment="good",
                 attack_parameters=[['claw', 5, 4, 8], ['bite', 5, 4, 6, 6]], ability=[0, 0, 0, 0, 3, 0],
                 sc_ability='wis', buff='cast_barkskin', buff_spells=4,
                 log='The hp is bear x 2 + druid')

barbarian = Creature("Barbarian",
                     ac=18, hp=66, alignment="good",
                     attack_parameters=[['greatsword', 4, 1, 6, 6], ['frenzy greatsword', 4, 1, 6, 6]],
                     log="hp is doubled due to resistance")
 
bard = Creature("Bard", "good",
                hp=18, ac=18,
                initiative_bonus=2,
                healing_spells=6, healing_bonus=3, healing_dice=4,
                attack_parameters=[['rapier', 4, 2, 8]], alt_attack=['net', 4, 0, 0])
 
 
2 ankylosauruses? 2.1k. 26% for the generic mark 2 party, 92% for the actual party.
3 ankylosauruses? 4.2k. 8‰ and 38%.
In other words, the maths is for a rather rubbish party. For a balanced party is seems like a safe bet to double the challenge.

EDIT: Further thoughts and (inconclusive) analyses:
http://squidonius.blogspot.co.nz/2015/04/d-battle-simulator-part-2.html

No comments:

Post a Comment