Wie man Ausrüstung für ein persisches Spiel in Python auswählt

Mit Hilfe der Programmierung lernen wir, das Beste für unseren Räuber zu finden. Wir finden auch heraus, ob das Programm "an der Nase vorbeigeht".

Bild

Zweck: Lernen, Schritt für Schritt den notwendigen Teil der Spielmechanik in vitro zu simulieren, die notwendigen Daten zu erhalten und daraus Schlussfolgerungen zu ziehen.

Was Sie brauchen: Python 3, die Umgebung für die Arbeit mit Code (ich habe PyCharm).

In Spielen möchten viele Leute das Beste aus ihren Charakteren herausholen, und dafür müssen Sie die optimalste Kombination von Ausrüstung auswählen, was oft sehr viel ist. Versuchen wir, einen eigenen Algorithmus zum Testen verschiedener Gerätekombinationen und zum Sammeln von Daten zu schreiben.

Anfangs war ich vom Spiel „World of Warcraft: Classic“ inspiriert (ich habe die Symbole von dort übernommen ), aber dabei habe ich einige Vereinfachungen vorgenommen. Link zum gesamten Projekt am Ende des Artikels.

SCHRITT 1 - Suchbereich auswerten


Angenommen, wir haben einen Charakter in der Rogue-Klasse. Es ist notwendig, Ausrüstung für ihn aufzunehmen, mit der er dem Feind maximalen Schaden zufügt. Wir interessieren uns für Dinge für die Slots "Waffen in der rechten Hand" (4 Stk.), "Waffen in der linken Hand" (4 Stk.), "Handschuhe" (2 Stk.), "Kopf" (3 Stk.), "Brust" (3 Stk.), "Beine" (3 Stk.), "Füße" (2 Stk.). Wir werden ihre verschiedenen Kombinationen auf den Charakter setzen und den Kampf simulieren. Und wenn Sie die Idee einer umfassenden Suche anwenden (mit der wir beginnen werden), müssen Sie mindestens 4 * 4 * 2 * 3 * 3 * 3 * 2 = 1728 Kämpfe ausgeben, um alle Kombinationen zu bewerten .

Für eine genauere Bewertung der besten Kombinationen müssen Sie zusätzliche Schlachten durchführen.

Bereits zu diesem Zeitpunkt können wir das Projektschema wie folgt präsentieren:

Bild

SCHRITT 2 - Spielmechanik analysieren


Beginnen wir mit dem Charakter. Er hat solche Eigenschaften, die sich auf den verursachten Schaden und auf einander auswirken:

  1. Angriffskraft - wird direkt in den durch einen normalen Schlag verursachten Schaden umgewandelt (1 zu 1). Berechnet nach der Formel: Angriffspunkte + Stärkepunkte + Geschicklichkeitspunkte
  2. Stärke - +1, um Macht und alles anzugreifen (was zu tun ist, das ist das Spieldesign)
  3. Geschicklichkeit - +1 für Angriffskraft und alle 20 Einheiten Beweglichkeit erhöhen die kritische Chance um 1%
  4. Kreta. Chance - die Chance, doppelten Schaden zu verursachen, wenn der Schlag nicht rutscht und verfehlt
  5. Genauigkeit - erhöhte Chance, einen Gegner zu treffen
  6. Beherrschung - Jede Beherrschungseinheit verringert die Wahrscheinlichkeit eines Gleitschlags um 4% (was anfänglich 40% entspricht, was bedeutet, dass 10 Beherrschungseinheiten die Möglichkeit eines Gleitschlags vollständig ausschließen).

Das folgende Diagramm zeigt die Grundwerte für unseren Räuber und wie das Anlegen eines Geräts diese verändert:

Bild

Es ist also Zeit, mit dem Schreiben von Code zu beginnen. Wir beschreiben, was wir bereits in der Rogue-Klasse wissen. Die Methode set_stats_without_equip stellt den Status des Charakters ohne Ausrüstung wieder her, was beim Ändern von Sammlungen hilfreich ist. Die Methoden berechne_Kritischer_Prozent und Berechnen_Glanz_Prozent werden in Zukunft nur bei Bedarf aufgerufen, wobei die Werte bestimmter Merkmale aktualisiert werden.

erste Zeilen der Klasse
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


Jetzt müssen Sie sich mit der Ausrüstung befassen. Um alle Dinge bequem zu sortieren und ihre Kombinationen zu erstellen, habe ich beschlossen, für jeden Gerätetyp ein eigenes Wörterbuch zu erstellen: RECHTE HÄNDE, LINKE HÄNDE, HANDSCHUHE, KÖPFE, KÄSTEN, HOSEN, STIEFEL. Die folgenden Tupel werden als Werte in den Wörterbüchern gespeichert:

Bild

Erstellen Sie eine separate Datei für Wörterbücher mit Geräten. Ich habe mehrere solcher Dateien mit unterschiedlichen Sätzen.

abstrakte Testausrüstung
#    ,     :
# 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)


Fügen Sie dem Konstruktor der Rogue-Klasse die Equipype-Zeichenfolge hinzu
    ...
    #    ,    id  :
    # 0 -  , 1 -  , 2 - , 3 - , 4 - , 5 - , 6 - 
    self.equipment_slots = [0] * 7

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


Wir fügen unserer Klasse auch die Methoden wear_item (Berechnung der Eigenschaften beim Anlegen von Dingen) und unwear_all (Entfernen aller Dinge) hinzu.

Klassenmethoden, die für die Arbeit mit Geräten verantwortlich sind
    ...
    #   "  ":
    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()


Die Tatsache, dass einige Dinge kombiniert werden, bietet zusätzliche Boni (in „World of Warcraft“ wird dies als „Set-Bonus“ bezeichnet). In meinem abstrakten Set wird ein solcher Bonus durch das gleichzeitige Anlegen der rechtshändigen Waldwache und der linkshändigen Waldwache gewährt. Fügen Sie dies dem Code der wear_item- Methode hinzu :

Legen Sie Boni in der Methode wear_item fest
    ...
    #      "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('  ...')


Jetzt muss unserem Räuber beigebracht werden, wie man kämpft. Wir werden einen Kampf als eine Reihe von 1000 Angriffen auf einen Gegner betrachten, der mit dem Rücken zu uns steht und mit etwas anderem beschäftigt ist (eine typische Situation für World of Warcraft). Jeder Schlag, unabhängig von den vorherigen, kann sein:

  • Normal - Standardschaden, in unserem Modell entsprechend der "Angriffskraft" des Charakters
  • Bewegen - 70% Schaden vom Normalen
  • kritisch - doppelter Schaden von normal
  • Miss - 0 Schaden

Dies wird durch eine Reihe von Überprüfungen nach diesem Schema bestimmt:

Bild

Und für einen Räuber mit Grundwerten hat dieses Schema die Form: Programmieren Sie

Bild

diese Mechanik, indem Sie dem Code unserer Klasse die Methode do_attack hinzufügen . Es wird ein Tupel mit zwei Zahlen zurückgegeben: (Ergebnis des Angriffs, verursachter Schaden).

Angriffscode
    ...
    #    :
    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


Wir werden eine bequeme Anzeige des aktuellen Zustands des Räubers erhalten, so dass Sie jederzeit überprüfen können, was mit ihm passiert:

Definieren Sie die magische Methode __str__ neu
    ...
    #  " "     :
    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


SCHRITT 3 - Vorbereiten des Starts


Jetzt ist es Zeit, einen Code zu schreiben, der Kämpfe für alle möglichen Ausrüstungssätze bietet. Dazu rufe ich nacheinander Funktionen nach diesem Schema auf:

Bild

  1. run_session - Hier werden verschachtelte Schleifen implementiert, die alle erforderlichen Wörterbücher mit Dingen sortieren und für jede Kombination die folgende Funktion aufrufen. Am Ende wird der Berichtstext generiert und im Sitzungsprotokoll gespeichert
  2. test_combination - Alle zuvor getragenen Kleidungsstücke werden zurückgesetzt und die Methode wear_item wird immer wieder aufgerufen. Dabei wird der Charakter in ein neues „Outfit“ gekleidet, wonach die nächste Funktion aufgerufen wird
  3. simulate_fight - Die gleiche do_attack-Methode wird 1000 Mal aufgerufen. Die empfangenen Daten werden gespeichert. Falls erforderlich, wird für jede Schlacht ein detailliertes Protokoll geführt

Funktionen 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



Um die Protokolle zu speichern, verwende ich zwei einfache Funktionen:

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


Jetzt müssen nur noch einige Zeilen geschrieben werden, um die Sitzung zu starten und die Ergebnisse zu speichern. Wir importieren auch die notwendigen Standard-Python-Module. Hier können Sie festlegen, welche Geräte getestet werden sollen. Für Fans von World of Warcraft habe ich Ausrüstung von dort abgeholt, aber denken Sie daran, dass dieses Projekt nur eine ungefähre Rekonstruktion des Mechanikers von dort ist.

Programmcode
#     :
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__".')


Eine Sitzung mit 1728 Schlachten dauert auf meinem Laptop 5 Sekunden. Wenn Sie LOG_EVERY_FIGHT = True setzen, werden Dateien mit Daten für jeden Kampf im Ordner Fight_logs angezeigt, die Sitzung dauert jedoch bereits 9 Sekunden. In jedem Fall wird das Sitzungsprotokoll im Ordner session_logs angezeigt:

Die ersten 10 Zeilen des Protokolls
#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


Wie Sie sehen, reicht es nicht aus, nur eine Sitzung abzuhalten. Sie müssen Informationen aus diesen Hunderten von Zeilen über die Kombinationen von Dingen extrahieren, die zu den besten Ergebnissen geführt haben. Dazu schreiben wir zwei weitere Funktionen. Die allgemeine Idee ist, das empfangene Protokoll zu öffnen, eine Liste der Schadensbeträge für jede Schlacht zu erstellen, diese zu sortieren und beispielsweise die Namen der Dinge aufzuschreiben, die für die 5 besten Situationen verwendet wurden.

Funktionen zur Bestimmung des oberen Gangs
#       :
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]


Am Ende der Protokollzeilen werden nun 5 Auswahlen angezeigt, die das beste Ergebnis zeigen:

endlich lesbare Protokollzeilen
 : 4.89 .

#1 - 293959   :
 
 
 
 
 
 
  

#2 - 293102   :
 
 
 
 
 
 
 

#3 - 290573   :
 
 
 
 
 
 
  

#4 - 287592   :
 
 
 
 
 
 
 

#5 - 284929   :
 
 
 
 
 
 
  


SCHRITT 4 - Bewertung der Nachhaltigkeit der Ergebnisse


Es ist wichtig zu bedenken, dass dieses Projekt zufällige Elemente enthält: bei der Bestimmung der Strichart mithilfe der Randint- Funktion . Bei wiederholten Tests stellte ich fest, dass bei Wiederholung von Sitzungen mit denselben Eingabedaten die Top-5-Auswahlmöglichkeiten variieren können. Dies ist nicht sehr glücklich und nahm das Problem zu lösen.

Zuerst habe ich ein Testkit für das Gerät „Offensichtliche_Starke“ erstellt, bei dem auch ohne Tests klar ist, welche Sammlungen von Dingen hier die besten sind:

Beobachten Sie das offensichtlich_starke 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)


Mit diesem Set gibt es 6 Kämpfe (3 Schwerter * 2 Dolche * 1 * 1 * 1 * 1 * 1). In den Top 5 sollte der Kampf, in dem das schlimmste Schwert und der schlimmste Dolch genommen werden, nicht enthalten sein. Nun, natürlich sollte der erste Platz eine Auswahl mit zwei stärksten Klingen sein. Wenn Sie darüber nachdenken, ist es für jede Sammlung offensichtlich, an welchen Ort sie fallen wird. Durchgeführte Tests, Erwartungen wurden erfüllt.

Hier ist eine Visualisierung des Ergebnisses eines der Tests dieses Sets:

Bild

Als nächstes habe ich die Lücke in der Größe der Boni, die diese Klingen geben, von 5000, 800, 20 und 4000, 10 auf ein Minimum reduziert5, 4, 3 bzw. 2, 1 (im Projekt befindet sich dieser Satz in der Datei „Equipment_obvious_weak.py“). Und hier setzte sich plötzlich die Kombination aus dem stärksten Schwert und dem schlimmsten Dolch durch. Außerdem kamen in einem der Tests plötzlich die beiden besten Waffen auf den letzten Platz:

Bild

Wie kann man das verstehen? Die Erwartungen an die offensichtlich korrekte Anordnung der Sammlungen blieben unverändert, der Unterschied zwischen ihnen wurde jedoch erheblich verringert . Und jetzt haben Unfälle während der Kämpfe (das Verhältnis von Fehlschlägen und Treffern, kritischen und unkritischen Treffern usw.) entscheidende Bedeutung erlangt.

Lassen Sie uns überprüfen, wie oft das "Duett der oberen Klingen" nicht an erster Stelle steht. Ich habe 100 solcher Starts durchgeführt (dafür habe ich die "Programmstartlinien" in einen Zyklus von 100 Iterationen eingeschlossen und begonnen, ein spezielles Protokoll für diese gesamte "Supersitzung" zu führen). Hier ist die Visualisierung der Ergebnisse:

Bild

Die Ergebnisse in unserem Programm sind also nicht immer stabil (34% der „richtigen“ Ergebnisse gegenüber 66% der „falschen“).

Die Stabilität der Ergebnisse ist direkt proportional zur Differenz der Boni der getesteten Gegenstände.

Angesichts der Tatsache, dass der Unterschied in der Höhe der Boni für gute Dinge, deren Testen sinnvoll ist, nur schwach wahrnehmbar ist (wie in "World of Warcraft"), sind die Ergebnisse solcher Tests relativ instabil (instabil, instabil usw.).

