Python YoGiOh Masterduel: как эффективно узнать шанс на создание хорошей финальной доски? [закрыто]

Вопрос или проблема

В игре YoGiOh игрок начинает с того, что тянет 5 карт из колоды, содержащей от 40 до 60 карт. Затем игрок может использовать стартовые карты из этих 5 карт, чтобы разработать финальную комбинацию, которую будет сложно сломать противнику. Не каждая стартовая рука хороша. Некоторые карты требуют наличия одной или нескольких других конкретных карт на руке для развития. Сложно оптимизировать колоду для комбинаций из 2 карт.

Я пишу программу, чтобы оценить свои шансы на наличие комбинаций. Она работает, но 40 секунд для маленькой колоды из 42 карт не очень эффективно. Я ищу оптимизацию.

Ввод:

Код карты Название карты Количество Комбинация
0 Эффект Вейлера 3
1 Дух Теньи – Адхара 2 1D
2 Макс “С” 3
3 Призрачный Огурец и Снежная Кролик 1
4 Цветок Пепла и Радости Весны 3
5 Невероятная Эклесия, Добродетельная 3 51 56 57 58 5C 5D 5E 5G 5J
6 Меч Души Мо Е 3 61 66 67 68 6C 6D 6E 6G 6J
7 Меч Души Тайя 2 71 7C 7D
8 Стратег Слов Долгие 3 81 87 8C 8D 8E 8G 8J
9 Бистиал Друисвурм 2
A Бистиал Магнхамут 1
B Бистиал Балдрак 1
C Дух Теньи – Вишуда 1
D Дух Теньи – Ашуна 3
E Арх-Немезис Протос 1
F Кубок Желаний 1
G Пояснительная Книга Мечей 3 G G1 G7 GC GD GE GG GJ
H Вызванный К Grave 2
I Бесконечная Неопределенность 3
J Темный Взрыв Мечей 1

Это колода на 42 карты с картами “Слов Долгие”. Комбинация “1D” означает, что если у игрока есть “Дух Теньи – Адхара” с кодом 1 и “Дух Теньи – Ашуна” с кодом D, рука играбельна.

Колоду можно закодировать как 0001122234445556667788899ABCDDDEFGGGHHIIIJ. Объяснение: “000”, потому что мы используем 3 копии карты “Эффект Вейлера” с кодом 0.

Рука с комбинациями содержит символы в разделе комбинаций. 04568 содержит 56 и 68: это означает, что 2 разные комбинации играбельны.


     from itertools import combinations
     from collections import Counter
     from math import comb
     import functools
     import operator
     import time
     import multiprocessing

     import concurrent.futures
     #многопоточность 

     number_threads = 10
     number_process = 6

     #разделить список на равные части
     def chunkify(iterable,n):
          lst =  list(iterable)
          return [lst[i::n] for i in range(n)]
     #конец многопоточности

     ######ВВОД
     deck_input="0001122234445556667788899ABCDDDEFGGGHHIIIJ"
     success_input="1D 51 56 57 58 5C 5D 5E 5G 5J 61 66 67 68 6C 6D 6E 6G 6J 71 7C 7D 81 87 8C 8D 8E 8G 8J G1 G7 GC GD GE GG GJ"

     number_card_in_hand = 5
     ######КОНЕЦ ВВОДА

     #обработка входных данных
     deck_counter = Counter(deck_input)
     all_possible_hand_combination = list(combinations(deck_input, number_card_in_hand))

     successes_list_of_counter = []
     for combo in success_input.split(' '):
         successes_list_of_counter.append(Counter(combo))
     successes_set_of_counter = []

     #удаление дубликатов объектов счетчиков
     for i in successes_list_of_counter:
         if i not in successes_set_of_counter:
             successes_set_of_counter.append(i)
     #конец обработки входных данных


     #тест на наличие какого-либо успешного шаблона в руке, оценка в зависимости от количества разных стартовых карт в руке
     def is_success_with_score(hand, successes_set_of_counter = successes_set_of_counter):
         counter_hand = Counter(hand)
         res = 0
         for s in successes_set_of_counter:
             if counter_hand >= s:
                 res+=1
         res = min(res, len(hand))
         return res

     def countingWorker(possible_hand_chunk):
         return count_successes(hand_combination = possible_hand_chunk)

     def count_success_thread_starter():
         with concurrent.futures.ThreadPoolExecutor(max_workers=number_threads) as executor:
             worker_input = chunkify(all_possible_hand_combination, number_threads)
             futures = [executor.submit(countingWorker, chunk) for chunk in worker_input]
             res={"hand_with_starter":0, "total_number_starter":0}
             for future in futures:
                 fr = future.result() # блокировка до возвращения результата
                 res["hand_with_starter"] += fr["hand_with_starter"]
                 res["total_number_starter"] += fr["total_number_starter"]
         return res

     def count_success_process_starter():
         pool = multiprocessing.Pool(processes=number_process)
         worker_inputs = chunkify(all_possible_hand_combination, number_threads)
         res={"hand_with_starter":0, "total_number_starter":0}
         for worker_result in pool.map(countingWorker, worker_inputs):
             res["hand_with_starter"] += worker_result["hand_with_starter"]
             res["total_number_starter"] += worker_result["total_number_starter"]
         return res
             

     def count_successes(hand_combination = all_possible_hand_combination, deck = deck_input, number_card_in_hand = number_card_in_hand, successes_set_of_counter = successes_set_of_counter):
         res={"hand_with_starter":0, "total_number_starter":0}
         for hand_tuple in hand_combination:
             success_score = is_success_with_score(hand_tuple)
             if success_score > 0:
                 res["hand_with_starter"] += 1
                 res["total_number_starter"] += success_score
         return res
             
     def total_hands(deck = deck_input, number_card_in_hand = number_card_in_hand):
         return comb(len(deck), number_card_in_hand)



     print('Колода:',deck_input)
     print('Счетчик колоды:',deck_counter)
     print('Размер колоды:',len(deck_input))
     print('Размер руки:',number_card_in_hand)


     all_possible_hand_combination = list(combinations(deck_input, number_card_in_hand))

     time_start = time.time()
     c = count_success_thread_starter()
     print('Шанс получить хотя бы 1 стартовую карту',c["hand_with_starter"]/total_hands())
     print('Среднее количество разных стартовых карт в руке',c["total_number_starter"]/total_hands())
     time_end  = time.time()
     print('Прошедшее время:', time_end-time_start)

