Cómo elegir el equipo para un juego persa en Python

Aprendemos a encontrar lo mejor para nuestro ladrón con la ayuda de la programación. También descubrimos si el programa nos está conduciendo "por la nariz".

imagen

Propósito: aprender a simular etapa por etapa la parte necesaria de la mecánica del juego in vitro, para obtener los datos necesarios y sacar conclusiones de ellos.

Lo que necesitas: Python 3, el entorno para trabajar con código (tengo PyCharm).

En los juegos, muchas personas quieren sacar el máximo provecho de sus personajes, y para esto debes elegir la combinación más óptima de equipos, que a menudo es mucho. Intentemos escribir nuestro propio algoritmo para probar varias combinaciones de equipos y recopilar datos.

Inicialmente, me inspiré en el juego "World of Warcraft: Classic" (tomé los íconos de allí ), pero en el proceso hice algunas simplificaciones. Enlace al proyecto completo al final del artículo.

PASO 1 - evalúe el área de búsqueda


Supongamos que tenemos un personaje en la clase Rogue. Es necesario recoger equipo para él en el que infligirá el máximo daño al enemigo. Estamos interesados ​​en cosas para las máquinas tragamonedas "armas en la mano derecha" (4 piezas), "Armas en la mano izquierda" (4 piezas), "Guantes" (2 piezas), "Cabeza" (3 piezas), "Cofre" (3 piezas), “Piernas” (3 piezas), “Pies” (2 piezas). Pondremos sus diversas combinaciones sobre el personaje y simularemos la batalla. Y si aplicas la idea de búsqueda exhaustiva (con la cual comenzaremos), para evaluar todas las combinaciones tendrás que gastar al menos 4 * 4 * 2 * 3 * 3 * 3 * 2 = 1728 peleas.

Para una evaluación más precisa de las mejores combinaciones, deberás realizar batallas adicionales.

Entonces, ya en esta etapa podemos presentar el esquema del proyecto de la siguiente manera:

imagen

PASO 2 - analiza la mecánica del juego


Comencemos con el personaje. Tiene tales características que afectan el daño hecho y entre sí:

  1. Poder de ataque : se convierte directamente en el daño causado por un golpe normal (1 a 1). Calculado por la fórmula: puntos de poder de ataque + puntos de fuerza + puntos de destreza
  2. Fuerza : +1 al poder de ataque y todo (qué hacer, este es el diseño del juego)
  3. Destreza - +1 al poder de ataque, y también cada 20 unidades de agilidad agregan 1% de probabilidad crítica
  4. Creta. chance - la posibilidad de causar doble daño si el golpe no se desliza y falla
  5. precisión : mayor probabilidad de golpear a un oponente
  6. maestría : cada unidad de maestría reduce la probabilidad de un golpe deslizante en un 4% (que inicialmente es igual al 40%, lo que significa que 10 unidades de maestría excluyen por completo la posibilidad de un golpe deslizante)

El diagrama a continuación muestra los valores básicos para nuestro ladrón y cómo cambiarlos de un elemento del equipo:

imagen

Entonces, es hora de comenzar a escribir código. Describimos lo que ya sabemos en la clase Rogue. El método set_stats_without_equip restaurará el estado del personaje sin equipo, lo cual es útil cuando se cambian las colecciones. Los métodos Calculate_critical_percent y Calculate_glancing_percent en el futuro solo se llamarán si es necesario, actualizando los valores de características específicas.

primeras líneas de clase
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


Ahora necesita lidiar con el equipo. Con el fin de ordenar convenientemente todas las cosas, creando sus combinaciones, decidí crear un diccionario separado para cada tipo de equipo: RIGHT_HANDS, LEFT_HANDS, GLOVES, HEADS, CHESTS, PANTS, BOOTS. Las siguientes tuplas se almacenan como valores en los diccionarios:

imagen

Cree un archivo separado para diccionarios con equipo. Tengo varios de esos archivos con diferentes conjuntos.

equipo de prueba abstracta
#    ,     :
# 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)


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


agregue la cadena equipype al constructor de la clase Rogue
    ...
    #    ,    id  :
    # 0 -  , 1 -  , 2 - , 3 - , 4 - , 5 - , 6 - 
    self.equipment_slots = [0] * 7

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


También agregamos a nuestra clase el método wear_item (cálculo de características al poner cosas) y unwear_all (eliminar todas las cosas).

métodos de clase responsables de trabajar con equipos
    ...
    #   "  ":
    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()


Además, el hecho de combinar algunas cosas proporciona bonificaciones adicionales (en "World of Warcraft" esto se conoce como "bonificación de conjunto"). En mi conjunto abstracto, tal bonificación se otorga al poner a la Guardia de los Bosques diestra y a la Guardia de los Bosques Zurdos al mismo tiempo. Agregue esto al código del método wear_item :

establecer bonificaciones en el 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('  ...')