SCHRITT 5 - Erhöhung der Belastbarkeit


Wir versuchen logisch zu denken.

Wir skizzieren das Erfolgskriterium: In 99% der Fälle sollte das „Duett der Top Blades“ an erster Stelle stehen.

Aktuelle Situation: 34% dieser Fälle.

Wenn Sie den akzeptierten Ansatz im Prinzip nicht ändern (z. B. den Übergang von der Simulation von Schlachten für alle Auswahlen zu einer einfachen Berechnung von Merkmalen), müssen einige quantitative Parameter unseres Modells geändert werden.

Zum Beispiel:

  • Um nicht einen Kampf für jede Sammlung, sondern mehrere zu führen, notieren Sie dann das arithmetische Mittel im Protokoll, verwerfen Sie das Beste und das Schlechteste usw.
  • Führen Sie im Allgemeinen mehrere Testsitzungen durch und nehmen Sie dann auch den „Durchschnitt“.
  • den Kampf selbst von 1000 Treffern auf einen bestimmten Wert zu verlängern, was ausreicht, damit die Ergebnisse für die Sammlungen fair sind, die in Bezug auf den Gesamtbonus sehr nahe beieinander liegen

Zunächst schien es mir eine erfolgreiche Idee mit einer Erweiterung des Kampfes zu sein, denn an dieser Stelle geschieht alles, was die Erweiterung dieses Artikels verursacht hat.

Ich werde die Hypothese testen , dass eine Verlängerung des Kampfes von 1.000 auf 10.000 Schläge die Stabilität der Ergebnisse erhöht (dazu müssen Sie die Konstante ATTACKS_IN_FIGHT auf 10000 setzen). Und das ist so:

Bild

Dann entschied er sich, von 10.000 auf 100.000 Schüsse zu erhöhen , und dies führte zu einem hundertprozentigen Erfolg. Danach begann er mit der binären Suchmethode, die Anzahl der Treffer auszuwählen, die 99% der Erfolge bringen würden, um übermäßige Berechnungen loszuwerden. Bei 46 875 gestoppt.

Bild

Wenn meine Schätzung der 99% igen Zuverlässigkeit eines Systems mit einer solchen Kampflänge korrekt ist, reduzieren zwei Tests hintereinander die Fehlerwahrscheinlichkeit auf 0,01 * 0,01 = 0,0001 .

Und jetzt, wenn Sie einen Test mit einer Schlacht von 46.875 Schlägen für eine Ausrüstung für 1.728 Schlachten durchführen, dauert es 233 Sekunden und schafft Vertrauen, dass das „Schwert des Meisters“ regiert:

die Ergebnisse von 1728 Schlachten um 46.875 Streiks
 : 233.89 .

#1 - 13643508   :
 
 
 
 
 
 
 

#2 - 13581310   :
 
 
 
 
 
 
  

#3 - 13494544   :
 
 
 
 
 
 
 

#4 - 13473820   :
 
 
 
 
 
 
  

#5 - 13450956   :
 
 
 
 
 
 
 


PS Und das ist leicht zu erklären: Mit zwei „Schwertern des Meisters“ können Sie 10 Fertigkeitseinheiten erhalten, die laut der inhärenten Mechanik die Möglichkeit von Gleitschlägen ausschließen, und dies fügt etwa 40% der Schläge hinzu, wenn X- oder 2X-Schaden anstelle von 0,7X angewendet wird.

Das Ergebnis eines ähnlichen Tests für WoW-Fans:

die Ergebnisse von 1296 Schlachten mit 46 875 Treffern (wow classic preraid)
 : 174.58 .


#1 - 19950930   :
  '
  '
  
 
 
 
  

#2 - 19830324   :
  '
  '
 
 
 
 
  

#3 - 19681971   :
  '
  '
  
 
 
 
  

#4 - 19614600   :
  '
  '
 
 
 
 
  

#5 - 19474463   :
  '
  '
  
 
 
 
 


Zusammenfassung


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

Ich habe den gesamten Projektcode auf dem Github gepostet .

Liebe Community, ich freue mich über Feedback zu diesem Thema.

UPD vom 08.04.2020:
Danke an die KommentareDeerenaros, knotri und GriboksIch erkannte, dass Sie anstelle von Tausenden von Schlachten die mathematische Erwartung für einen Treffer berechnen und auf dieser Grundlage die Ausrüstung bewerten können. Wirf alles, was mit Kämpfen zu tun hat, aus dem Code heraus, anstatt der Funktion simulate_fight , berechnete die Erwartung . Am Ausgang erhalte ich die gleichen Ergebnisse. Der resultierende Code wurde dem Repository hinzugefügt .

All Articles