def get_damage(sp, hit, crit, num_mages, rotation, response, sim_size):
    C = Constant(sim_size=sim_size)
    arrays = init_arrays(C, num_mages, response)
    if  C._LOG_SIM >= 0:
        log_message(sp, hit, crit)

    while advance(arrays, sp, hit, crit, rotation, sim_size):
        still_going = np.where(arrays['running_time'] < arrays['duration'])[0]
        arrays['total_damage'][still_going] += arrays['damage'][still_going]
    if  C._LOG_SIM >= 0:
        print('total damage = {:7.0f}'.format(arrays['total_damage'][ C._LOG_SIM][0]))

    return (arrays['total_damage']/arrays['duration']).mean()
Esempio n. 2
0
import os
import hashlib
import subprocess
import sys
from mako.template import Template
import cv2
from constants import Constant
import numbers
from subprocess import call

constant = Constant()


def set_workspace(ws):
    constant.set_workspace(ws)
    if not os.path.isdir(ws):
        os.makedirs(ws)


def dir(path):
    dir_path = constant.get_workspace() + "/" + path
    file_dir = os.path.dirname(dir_path)
    if not os.path.isdir(file_dir):
        os.makedirs(file_dir)
    return dir_path


def file_already_exists(file_path):
    if os.path.isfile(file_path):
        checksum_file = file_path + ".checksum"
        if os.path.isfile(checksum_file):