Ahora nuestro ladrón necesita que se le enseñe a luchar. Consideraremos una serie de 1000 ataques como una lucha contra un enemigo que está de espaldas a nosotros y está ocupado con algo más (una situación típica de World of Warcraft). Cada golpe, independientemente de los anteriores, puede ser:

  • normal : daño estándar, en nuestro modelo equivalente al "poder de ataque" del personaje
  • en movimiento : 70% de daño de lo normal
  • crítico : doble daño de lo normal
  • señorita - 0 daños

Esto estará determinado por una serie de comprobaciones de acuerdo con este esquema:

imagen

Y para un ladrón con valores básicos, este esquema toma la forma: programemos

imagen

esta mecánica agregando el método do_attack al código de nuestra clase. Devolverá una tupla de dos números: (resultado del ataque, daño hecho).

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


Lograremos una visualización conveniente del estado actual del ladrón, para que en cualquier momento pueda verificar lo que le sucede:

redefinir el 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


PASO 3: preparación para el lanzamiento


Ahora es el momento de escribir un código que proporcione peleas para todos los conjuntos de equipos posibles. Para hacer esto, invoca secuencialmente funciones de acuerdo con este esquema:

imagen

  1. run_session : los bucles anidados se implementan aquí, clasificando todos los diccionarios necesarios con elementos y llamando a la siguiente función para cada combinación; al final se generará el texto del informe y se guardará en el registro de la sesión
  2. test_combination : todas las cosas usadas anteriormente se reinician y el método wear_item se llama una y otra vez, vistiendo al personaje con un nuevo "atuendo", después de lo cual se llama a la siguiente función
  3. simulate_fight : el mismo método do_attack se llama 1000 veces, los datos recibidos se guardan, si es necesario, se mantiene un registro detallado para cada batalla

funciones 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 guardar los registros, utilizo dos funciones simples:

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


Entonces, ahora queda escribir algunas líneas para comenzar la sesión y guardar sus resultados. También importamos los módulos Python estándar necesarios. Aquí es donde puede determinar qué conjunto de equipos se probará. Para los fanáticos de World of Warcraft, recogí equipos de allí, pero recuerden que este proyecto es solo una reconstrucción aproximada de la mecánica desde allí.

código de 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__".')


Una sesión de 1728 batallas dura 5 segundos en mi computadora portátil. Si configura LOG_EVERY_FIGHT = True, los archivos con datos para cada pelea aparecerán en la carpeta fight_logs, pero la sesión ya tomará 9 segundos. En cualquier caso, el registro de la sesión aparecerá en la carpeta session_logs:

primeras 10 líneas del registro
#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 puede ver, simplemente mantener una sesión no es suficiente; necesita extraer información de esas cientos de líneas sobre esas combinaciones de cosas que condujeron a los mejores resultados. Para hacer esto, escribiremos dos funciones más. La idea general es abrir el registro recibido, crear una lista de cantidades de daño para cada batalla, ordenarla y, por ejemplo, anotar los nombres de las cosas utilizadas para las 5 mejores situaciones.

funciones para determinar la 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]


Ahora al final de las líneas de registro con 5 selecciones aparecen, mostrando el mejor resultado:

líneas de registro finalmente legibles
 : 4.89 .

#1 - 293959   :
 
 
 
 
 
 
  

#2 - 293102   :
 
 
 
 
 
 
 

#3 - 290573   :
 
 
 
 
 
 
  

#4 - 287592   :
 
 
 
 
 
 
 

#5 - 284929   :
 
 
 
 
 
 
  


PASO 4: evalúe la sostenibilidad de los resultados


Es importante recordar que hay elementos de aleatoriedad en este proyecto: al determinar el tipo de accidente cerebrovascular utilizando la función randint . Al realizar pruebas repetidamente, noté que cuando se repiten sesiones con los mismos datos de entrada, las 5 mejores selecciones pueden variar. Esto no es muy feliz, y tomó para resolver el problema.

Primero hice un kit de prueba del equipo "obviamente_fuerte", donde incluso sin pruebas es obvio qué colecciones de cosas son las mejores aquí:

mira el conjunto obvio_fuerte
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)


Con este set habrá 6 peleas (3 espadas * 2 dagas * 1 * 1 * 1 * 1 * 1). En el top 5, la batalla donde se toma la peor espada y la peor daga no debe incluirse. Y, por supuesto, el primer lugar debe ser una selección con dos cuchillas más fuertes. Si lo piensa, entonces para cada colección es obvio en qué lugar caerá. Pruebas realizadas, se cumplieron las expectativas.

Aquí hay una visualización del resultado de una de las pruebas de este conjunto: a

imagen

continuación, reduje al mínimo la brecha en la cantidad de bonificaciones otorgadas por estas cuchillas de 5000, 800, 20 y 4000, 10 a5, 4, 3 y 2, 1, respectivamente (en el proyecto, este conjunto se encuentra en el archivo "equipment_obvious_weak.py"). Y aquí, de repente, la combinación de la espada más fuerte y la peor daga salió encima. Además, en una de las pruebas, las dos mejores armas llegaron repentinamente al último lugar:

imagen

