Como escolher o equipamento para um jogo persa em Python

Aprendemos a encontrar o melhor para o nosso ladrão com a ajuda da programação. Também descobrimos se o programa está nos levando “pelo nariz”.

imagem

Objetivo: aprender a simular etapa por etapa a parte necessária da mecânica do jogo in vitro, para obter os dados necessários e tirar conclusões deles.

O que você precisa: Python 3, o ambiente para trabalhar com código (eu tenho PyCharm).

Nos jogos, muitas pessoas querem extrair o máximo de seus personagens, e para isso você precisa escolher a melhor combinação de equipamentos, o que geralmente é muito. Vamos tentar escrever nosso próprio algoritmo para testar várias combinações de equipamentos e coletar dados.

Inicialmente, fui inspirado pelo jogo “World of Warcraft: Classic” (tirei os ícones de lá ), mas no processo fiz algumas simplificações. Link para o projeto inteiro no final do artigo.

PASSO 1 - avalie a área de pesquisa


Suponha que tenhamos um personagem na classe Rogue. É necessário pegar equipamentos para ele, nos quais ele infligirá o máximo dano ao inimigo. Estamos interessados ​​em itens para os slots "armas na mão direita" (4 peças), "armas na mão esquerda" (4 peças), "luvas" (2 peças), "cabeça" (3 peças), "peito" (3 peças), “Pernas” (3 peças), “Pés” (2 peças). Vamos colocar suas várias combinações no personagem e simular a batalha. E se você aplicar a idéia de busca exaustiva (com a qual começaremos), para avaliar todas as combinações, você terá que gastar pelo menos 4 * 4 * 2 * 3 * 3 * 3 * 2 = 1728 lutas.

Para uma avaliação mais precisa das melhores combinações, você precisará realizar batalhas adicionais.

Portanto, já nesta fase, podemos apresentar o esquema do projeto da seguinte forma:

imagem

PASSO 2 - analisar a mecânica do jogo


Vamos começar com o personagem. Ele tem características que afetam o dano causado e um ao outro:

  1. poder de ataque - é convertido diretamente no dano causado por um golpe normal (1 a 1). É calculado pela fórmula: pontos de poder de ataque + pontos de força + pontos de agilidade
  2. Força - +1 para atacar o poder e tudo (o que fazer, este é o design do jogo)
  3. Destreza - +1 ao poder de ataque, e também a cada 20 unidades de agilidade adiciona 1% de chance crítica
  4. Creta. chance - a chance de causar dano duplo, se o ataque não deslizar e errar
  5. precisão - maior chance de acertar um oponente
  6. domínio - cada unidade de domínio reduz a probabilidade de um golpe de deslizamento em 4% (inicialmente igual a 40%, o que significa que 10 unidades de domínio eliminam completamente a possibilidade de impacto de deslizamento)

O diagrama abaixo mostra os valores básicos para o ladrão e como a colocação de um item de equipamento os altera:

imagem

Portanto, é hora de começar a escrever o código. Descrevemos o que já sabemos na classe Rogue. O método set_stats_without_equip restaurará o estado do caractere sem equipamento, o que é útil ao alterar as coleções. Os métodos calcule_critical_percent e calcule_glancing_percent no futuro serão chamados apenas se necessário, atualizando os valores de características específicas.

primeiras linhas de aula
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


Agora você precisa lidar com o equipamento. Para classificar de maneira conveniente todas as coisas, criando suas combinações, decidi criar um dicionário separado para cada tipo de equipamento: DIREITO_MÃO, ESQUERDA_MÃO, LUVAS, CABEÇAS, CHESTS, CALÇAS, BOTAS. As seguintes tuplas são armazenadas como valores nos dicionários:

imagem

Crie um arquivo separado para dicionários com equipamento. Eu tenho vários desses arquivos com conjuntos diferentes.

equipamento de teste abstrato
#    ,     :
# 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)


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


adicione a sequência de equipamentos ao construtor da classe Rogue
    ...
    #    ,    id  :
    # 0 -  , 1 -  , 2 - , 3 - , 4 - , 5 - , 6 - 
    self.equipment_slots = [0] * 7

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


Também adicionamos à nossa classe os métodos wear_item (cálculo de características ao colocar as coisas) e métodos unwear_all (removem todas as coisas).

métodos de classe responsáveis ​​por trabalhar com equipamentos
    ...
    #   "  ":
    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()


Além disso, o fato de combinar algumas coisas fornece bônus adicionais (em "World of Warcraft", isso é conhecido como "bônus definido"). No meu conjunto abstrato, esse bônus é dado ao colocar a Guarda das Florestas destro e a Guarda das Florestas Canhotos ao mesmo tempo. Adicione isso ao código do método wear_item :

definir bônus no método 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('  ...')


