Comment choisir l'équipement pour un jeu persan en Python

Nous apprenons à trouver le meilleur pour notre voleur à l'aide de la programmation. Nous déterminons également si le programme nous conduit «par le nez».

image

Objectif: apprendre à simuler étape par étape la partie nécessaire de la mécanique du jeu in vitro, à obtenir les données nécessaires et à en tirer des conclusions.

Ce dont vous avez besoin: Python 3, l'environnement pour travailler avec du code (j'ai PyCharm).

Dans les jeux, beaucoup de gens veulent tirer le meilleur parti de leurs personnages, et pour cela, vous devez choisir la combinaison d'équipement la plus optimale, ce qui est souvent beaucoup. Essayons d'écrire notre propre algorithme pour tester différentes combinaisons d'équipements et collecter des données.

Dans un premier temps , je me suis inspiré par le jeu « World of Warcraft: Classic » (j'ai pris les icônes à partir de ), mais dans le processus que je fait quelques simplifications. Lien vers l'ensemble du projet à la fin de l'article.

ÉTAPE 1 - évaluer la zone de recherche


Supposons que nous ayons un personnage dans la classe Rogue. Il est nécessaire de lui ramasser du matériel dans lequel il infligera un maximum de dégâts à l'ennemi. Nous sommes intéressés par les choses pour les machines à sous «armes dans la main droite» (4 pièces), «Armes dans la main gauche» (4 pièces), «Gants» (2 pièces), «Tête» (3 pièces), «Poitrine» (3 pièces), «Jambes» (3 pièces), «Pieds» (2 pièces). Nous allons mettre leurs différentes combinaisons sur le personnage et simuler la bataille. Et si vous appliquez l'idée de recherche exhaustive (avec laquelle nous commencerons), pour évaluer toutes les combinaisons, vous devrez dépenser au moins 4 * 4 * 2 * 3 * 3 * 3 * 2 = 1728 combats.

Pour une évaluation plus précise des meilleures combinaisons, vous devrez mener des batailles supplémentaires.

Ainsi, déjà à ce stade, nous pouvons présenter le schéma du projet comme suit:

image

ÉTAPE 2 - analyser la mécanique du jeu


Commençons par le personnage. Il a de telles caractéristiques qui affectent les dommages causés les uns aux autres:

  1. puissance d'attaque - elle est convertie directement en dégâts causés par un coup normal (1 à 1). Calculé par la formule: points de puissance d'attaque + points de force + points de dextérité
  2. Force - +1 pour attaquer la puissance et tout (quoi faire, c'est la conception du jeu)
  3. Dextérité - +1 pour attaquer la puissance, et également toutes les 20 unités d'agilité ajoutent 1% de chances de critique
  4. Crète. chance - la chance de causer un double dommage si la frappe ne glisse pas et manque
  5. précision - augmentation des chances de toucher un adversaire
  6. maîtrise - chaque unité de maîtrise réduit la probabilité d'une frappe glissante de 4% (ce qui est initialement égal à 40%, ce qui signifie que 10 unités de maîtrise excluent complètement la possibilité d'une frappe glissante)

Le diagramme ci-dessous montre les valeurs de base de notre voleur et comment les mettre sur un équipement les change:

image

Il est donc temps de commencer à écrire du code. Nous décrivons ce que nous savons déjà dans la classe Rogue. La méthode set_stats_without_equip restaurera l'état du personnage sans équipement, ce qui est utile lors du changement de collections. Les méthodes Calculate_critical_percent et Calculate_glancing_percent à l'avenir ne seront appelées que si nécessaire, mettant à jour les valeurs de caractéristiques spécifiques.

premières lignes de classe
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


Vous devez maintenant vous occuper de l'équipement. Afin de trier facilement toutes choses, en créant leurs combinaisons, j'ai décidé de créer un dictionnaire séparé pour chaque type d'équipement: RIGHT_HANDS, LEFT_HANDS, GLOVES, HEADS, CHESTS, PANTS, BOOTS. Les tuples suivants sont stockés en tant que valeurs dans les dictionnaires:

image

Créez un fichier séparé pour les dictionnaires avec équipement. J'ai plusieurs de ces fichiers avec différents ensembles.

équipement de test abstrait
#    ,     :
# 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)


Tenue de World of Warcraft
#    ,     :
# 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)


ajouter la chaîne equipype au constructeur de la classe Rogue
    ...
    #    ,    id  :
    # 0 -  , 1 -  , 2 - , 3 - , 4 - , 5 - , 6 - 
    self.equipment_slots = [0] * 7

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


Nous ajoutons également à notre classe les méthodes wear_item (calcul des caractéristiques lors de l' enfilage ) et les méthodes unwear_all (supprimer tout).

méthodes de classe responsables du travail avec l'équipement
    ...
    #   "  ":
    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()


De plus, le fait de combiner certaines choses donne des bonus supplémentaires (dans «World of Warcraft», c'est ce qu'on appelle un «bonus d'ensemble»). Dans mon ensemble abstrait, un tel bonus est donné en mettant en même temps la garde à droite des forêts et la garde à gauche des forêts. Ajoutez ceci au code de la méthode wear_item :

définir des bonus dans la méthode wear_item
    ...
    #      "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('  ...')


Maintenant, notre voleur doit apprendre à se battre. Nous considérerons un combat comme une série de 1000 attaques contre un adversaire qui nous tourne le dos et est occupé par autre chose (une situation typique pour World of Warcraft). Chaque grève, indépendamment des précédentes, peut être:

  • normal - dégâts standard, dans notre modèle équivalent à la "puissance d'attaque" du personnage
  • en mouvement - 70% de dégâts par rapport à la normale
  • critique - double dommage par rapport à la normale
  • manquer - 0 dégât

Cela sera déterminé par une série de vérifications selon ce schéma:

image

Et pour un voleur avec des valeurs de base, ce schéma prend la forme: Programmons

image

cette mécanique en ajoutant la méthode do_attack au code de notre classe. Il renverra un tuple de deux nombres: (résultat de l'attaque, dégâts causés).

code d'attaque
    ...
    #    :
    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


Nous obtiendrons un affichage pratique de l'état actuel du voleur, afin qu'à tout moment vous puissiez vérifier ce qui lui arrive:

redéfinir la méthode magique __str__
    ...
    #  " "     :
    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


ÉTAPE 3 - Préparation au lancement


Il est maintenant temps d'écrire un code qui fournira des combats pour tous les ensembles d'équipements possibles. Pour ce faire, j'appelle séquentiellement des fonctions selon ce schéma:

image

  1. run_session - les boucles imbriquées sont implémentées ici, triant tous les dictionnaires requis avec des choses et appelant la fonction suivante pour chaque combinaison; à la fin, le texte du rapport sera généré et enregistré dans le journal de session
  2. test_combination - tous les vêtements précédemment portés sont réinitialisés et la méthode wear_item est appelée encore et encore, habillant le personnage dans une nouvelle «tenue», après quoi la fonction suivante est appelée
  3. simulate_fight - la même méthode do_attack est appelée 1000 fois, les données reçues sont conservées, si nécessaire, un journal détaillé est conservé pour chaque bataille

fonctions 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



Pour enregistrer les journaux, j'utilise deux fonctions simples:

fonctions 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)


Il ne reste donc plus qu'à écrire quelques lignes pour démarrer la session et enregistrer ses résultats. Nous importons également les modules Python standard nécessaires. C'est là que vous pouvez déterminer quel équipement sera testé. Pour les fans de World of Warcraft, j'ai ramassé de l'équipement à partir de là, mais rappelez-vous que ce projet n'est qu'une reconstruction approximative du mécanicien à partir de là.

code de programme
#     :
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__".')


Une session de 1728 batailles prend 5 secondes sur mon ordinateur portable. Si vous définissez LOG_EVERY_FIGHT = True, les fichiers contenant des données pour chaque combat apparaîtront dans le dossier fight_logs, mais la session prendra déjà 9 secondes. Dans tous les cas, le journal de session apparaîtra dans le dossier session_logs:

10 premières lignes du journal
#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


Comme vous pouvez le voir, il ne suffit pas de tenir une session; vous devez extraire des informations de ces centaines de lignes sur ces combinaisons de choses qui ont conduit aux meilleurs résultats. Pour ce faire, nous allons écrire deux autres fonctions. L'idée générale est d'ouvrir le journal reçu, de créer une liste des montants des dégâts pour chaque bataille, de les trier et, par exemple, d'écrire les noms des objets utilisés pour les 5 meilleures situations.

fonctions pour déterminer la vitesse supérieure
#       :
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]


Maintenant, à la fin des lignes de journal avec 5 sélections apparaissent, montrant le meilleur résultat:

lignes de journal enfin lisibles
 : 4.89 .

#1 - 293959   :
 
 
 
 
 
 
  

#2 - 293102   :
 
 
 
 
 
 
 

#3 - 290573   :
 
 
 
 
 
 
  

#4 - 287592   :
 
 
 
 
 
 
 

#5 - 284929   :
 
 
 
 
 
 
  


ÉTAPE 4 - évaluer la durabilité des résultats


Il est important de se rappeler qu'il y a des éléments de hasard dans ce projet: lors de la détermination du type de trait à l'aide de la fonction randint . Lors de tests répétés, j'ai remarqué que lors de la répétition de sessions avec les mêmes données d'entrée, les 5 premières sélections peuvent varier. Ce n'est pas très heureux, et a pris pour résoudre le problème.

J'ai d’abord fait un kit de test de l’équipement «evident_strong», où même sans tests, il est évident quelles sélections sont les meilleures:

regardez le set evident_strong
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)


Avec cet ensemble, il y aura 6 combats (3 épées * 2 poignards * 1 * 1 * 1 * 1 * 1). Dans le top 5, la bataille où la pire épée et la pire dague sont prises ne doit pas être incluse. Eh bien, bien sûr, la première place devrait être une sélection avec deux lames les plus solides. Si vous y pensez, alors pour chaque collection, il est évident dans quel endroit elle tombera. Tests effectués, les attentes ont été satisfaites.

Voici une visualisation du résultat d'un des tests de cet ensemble:

image

Ensuite, j'ai réduit au minimum l'écart dans le montant des bonus accordés par ces lames de 5000, 800, 20 et 4000, 10 à5, 4, 3 et 2, 1, respectivement (dans le projet, cet ensemble se trouve dans le fichier «equipment_obvious_weak.py»). Et ici, tout à coup, la combinaison de l'épée la plus forte et du pire poignard est arrivée en tête. De plus, dans l'un des tests, les deux meilleures armes sont soudainement arrivées en dernière position:

image

comment comprendre cela? Les attentes quant à la disposition manifestement correcte des collections sont restées inchangées, mais le degré de différence entre elles a été considérablement réduit . Et maintenant, les accidents pendant les batailles (le rapport des coups et des coups, des coups critiques et non critiques, etc.) ont acquis une importance décisive.

Vérifions à quelle fréquence le "duo de lames supérieures" ne tombera pas en premier lieu. J'ai fait 100 lancements de ce type (pour cela, j'ai encapsulé les «lignes de lancement de programme» dans un cycle de 100 itérations et j'ai commencé à tenir un journal spécial pour toute cette «super session»). Voici la visualisation des résultats:

image

Ainsi, les résultats de notre programme ne sont pas toujours stables (34% des «bons» résultats contre 66% des «mauvais»).

La stabilité des résultats est directement proportionnelle à la différence des bonus des articles testés.

Étant donné que la différence dans le montant des bonus de bonnes choses qu'il est logique de tester est faiblement perceptible (comme dans "World of Warcraft"), les résultats de ces tests seront relativement instables (instable, instable, etc.).

ÉTAPE 5 - Accroître la résilience


Nous essayons de penser logiquement.

Nous décrivons le critère de réussite: le «duo de lames supérieures» devrait être en première place dans 99% des cas.

Situation actuelle: 34% de ces cas.

Si vous ne changez pas l'approche acceptée en principe (le passage de la simulation de batailles pour toutes les sélections à un simple calcul de caractéristiques par exemple), alors il reste à changer certains paramètres quantitatifs de notre modèle.

Par exemple:

  • pour mener non pas une bataille pour chaque collection, mais plusieurs, puis enregistrer la moyenne arithmétique dans le journal, jeter le meilleur et le pire, etc.
  • effectuer plusieurs séances de test en général, puis prendre également la «moyenne»
  • étendre la bataille elle-même de 1000 hits à une certaine valeur, ce qui sera suffisant pour que les résultats soient justes pour les collections qui sont très proches en termes de bonus total

Tout d'abord, il m'a semblé une idée réussie avec une extension de la bataille, car c'est à cet endroit que se passe tout ce qui a provoqué l'extension de cet article.

Je testerai l' hypothèse selon laquelle l'allongement de la bataille de 1 000 à 10 000 coups augmentera la stabilité des résultats (pour cela, vous devez définir la constante ATTACKS_IN_FIGHT à 10000). Et c'est ainsi:

image

Puis il a décidé de passer de 10 000 à 100 000 tirs, ce qui a conduit à un succès à cent pour cent. Après cela, en utilisant la méthode de recherche binaire, il a commencé à sélectionner le nombre de hits qui donnerait 99% de succès pour se débarrasser des calculs excessifs. Arrêté à 46875.

image

Si mon estimation de la fiabilité de 99% d'un système avec une telle longueur de bataille est correcte, alors deux tests consécutifs réduisent la probabilité d'erreur à 0,01 * 0,01 = 0,0001 .

Et maintenant, si vous exécutez un test avec une bataille de 46 875 coups pour un ensemble d'équipement pour 1 728 batailles, cela prendra 233 secondes et inspirera la confiance que les règles de «l'épée du maître»:

les résultats de 1728 batailles pour 46875 frappes
 : 233.89 .

#1 - 13643508   :
 
 
 
 
 
 
 

#2 - 13581310   :
 
 
 
 
 
 
  

#3 - 13494544   :
 
 
 
 
 
 
 

#4 - 13473820   :
 
 
 
 
 
 
  

#5 - 13450956   :
 
 
 
 
 
 
 


PS Et cela est facile à expliquer: deux «épées du maître» vous permettent d'obtenir 10 unités de compétence, ce qui, selon la mécanique inhérente, élimine la possibilité de coups coulissants, et cela ajoute environ 40% des coups lorsque des dommages X ou 2X sont appliqués au lieu de 0,7X.

Le résultat d'un test similaire pour les fans de WoW:

les résultats de 1296 batailles de 46875 coups sûrs (wow classique preraid)
 : 174.58 .


#1 - 19950930   :
  '
  '
  
 
 
 
  

#2 - 19830324   :
  '
  '
 
 
 
 
  

#3 - 19681971   :
  '
  '
  
 
 
 
  

#4 - 19614600   :
  '
  '
 
 
 
 
  

#5 - 19474463   :
  '
  '
  
 
 
 
 


Sommaire


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

J'ai posté tout le code du projet sur le github .

Chère communauté, je serai heureux de recevoir des commentaires sur ce sujet.

UPD du 04/08/2020:
Merci aux commentairesDeerenaros, knotri et GriboksJ'ai réalisé qu'au lieu de simuler des milliers de batailles, vous pouvez calculer l'attente mathématique pour un coup et, sur cette base, classer l'équipement. J'ai jeté tout ce qui concerne les combats hors du code, au lieu de la simulate_fight fonction que je l'ai fait calculate_expectation . En sortie, j'obtiens les mêmes résultats. Ajout du code résultant au référentiel .

All Articles