Вывод кода:

    Колода: 0001122234445556667788899ABCDDDEFGGGHHIIIJ
    Размер колоды: 42
    Размер руки: 5
    Шанс получить хотя бы 1 стартовую карту 0.8039293825558267
    Среднее количество разных стартовых карт в руке 1.921834370165564
    Прошедшее время: 38.807377576828
    профилировщик
    195255471 вызовов функций (195254994 примитивных вызовов) за 47.338 секунд

    Упорядочено по: накопленное время

    ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     24/1    0.000    0.000   47.338   47.338 {встроенный метод builtins.exec}
        1    0.045    0.045   47.338   47.338 2card combo.py:1(<module>)
        1    0.349    0.349   47.261   47.261 2card combo.py:200(count_successes)
   658008    3.395    0.000   46.912    0.000 2card combo.py:165(is_success_with_score)
 12502152    8.150    0.000   42.003    0.000 __init__.py:802(__ge__)
 12502323    6.879    0.000   32.231    0.000 {встроенный метод builtins.all}
 87423969   22.410    0.000   26.229    0.000 __init__.py:806(<genexpr>)

в настоящее время решение с потоками и без потоков работают одинаково медленно. Решение с процессами работает очень медленно.

Я думал, что генератор комбинаций медленный, но ему понадобилось всего 0.04 секунды, чтобы кэшировать список. Я попробовал многопоточность, многопроцессорность. Результаты те же или хуже.

Стоит ли мне использовать numpy и перемножать тензоры? 🙁

Ответ или решение

Чтобы эффективно определить вероятность формирования хорошей конечной доски в игре Yu-Gi-Oh! Master Duel с помощью Python, вам нужно оптимизировать свой код и использовать более быстрые алгоритмы. Давайте разберем ваш итеративный подход и предложим некоторые улучшения.

Оптимизация алгоритма

  1. Использование numpy:
    Библиотека numpy может помочь улучшить производительность, особенно при работе с массивами и матрицами. Вместо использования Counter для подсчета карточек и проверки комбинаций, вы можете представлять вашу колоду в виде массива и производить операции более эффективно.

  2. Избегание повторного вычисления:
    Вычисления, которые можно кэшировать или хранить, должны использоваться повторно, чтобы снизить время выполнения. Сохранение результатов для уже обработанных рук поможет избежать повторных вычислений.

  3. Уменьшение сложности:
    Для каждой возможной руки вы можете сразу проверять, удовлетворяет ли она вашим условиям, вместо проверки всех возможных комбинаций. Это сокращает общее количество проверок.

Пример кода с оптимизациями

Вот пример того, как можно переписать ваш код с учетом вышеуказанных предложений:

import numpy as np
from itertools import combinations
from collections import Counter
from math import comb
import time

# Определим входные данные
deck_input = "0001122234445556667788899ABCDDDEFGGGHHIIIJ"
success_input = "1D 51 56 57 58 5C 5D 5E 5G 5J 61 66 67 68 6C 6D 6E 6G 6J 71 7C 7D 81 87 8C 8D 8E 8G 8J G1 G7 GC GD GE GG GJ"
number_card_in_hand = 5

# Преобразуем входные данные
deck_counter = Counter(deck_input)
success_combos = [Counter(combo) for combo in success_input.split()]

# Функция для проверки комбинаций в руке
def check_success(hand_counter, success_combos):
    count = sum(1 for combo in success_combos if all(hand_counter[c] >= combo[c] for c in combo))
    return count

# Генерация всех возможных рук
all_possible_hands = list(combinations(deck_input, number_card_in_hand))

# Счетчики для успешных рук
successful_hands = 0
total_starters = 0

# Оптимизированный обход рук
start_time = time.time()
for hand in all_possible_hands:
    hand_counter = Counter(hand)
    score = check_success(hand_counter, success_combos)
    if score > 0:
        successful_hands += 1
        total_starters += score

# Определение общего количества рук
total_hands = comb(len(deck_input), number_card_in_hand)

# Результаты
print('Вероятность наличия хотя бы 1 стартерной карты:', successful_hands / total_hands)
print('Среднее количество разных стартеров в руке:', total_starters / total_hands)
print('Время выполнения:', time.time() - start_time)

Пояснения к коду

  • Использование Counter: Здесь мы используем Counter для представления карт в руке и проверяем, удовлетворяет ли она условиям для успеха.
  • Обход рук: Вместо использования многопоточности или множественных процессов, мы просто проходим все возможные руки, что и проще для отладки.
  • Время выполнения: Код должен выполняться значительно быстрее, поскольку объем вычислений был уменьшен.

Заключение

Оптимизация вашего кода позволит уменьшить время выполнения, что является жизненно важным для обработки больших наборов комбинаций. Используйте библиотеки, такие как numpy, чтобы воспользоваться векторизированными операциями, если это возможно.
Это базовое решение можно дополнительно улучшать, в зависимости от специфики вашей задачи.

Оцените материал
Добавить комментарий

Капча загружается...