Skip to content

Instantly share code, notes, and snippets.

@nickschurch
Created October 26, 2012 15:42
Show Gist options
  • Save nickschurch/3959498 to your computer and use it in GitHub Desktop.
Save nickschurch/3959498 to your computer and use it in GitHub Desktop.
Code for simulating single a dual-wield weapon attacks with DnD next characters
#!/local/bin/python
'''
Code for doing some simple stats on the dndnext playtest rules
'''
import sys, os, numpy
import matplotlib.pyplot as plt
from numpy.random import randint
version_string="1.0"
# force sequential output to the screen, rather than buffered output.
def printf(fmt, *args): sys.stdout.write(fmt % args)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
class classType(type):
def __str__(self, cls):
return cls.__name__
class weapon:
""" This class defines weapon wielded by a character. It could be
a physical weapon or a spell."""
__metaclass__ = classType
def __init__(self, name=None, ttype=None, attStat=None, dmgType=None, dmgDice=None):
if name is None and attStat is None and dmgType is None and dmgDice is None:
self.name = "shortsword"
self.type = "weapon"
self.attStat = "str"
self.dmgType = "slashing"
self.dmgDice = 6
else:
try:
self.name = str(name)
self.type = str(ttype)
self.attStat = str(attStat)
self.dmgType = str(dmgType)
self.dmgDice = int(dmgDice)
except ValueError as e:
print "Cannot cast a input property to the correct type."
print e
raise
class character:
""" This class defines a characters base properties. """
__metaclass__ = classType
# set char defaults
__allowed_classes = {"fighter":{"name":"fighter",
"hitdice":10,
"weaponAttBase": 3,
"weaponAttInc": 4,
"magicAttBase": None,
"magicAttInc": None,
}
}
__default_stats = {"str":10,
"dex":10,
"con":10,
"int":10,
"wis":10,
"chr":10
}
char_class = __allowed_classes.keys()[0]
stats = __default_stats
weapon1 = weapon()
weapon2 = None
level = 1
verbose=True
# constructor
def __init__(self,
c_class,
lev,
statarray = None,
weapon1 = None,
weapon2 = None
):
try:
self.char_class = self.__allowed_classes[c_class]
except KeyError:
pass
try:
if lev<6:
self.level = int(lev)
if type(statarray) is list and len(statarray)==6:
self.char_stats["str"] = int(statarray[0])
self.char_stats["dex"] = int(statarray[1])
self.char_stats["con"] = int(statarray[2])
self.char_stats["int"] = int(statarray[3])
self.char_stats["wis"] = int(statarray[4])
self.char_stats["chr"] = int(statarray[5])
elif type(statarray) is dict and len(statarray.keys())==6:
for key in self.__default_stats:
try:
self.char_stats[key] = int(statarray[key])
except KeyError:
pass
except ValueError as e:
print "Failed to cast a parameter to an int (probably)."
print e
raise
if type(weapon1) is weapon:
self.char_weapon1 = weapon1
if type(weapon2) is weapon:
self.char_weapon1 = weapon2
def __getStatbonus(self, stat):
""" returns the bonus value for a given stat value """
return (self.stats[stat]-10)/2
def __rollDice(self, diceVal):
""" returns a random roll of an n-sided dice """
return randint(1,diceVal+1)
def __hitRoll(self, weapon):
""" rolls to hit for the character with a weapon"""
# attack with weapon 1
roll = self.__rollDice(20)
class_base_bonus = self.char_class["weaponAttBase"]
class_level_bonus = self.level/self.char_class["weaponAttInc"]
if weapon.type == "spell":
try:
class_base_bonus = self.char_class["magicAttBase"]
class_level_bonus = self.level/self.char_class["magicAttInc"]
except:
print "this char cannot use spells I'm afraid!"
return(None, None)
stat_bonus = self.__getStatbonus(weapon.attStat)
total = roll+class_base_bonus+class_level_bonus+stat_bonus
isCrit=False
critstring=""
if roll==20:
isCrit=True
critstring="(crit!)"
if self.verbose:
print "Hit\n---\n roll:\t%i %s\n base %s bonus:\t%i\n level bonus:\t%i\n stat " \
"bonus:\t%i\n total:\t%i" \
"" % (roll, critstring, self.char_class["name"], class_base_bonus,
class_level_bonus, stat_bonus, total
)
return(total, isCrit)
def __attack(self):
""" rolls hit rolls for the character """
hit1 = self.__hitRoll(self.weapon1)
hit2 = None
if type(self.weapon2) is weapon:
hit2 = self.__hitRoll(self.weapon2)
return(hit1, hit2)
def __rollDamage(self, attack, weapon, dualMethod=None):
""" rolls damage for an attack given a method """
roll = 0
stat = 0
total = 0
critstring = ""
if attack[0] or attack[1]:
if attack[1]:
critstring = "crit! - max dmg"
if dualMethod is None:
if attack[1]:
roll = self.weapon1.dmgDice
elif attack[0]:
roll = self.__rollDice(self.weapon1.dmgDice)
stat = self.__getStatbonus(weapon.attStat)
total = roll+stat
elif dualMethod=="full_half":
if attack[1]:
roll = self.weapon1.dmgDice
elif attack[0]:
roll = self.__rollDice(self.weapon1.dmgDice)
stat = self.__getStatbonus(weapon.attStat)
total = int((roll+stat)*0.5)
elif dualMethod=="half_exCrit":
if attack[1]:
roll = self.weapon1.dmgDice
stat = self.__getStatbonus(weapon.attStat)
total = roll+stat
elif attack[0]:
roll = self.__rollDice(self.weapon1.dmgDice)
stat = self.__getStatbonus(weapon.attStat)
total = int((roll+stat)*0.5)
elif dualMethod=="half_exStat":
if attack[1]:
roll = self.weapon1.dmgDice
elif attack[0]:
roll = self.__rollDice(self.weapon1.dmgDice)
stat = self.__getStatbonus(weapon.attStat)
total = int((roll*0.5)+stat)
if total==0:
total=1
if self.verbose:
print "\nDmg\n---\n roll:\t%i %s\n stat bonus:\t%i\n total:\t%i" \
"" % (roll, critstring, stat, total)
return(total)
def makeAttack(self, target, dualMethod=None):
""" rolls attack(s) against a target AC & rolls appropriate damage """
try:
target = int(target)
except ValueError as e:
print "Failed to cast a parameter to an int (probably)."
print e
raise
attacks = self.__attack()
results=[]
for attack in attacks:
if type(attack) is tuple:
if attack[1]:
results.append((True, True))
elif attack[0]>=target:
results.append((True, False))
else:
results.append((False,False))
dmg = 0
if len(results)==1:
dmg = self.__rollDamage(results[0], self.weapon1, dualMethod=None)
elif len(results)==2:
dmg = self.__rollDamage(results[0], self.weapon1, dualMethod=dualMethod)
dmg = dmg + self.__rollDamage(results[1], self.weapon2, dualMethod=dualMethod)
return(results, dmg)
def runAttackSims(char, target, nrounds, dualMethod=None, verbose=True):
""" runs a simulation of n rounds of attacks recording the hits misses and damage """
hits=[]
dmg=[]
i=0
while i<nrounds:
if dualMethod is None:
these_att, this_dmg = char.makeAttack(target)
else:
these_att, this_dmg = char.makeAttack(target, dualMethod=dualMethod)
for attack in these_att:
if attack[0] or attack[1]:
hits.append(True)
else:
hits.append(False)
dmg.append(this_dmg)
i+=1
hits = numpy.array(hits)
dmg = numpy.array(dmg)
if verbose:
print "character level: %i" % char.level
print "character class: "
for key in char.char_class:
print "\t%s: %s" % (key, str(char.char_class[key]))
print "weapon 1: %s" % str(char.weapon1.name)
label = "standard single attack"
try:
print "weapon 2: %s" % str(char.weapon2.name)
label = "duel attack, %s method" % dualMethod
except AttributeError:
print "weapon 2: None"
print "number of rounds: %i" % nrounds
print "number of attacks: %i" % len(hits)
print "number of hits: %i (%s%s)" % (len(numpy.where(hits)[0]),
100*(float(len(numpy.where(hits)[0]))/len(hits)),
"%")
print "number of misses: %i (%s%s)" % (len(numpy.where(hits==False)[0]),
100*(float(len(numpy.where(hits==False)[0]))/len(hits)),
"%")
print "mean dmg/round: %.2f" % (dmg.mean())
print "std dev dmg/round: %.2f" % (dmg.std())
fig = plt.figure()
dmghist = plt.hist(dmg, numpy.arange(dmg.max()+2)-0.5, alpha=0.3)
yvals = plt.ylim()
plt.plot([dmg.mean(), dmg.mean()],
yvals,
color="r",
label="mean dmg/round (%.2f)" % dmg.mean()
)
plt.legend()
plt.title(label)
plt.xlabel("damage in a round")
plt.ylabel("frequency")
return(hits, dmg, dmghist, fig)
else:
return(hits, dmg)
def iterateDex(statMin, statMax):
char1 = character("fighter",1)
char2 = character("fighter",1)
char1.verbose=False
char2.verbose=False
# give char1 a dagger not a shortsword
dagger = weapon("dagger","weapon","dex","piercing",4)
char1.weapon1 = dagger
# give char2 2 daggers not a shortsword
dagger = weapon("dagger","weapon","dex","piercing",4)
char2.weapon1 = dagger
char2.weapon2 = dagger
dex=[]
single_dpr=[]
dual_dpr=[]
stat = statMin
while stat<=statMax:
char1.stats["dex"]=stat
char2.stats["dex"]=stat
results1 = runAttackSims(char1, 15, 50000, dualMethod=None, verbose=False)
results2 = runAttackSims(char2, 15, 50000, dualMethod="full_half", verbose=False)
dex.append(stat)
single_dpr.append(results1[1].mean())
dual_dpr.append(results2[1].mean())
stat+=1
fig = plt.figure()
plt.plot(dex, single_dpr, color="r", label="single dagger attack/round")
plt.plot(dex, dual_dpr, color="b", label="dual-wield dagger attacks/round")
plt.legend()
plt.title("damage per round vs AC15 as a function of dex stat")
plt.xlabel("dex value")
plt.ylabel("damage per round over 50k rounds of attacks")
return(fig)
if __name__ == '__main__':
print "\n"
print "================================================"
print "Welcome to the dual-wield dndnext stat simulator"
print "================================================"
# create a level 1 fighter with a shortsword and 10s in everything
char = character("fighter",1)
# give the char some more real stats (these actually come from my current
#dndnext char, jonjo!)
char.stats = {"str": 11,
"dex": 14,
"con": 11,
"int": 11,
"wis": 14,
"chr": 17
}
# give the char a dagger not a shortsword
dagger = weapon("dagger","weapon","dex","piercing",4)
char.weapon1 = dagger
# make an attack against AC 15
this_att, this_dmg = char.makeAttack(15)
# turn off individual attack reporting before running simulations
char.verbose = False
# simulate 50000 rounds of standard single attacks with the dagger vs AC 15
print "\nSimple single dagger attack simulation:"
results = runAttackSims(char, 15, 50000, dualMethod=None)
# give the char a second weapon, also a dagger, to enable dual wielding
char.weapon2 = dagger
# simulate 50000 rounds of dual wieid attacks with daggers, each of which
# does half damage (as per the written rules) on a hit
print "\nDual-wield dagger attack simulation (half damage):"
results = runAttackSims(char, 15, 50000, dualMethod="full_half")
# simulate 50000 rounds of dual wieid attacks with daggers, each of which
# does half damage on a hit, unless it is a crit, in which case it does
#full damage
print "\nDual-wield dagger attack simulation (half damage, except on crit):"
results = runAttackSims(char, 15, 50000, dualMethod="half_exCrit")
# simulate 50000 rounds of dual wieid attacks with daggers, each of which
# does half damage on a hit, but the stat bonus is not halfed
print "\nDual-wield dagger attack simulation (half roll damage, full stat " \
"bonus damage):"
results = runAttackSims(char, 15, 50000, dualMethod="half_exStat")
# give our fighter dual rapiers for max damage in dual-wield mode
rapier = weapon("rapier","weapon","dex","piercing",6)
char.stats["dex"]=18
char.weapon1 = rapier
char.weapon2 = rapier
print "\nDual-wield rapier attack simulation (half roll damage, full stat " \
"bonus damage):"
results = runAttackSims(char, 15, 50000, dualMethod="half_exStat")
# give our fighter a longsword, no second weapon and swap str and dex (kind
# of a cookie-cutter longsword sword-and-board configuration)
longsword = weapon("longsword","weapon","str","slashing",8)
char.weapon1 = longsword
char.weapon2 = None
char.stats["str"]=18
char.stats["dex"]=11
print "\nSimple single longsword attack simulation:"
results = runAttackSims(char, 15, 50000, dualMethod=None)
iterateDex(10, 20)
plt.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment