This is a wiki for a reason. Anyone can contribute. If you see something that is inaccurate or can be improved, don't ask that it be fixed--just improve it.
[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]

User:Bcbrown

From Project 1999 Wiki
Jump to: navigation, search

Contents

Building a Monte Carlo Markov Chain for Damage Simulation

I'd like to demonstrate all the steps required to build a Monte Carlo Markov Chain for damage simulation. I will be building one to simulate damage while charm kiting, but I'll build it as generally as possible to make it easy to apply to as many problems as possible. Let's start by defining some terms.

Monte Carlo

A Monte Carlo method is using repeated simulations of some process to answer mathematical questions that are too hard to solve directly. For example, say you wanted to calculate the odds of getting a straight flush in a five-card poker hand, and you didn't know how to calculate that directly. You could simulate dealing a thousand hands and just count up how many of them are straight flushes. We're going to repeatedly run a Monte Carlo process and calculate the average remaining health of a mob after breaking charm, and how frequently the mob dies before charm breaks.

Markov Chain

A Markov Chain is just a flow-chart with loops. You have a bunch of different possible states, and you have arrows, or transitions, between them. For example we can build a simple markov chain for hitting a mob with a weapon. There's only one state, with current health of the mob. There's two possible transitions: 1) if the last swing killed the mob, combat is over; 2) if the mob survived the last swing, swing again.

Implementing the Damage Simulation

This is going to use a very simple damage simulation using actual data derived from a log file of charm fights. I took the last 1000 hits of gator v gator charming in Cazic Thule and processes it into a file with two columns, damage and number of hits, called histogram. I then wrote a Python method to load it.

This method has to do several things. First, we're going to be thinking of damage in terms of a percentage of the mobs total health, not the raw damage done. Second, we're going to convert raw numbers of hits into frequencies. So the output of this will be something like:

8% 11.5%
7% 15.0%
6% 19.4%
5% 9.8%
4% 8.7%
3% 7.9%
2% 9.2%
1% 8.4%
0% 10.1%

The second part of damage simulation is using that data to roll some random numbers to determine the damage each round. I counted a 57.7% hit rate, or a 42.3% miss rate, so first we'll use that to see if the swing is a hit or a miss. If it's a hit, we'll compare a second random number to determine how much damage it does.

   from collections import defaultdict
   from random import random
   
   def build_histogram(filename='histogram'):
       with open('histogram') as f:
           max_damage = 62
           max_percent = 8
           scale_factor = max_percent / max_damage
           counts = defaultdict(int)
           total_hits = 0
           for line in f:
               damage, count = line.strip().split(' ')
               damage = int(damage)
               count = int(count)
               damage_percent = int(damage * scale_factor)
               counts[damage_percent] += count
               total_hits += count
           histogram = {}
           for damage, count in counts.items():
               percent = count / total_hits
               histogram[damage] = percent
       return histogram
   
   class Combat:
       def __init__(self):
           self.histogram = build_histogram()
           self.miss_percent = 1 - .577
       def hit(self):
           hit_roll = random()
           return hit_roll > self.miss_percent
       def damage(self):
           damage_roll = random()
           accumulator = 0
           for damage, percent in self.histogram.items():
               accumulator += percent
               if accumulator > damage_roll:
                   return damage
           return damage
       def swing(self):
           if not self.hit():
               return 0
           return self.damage()

Implementing the Markov Chains

As discussed earlier, a Markov Chain consists of states and transitions. We'll implement this by having each state be an object, and if you tell it the current mob health and the damage from the last combat round it'll tell you the next state to transition to. For an instant cast charm break like the Goblin Gazhugi Ring this is very simple. There's a single state. As soon as the mob health drops below some threshold, you click the ring, charm breaks, and the simulation is over.

The implementation for the Ring of Stealthy Travel is only slightly more complicated. First there's a waiting state that just transistions back to itself. As soon as the mob health drops below some threshold, you click the ring and move to a Casting state. Then, determined by the damage in the last combat round and the new mob health, you either duck and recast (transitioning back to the Casting state), or you let the cast go through and the simulation is over.

