Background

In the previous post, I’ve presented my initial plan for a combat simulator. Continuing on with this goal, I still check its validity. While doing this, I try to check how changes in the AC or damage die alter the win probabilities. This required me to change the code from last post a little, mostly adding a proper fight function that I expect will grow (in the number of lines of code) significantly. I will probably have to create a class to store characters or monsters info on files and load them. Finally, I don’t expect to have any significant math in the upcoming posts as the simulator will be largely in the field of unintuitive equations, if they exist at all. I don’t think that it will be impossible to derive an exact solution (or at least a numeric solution with exact method). For now though, I don’t think this is worth the as the simulation provide accurate enough output, and the amount of spare time I have to dedicate to this task is quite limited.

The model

I continue with the model of two men bashing each other with a mace. However, this time I change the AC and damage die. I set one fixed opponent (FO)

HP 30 AC 16 Damage 1d8

The player AC , HP , and damage die change however. I set 6 simulations. One for character that is exactly the same as the fixed opponent, one for the same conditions with the critical hit options turned off. In the remaining four simulations I utilize critical hit, setting it to double. The AC is set to 14, or 18, and a damage die of d6 or d10 . For each HP value I simulate 30,000 battles. In total, the code took about half an hour to run on my machine (Lenovo ideapad u330p with 8GB of memory and i5 processor running Fedora 21 and gnome).

The effect of critical hit.

In the figure 1, I’ve used the simulator to check the effect of critical on the chance of wining. The blue solid line presents the chance of win the standard opponent at various HP values with only 1d8 possible as damage die. The orange die presents the same, with critical hit ”activated” both for the player and the opponent. The solid lines are measured by the left axis, and their sigmoid shapes are expected as the higher the HP of the player is, the easier it is for him to min. I find it a little interesting that turning critical hit on is beneficial to the character with the lower HP , with the crossover occurring when the two combatant have the same HP . This is so since there is a scenario with less blows to win the battles. We can conclude from this a good, intuitive, rule of thumb, while fighting multiple opponents, focus on extra attakcs/ region damage. While fighting single strong (e.g. better AC) opponent, focus on extra damage, and extra chance of hit.

Figure 1: The effect of critical hit on the chance of win. Blue solid line mark attack without critical, and orange solid line marks the attack with critical hit chance (on attack roll of 20 only). The left axis measure those probabilities. blue dotted line notes the ratio which is measured by the right axis.



The ratio between the two cases, presented as a blue dotted line, is measured by the right axis. Note that all the line are a little noisy part of this is due to small number of simulations, but there is an element to it, that stems from the fact that it is significantly harder to kill an opponent with 9HP that ‘8HP’.

Damage die vs AC.

The effect of changing weapon or changing armor is depicted in the figure 2. As mentioned, I’ve checked 4 cases. which roughly amount to the difference between better armor and better weapon (such as upgrading an armor or a sword, or by switching to two handed weapon or using a shield). As with the previous plot, the probability is noted by the left axis, and the ratio by the right axis. As expected, better equipment yields with better chance of surviving the encounter. However, there is a difference between the effect when having better equipment and or worse equipment than your opponent. It appears that the effect of AC on the survival chance than the damage dealing, hence, you should always prefer the armor over the sword, and you should prefer to increase your hitting chance over increasing your damage.

Figure 2: Chance of winning a battle against the standard opponent while armed with `1d6` weapon ( blue ), `1d10` weapon ( orange ), `AC` of 14 ( cyan ), or 18 ( magenta ). The black line mark a character which has the same stats as the FO. The dash line mark the ratio between the altered character, and that of the standard character with the same color coding. The left axis used for the probabilities, and the right axis used for the ratios.



# Conclusion

This concludes my validation of the simulations concept, and I can now start to add features to the simulator. I am planing to post soon (within this week) where I do the same calculation numerically. I have a code that produce same plot as figure 2 via numeric calculation, and I mean to expand on eat to scan the entire spectrum of valid values.

The code

As usual, I post here the code. In this case, I used two files. The first one for the robot class which I used as combatant, it should be moved into a separate file that holds also the loader and saver capabilities. The file also manage the simulations and the plotting. The second file holds a function that handle individual battle simulation. The main simulation file:

import numpy as np import matplotlib.pyplot as plt import operator from fight import fight class Robot: #A class for the combatant. #For now, it can roall attack, damage, and suffer imjurt def __init__(self,hp,attack,ac,damage,player=False): #Set constant. As we extend the simulator, it will be more complicated self.player = player self.attack = attack self.hp = hp self.ac = ac self.nhits = [0,0] self.damage = damage self.roll_init() return def die(self,d): return np.random.randint(1,d+1) def roll_init(self): self.init = self.die(20) def injure(self,hp): #reduce hp, return True if dead self.hp -= hp return self.isDead() def isDead(self): if self.hp <=0: return True else : return False def crit(self): return self.critical def attack_roll(self): roll = self.die(20) if roll == 20: self.critical = True else: self.critical = False return roll+ self.attack return roll+ self.attack def damage_roll(self): return self.die(self.damage[0]) + self.damage[1] def ratios(A,B): X = [] Y = [] for i in range (len(A)): a = A[i] b = B[i] if a == 0: continue X.append(i) Y.append(b/a) return X,Y #Fight simulators - with and without critical nc = 2 nwins = [] nwinsc = [] d6 = [] d10 = [] ac18 = [] ac14 = [] for hp in range (80): print hp, 'hp' nsim = 30000 nwins.append(0) nwinsc.append(0) ac14.append(0) ac18.append(0) d6.append(0) d10.append(0) d = 1.0/nsim while nsim > 0: #Raw, no critical allc = [Robot(hp+1,0,16,[8,0],player=True), Robot(30,0,16,[8,0],player=False)] allc.sort(key=operator.attrgetter('init'),reverse=True) #sort by initiative hpo = fight(allc,1) if hpo > 0: nwins[-1] += d #critical allc = [Robot(hp+1,0,16,[8,0],player=True), Robot(30,0,16,[8,0],player=False)] allc.sort(key=operator.attrgetter('init'),reverse=True) #sort by initiative hpo = fight(allc,2) if hpo > 0: nwinsc[-1] += d #lower AC allc = [Robot(hp+1,0,14,[8,0],player=True), Robot(30,0,16,[8,0],player=False)] allc.sort(key=operator.attrgetter('init'),reverse=True) hpo = fight(allc,2) if hpo > 0: ac14[-1] += d #higher AC allc = [Robot(hp+1,0,18,[8,0],player=True), Robot(30,0,16,[8,0],player=False)] allc.sort(key=operator.attrgetter('init'),reverse=True) hpo = fight(allc,2) if hpo > 0: ac18[-1] += d #higher damage allc = [Robot(hp+1,0,16,[10,0],player=True), Robot(30,0,16,[8,0],player=False)] allc.sort(key=operator.attrgetter('init'),reverse=True) #sort by initiative hpo = fight(allc,2) if hpo > 0: d10[-1] += d #lower damage allc = [Robot(hp+1,0,16,[6,0],player=True), Robot(30,0,16,[8,0],player=False)] allc.sort(key=operator.attrgetter('init'),reverse=True) #sort by initiative hpo = fight(allc,2) if hpo > 0: d6[-1] += d nsim -= 1 #plotting the critical effect fig = plt.figure() ax = fig.add_axes([0.1,0.1,0.8,0.8]) ax2 = ax.twinx() ax.plot(nwins,label='Without critical',linewidth=2,color='blue') ax.plot(nwinsc,label='With critical',linewidth=2,color='orange') ax.set_xlabel('player HP') ax.set_ylabel('probability of win') ax2.set_ylabel('probability ratio') #Ratio X,Y = ratios(nwins,nwinsc) ax2.plot(X,Y,color='blue',linestyle=':', linewidth = 2) ax.set_ylim(0,1.1) ax.legend(loc=5) fig.savefig('/tmp/naive.pdf') fig.savefig('/tmp/naive.png') plt.close() #plotting the damage and AC effect fig = plt.figure() ax = fig.add_axes([0.1,0.1,0.8,0.8]) ax2 = ax.twinx() ax.plot(nwinsc,color='black',label='Base',linewidth=2) ax.plot(d6,color='blue',label='d6',linewidth=2) ax.plot(d10,color='orange',label='d10',linewidth=2) ax.plot(ac14,color='cyan',label='AC - 14',linewidth=2) ax.plot(ac18,color='magenta',label='AC - 18',linewidth=2) #ratios X,Y = ratios(nwinsc,d6) ax2.plot(X,Y,color='blue',linewidth=2,linestyle=':') X,Y = ratios(nwinsc,d10) ax2.plot(X,Y,color='orange',linewidth=2,linestyle=':') X,Y = ratios(nwinsc,ac14) ax2.plot(X,Y,color='cyan',linewidth=2,linestyle=':') X,Y = ratios(nwinsc,ac18) ax2.plot(X,Y,color='magenta',linewidth=2,linestyle=':') ax.set_xlim(0,81) ax.legend(loc=5) ax.set_xlabel('HP') ax.set_ylabel('Probablity of win') ax2.set_ylabel('Probability ratio') fig.savefig('/tmp/acdd.pdf') fig.savefig('/tmp/acdd.png')

The fight function is placed in a file called fight.py and is imported by the main file in the forth line.

import numpy as np from collections import deque def fight(robot_list,crit_num,test=False): #A class to handle the fight between #several opponents. #Currently handle properly only two opponents. nc = len(robot_list) rlist = deque(robot_list) while True: j = np.random.randint(1,nc) aroll = rlist[0].attack_roll() droll = rlist[0].damage_roll() rlist[0].nhits[0] += 1 if rlist[0].crit(): for k in range (crit_num-1): droll += rlist[0].damage_roll() if aroll < rlist[j].ac and not rlist[0].crit() : rlist.rotate() continue rlist[0].nhits[1] += 1 if rlist[j].injure(droll): #true if is dead, break the loop if rlist[j].player: #the monster has won return 0 else: #The player has won return 1 rlist.rotate()