Agora, nosso ladrão precisa ser ensinado a lutar. Vamos considerar uma luta uma série de mil ataques contra um oponente que está de costas para nós e está ocupado com outra coisa (uma situação típica do World of Warcraft). Cada greve, independentemente das anteriores, pode ser:

  • normal - dano padrão, em nosso modelo equivalente ao "poder de ataque" do personagem
  • em movimento - 70% de dano do normal
  • crítico - dano dobro do normal
  • falta - 0 dano

Isso será determinado por uma série de verificações de acordo com este esquema:

imagem

E para um ladrão com valores básicos, esse esquema assume a forma: Vamos programar

imagem

essa mecânica adicionando o método do_attack ao código da nossa classe. Ele retornará uma tupla de dois números: (resultado do ataque, dano causado).

código de ataque
    ...
    #    :
    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


Obteremos uma exibição conveniente do estado atual do ladrão, para que a qualquer momento você possa verificar o que acontece com ele:

redefinir o método mágico __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


PASSO 3 - Preparando-se para o lançamento


Agora é hora de escrever um código que forneça brigas para todos os conjuntos de equipamentos possíveis. Para fazer isso, eu sequencialmente chamo funções de acordo com este esquema:

imagem

  1. Os loops aninhados run_session são implementados aqui, classificando todos os dicionários necessários com as coisas e chamando a seguinte função para cada combinação; no final, o texto do relatório será gerado e salvo no log da sessão
  2. test_combination - todos os itens usados ​​anteriormente são redefinidos e o método wear_item é chamado repetidamente, vestindo o personagem com uma nova "roupa", após a qual a próxima função é chamada
  3. simulate_fight - o mesmo método do_attack é chamado 1000 vezes, os dados recebidos são mantidos, se necessário, um log detalhado é mantido para cada batalha

funções 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



Para salvar os logs, eu uso duas funções simples:

funções 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)


Portanto, agora resta escrever algumas linhas para iniciar a sessão e salvar seus resultados. Também importamos os módulos padrão necessários do Python. É aqui que você pode determinar qual conjunto de equipamentos será testado. Para os fãs de World of Warcraft, peguei equipamentos de lá, mas lembre-se de que este projeto é apenas uma reconstrução aproximada do mecânico a partir daí.

código do programa
#     :
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__".')


Uma sessão de 1728 batalhas leva 5 segundos no meu laptop. Se você definir LOG_EVERY_FIGHT = True, os arquivos com dados para cada luta aparecerão na pasta fight_logs, mas a sessão já levará 9 segundos. De qualquer forma, o log da sessão aparecerá na pasta session_logs:

primeiras 10 linhas do 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


Como você pode ver, apenas realizar uma sessão não é suficiente; você precisa extrair informações dessas centenas de linhas sobre as combinações de coisas que levaram aos melhores resultados. Para fazer isso, escreveremos mais duas funções. A idéia geral é abrir o registro recebido, criar uma lista de montantes de dano para cada batalha, classificá-lo e, por exemplo, anotar os nomes das coisas usadas nas 5 melhores situações.

funções para determinar a marcha superior
#       :
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]


Agora, no final das linhas de log, aparecem 5 seleções, mostrando o melhor resultado:

linhas de log finalmente legíveis
 : 4.89 .

#1 - 293959   :
 
 
 
 
 
 
  

#2 - 293102   :
 
 
 
 
 
 
 

#3 - 290573   :
 
 
 
 
 
 
  

#4 - 287592   :
 
 
 
 
 
 
 

#5 - 284929   :
 
 
 
 
 
 
  


PASSO 4 - avaliar a sustentabilidade dos resultados


É importante lembrar que existem elementos de aleatoriedade neste projeto: ao determinar o tipo de golpe usando a função randint . Realizando testes repetidamente, notei que, ao repetir sessões com os mesmos dados de entrada, as 5 principais seleções podem variar. Isso não é muito feliz, e levou para resolver o problema.

Primeiro, fiz um kit de teste do equipamento “óbvio_forte”, onde mesmo sem testes, é óbvio quais coleções de coisas são as melhores aqui:

assista ao conjunto óbvio_forte
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)


Com este conjunto, haverá 6 lutas (3 espadas * 2 punhais * 1 * 1 * 1 * 1 * 1). No top 5, a batalha em que a pior espada e a pior adaga é tomada não deve ser incluída. Bem, é claro, o primeiro lugar deve ser uma seleção com duas lâminas mais fortes. Se você pensar bem, então, para cada coleção, é óbvio em que lugar ela se encaixará. Testes realizados, as expectativas foram atendidas.

Aqui está uma visualização do resultado de um dos testes deste conjunto:

imagem

Em seguida, reduzi ao mínimo a diferença no tamanho dos bônus dados por essas lâminas de 5000, 800, 20 e 4000, 10 a5, 4, 3 e 2, 1, respectivamente (no projeto, este conjunto está localizado no arquivo “equipment_obvious_weak.py”). E aqui, de repente, a combinação da espada mais forte e da pior adaga saiu por cima. Além disso, em um dos testes, as duas melhores armas surgiram subitamente em último lugar:

