How to choose equipment for a game Persian in Python

We learn to find the best for our robber with the help of programming. We also figure out if the program is driving us “by the nose”.

image

Purpose: to learn to stage-by-stage simulate the necessary part of the game mechanics in a “test tube”, obtain the necessary data and draw conclusions from them.

What you need: Python 3, the environment for working with code (I have PyCharm).

In games, many people want to squeeze the most out of their characters, and for this you need to choose the most optimal combination of equipment, which is often a lot. Let's try to write our own algorithm for testing various combinations of equipment and collecting data.

Initially, I was inspired by the game “World of Warcraft: Classic” (I took the icons from there ), but in the process I made some simplifications. Link to the entire project at the end of the article.

STEP 1 - evaluate the search area


Suppose we have a character in the Rogue class. It is necessary to pick up equipment for him in which he will inflict maximum damage to the enemy. We are interested in things for the slots “weapons in the right hand” (4 pcs.), “Weapons in the left hand” (4 pcs.), “Gloves” (2 pcs.), “Head” (3 pcs.), “Chest” (3 pcs.), “Legs” (3 pcs.), “Feet” (2 pcs.). We will put on their various combinations on the character and simulate the battle. And if you apply the idea of ​​exhaustive search (with which we will begin), to evaluate all the combinations you will have to spend at least 4 * 4 * 2 * 3 * 3 * 3 * 2 = 1728 fights.

For a more accurate assessment of the best combinations, you will need to conduct additional battles.

So, already at this stage we can present the project scheme as follows:

image

STEP 2 - analyze game mechanics


Let's start with the character. He has such characteristics that affect the damage done and on each other:

  1. attack power - it is converted directly to the damage caused by a normal blow (1 to 1). Calculated by the formula: points of attack power + points of strength + points of dexterity
  2. Strength - +1 to attack power and all (what to do, this is the game design)
  3. Dexterity - +1 to attack power, and also every 20 units of agility add 1% critical chance
  4. Crete. chance - the chance of causing double damage if the strike is not sliding and miss
  5. accuracy - increased chance to hit an opponent
  6. mastery - each unit of mastery reduces the probability of a sliding strike by 4% (which is initially equal to 40%, which means that 10 units of mastery completely exclude the possibility of sliding impact)

The diagram below shows the basic values ​​for our robber and how putting on an item of equipment changes them:

image

So, it's time to start writing code. We describe what we already know in the Rogue class. The set_stats_without_equip method will restore the state of the character without equipment, which is useful when changing collections. The methods calculate_critical_percent and calculate_glancing_percent in the future will be called only if necessary, updating the values ​​of specific characteristics.

first lines of class
class Rogue:
    """    ."""

    def __init__(self):

        #    ( -     ):
        self.basic_stat_agility = 50
        self.basic_stat_power = 40
        self.basic_stat_hit = 80
        self.basic_stat_crit = 20
        self.basic_stat_mastery = 0

        #     :
        self.set_stats_without_equip()


    #       :
    def set_stats_without_equip(self):
        self.stat_agility = self.basic_stat_agility
        self.stat_power = self.basic_stat_power
        self.stat_attackpower = self.stat_agility + self.stat_power
        self.stat_hit = self.basic_stat_hit
        self.direct_crit_bonus = 0
        self.calculate_critical_percent()
        self.stat_mastery = self.basic_stat_mastery
        self.calculate_glancing_percent()


    #      :
    def calculate_critical_percent(self):
        self.stat_crit = self.basic_stat_crit + self.direct_crit_bonus + self.stat_agility // 20


    #      :
    def calculate_glancing_percent(self):
        self.stat_glancing_percent = 40 - self.stat_mastery * 4


Now you need to deal with the equipment. In order to conveniently sort through all things, creating their combinations, I decided to create a separate dictionary for each type of equipment: RIGHT_HANDS, LEFT_HANDS, GLOVES, HEADS, CHESTS, PANTS, BOOTS. The following tuples are stored as values ​​in the dictionaries:

image

Create a separate file for dictionaries with equipment. I have several such files with different sets.