def advance(arrays, plus_damage, hit_chance, crit_chance, rotation, sim_size):
    C = Constant(sim_size=sim_size)

    total_damage = arrays['total_damage']
    ignite_count = arrays['ignite_count']
    ignite_time = arrays['ignite_time']
    ignite_tick = arrays['ignite_tick']
    ignite_value = arrays['ignite_value']
    scorch_count = arrays['scorch_count']
    scorch_time = arrays['scorch_time']
    running_time = arrays['running_time']
    cast_timer = arrays['cast_timer']
    cast_type = arrays['cast_type']
    comb_stack = arrays['comb_stack']
    comb_left = arrays['comb_left']
    spell_timer = arrays['spell_timer']
    spell_type = arrays['spell_type']
    cast_number = arrays['cast_number']
    duration = arrays['duration']
    arrays['damage'] = np.zeros((C._SIM_SIZE, 1))
    damage = arrays['damage']

    epsilon = 0.000001

    num_mages = cast_timer.shape[1]
    extra_scorches = C._EXTRA_SCORCHES[num_mages]

    still_going = np.where(running_time < duration)[0]
    if still_going.size == 0:
        return False

    cast_time = np.min(cast_timer[still_going], axis=1, keepdims=True)
    spell_time = np.min(spell_timer[still_going], axis=1, keepdims=True)
    ignite_copy = np.copy(ignite_time[still_going])

    zi_array = np.logical_and(np.logical_and(ignite_copy < cast_time,
                                             ignite_count[still_going]),
                              np.logical_or(ignite_copy < scorch_time[still_going],
                                            np.logical_not(scorch_count[still_going])))
    zi_array = np.logical_and(zi_array, ignite_copy < ignite_tick[still_going])
    zi_array = np.logical_and(zi_array, ignite_copy < spell_time)
    zero_ignite = np.where(zi_array)[0]
    if zero_ignite.size > 0:
        running_time[still_going[zero_ignite]] += ignite_time[still_going[zero_ignite]]
        cast_timer[still_going[zero_ignite], :] -= ignite_time[still_going[zero_ignite]]
        spell_timer[still_going[zero_ignite], :] -= ignite_time[still_going[zero_ignite]]
        scorch_time[still_going[zero_ignite]] -= ignite_time[still_going[zero_ignite]]
        if C._LOG_SIM >= 0:
            if C._LOG_SIM in still_going[zero_ignite]:
                sub_index = still_going[zero_ignite].tolist().index(C._LOG_SIM)
                message = ' {:7.0f} ({:6.2f}): ignite expired'
                print(message.format(total_damage[C._LOG_SIM][0] + damage[C._LOG_SIM][0], running_time[C._LOG_SIM][0]))
        ignite_count[still_going[zero_ignite]] = 0
        ignite_time[still_going[zero_ignite]] = 0.0
        ignite_value[still_going[zero_ignite]] = 0.0
        ignite_tick[still_going[zero_ignite]] = 0.0

    ti_array = np.logical_and(ignite_tick[still_going] < cast_time,
                              ignite_tick[still_going] < scorch_time[still_going])
    ti_array = np.logical_and(ti_array, ignite_count[still_going])
    ti_array = np.logical_and(ti_array, ignite_tick[still_going] < spell_time)
    tick_ignite = np.where(ti_array)[0]
    if tick_ignite.size > 0:
        running_time[still_going[tick_ignite]] += ignite_tick[still_going[tick_ignite]]
        cast_timer[still_going[tick_ignite], :] -= ignite_tick[still_going[tick_ignite]]
        spell_timer[still_going[tick_ignite], :] -= ignite_tick[still_going[tick_ignite]]
        scorch_time[still_going[tick_ignite]] -= ignite_tick[still_going[tick_ignite]]
        ignite_time[still_going[tick_ignite]] -= ignite_tick[still_going[tick_ignite]]
        ignite_tick[still_going[tick_ignite]] = C._IGNITE_TICK
        multiplier = 1.0 + 0.03*scorch_count[still_going[tick_ignite]]
        damage[still_going[tick_ignite]] += ignite_value[still_going[tick_ignite]]*multiplier
        if  C._LOG_SIM >= 0:
            if  C._LOG_SIM in still_going[tick_ignite]:
                sub_index = still_going[tick_ignite].tolist().index( C._LOG_SIM)
                message = ' {:7.0f} ({:6.2f}): ignite ticked   {:4.0f} damage done'
                print(message.format(total_damage[ C._LOG_SIM][0] + damage[ C._LOG_SIM][0], running_time[ C._LOG_SIM][0], ignite_value[ C._LOG_SIM][0]*multiplier[sub_index][0]))

    ig_array = np.logical_or(zi_array, ti_array)
    se_array = np.logical_and(np.logical_and(np.logical_not(ig_array),
                                             scorch_time[still_going] < cast_time),
                              scorch_count[still_going])
    
    se_array = np.logical_and(se_array, ignite_copy < spell_time)
    scorch_expire = np.where(se_array)[0]
    if scorch_expire.size > 0:
        if  C._LOG_SIM >= 0:
            if  C._LOG_SIM in still_going[scorch_expire]:
                message = '         ({:6.2f}): scorch expired {:4.2f}'
                print(message.format(running_time[ C._LOG_SIM][0], scorch_time[still_going[scorch_expire]][0][0]))
        running_time[still_going[scorch_expire]] += scorch_time[still_going[scorch_expire]]
        cast_timer[still_going[scorch_expire], :] -= scorch_time[still_going[scorch_expire]]
        spell_timer[still_going[scorch_expire], :] -= scorch_time[still_going[scorch_expire]]
        ignite_time[still_going[scorch_expire]] -= scorch_time[still_going[scorch_expire]]
        ignite_tick[still_going[scorch_expire]] -= scorch_time[still_going[scorch_expire]]
        scorch_count[still_going[scorch_expire]] = 0
        scorch_time[still_going[scorch_expire]] = 0.0

    cast_array = np.logical_not(np.logical_or(ig_array, se_array))
    cast_array = np.logical_and(cast_array, cast_time < spell_time)
    cast_ends = np.where(cast_array)[0]
    if cast_ends.size > 0:
        cst = still_going[cast_ends]
        next_hit = np.argmin(cast_timer[cst, :], axis=1)
        add_time = np.min(cast_timer[cst, :], axis=1, keepdims=True)
        running_time[cst] += add_time
        ignite_time[cst] -= add_time
        ignite_tick[cst] -= add_time
        cast_timer[cst] -= add_time
        spell_timer[cst] -= add_time
        scorch_time[cst] -= add_time
       
        react_time = np.abs(C._CONTINUING_SIGMA*np.random.randn(cst.size))
        cast_copy = np.copy(cast_type[cst, next_hit])

        if C._LOG_SIM >= 0:
            if  C._LOG_SIM in cst:
                message = '         ({:6.2f}): player {:d} finished casting {:s}'
                sub_index = cst.tolist().index( C._LOG_SIM)
                message = message.format(running_time[ C._LOG_SIM][0],
                                         next_hit[sub_index] + 1,
                                         C._LOG_SPELL[cast_copy[sub_index]])
                print(message)

        # special spell next
        special_array = cast_number[cst, next_hit] == extra_scorches
        special = np.where(special_array)[0]
        if rotation == C._FIRE_BLAST:
            cast_timer[cst[special], next_hit[special]] = epsilon + react_time[special]
            cast_type[cst[special], next_hit[special]] = C._CAST_FIRE_BLAST
        elif rotation == C._FROSTBOLT:
            cast_timer[cst[special], next_hit[special]] = C._FIREBALL_CASTTIME + C._FROSTBOLT_CASTTIME + react_time[special]
            cast_type[cst[special], next_hit[special]] = C._CAST_FIREBALL
            damage[cst[special]] += hit_chance*(1 + C._FROSTBOLT_CRIT_DAMAGE*(crit_chance - C._FROSTBOLT_CRIT_MOD))*(C._FROSTBOLT_DAMAGE + C._FROSTBOLT_MODIFIER*(plus_damage - C._FROSTBOLT_PLUS))*C._FROSTBOLT_OVERALL
        else:
            cast_timer[cst[special], next_hit[special]] = C._PYROBLAST_CASTTIME + react_time[special]
            cast_type[cst[special], next_hit[special]] = C._CAST_PYROBLAST

        # scorch next
        scorcher = np.logical_not(np.logical_or(next_hit, special_array)) # scorch mage only
        scorch_array = np.logical_and(scorcher,
                                      np.logical_or(np.squeeze(scorch_count[cst]) < C._SCORCH_STACK,
                                                    np.squeeze(scorch_time[cst]) < C._MAX_SCORCH_REMAIN))
        scorch_array = np.logical_and(scorch_array, cast_copy != C._CAST_SCORCH)
        scorch_array = np.logical_and(scorch_array, cast_number[cst, next_hit] > extra_scorches + 1)
        # now scorch array is just for scorcher
        scorch_array = np.logical_or(scorch_array, cast_number[cst, next_hit] < extra_scorches)
        # now everyone
        scorch = np.where(scorch_array)[0]
        cast_timer[cst[scorch], next_hit[scorch]] = C._SCORCH_CASTTIME + react_time[scorch]
        cast_type[cst[scorch], next_hit[scorch]] = C._CAST_SCORCH

        # fireball next
        fireball = np.where(np.logical_not(np.logical_or(scorch_array, special_array)))[0]
        cast_timer[cst[fireball], next_hit[fireball]] = C._FIREBALL_CASTTIME + react_time[fireball]
        cast_type[cst[fireball], next_hit[fireball]] = C._CAST_FIREBALL

        spell_type[cst, next_hit] = cast_copy
        spell_timer[cst, next_hit] = C._SPELL_TIME[cast_copy]
        if rotation == C._FIRE_BLAST:
            gcd = np.where(cast_number[cst, next_hit] == extra_scorches + 1)[0]
            cast_timer[cst[gcd], next_hit[gcd]] += C._GLOBAL_COOLDOWN
        cast_number[cst, next_hit] += 1

    spell_lands = np.where(np.logical_not(np.logical_or(np.logical_or(ig_array, se_array), cast_array)))[0]
    if spell_lands.size > 0:
        spl = still_going[spell_lands]
        next_hit = np.argmin(spell_timer[spl, :], axis=1)
        add_time = np.min(spell_timer[spl, :], axis=1, keepdims=True)
        running_time[spl] += add_time
        ignite_time[spl] -= add_time
        ignite_tick[spl] -= add_time
        cast_timer[spl] -= add_time
        spell_timer[spl] -= add_time
        scorch_time[spl] -= add_time

        spell_copy = spell_type[spl, next_hit]

        if  C._LOG_SIM >= 0:
            if  C._LOG_SIM in spl:
                message = ' ({:6.2f}): player {:d} {:s} landed '
                sub_index = spl.tolist().index( C._LOG_SIM)
                message = message.format(running_time[ C._LOG_SIM][0],
                                         next_hit[sub_index] + 1,
                                         C._LOG_SPELL[spell_copy[sub_index]])
                message2 = 'misses         '

        for spell in range(C._CASTS):
            
            is_spell = np.where(spell_copy == spell)[0]
            spell_hits = np.where(np.random.rand(is_spell.size) < hit_chance)[0]
            if spell_hits.size > 0:

                sph = spl[is_spell][spell_hits]
                spell_damage = C._SPELL_BASE[spell] + \
                               C._SPELL_RANGE[spell]*np.random.rand(sph.size, 1) +\
                               C._MULTIPLIER[spell]*plus_damage
                spell_damage *= (1.0 + 0.03*scorch_count[sph])*C._DAMAGE_MULTIPLIER
                # ADD ADDITIONAL OVERALL MULTIPLIERS TO _DAMAGE_MULTIPLIER

                # handle critical hit/ignite ** READ HERE FOR MOST OF THE IGNITE MECHANICS **
                ccrit_chance = crit_chance + C._PER_COMBUSTION*comb_stack[sph, next_hit[is_spell][spell_hits]]*comb_left[sph, next_hit[is_spell][spell_hits]]
                crit_array = np.random.rand(sph.size) < ccrit_chance
                lcrits = np.where(crit_array)[0]
                crits = sph[lcrits]
                # refresh ignite to full 4 seconds
                ignite_time[crits] = C._IGNITE_TIME + epsilon
                # if we dont have a full stack
                mod_val = np.where(ignite_count[crits] < C._IGNITE_STACK)[0]
                # add to the ignite tick damage -- 1.5 x  0.2 x spell hit damage
                ignite_value[crits[mod_val]] += C._CRIT_DAMAGE*C._IGNITE_DAMAGE*spell_damage[lcrits[mod_val]]
                mod_val2 = np.where(ignite_count[crits] == 0)[0]
                # set the tick
                ignite_tick[crits[mod_val2]] = C._IGNITE_TICK
                # increment to max of five (will do nothing if alreeady at 5)
                ignite_count[crits] = np.minimum(ignite_count[crits] + 1, C._IGNITE_STACK)
                damage[crits] += C._CRIT_DAMAGE*spell_damage[lcrits]
                comb_left[crits, next_hit[is_spell][spell_hits][lcrits]] = np.maximum(comb_left[crits, next_hit[is_spell][spell_hits][lcrits]] - 1, 0)

                # normal hit
                nocrits = np.where(np.logical_not(crit_array))[0]
                damage[sph[nocrits]] += spell_damage[nocrits]

                if  C._LOG_SIM >= 0:
                    if  C._LOG_SIM in sph:
                        sub_index = sph.tolist().index(C._LOG_SIM)
                        if  C._LOG_SIM in crits:
                            message2 = 'crits for {:4.0f} '.format(C._CRIT_DAMAGE*spell_damage[sub_index][0])
                        else:
                            message2 = ' hits for {:4.0f} '.format(spell_damage[sub_index][0])

                # scorch
                if C._IS_SCORCH[spell]:
                    scorch_time[sph] = C._SCORCH_TIME
                    scorch_count[sph] = np.minimum(scorch_count[sph] + 1, C._SCORCH_STACK)
                    
                comb_stack[sph, next_hit[is_spell][spell_hits]] += 1
        spell_timer[spl, next_hit] = C._DURATION_AVERAGE

        # cast combustion before pyroblast (don't apply to scorch)
        comb_array = cast_number[spl, next_hit] == int(rotation == C._FIRE_BLAST) + extra_scorches + 1
        do_comb = np.where(comb_array)[0]
        if  C._LOG_SIM >= 0:
            cmessage = ''
            if C._LOG_SIM in spl[do_comb]:
                sub_index = spl[do_comb].tolist().index(C._LOG_SIM)
                cmessage = '         (------): combustion cast by player {:d}'.format(next_hit[do_comb[sub_index]] + 1)
        comb_left[spl[do_comb], next_hit[do_comb]] = C._COMBUSTIONS
        comb_stack[spl[do_comb], next_hit[do_comb]] = 0


        if  C._LOG_SIM >= 0:
            if C._LOG_SIM in spl:
                if cmessage:
                    print(cmessage)
                sub_index = spl.tolist().index(C._LOG_SIM)
                dam_done = ' {:7.0f}'.format(total_damage[ C._LOG_SIM][0] + damage[C._LOG_SIM][0])
                message3 = C._LOG_SPELL[cast_type[C._LOG_SIM][next_hit[sub_index]]]
                message = message + message2 + 'next is ' + message3
                status = ' ic {:d} it {:4.2f} in {:4.2f} id {:4.0f} sc {:d} st {:5.2f} cs {:2d} cl {:d}'
                status = status.format(ignite_count[C._LOG_SIM][0],
                                       max([ignite_time[C._LOG_SIM][0], 0.0]),
                                       max([ignite_tick[C._LOG_SIM][0], 0.0]),
                                       ignite_value[C._LOG_SIM][0],
                                       scorch_count[C._LOG_SIM][0],
                                       max([scorch_time[C._LOG_SIM][0], 0.0]),
                                       comb_stack[C._LOG_SIM][next_hit[sub_index]],
                                       comb_left[C._LOG_SIM][next_hit[sub_index]])
                print(dam_done + message + status)

    
    return True