Finally, the end state when you break charm is called the Terminal state, and doesn't have to do anything. You can see those states below.

   Terminal = object()
   
   class BreakInstant:
       
       This is the only state for the GGR. As soon as the mob health hits the 
       threshold, break instantly and end
       
       def __init__(self, threshold):
           self.threshold = threshold
           self.next_state = Terminal
       def transition(self, mob_health, last_damage):
           if mob_health <= self.threshold:
               return self.next_state
           else:
               return self
       def reset(self):
           pass
   
   class Waiting:
        
       This is the initial state for the RoST. As soon as the mob health hits the
       threshold, cast invis and wait to see the outcome of the next round before
       deciding whether or not to duck
       
       def __init__(self, threshold, next_state):
           self.threshold = threshold
           self.next_state = next_state
       def transition(self, mob_health, last_damage):
           if mob_health <= self.threshold:
               return self.next_state
           else:
               return self
       def reset(self):
           self.next_state.reset()
   
   class Casting:
        
       This is the state where we've cast the RoST. Depending on the outcome of
       the next combat round we'll either duck the cast or let it go through
       
       def __init__(self, threshold):
           self.threshold = threshold
           self.num_ducks = 0 
       def transition(self, mob_health, last_damage):
           if mob_health <= self.threshold:
               # mob's injured enough, let the cast go through
               return Terminal
           else:
               # duck
               self.num_ducks += 1
               self.current_ducks += 1
               if self.current_ducks > self.max_ducks:
                   self.max_ducks = self.current_ducks
               return self
       def reset(self):
           self.all_ducks[self.current_ducks] += 1
           self.current_ducks = 0

Implementing the Monte Carlo simulation

There's two methods in this implementation, first to run the simulation for a single charm fight, second to repeat it thousands of times and calculate the results.

To run a single simulation you repeat a small loop. First you simulate a combat round and update the mob's health with the damage done. If the mob dies, you end the simulation. Otherwise you transition to the next state in the Markov chain, to allow the user to choose whether to start casting, duck and recast, or allow a spell to finish. Repeat until the cast finishes and charm breaks or the mob dies.

   import markov
   import combat
   DeadMob = object()
   engine = combat.Combat()
   def run_once(state):
       mob_health = 100 
       while state is not markov.Terminal:
           damage = engine.swing()
           mob_health -= damage
           if mob_health < 0:
               return DeadMob
           state = state.transition(mob_health, damage)
       return mob_health

The other method runs the simulation as many times as requested. It tracks the outcome of each simulation: whether the mob dies before charm is broken, and if charm is broken, what percentage of health the mob is left at. Then after running all the simulations it computes the percentage of lost mobs and the average health remaining.

   def run_loop(starting_state, num_loops):
       mob_deaths = 0 
       final_healths = []
       for i in range(num_loops):
           result = run_once(starting_state)
           if result is DeadMob:
               mob_deaths += 1
           else:
               final_healths.append(result)
       mob_death_percentage = 100 * mob_deaths / num_loops
       print("Percentage of lost mobs: " + str(mob_death_percentage) + "%")
       average_health_remaining = sum(final_healths) / num_loops
       print("Average health percentage remaining: " + str(average_health_remaining))

The final section launches the simulation of each approach and also calculates the average number of ducks when using the RoST:

   def count_ducks(all_ducks, threshold):
       ducks_over_10 = 0
       for k, v in all_ducks.items():
           if k >= threshold:
               ducks_over_10 += v
       print("Frequency of {}++ ducks: {}".format(threshold, str(100 * ducks_over_10/num_loops)))
   
   if __name__ == '__main__':
       num_loops = 10 * 1000
       print("Running simulation for GGR")
       starting_state = markov.BreakInstant(9)
       run_loop(starting_state, num_loops)
       
       print("Running simulation for RoST")
       casting_state = markov.Casting(9)
       starting_state = markov.Waiting(18, casting_state)
       run_loop(starting_state, num_loops)
       
       print("Average number of ducks: " + str(casting_state.num_ducks / num_loops))
       print("Max ducks: " + str(casting_state.max_ducks))
       
       all_ducks = casting_state.all_ducks
       count_ducks(all_ducks, 5)
       count_ducks(all_ducks, 10)