abstract test equipment
#    ,     :
# 0 - , 1 - , 2 - , 3 - , 4 - , 5 - , 6 - 

EQUIPMENT_COLLECTION = 'custom'

RIGHT_HANDS = dict()
RIGHT_HANDS[1] = ('  ', 50, 3, 0, 0, 0, 0)
RIGHT_HANDS[2] = (' ', 40, 22, 0, 0, 0, 0)
RIGHT_HANDS[3] = (' ', 40, 0, 0, 3, 0, 0)
RIGHT_HANDS[4] = (' ', 40, 0, 0, 0, 0, 5)

LEFT_HANDS = dict()
LEFT_HANDS[1] = ('  ', 35, 3, 0, 0, 0, 0)
LEFT_HANDS[2] = (' ', 40, 22, 0, 0, 0, 0)
LEFT_HANDS[3] = (' ', 40, 0, 0, 3, 0, 0)
LEFT_HANDS[4] = (' ', 40, 0, 0, 0, 0, 5)

GLOVES = dict()
GLOVES[1] = (' ', 0, 12, 0, 2, 0, 0)
GLOVES[2] = (' ', 2, 2, 2, 1, 1, 0)

HEADS = dict()
HEADS[1] = (' ', 0, 22, 0, 0, 0, 0)
HEADS[2] = (' ', 0, 0, 0, 0, 2, 0)
HEADS[3] = (' ', 0, 0, 0, 2, 0, 0)

CHESTS = dict()
CHESTS[1] = (' ', 0, 30, 0, 0, 0, 0)
CHESTS[2] = (' ', 0, 0, 0, 0, 3, 0)
CHESTS[3] = (' ', 0, 0, 0, 3, 0, 0)

PANTS = dict()
PANTS[1] = (' ', 0, 24, 0, 0, 0, 0)
PANTS[2] = (' ', 0, 0, 0, 0, 2, 0)
PANTS[3] = (' ', 0, 0, 0, 2, 0, 0)

BOOTS = dict()
BOOTS[1] = ('  ', 14, 0, 5, 0, 1, 0)
BOOTS[2] = (' ', 0, 18, 0, 1, 0, 0)


World of Warcraft outfit
#    ,     :
# 0 - , 1 - , 2 - , 3 - , 4 - , 5 - , 6 - 

EQUIPMENT_COLLECTION = "wow_classic_preraid"

RIGHT_HANDS = dict()
RIGHT_HANDS[1] = ('  \'', 81, 0, 4, 0, 1, 0)
RIGHT_HANDS[2] = (' ', 49, 0, 4, 0, 1, 0)
RIGHT_HANDS[3] = (' ', 57, 9, 9, 0, 0, 0)

LEFT_HANDS = dict()
LEFT_HANDS[1] = ('  \'', 52, 0, 0, 0, 0, 0)
LEFT_HANDS[2] = (' ', 49, 0, 4, 0, 1, 0)
LEFT_HANDS[3] = (' ', 57, 9, 9, 0, 0, 0)

GLOVES = dict()
GLOVES[1] = (' ', 28, 0, 0, 0, 1, 0)
GLOVES[2] = ('  ', 40, 0, 0, 0, 0, 0)

HEADS = dict()
HEADS[1] = (' ', 0, 0, 0, 2, 1, 0)
HEADS[2] = (' ', 0, 0, 13, 0, 2, 0)
HEADS[3] = (' ', 32, 0, 8, 0, 0, 0)
HEADS[4] = (' ', 0, 19, 12, 0, 0, 0)

CHESTS = dict()
CHESTS[1] = (' ', 60, 8, 8, 0, 0, 0)
CHESTS[2] = ('  ', 50, 5, 0, 0, 0, 0)
CHESTS[3] = (' ', 0, 11, 18, 0, 0, 0)

PANTS = dict()
PANTS[1] = (' ', 46, 0, 0, 0, 1, 0)
PANTS[2] = ('  ', 0, 5, 0, 1, 1, 0)

BOOTS = dict()
BOOTS[1] = (' ', 0, 21, 4, 0, 0, 0)
BOOTS[2] = ('  ', 40, 0, 0, 0, 0, 0)
BOOTS[3] = (' ', 0, 23, 0, 0, 0, 0)