def main():
    C = Constant()
    t0 = time.time()

    spell_damage = np.arange(C._SP_START, C._SP_END + C._SP_STEP/2.0, C._SP_STEP)
    hit_chance = np.arange(C._HIT_START, C._HIT_END + C._HIT_STEP/2.0, C._HIT_STEP)
    crit_chance = np.arange(C._CRIT_START, C._CRIT_END + C._CRIT_STEP/2.0, C._CRIT_STEP)
    nmages = np.arange(1, C._MAGES_END + 1, dtype=np.int32)

    for hit in hit_chance:
        for spd in spell_damage:
            # First look through the 3 {scorch -> spell -> fireball} rotations
            # where spell can be fire blast, frostbolt, or pyroblast.
            # The best rotation is recorded for each combination of:
            #   * spell power
            #   * hit chance
            #   * crit chance
            #   * number of mages
            filename = '../savestates/rotation_{:3.0f}_{:2.0f}.dat'.format(spd, 100.0*hit)
            if _DO_ROTATION_SEARCH:
                sps = np.array([spd])
                hits = np.array([hit])
                sim_size = np.array([C._ROTATION_SIMSIZE]).astype(np.int32)
                big_desc = ['Damage(frostbolt rotation)/Damage(pyroblast rotation)',
                            'Damage(fire blast rotation)/Damage(pyroblast rotation)']
                big_fn_desc = ['frostbolt', 'fire_blast']
                big_rotations = [C._FROSTBOLT, C._FIRE_BLAST]
            
                if _DO_RESPONSE_SEARCH:
                    responses = np.arange(0.0, 3.05, 0.10)
                else:
                    responses = np.array([C._INITIAL_SIGMA])
        
                print('Hit = {:2.0f} spd = {:3.0f}'.format(100.0*hit, spd))
                rotation_map = -np.ones((len(crit_chance), len(nmages)), dtype=np.int16)
                damage_map = np.zeros((len(crit_chance), len(nmages)))
                for rindex, (rotation, desc, fn_desc) in enumerate(zip(big_rotations, big_desc, big_fn_desc)):
                    rotations = np.array([0, rotation], dtype=np.int32)
            
                    args = itertools.product(sps, hits, crit_chance, nmages, rotations, responses, sim_size)
                    largs = [*args]
                    mean_mages = adjust_sim_size(largs)
                    
                    print('  Starting {:d} sims with {:d} samples each.  Estimated run time is {:.2f} minutes.'.format(len(largs), C._ROTATION_SIMSIZE, 8e-5*len(largs)*mean_mages*C._ROTATION_SIMSIZE/60.0))
                    with Pool() as p:
                        out = np.array(p.starmap(get_damage, largs)).reshape((len(sps), len(hits), len(crit_chance), len(nmages), len(rotations), len(responses), len(sim_size)))

                    if _DO_RESPONSE_SEARCH:
                        resp_index = np.argmin(np.abs(responses  - C._INITIAL_SIGMA))
                        dd = [np.squeeze(out[:, :, :, :, 0, resp_index]),
                              np.squeeze(out[:, :, :, :, 1, resp_index])]
                    else:
                        dd = [np.squeeze(out[:, :, :, :, 0, :]), 
                              np.squeeze(out[:, :, :, :, 1, :])]
                    for index, damage in zip([0, rotation], dd):
                        replacer = np.where(damage > damage_map)
                        damage_map[replacer] = damage[replacer]
                        rotation_map[replacer] = index

                    if _DO_RESPONSE_SEARCH:
                        for index, crit in enumerate(crit_chance):
                            damages = np.squeeze(out[:, :, index, :, 1, :])/np.squeeze(out[:, :, index, :, 0, :])
                            plots.plot_response(responses, damages, spd, hit, crit, fn_desc, desc, nmages, sim_size[0], C._DURATION_AVERAGE)

                os.makedirs('../savestates/', exist_ok=True)
                with open(filename, 'wb') as fid:
                    pickle.dump(sim_size, fid)
                    pickle.dump(rotation_map, fid)
                    pickle.dump(damage_map, fid)
                        
                plots.plot_rotation(crit_chance, nmages, rotation_map, spd, hit, sim_size[0], C._DURATION_AVERAGE, C._INITIAL_SIGMA)

        if _DO_ROTATION_SEARCH:
            print('{:2.0f} hit rotation/spread complete after {:.0f} seconds'.format(hit*100.0, time.time() - t0))

        # Calculate spell power to crit chance equivalency
        # Use the previously computed best rotation for each parameter set
        for num_mages in nmages:
            filename = '../savestates/crit_equiv_{:2.0f}_{:d}.dat'.format(100.0*hit, num_mages)
            if _DO_CRIT_SP_EQUIV:
                hits = np.array([hit])
                mages = np.array([num_mages])
                sim_size = C._CRIT_SIMSIZE*nmages.astype(np.float32).mean()/num_mages
                sim_size = np.array([sim_size]).astype(np.int32)
                rotations = np.array([C._DEFAULT_ROTATION]).astype(np.int16)
                responses = np.array([C._INITIAL_SIGMA])

                args = itertools.product(spell_damage, hits, crit_chance, mages, rotations, responses, sim_size)
                largs = [*args]
                if C._ADAPT_ROTATION:
                    adjust_rotation(largs, crit_chance, nmages)

                print('Hit = {:2.0f} # mages = {:d}'.format(100.0*hit, num_mages))

                lsim_size = 0
                if os.path.exists(filename):
                    with open(filename, 'rb') as fid:
                        lsim_size = pickle.load(fid)[0]
                        conversions = pickle.load(fid)
                if lsim_size != sim_size[0]:
                    print('  Starting {:d} sims with {:d} mages and {:d} samples.  Estimated run time is {:.2f} minutes.'.format(len(largs), num_mages, sim_size[0], 8e-5*len(largs)*num_mages*sim_size[0]/60.0))
                    
                    with Pool() as p:
                        out = np.array(p.starmap(get_crit_damage_diff, largs)).reshape((len(spell_damage), len(hits), len(crit_chance), len(mages), len(rotations), len(responses), len(sim_size)))
                    conversions = np.squeeze(out)

                os.makedirs('../savestates/', exist_ok=True)
                with open(filename, 'wb') as fid:
                    pickle.dump(sim_size, fid)
                    pickle.dump(conversions, fid)

                plots.plot_equiv(spell_damage, crit_chance, conversions, hit, num_mages, sim_size[0], C._DURATION_AVERAGE, 'crit')


        # Calculate spell power to hit chance equivalency
        # Use the previously computed best rotation for each parameter set
        for num_mages in nmages:
            filename = '../savestates/hit_equiv_{:2.0f}_{:d}.dat'.format(100.0*hit, num_mages)
            if _DO_HIT_SP_EQUIV:
                hits = np.array([hit])
                mages = np.array([num_mages])
                sim_size = C._HIT_SIMSIZE*nmages.astype(np.float32).mean()/num_mages
                sim_size = np.array([sim_size]).astype(np.int32)
                rotations = np.array([C._DEFAULT_ROTATION]).astype(np.int16)
                responses = np.array([C._INITIAL_SIGMA])

                args = itertools.product(spell_damage, hits, crit_chance, mages, rotations, responses, sim_size)
                largs = [*args]
                if C._ADAPT_ROTATION:
                    adjust_rotation(largs, crit_chance, nmages)

                print('Hit = {:2.0f} # mages = {:d}'.format(100.0*hit, num_mages))

                lsim_size = 0
                if os.path.exists(filename):
                    with open(filename, 'rb') as fid:
                        lsim_size = pickle.load(fid)[0]
                        conversions = pickle.load(fid)
                if lsim_size != sim_size[0]:
                    print('  Starting {:d} sims with {:d} mages and {:d} samples each.  Estimated run time is {:.2f} minutes.'.format(len(largs), num_mages, sim_size[0], 8e-5*len(largs)*num_mages*sim_size[0]/60.0))
                    with Pool() as p:
                        out = np.array(p.starmap(get_hit_damage_diff, largs)).reshape((len(spell_damage), len(hits), len(crit_chance), len(mages), len(rotations), len(responses), len(sim_size)))
                    conversions = np.squeeze(out)

                os.makedirs('../savestates/', exist_ok=True)
                with open(filename, 'wb') as fid:
                    pickle.dump(sim_size, fid)
                    pickle.dump(conversions, fid)

                plots.plot_equiv(spell_damage, crit_chance, conversions, hit, num_mages, sim_size[0], C._DURATION_AVERAGE, 'hit')


    if _DO_HIT_SP_EQUIV or _DO_CRIT_SP_EQUIV:
        os.makedirs('../data/', exist_ok=True)
        fid = open('../data/equivalencies.csv', 'wt')
        fid.write('spell damage,hit chance,crit chance,number of mages,hit_simulations,crit_simulations,hit equivalency,crit equivalency\n')
        for hit in hit_chance:
            for num_mages in nmages:
                have_files = [False, False]
                filename = '../savestates/hit_equiv_{:2.0f}_{:d}.dat'.format(100.0*hit, num_mages)
                if os.path.exists(filename):
                    have_files[0] = True
                    with open(filename, 'rb') as fid2:
                        sim_size1 = pickle.load(fid2)
                        hit_conversions = pickle.load(fid2)
                    filename = '../savestates/crit_equiv_{:2.0f}_{:d}.dat'.format(100.0*hit, num_mages)
                if os.path.exists(filename):
                    have_files[1] = True
                    with open(filename, 'rb') as fid2:
                        sim_size2 = pickle.load(fid2)
                        crit_conversions = pickle.load(fid2)
                if all(have_files):
                    for sindex, spd in enumerate(spell_damage):
                        for cindex, crit in enumerate(crit_chance):
                            output = '{:3.0f},{:2.0f},{:2.0f},{:d},{:d},{:d},{:6.3f},{:6.3f}\n'
                            output = output.format(spd, 100.0*hit, 100.0*crit, num_mages,
                                                   sim_size1[0], sim_size2[0],
                                                   hit_conversions[sindex, cindex],
                                                   crit_conversions[sindex, cindex])
                            fid.write(output)
                            
        fid.close()

    if _DO_DPS_PER_MAGE:
        os.makedirs('../data/', exist_ok=True)
        fid = open('../data/damage_per_mage.csv', 'wt')
        fid.write('spell damage,hit chance,crit chance,number of mages,simulations,dps per mage\n')
        for spd in spell_damage:
            for hit in hit_chance:
                filename = '../savestates/rotation_{:3.0f}_{:2.0f}.dat'.format(spd, 100.0*hit)
                if os.path.exists(filename):
                    with open(filename, 'rb') as fid2:
                        sim_size = pickle.load(fid2)
                        rotation_map = pickle.load(fid2)
                        damage_map = pickle.load(fid2)
                    for cindex, crit in enumerate(crit_chance):
                        for nindex, num_mages in enumerate(nmages):
                            damage_map[cindex, nindex] /= num_mages
                            output = '{:3.0f},{:2.0f},{:2.0f},{:d},{:d},{:.2f}\n'
                            output = output.format(spd, 100.0*hit, 100.0*crit, num_mages, sim_size[0],
                                                   damage_map[cindex, nindex])
                            fid.write(output)
                    plots.plot_dps(crit_chance, nmages, damage_map, spd, hit, sim_size[0], C._DURATION_AVERAGE, C._INITIAL_SIGMA)
        fid.close()