imagem

como entender isso? As expectativas no arranjo obviamente correto das coleções permaneceram inalteradas, mas o grau de diferença entre elas foi significativamente reduzido . E agora, os acidentes durante as batalhas (a proporção de erros e acertos, acertos críticos e não críticos, etc.) adquiriram importância decisiva.

Vamos verificar com que frequência o "dueto das lâminas superiores" não cai em primeiro lugar. Fiz 100 desses lançamentos (para isso, envolvi as “linhas de lançamento do programa” em um ciclo de 100 iterações e comecei a manter um registro especial para toda essa “super sessão”). Aqui está a visualização dos resultados:

imagem

Portanto, os resultados em nosso programa nem sempre são estáveis ​​(34% dos resultados “certos” versus 66% dos “errados”).

A estabilidade dos resultados é diretamente proporcional à diferença nos bônus dos itens testados.

Dado que a diferença na quantidade de bônus de coisas boas que faz sentido testar é fracamente perceptível (como em "World of Warcraft"), os resultados desses testes serão relativamente instáveis ​​(instáveis, instáveis ​​etc.).

PASSO 5 - Aumentando a resiliência


Tentamos pensar logicamente.

Delineamos o critério de sucesso: o "dueto das lâminas superiores" deve estar em primeiro lugar em 99% dos casos.

Situação atual: 34% desses casos.

Se você não alterar a abordagem aceita em princípio (a transição da simulação de batalhas para todas as seleções para um cálculo simples de características, por exemplo), resta alterar alguns parâmetros quantitativos do nosso modelo.

Por exemplo:

  • para conduzir não uma batalha por cada coleção, mas várias, depois registre a média aritmética no log, descarte o melhor e o pior, etc.
  • conduza várias sessões de teste em geral e depois faça a "média"
  • estender a batalha em si de 1.000 acertos para um determinado valor, o que será suficiente para que os resultados sejam justos para as coleções muito próximas em termos de bônus total

Antes de tudo, pareceu-me uma ideia bem-sucedida com o prolongamento da batalha, porque é neste local que tudo acontece que causa o prolongamento deste artigo. Testarei a

hipótese de que prolongar a batalha de 1.000 para 10.000 golpes aumentará a estabilidade dos resultados (para isso, é necessário definir a constante ATTACKS_IN_FIGHT para 10000). E é assim:

imagem

então ele decidiu aumentar de 10.000 para 100.000 tiros, e isso levou a cem por cento de sucesso. Depois disso, usando o método de pesquisa binária, ele começou a selecionar o número de ocorrências que dariam 99% de sucessos para se livrar de cálculos excessivos. Parou em 46 875.

imagem

Se minha estimativa de 99% de confiabilidade de um sistema com essa duração de batalha estiver correta, dois testes seguidos reduzirão a probabilidade de erro para 0,01 * 0,01 = 0,0001 .

E agora, se você executar um teste com uma batalha de 46.875 golpes por um conjunto de equipamentos para 1.728 batalhas, levará 233 segundos e inspirará a confiança de que a “Espada do Mestre” governa:

os resultados de 1728 batalhas por 46.875 ataques
 : 233.89 .

#1 - 13643508   :
 
 
 
 
 
 
 

#2 - 13581310   :
 
 
 
 
 
 
  

#3 - 13494544   :
 
 
 
 
 
 
 

#4 - 13473820   :
 
 
 
 
 
 
  

#5 - 13450956   :
 
 
 
 
 
 
 


PS E isso é fácil de explicar: duas “Espadas do Mestre” permitem que você obtenha 10 unidades de habilidade, o que, de acordo com a mecânica inerente, elimina a possibilidade de golpes deslizantes, e isso adiciona cerca de 40% dos golpes quando dano X ou 2X é aplicado em vez de 0,7X.

O resultado de um teste semelhante para os fãs de WoW:

os resultados de 1296 batalhas de 46 875 hits (uau classic preraid)
 : 174.58 .


#1 - 19950930   :
  '
  '
  
 
 
 
  

#2 - 19830324   :
  '
  '
 
 
 
 
  

#3 - 19681971   :
  '
  '
  
 
 
 
  

#4 - 19614600   :
  '
  '
 
 
 
 
  

#5 - 19474463   :
  '
  '
  
 
 
 
 


Sumário


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

Publiquei todo o código do projeto no github .

Caro comunidade, terei prazer em receber feedback sobre este tópico.

UPD de 04/08/2020:
Graças aos comentáriosDeerenaros, knotri e GriboksPercebi que, em vez de simular milhares de batalhas, é possível calcular a expectativa matemática de um golpe e, nessa base, classificar o equipamento. Joguei tudo relacionado a brigas fora do código, em vez da função simulate_fight que calculei a expectativa . Na saída, eu obtenho os mesmos resultados. Adicionado o código resultante ao repositório .

All Articles