add the equipype string to the constructor of the Rogue class
    ...
    #    ,    id  :
    # 0 -  , 1 -  , 2 - , 3 - , 4 - , 5 - , 6 - 
    self.equipment_slots = [0] * 7

    #    ,      :
    self.equipment_names = [''] * 7


We also add to our class the wear_item (calculation of characteristics when putting on things) and unwear_all methods (remove all things).

class methods responsible for working with equipment
    ...
    #   "  ":
    def unwear_all(self):
        #  id      :
        for i in range(0, len(self.equipment_slots) ):
            self.equipment_slots[i] = 0
            self.equipment_names[i] = ''

        self.set_stats_without_equip()


    #    :
    def wear_item(self, slot, item_id, items_list):

        #      ,        ,   :
        if self.equipment_slots[slot] == 0:
            self.equipment_slots[slot] = item_id
            self.equipment_names[slot] = items_list[item_id][0]
            self.stat_agility += items_list[item_id][2]
            self.stat_power += items_list[item_id][3]
            #  ,            :
            self.stat_attackpower += items_list[item_id][1] + items_list[item_id][2] + items_list[item_id][3]
            self.stat_hit += items_list[item_id][4]
            self.direct_crit_bonus += items_list[item_id][5]
            self.stat_mastery += items_list[item_id][6]

            #         . ,   . :
            if items_list[item_id][2] != 0 or items_list[item_id][5] != 0:
                self.calculate_critical_percent()

            #    ,    :
            if items_list[item_id][6] != 0:
                self.calculate_glancing_percent()


Also, the fact of combining some things gives additional bonuses (in “World of Warcraft” this is known as a “set bonus”). In my abstract set, such a bonus is given from putting on the right-handed Guard of the Forests and the Left-Handed Guard of the Forests at the same time. Add this to the code of the wear_item method :

set bonuses in the wear_item method
    ...
    #      "custom":
            if EQUIPMENT_COLLECTION == 'custom':
                #      "  " (id 1   " "),     "  " (id 1   " "),   2  . :
                if slot == 1:
                    if self.equipment_slots[1] == 1 and self.equipment_slots[0] == 1:
                        self.direct_crit_bonus += 2
                        self.calculate_critical_percent()
                        print('  ...')


Now our robber needs to be taught how to fight. We will consider a fight a series of 1000 attacks on an opponent who is standing with his back to us and is busy with something else (a typical situation for World of Warcraft). Each strike, regardless of the previous ones, may be:

  • normal - standard damage, in our model equivalent to character’s “attack power”
  • moving - 70% damage from normal
  • critical - double damage from normal
  • miss - 0 damage

This will be determined by a series of checks according to this scheme:

image

And for a robber with basic values, this scheme takes the form: Let's program

image

this mechanics by adding the do_attack method to the code of our class. It will return a tuple of two numbers: (outcome of the attack, damage done).

attack code
    ...
    #    :
    def do_attack(self):
        #   :
        event_hit = randint(1, 100)

        #  :
        if event_hit > self.stat_hit:
            return 0, 0

        #  :
        else:
            #   :
            event_glancing = randint(1, 100)

            #    ,    ,
            #      10  "",  stat_glancing_percent   0,
            #      
            if event_glancing <= self.stat_glancing_percent:
                damage = floor(self.stat_attackpower * 0.7)
                return 1, damage

            #    :
            else:
                #   :
                event_crit = randint(1, 100)

                #    :
                if event_crit > self.stat_crit:
                    damage = self.stat_attackpower
                    return 2, damage

                #   :
                else:
                    damage = self.stat_attackpower * 2
                    return 3, damage


We will achieve a convenient display of the current state of the robber, so that at any time you can check what happens to him:

redefine the __str__ magic method
    ...
    #  " "     :
    def __str__(self):

        #      :
        using_equipment_names = ''
        for i in range(0, len(self.equipment_names) - 1 ):
            using_equipment_names += self.equipment_names[i] + '", "'
        using_equipment_names = '"' + using_equipment_names + self.equipment_names[-1] + '"'

        #  :
        description = ' 60 \n'
        description += using_equipment_names + '\n'
        description += ' : ' + str(self.stat_attackpower) + ' .\n'
        description += ': ' + str(self.stat_agility) + ' .\n'
        description += ': ' + str(self.stat_power) + ' .\n'
        description += ': ' + str(self.stat_hit) + '%\n'
        description += '. : ' + str(self.stat_crit) + '%\n'
        description += ': ' + str(self.stat_mastery) + ' .\n'
        description += ' . .: ' + str(self.stat_glancing_percent) + '%\n'
        return description


STEP 3 - Preparing to Launch


Now it's time to write a code that will provide fights for all possible sets of equipment. To do this, I sequentially call functions according to this scheme:

image

  1. run_session - nested loops are implemented here, sorting through all the required dictionaries with things and calling the following function for each combination; at the end the report text will be generated and saved in the session log
  2. test_combination - all previously worn items are reset and the wear_item method is called over and over, dressing the character in a new “outfit”, after which the next function is called
  3. simulate_fight - the same do_attack method is called 1000 times, the received data are kept, if necessary, a detailed log is kept for each battle

functions run_session, test_combination, simulate_fight
#     :
def run_session(SESSION_LOG):

    #  :
    fight_number = 1

    #    :
    all_fight_data = ''

    #      :
    for new_righthand_id in RIGHT_HANDS:
        #      :
        for new_lefthand_id in LEFT_HANDS:
            #   :
            for new_gloves_id in GLOVES:
                #   :
                for new_head_id in HEADS:
                    #   :
                    for new_chest_id in CHESTS:
                        #   :
                        for new_pants_id in PANTS:
                            #   :
                            for new_boots_id in BOOTS:

                                new_fight_data = test_combination(fight_number,
                                                                  new_righthand_id,
                                                                  new_lefthand_id,
                                                                  new_gloves_id,
                                                                  new_head_id,
                                                                  new_chest_id,
                                                                  new_pants_id,
                                                                  new_boots_id
                                                                  )

                                all_fight_data += new_fight_data
                                fight_number += 1

    #       :
    save_data_to_file(SESSION_LOG, all_fight_data)

#       :
def test_combination(fight_number, righthand_id, lefthand_id, gloves_id, head_id, chest_id, pants_id, boots_id):

    #   :
    my_rogue.unwear_all()

    #     :
    my_rogue.wear_item(0, righthand_id, RIGHT_HANDS)

    #     :
    my_rogue.wear_item(1, lefthand_id, LEFT_HANDS)

    #  :
    my_rogue.wear_item(2, gloves_id, GLOVES)

    #  :
    my_rogue.wear_item(3, head_id, HEADS)

    #  :
    my_rogue.wear_item(4, chest_id, CHESTS)

    #  :
    my_rogue.wear_item(5, pants_id, PANTS)

    #  :
    my_rogue.wear_item(6, boots_id, BOOTS)


    #    "" :
    equipment_profile = str(righthand_id) + ',' + str(lefthand_id) + ',' + str(gloves_id) + \
                            ',' + str(head_id) + ',' + str(chest_id) + ',' + str(pants_id) + \
                            ',' + str(boots_id)

    print(my_rogue)
    print('equipment_profile =', equipment_profile)

    #        :
    return simulate_fight(equipment_profile, fight_number)


#  ,    attacks_total   :
def simulate_fight(equipment_profile, fight_number):
    global LOG_EVERY_FIGHT

    #   :
    sum_of_attack_types = [0, 0, 0, 0]
    sum_of_damage = 0

    #  ,     :
    if LOG_EVERY_FIGHT:
        fight_log = ''
        verdicts = {
            0: '.',
            1: '.',
            2: '.',
            3: '.'
        }

    attacks = 0
    global ATTACKS_IN_FIGHT

    #  ,      :
    while attacks < ATTACKS_IN_FIGHT:
        #  - :
        damage_info = my_rogue.do_attack()

        #   :
        sum_of_damage += damage_info[1]

        #   :
        sum_of_attack_types[ damage_info[0] ] += 1

        attacks += 1

        #  ,   :
        if LOG_EVERY_FIGHT:
            fight_log += verdicts[ damage_info[0] ] + ' ' + str(damage_info[1]) + ' ' + str(sum_of_damage) + '\n'

    #  ,  :
    if LOG_EVERY_FIGHT:
        #  :
        filename = 'fight_logs/log ' + str(fight_number) + '.txt'
        save_data_to_file(filename, fight_log)

    #       :
    attacks_statistic = ','.join(map(str, sum_of_attack_types))
    fight_data = '#' + str(fight_number) + '/' + equipment_profile + '/' + str(sum_of_damage) + ',' + attacks_statistic + '\n'

    return fight_data



