Created
October 26, 2012 15:42
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode 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