¿Cómo entender esto? Las expectativas en la disposición obviamente correcta de las colecciones se mantuvieron sin cambios, pero el grado de diferencia entre ellas se redujo significativamente . Y ahora, los accidentes durante las batallas (la proporción de fallas y golpes, golpes críticos y no críticos, etc.) han adquirido una importancia decisiva.

Vamos a ver con qué frecuencia el "dúo de las cuchillas superiores" no caerá en primer lugar. Hice 100 de estos lanzamientos (para esto, envolví las "líneas de lanzamiento del programa" en un ciclo de 100 iteraciones y comencé a mantener un registro especial para toda esta "super sesión"). Aquí está la visualización de los resultados: por lo

imagen

tanto, los resultados en nuestro programa no siempre son estables (34% de los resultados "correctos" versus 66% de los "incorrectos").

La estabilidad de los resultados es directamente proporcional a la diferencia en las bonificaciones de los artículos probados.

Dado que la diferencia en la cantidad de bonos de cosas buenas que tiene sentido probar es débilmente perceptible (como en "World of Warcraft"), los resultados de tales pruebas serán relativamente inestables (inestables, inestables, etc.).

PASO 5 - Aumento de la resiliencia


Tratamos de pensar lógicamente.

Esbozamos el criterio de éxito: el "dúo de las cuchillas superiores" debe estar en primer lugar en el 99% de los casos.

Situación actual: 34% de estos casos.

Si no cambia el enfoque adoptado en principio (la transición de la simulación de batallas para todas las selecciones a un simple cálculo de características, por ejemplo), queda por cambiar algún parámetro cuantitativo de nuestro modelo.

Por ejemplo:

  • para llevar a cabo no una batalla por cada colección, sino varias, luego registre la media aritmética en el registro, descarte lo mejor y lo peor, etc.
  • realizar varias sesiones de prueba en general, y luego también tomar el "promedio"
  • extender la batalla en sí de 1000 golpes a un cierto valor, lo que será suficiente para que los resultados sean justos para las colecciones que están muy cerca en términos de la bonificación total

En primer lugar, me pareció una idea exitosa con un alargamiento de la batalla, porque es en este lugar donde sucede todo lo que causó el alargamiento de este artículo. Probaré la

hipótesis de que alargar la batalla de 1,000 a 10,000 golpes aumentará la estabilidad de los resultados (para esto debes establecer el ATTACKS_IN_FIGHT constante en 10000). Y esto es así:

imagen

luego decidió aumentar de 10,000 a 100,000 disparos, y esto condujo al cien por ciento de éxito. Después de eso, utilizando el método de búsqueda binaria, comenzó a seleccionar el número de resultados que darían el 99% de los éxitos para deshacerse de los cálculos excesivos. Se detuvo en 46875.

imagen

Si mi estimación del 99% de confiabilidad de un sistema con tal longitud de batalla es correcta, entonces dos pruebas seguidas reducen la probabilidad de error a 0.01 * 0.01 = 0.0001 .

Y ahora, si ejecutas una prueba con una batalla de 46,875 golpes para un conjunto de equipos para 1,728 batallas, tomará 233 segundos e inspirará confianza en que la "Espada del Maestro" gobierna:

los resultados de 1728 batallas por 46 875 ataques
 : 233.89 .

#1 - 13643508   :
 
 
 
 
 
 
 

#2 - 13581310   :
 
 
 
 
 
 
  

#3 - 13494544   :
 
 
 
 
 
 
 

#4 - 13473820   :
 
 
 
 
 
 
  

#5 - 13450956   :
 
 
 
 
 
 
 


PD: Esto es fácil de explicar: dos "Espadas del maestro" te permiten obtener 10 unidades de habilidad, lo que, de acuerdo con la mecánica inherente, elimina la posibilidad de golpes deslizantes, y esto agrega alrededor del 40% de golpes cuando se aplica X o 2X de daño en lugar de 0.7X.

El resultado de una prueba similar para los fanáticos de WoW:

los resultados de 1296 batallas de 46 875 golpes (wow classic preraid)
 : 174.58 .


#1 - 19950930   :
  '
  '
  
 
 
 
  

#2 - 19830324   :
  '
  '
 
 
 
 
  

#3 - 19681971   :
  '
  '
  
 
 
 
  

#4 - 19614600   :
  '
  '
 
 
 
 
  

#5 - 19474463   :
  '
  '
  
 
 
 
 


Resumen


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

Publiqué todo el código del proyecto en el github .

Estimada comunidad, me complacerá recibir comentarios sobre este tema.

UPD del 04/08/2020:
gracias a los comentariosDeerenaros, knotri y GriboksMe di cuenta de que en lugar de simular miles de batallas, puedes calcular la expectativa matemática de un golpe y, sobre esta base, clasificar el equipo. Tiré todo lo relacionado con peleas fuera del código, en lugar de la simulate_fight función que hice calculate_expectation . En la salida, obtengo los mismos resultados. Se agregó el código resultante al repositorio .

All Articles