To save the logs, I use two simple functions:

functions save_data, add_data
#     :
def save_data_to_file(filename, data):
    with open(filename, 'w', encoding='utf8') as f:
        print(data, file=f)


#     :
def append_data_to_file(filename, data):
    with open(filename, 'a+', encoding='utf8') as f:
        print(data, file=f)


So, now it remains to write a few lines to start the session and save its results. We also import the necessary standard Python modules. This is where you can determine which set of equipment will be tested. For fans of World of Warcraft, I picked up equipment from there, but remember that this project is just an approximate reconstruction of the mechanic from there.

program code
#     :
from random import randint

#     :
from math import floor

#    :
from datetime import datetime
from time import time

#    :
from operations_with_files import *

#      :
from equipment_custom import *
#from equipment_wow_classic import *
#from equipment_obvious_strong import *
#from equipment_obvious_weak import *


# :
if __name__ == "__main__":

    #     :
    ATTACKS_IN_FIGHT = 1000

    #     :
    LOG_EVERY_FIGHT = False

    #     :
    SESSION_LOG = 'session_logs/for ' + EQUIPMENT_COLLECTION + ' results ' + datetime.strftime(datetime.now(), '%Y-%m-%d_%H-%M-%S') + '.txt'
    print('SESSION_LOG =', SESSION_LOG)

    #  :
    my_rogue = Rogue()

    #  :
    time_begin = time()

    #   :
    run_session(SESSION_LOG)

    #   :
    time_session = time() - time_begin
    duration_info = ' : ' + str( round(time_session, 2) ) + ' .'
    print('\n' + duration_info)
    append_data_to_file(SESSION_LOG, duration_info + '\n')

    #  ,   5    :
    top_sets_info = show_best_sets(SESSION_LOG, 5)

    #          :
    append_data_to_file(SESSION_LOG, top_sets_info)

else:
    print('__name__ is not "__main__".')


A session of 1728 battles takes 5 seconds on my laptop. If you set LOG_EVERY_FIGHT = True, then files with data for each fight will appear in the fight_logs folder, but the session will already take 9 seconds. In any case, the session log will appear in the session_logs folder:

first 10 lines of the log
#1/1,1,1,1,1,1,1/256932,170,324,346,160
#2/1,1,1,1,1,1,2/241339,186,350,331,133
#3/1,1,1,1,1,2,1/221632,191,325,355,129
#4/1,1,1,1,1,2,2/225359,183,320,361,136
#5/1,1,1,1,1,3,1/243872,122,344,384,150
#6/1,1,1,1,1,3,2/243398,114,348,394,144
#7/1,1,1,1,2,1,1/225342,170,336,349,145
#8/1,1,1,1,2,1,2/226414,173,346,322,159
#9/1,1,1,1,2,2,1/207862,172,322,348,158
#10/1,1,1,1,2,2,2/203492,186,335,319,160


As you can see, just holding a session is not enough; you need to extract information from those hundreds of lines about those combinations of things that led to the best results. To do this, we will write two more functions. The general idea is to open the received log, create a list of damage amounts for each battle, sort it and, for example, write down the names of the things used for the 5 best situations.

functions for determining top gear
#       :
def show_best_sets(SESSION_LOG, number_of_sets):

    #      :
    list_log = list()

    #   ,      list_log ,
    #          :
    with open(SESSION_LOG, 'r', encoding='utf8') as f:
        lines = f.readlines()
        for line in lines:
            try:
                list_line = line.split('/')
                list_fight = list_line[2].split(',')
                list_log.append( ( int(list_fight[0]), list_line[1].split(',') ) )
            except IndexError:
                break

    #  ,      :
    list_log.sort(reverse=True)

    #   ,  number_of_sets     :
    top_sets_info = ''
    for i in range(0, number_of_sets):
        current_case = list_log[i]

        #           :
        clear_report = ''
        equipment_names = ''
        equip_group = 1

        for equip_id in current_case[1]:
            equipment_names += '\n' + get_equip_name(equip_id, equip_group)
            equip_group += 1

        line_for_clear_report = '\n#' + str(i+1) + ' - ' + str(current_case[0]) + '   :' + equipment_names
        clear_report += line_for_clear_report

        print('\n', clear_report)
        top_sets_info += clear_report + '\r'

    return top_sets_info


#     id:
def get_equip_name(equip_id, equip_group):
    equip_id = int(equip_id)

    if equip_group == 1:
        return RIGHT_HANDS[equip_id][0]
    if equip_group == 2:
        return LEFT_HANDS[equip_id][0]
    if equip_group == 3:
        return GLOVES[equip_id][0]
    if equip_group == 4:
        return HEADS[equip_id][0]
    if equip_group == 5:
        return CHESTS[equip_id][0]
    if equip_group == 6:
        return PANTS[equip_id][0]
    if equip_group == 7:
        return BOOTS[equip_id][0]


Now at the end of the log lines with 5 selections appear, showing the best result:

finally readable log lines
 : 4.89 .

#1 - 293959   :
 
 
 
 
 
 
  

#2 - 293102   :
 
 
 
 
 
 
 

#3 - 290573   :
 
 
 
 
 
 
  

#4 - 287592   :
 
 
 
 
 
 
 

#5 - 284929   :
 
 
 
 
 
 
  


STEP 4 - evaluate the sustainability of the results


It is important to remember that there are elements of randomness in this project: when determining the type of stroke using the randint function . Repeatedly conducting tests, I noticed that when repeating sessions with the same input data, the top 5 selections may vary. This is not very happy, and took to solve the problem.

First I made a test kit of the equipment “obvious_strong”, where even without tests it is obvious which collections of things are the best here:

watch the obvious_strong set
EQUIPMENT_COLLECTION = 'obvious_strong'

RIGHT_HANDS = dict()
RIGHT_HANDS[1] = (' ', 5000, 0, 0, 0, 0, 0)
RIGHT_HANDS[2] = (' ', 800, 0, 0, 0, 0, 0)
RIGHT_HANDS[3] = (' ', 20, 0, 0, 0, 0, 0)

LEFT_HANDS = dict()
LEFT_HANDS[1] = (' ', 4000, 0, 0, 0, 0, 0)
LEFT_HANDS[2] = (' ', 10, 0, 0, 0, 0, 0)

GLOVES = dict()
GLOVES[1] = (' ', 1, 0, 0, 0, 0, 0)

HEADS = dict()
HEADS[1] = (' ', 1, 0, 0, 0, 0, 0)

CHESTS = dict()
CHESTS[1] = (' ', 1, 0, 0, 0, 0, 0)

PANTS = dict()
PANTS[1] = (' ', 1, 0, 0, 0, 0, 0)

BOOTS = dict()
BOOTS[1] = (' ', 1, 0, 0, 0, 0, 0)


With this set there will be 6 fights (3 swords * 2 daggers * 1 * 1 * 1 * 1 * 1). In the top 5, the battle where the worst sword and the worst dagger is taken should not be included. Well, of course, the first place should be a selection with two strongest blades. If you think about it, then for each collection it is obvious what place it will fall into. Conducted tests, expectations were met.

Here is a visualization of the outcome of one of the tests of this set:

image

Next, I reduced to a minimum the gap in the amount of bonuses given by these blades from 5000, 800, 20 and 4000, 10 to5, 4, 3 and 2, 1, respectively (in the project this set is located in the file “equipment_obvious_weak.py”). And here, suddenly, the combination of the strongest sword and the worst dagger came out on top. Moreover, in one of the tests, the two best weapons suddenly came in last place:

image

How to understand this? The expectations in the obviously correct arrangement of the collections remained unchanged, but the degree of difference between them was significantly reduced . And now, accidents during the battles (the ratio of misses and hits, critical and non-critical hits, etc.) have acquired decisive importance.

Let's check how often the "duet of top blades" will fall not in first place. I made 100 such launches (for this, I wrapped the “program launch lines” in a cycle of 100 iterations and started to keep a special log for this entire “super session”). Here is the visualization of the results:

image

So, the results in our program are not always stable (34% of the “right” outcomes versus 66% of the “wrong”).

The stability of the results is directly proportional to the difference in the bonuses of the tested items.

Given that the difference in the amount of bonuses of good things that it makes sense to test is weakly perceptible (as in "World of Warcraft"), the results of such tests will be relatively unstable (unstable, unstable, etc.).

STEP 5 - Increasing Resilience


We try to think logically.

We outline the success criterion: the “duet of top blades” should be in first place in 99% of cases.

Current situation: 34% of such cases.

If you do not change the accepted approach in principle (the transition from simulation of battles for all selections to a simple calculation of characteristics, for example), it remains to change some quantitative parameter of our model.

For instance:

  • to conduct not one battle for each collection, but several, then record the arithmetic mean in the log, discard the best and worst, etc.
  • conduct several test sessions in general, and then also take the “average”
  • to extend the battle itself from 1000 hits to a certain value, which will be enough for the results to be fair for the collections that are very close in terms of the total bonus

First of all, it seemed to me a successful idea with a lengthening of the battle, because it is in this place that everything happens that caused the lengthening of this article.

I ’ll test the hypothesis that lengthening the battle from 1,000 to 10,000 strokes will increase the stability of the results (for this you need to set the constant ATTACKS_IN_FIGHT to 10000). And this is so:

image

Then he decided to increase from 10,000 to 100,000 shots, and this led to one hundred percent success. After that, using the binary search method, he began to select the number of hits that would give 99% of successes to get rid of excessive calculations. Stopped at 46 875.

image

If my estimate of 99% reliability of a system with such a battle length is correct, then two tests in a row reduce the probability of error to 0.01 * 0.01 = 0.0001 .

And now, if you run a test with a battle of 46,875 strokes for a set of equipment for 1,728 battles, it will take 233 seconds and inspire confidence that the “Sword of the Master” rules:

the results of 1728 battles for 46 875 strikes
 : 233.89 .

#1 - 13643508   :
 
 
 
 
 
 
 

#2 - 13581310   :
 
 
 
 
 
 
  

#3 - 13494544   :
 
 
 
 
 
 
 

#4 - 13473820   :
 
 
 
 
 
 
  

#5 - 13450956   :
 
 
 
 
 
 
 


PS And this is easy to explain: two “Swords of the Master” allow you to get 10 units of skill, which, according to the inherent mechanics, eliminates the possibility of sliding strokes, and this adds about 40% of strokes when X or 2X damage is applied instead of 0.7X.

The result of a similar test for WoW fans:

the results of 1296 battles of 46 875 hits (wow classic preraid)
 : 174.58 .


#1 - 19950930   :
  '
  '
  
 
 
 
  

#2 - 19830324   :
  '
  '
 
 
 
 
  

#3 - 19681971   :
  '
  '
  
 
 
 
  

#4 - 19614600   :
  '
  '
 
 
 
 
  

#5 - 19474463   :
  '
  '
  
 
 
 
 


Summary


  1. — . , , 4 * 4 * 3 * 3 * 3 * 3 * 2 = 2592, .. 33% . .
  2. : , , , .
  3. , : , , , .

I posted all the project code on the github .

Dear community, I will be glad to receive feedback on this topic.

UPD from 04/08/2020:
Thanks to the commentsDeerenaros, knotri and GriboksI realized that instead of simulating thousands of battles, you can calculate the mathematical expectation for one hit and, on this basis, rank the equipment. I threw everything related to fights out of the code, instead of the simulate_fight function I did calculate_expectation . At the output, I get the same results. Added the resulting code to the repository .

All Articles