Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import random
- import numpy as np
- import matplotlib.pyplot as plt
- from collections import defaultdict
- class ArknightsEndfieldPitySimulator:
- def __init__(self, num_banners=10):
- # Базовые параметры гачи (только 6★)
- self.base_rate_6 = 0.008 # 0.8% базовый шанс 6★
- self.soft_pity_start = 65 # Начало софт-пити
- self.hard_pity = 80 # Хард-пити (гарантия 6★)
- # Награды за количество круток
- self.emergency_pity = 30 # Экстренная вербовка (10 бесплатных круток)
- self.dossier_pity = 60 # Досье охотника (10 бесплатных круток)
- # Система 50/50
- self.rate_up_chance = 0.5 # 50% шанс получить ивентового персонажа
- # Бесплатные крутки с магазина (обновляются каждый баннер)
- self.free_shop_pulls = 10
- # Количество баннеров
- self.num_banners = num_banners
- def calculate_6star_chance(self, pity_counter, is_emergency=False):
- """
- Рассчет шанса выпадения 6★
- is_emergency: для экстренной вербовки всегда базовый шанс 0.8%
- """
- if is_emergency:
- return self.base_rate_6 # Экстренная вербовка всегда 0.8%
- if pity_counter < self.soft_pity_start:
- return self.base_rate_6
- else:
- # Увеличение на 5% за каждую крутку после 65
- extra_rolls = pity_counter - self.soft_pity_start + 1
- return min(1.0, self.base_rate_6 + 0.05 * extra_rolls)
- def simulate_pull(self, pity_counter, count_for_pity=True, is_emergency=False):
- """
- Симуляция одной крутки (только проверка на 6★)
- count_for_pity: влияет ли крутка на счетчик гаранта
- is_emergency: для экстренной вербовки - всегда базовый шанс
- Возвращает: (выпал_6★, новый_pity)
- """
- # Проверяем выпадение 6★ с учетом типа крутки
- if random.random() < self.calculate_6star_chance(pity_counter, is_emergency):
- # Выпал 6★
- if count_for_pity:
- return True, 0
- else:
- return True, pity_counter # pity не меняется
- else:
- # Не выпал 6★
- if count_for_pity:
- return False, pity_counter + 1
- else:
- return False, pity_counter
- def check_50_50(self, is_target_banner):
- """Проверка 50/50 на получение ивентового персонажа"""
- return random.random() < self.rate_up_chance
- def simulate_banner(self, is_target_banner, strategy, banner_index,
- current_pity,
- has_dossier_from_prev=False):
- """
- Симуляция одного баннера
- Возвращает: платные крутки, полученных персонажей, новый pity, использована ли экстренная
- """
- pulls_spent = 0 # ТОЛЬКО платные крутки
- total_pulls_for_rewards = 0 # Крутки, которые считаются для получения наград (НЕ включая экстренную вербовку)
- got_target = False
- event_6star_count = 0
- total_6star_count = 0
- # Получили ли досье на ЭТОМ баннере (для следующего)
- got_dossier_this_banner = False
- # Счетчик пити
- local_pity = current_pity
- # 1. Досье с прошлого баннера (всегда используем полностью, если есть)
- if has_dossier_from_prev:
- for _ in range(10):
- got_six, local_pity = self.simulate_pull(local_pity, count_for_pity=True, is_emergency=False)
- if got_six:
- total_6star_count += 1
- if self.check_50_50(is_target_banner):
- event_6star_count += 1
- if is_target_banner:
- got_target = True
- total_pulls_for_rewards += 10 # Досье учитывается для наград
- # 2. Бесплатные крутки с магазина (всегда используем все 10)
- for _ in range(self.free_shop_pulls):
- got_six, local_pity = self.simulate_pull(local_pity, count_for_pity=True, is_emergency=False)
- if got_six:
- total_6star_count += 1
- if self.check_50_50(is_target_banner):
- event_6star_count += 1
- if is_target_banner:
- got_target = True
- total_pulls_for_rewards += self.free_shop_pulls # Магазин учитывается для наград
- # Флаг, что мы уже использовали экстренную вербовку на этом баннере
- emergency_used_this_banner = False
- # 3. ОСНОВНЫЕ КРУТКИ (платные) согласно стратегии
- while True:
- # Проверка на спарк (120 круток, учитываются только те, что идут в награды)
- if is_target_banner and total_pulls_for_rewards >= 120 and not got_target:
- got_target = True
- event_6star_count += 1
- total_6star_count += 1
- break
- # Проверка условий остановки
- stop_pulling = self.should_stop_pulling(
- strategy, is_target_banner, total_pulls_for_rewards,
- local_pity, got_target, emergency_used_this_banner
- )
- if stop_pulling:
- break
- # Платная крутка
- pulls_spent += 1
- total_pulls_for_rewards += 1 # Платные крутки учитываются для наград
- got_six, local_pity = self.simulate_pull(local_pity, count_for_pity=True, is_emergency=False)
- if got_six:
- total_6star_count += 1
- if self.check_50_50(is_target_banner):
- event_6star_count += 1
- if is_target_banner:
- got_target = True
- # Проверка на экстренную вербовку (30 круток в счетчике наград)
- if total_pulls_for_rewards >= self.emergency_pity and not emergency_used_this_banner:
- emergency_used_this_banner = True
- # ЭКСТРЕННАЯ ВЕРБОВКА - 10 круток
- # Важно: всегда базовый шанс 0.8%, не зависит от pity!
- for _ in range(10):
- got_six, _ = self.simulate_pull(local_pity, count_for_pity=False, is_emergency=True)
- if got_six:
- total_6star_count += 1
- if self.check_50_50(is_target_banner):
- event_6star_count += 1
- if is_target_banner:
- got_target = True
- # НЕ увеличиваем total_pulls_for_rewards!
- # Экстренная вербовка не идет в счет для новых наград
- # Проверка на досье (60 круток в счетчике наград)
- if total_pulls_for_rewards >= self.dossier_pity and not got_dossier_this_banner:
- got_dossier_this_banner = True
- return {
- 'pulls_spent': pulls_spent,
- 'new_pity': local_pity,
- 'got_target': got_target,
- 'event_6star_count': event_6star_count,
- 'total_6star_count': total_6star_count,
- 'got_dossier_for_next': got_dossier_this_banner,
- 'emergency_used': emergency_used_this_banner
- }
- def should_stop_pulling(self, strategy, is_target, total_pulls_for_rewards,
- current_pity, got_target, emergency_used):
- """Проверка нужно ли остановить пуллы"""
- # Для стратегии 3: не останавливаемся до 60, даже если получили цель
- if strategy == 3:
- if total_pulls_for_rewards < 60:
- return False # Крутим до 60 на любых баннерах, независимо от получения цели
- elif is_target:
- # На целевых после 60 крутим до получения цели (если еще не получили)
- # Если уже получили цель до 60, то после 60 все равно останавливаемся
- return got_target # Останавливаемся только если цель уже получена
- else:
- # На нецелевых останавливаемся после 60
- return True
- # Для стратегий 1 и 2: стандартная логика
- if is_target and got_target:
- return True
- if current_pity >= self.hard_pity - 1:
- return False
- if strategy == 1:
- # Стратегия 1: Только целевые баннеры
- if is_target:
- return False # На целевых крутим до получения цели
- else:
- return True # На нецелевых только бесплатные (уже использованы)
- elif strategy == 2:
- # Стратегия 2: На нецелевых крутим до получения экстренной вербовки
- if is_target:
- return False # На целевых крутим до получения цели
- else:
- # На нецелевых крутим пока не получим экстренную вербовку
- if emergency_used:
- return True # Уже получили и использовали экстренную - стоп
- else:
- # Не получили еще - продолжаем до 30
- return total_pulls_for_rewards >= self.emergency_pity
- return True
- def simulate_all_banners(self, target_banner_indices, strategy):
- """
- Симуляция всех баннеров
- target_banner_indices: список индексов баннеров, где нужен персонаж
- Возвращает: результаты одной симуляции
- """
- total_pulls_spent = 0 # Суммируются ВСЕ платные крутки за все баннеры
- total_free_pulls = 0 # Суммируются ВСЕ бесплатные крутки за все баннеры
- targets_obtained = 0
- total_event_6stars = 0
- total_6stars_all = 0
- current_pity = 0
- has_dossier_from_prev = False
- for banner_idx in range(self.num_banners):
- is_target = banner_idx in target_banner_indices
- banner_result = self.simulate_banner(
- is_target, strategy, banner_idx,
- current_pity,
- has_dossier_from_prev
- )
- # Добавляем платные крутки с этого баннера в общую сумму
- total_pulls_spent += banner_result['pulls_spent']
- # Подсчет бесплатных круток на этом баннере
- # Магазин всегда 10
- free_this_banner = 10
- # Досье с прошлого баннера (10 круток)
- if has_dossier_from_prev:
- free_this_banner += 10
- # Экстренная вербовка (10 круток) если использовали
- if banner_result['emergency_used']:
- free_this_banner += 10
- total_free_pulls += free_this_banner
- if banner_result['got_target']:
- targets_obtained += 1
- total_event_6stars += banner_result['event_6star_count']
- total_6stars_all += banner_result['total_6star_count']
- current_pity = banner_result['new_pity']
- has_dossier_from_prev = banner_result['got_dossier_for_next']
- return {
- 'total_pulls': total_pulls_spent, # Сумма ВСЕХ платных круток за все баннеры
- 'total_free': total_free_pulls, # Сумма ВСЕХ бесплатных круток за все баннеры
- 'targets_obtained': targets_obtained,
- 'total_event_6stars': total_event_6stars,
- 'total_6stars': total_6stars_all
- }
- def run_multiple_simulations(self, target_banner_indices, num_simulations=10000):
- """Запуск множества симуляций"""
- results = {1: defaultdict(list), 2: defaultdict(list), 3: defaultdict(list)}
- for sim in range(num_simulations):
- if sim % 1000 == 0:
- print(f"Прогресс: {sim}/{num_simulations} симуляций")
- for strategy in [1, 2, 3]:
- sim_result = self.simulate_all_banners(target_banner_indices, strategy)
- # Сохраняем результаты этой симуляции
- for key in ['total_pulls', 'total_free', 'targets_obtained',
- 'total_event_6stars', 'total_6stars']:
- results[strategy][key].append(sim_result[key])
- return results
- def print_statistics(self, results, target_banners_count):
- """Вывод статистики"""
- print("\n" + "=" * 100)
- print(f"📊 РЕЗУЛЬТАТЫ СИМУЛЯЦИИ (10,000 симуляций, {target_banners_count} целевых баннеров из {self.num_banners})")
- print("=" * 100)
- strategies_desc = {
- 1: "🔵 СТРАТЕГИЯ 1: Только целевые баннеры",
- 2: "🟢 СТРАТЕГИЯ 2: На нецелевых крутим ДО ЭКСТРЕННОЙ ВЕРБОВКИ (30) + сама вербовка",
- 3: "🔴 СТРАТЕГИЯ 3: МИНИМУМ 60 НА КАЖДОМ БАННЕРЕ (не останавливаемся до 60, даже если получили цель)"
- }
- for strategy in [1, 2, 3]:
- print(f"\n{strategies_desc[strategy]}")
- print("-" * 80)
- pulls = results[strategy]['total_pulls']
- free = results[strategy]['total_free']
- targets = results[strategy]['targets_obtained']
- event_6stars = results[strategy]['total_event_6stars']
- total_6stars = results[strategy]['total_6stars']
- print(f" 💰 ПЛАТНЫЕ КРУТКИ (всего за {self.num_banners} баннеров):")
- print(f" Средние затраты: {np.mean(pulls):.1f}")
- print(f" Медиана: {np.median(pulls):.1f}")
- print(f" 90-й перцентиль: {np.percentile(pulls, 90):.1f}")
- print(f" Минимум: {np.min(pulls):.0f}")
- print(f" Максимум: {np.max(pulls):.0f}")
- print(f"\n 🎁 БЕСПЛАТНЫЕ КРУТКИ (всего за {self.num_banners} баннеров):")
- print(f" Среднее количество: {np.mean(free):.1f}")
- print(f" Медиана: {np.median(free):.1f}")
- print(f" Минимум: {np.min(free):.0f}")
- print(f" Максимум: {np.max(free):.0f}")
- print(f"\n 🎯 ЦЕЛЕВЫЕ ПЕРСОНАЖИ (получены на целевых баннерах):")
- print(f" В среднем: {np.mean(targets):.2f} из {target_banners_count}")
- print(f" Вероятность получить всех: {np.mean([1 if t == target_banners_count else 0 for t in targets])*100:.1f}%")
- print(f" Риск без целей: {np.mean([1 if t == 0 else 0 for t in targets])*100:.1f}%")
- print(f"\n 🌟 ВСЕ ИВЕНТОВЫЕ 6★ (включая полученных на нецелевых баннерах):")
- print(f" В среднем: {np.mean(event_6stars):.2f}")
- print(f" На 1000 платных круток: {np.mean(event_6stars)/np.mean(pulls)*1000:.2f}")
- print(f" На 1000 ВСЕХ круток: {np.mean(event_6stars)/(np.mean(pulls)+np.mean(free))*1000:.2f}")
- print(f"\n 💎 ВСЕГО 6★ (ивентовые + стандартные):")
- print(f" В среднем: {np.mean(total_6stars):.2f}")
- print(f" На 1000 платных круток: {np.mean(total_6stars)/np.mean(pulls)*1000:.2f}")
- def plot_results(self, results, target_count):
- """Построение графиков"""
- fig, axes = plt.subplots(2, 4, figsize=(20, 12))
- colors = {1: 'blue', 2: 'green', 3: 'red'}
- labels = {1: 'Стратегия 1', 2: 'Стратегия 2 (до 30)', 3: 'Стратегия 3 (мин 60)'}
- # 1. Распределение платных круток
- ax = axes[0, 0]
- for s in [1, 2, 3]:
- ax.hist(results[s]['total_pulls'], bins=50, alpha=0.5,
- color=colors[s], label=labels[s], density=True)
- ax.set_xlabel(f'Всего платных круток за {self.num_banners} баннеров')
- ax.set_ylabel('Плотность')
- ax.set_title('Распределение платных круток')
- ax.legend()
- ax.grid(True, alpha=0.3)
- # 2. Распределение бесплатных круток
- ax = axes[0, 1]
- for s in [1, 2, 3]:
- ax.hist(results[s]['total_free'], bins=30, alpha=0.5,
- color=colors[s], label=labels[s], density=True)
- ax.set_xlabel(f'Всего бесплатных круток за {self.num_banners} баннеров')
- ax.set_ylabel('Плотность')
- ax.set_title('Распределение бесплатных круток')
- ax.legend()
- ax.grid(True, alpha=0.3)
- # 3. Целевые персонажи
- ax = axes[0, 2]
- x = np.arange(3)
- targets_mean = [np.mean(results[s]['targets_obtained']) for s in [1, 2, 3]]
- targets_std = [np.std(results[s]['targets_obtained']) for s in [1, 2, 3]]
- ax.bar(x, targets_mean, yerr=targets_std, capsize=5,
- color=[colors[s] for s in [1, 2, 3]], alpha=0.7)
- ax.set_xticks(x)
- ax.set_xticklabels(['Стратегия 1', 'Стратегия 2', 'Стратегия 3'])
- ax.set_ylabel('Среднее количество')
- ax.set_title(f'Получено ЦЕЛЕВЫХ персонажей (из {target_count})')
- ax.axhline(y=target_count, color='black', linestyle='--', alpha=0.5)
- ax.grid(True, alpha=0.3)
- # 4. Всего ивентовых 6★
- ax = axes[0, 3]
- event_mean = [np.mean(results[s]['total_event_6stars']) for s in [1, 2, 3]]
- event_std = [np.std(results[s]['total_event_6stars']) for s in [1, 2, 3]]
- ax.bar(x, event_mean, yerr=event_std, capsize=5,
- color=[colors[s] for s in [1, 2, 3]], alpha=0.7)
- ax.set_xticks(x)
- ax.set_xticklabels(['Стратегия 1', 'Стратегия 2', 'Стратегия 3'])
- ax.set_ylabel('Среднее количество')
- ax.set_title('Все полученные ИВЕНТОВЫЕ 6★')
- ax.grid(True, alpha=0.3)
- # 5. Эффективность по целевым
- ax = axes[1, 0]
- target_eff = [np.mean(results[s]['targets_obtained']) / np.mean(results[s]['total_pulls']) * 1000
- for s in [1, 2, 3]]
- ax.bar(x, target_eff, color=[colors[s] for s in [1, 2, 3]], alpha=0.7)
- ax.set_xticks(x)
- ax.set_xticklabels(['Стратегия 1', 'Стратегия 2', 'Стратегия 3'])
- ax.set_ylabel('Целей на 1000 платных круток')
- ax.set_title('Эффективность (целевые)')
- for i, v in enumerate(target_eff):
- ax.text(i, v + 0.1, f'{v:.2f}', ha='center')
- ax.grid(True, alpha=0.3)
- # 6. Эффективность по ивентовым
- ax = axes[1, 1]
- event_eff = [np.mean(results[s]['total_event_6stars']) / np.mean(results[s]['total_pulls']) * 1000
- for s in [1, 2, 3]]
- ax.bar(x, event_eff, color=[colors[s] for s in [1, 2, 3]], alpha=0.7)
- ax.set_xticks(x)
- ax.set_xticklabels(['Стратегия 1', 'Стратегия 2', 'Стратегия 3'])
- ax.set_ylabel('Ивентовых на 1000 платных круток')
- ax.set_title('Эффективность (все ивентовые)')
- for i, v in enumerate(event_eff):
- ax.text(i, v + 0.1, f'{v:.2f}', ha='center')
- ax.grid(True, alpha=0.3)
- # 7. Соотношение платных и бесплатных
- ax = axes[1, 2]
- pulls_mean = [np.mean(results[s]['total_pulls']) for s in [1, 2, 3]]
- free_mean = [np.mean(results[s]['total_free']) for s in [1, 2, 3]]
- ax.bar(x - 0.2, pulls_mean, width=0.4, label='Платные', alpha=0.7, color='blue')
- ax.bar(x + 0.2, free_mean, width=0.4, label='Бесплатные', alpha=0.7, color='green')
- ax.set_xticks(x)
- ax.set_xticklabels(['Стратегия 1', 'Стратегия 2', 'Стратегия 3'])
- ax.set_ylabel('Среднее количество круток')
- ax.set_title('Платные vs Бесплатные')
- ax.legend()
- ax.grid(True, alpha=0.3)
- # 8. Риск и успех
- ax = axes[1, 3]
- success = [np.mean([1 if t == target_count else 0 for t in results[s]['targets_obtained']])*100
- for s in [1, 2, 3]]
- risk = [np.mean([1 if t == 0 else 0 for t in results[s]['targets_obtained']])*100
- for s in [1, 2, 3]]
- ax.bar(x - 0.2, success, width=0.4, label='Успех (все цели)', alpha=0.7, color='green')
- ax.bar(x + 0.2, risk, width=0.4, label='Риск (0 целей)', alpha=0.7, color='red')
- ax.set_xticks(x)
- ax.set_xticklabels(['Стратегия 1', 'Стратегия 2', 'Стратегия 3'])
- ax.set_ylabel('Вероятность (%)')
- ax.set_title('Вероятность успеха и риска')
- ax.legend()
- ax.grid(True, alpha=0.3)
- plt.tight_layout()
- plt.show()
- # Основная программа
- if __name__ == "__main__":
- # ===== НАСТРОЙКИ =====
- NUM_BANNERS = 10 # Количество баннеров для симуляции
- # ====================
- # Создаем симулятор с указанным количеством баннеров
- simulator = ArknightsEndfieldPitySimulator(num_banners=NUM_BANNERS)
- # Задаем конкретные целевые баннеры (например каждый второй)
- target_banners = [1, 3, 5, 7, 9]
- target_count = len(target_banners)
- print("=" * 100)
- print(f"🎯 СИМУЛЯТОР ARKNIGHTS: ENDFIELD - 3 СТРАТЕГИИ ({NUM_BANNERS} БАННЕРОВ)")
- print("=" * 100)
- print(f"\nЦелевые баннеры: {target_banners}")
- print(f"Всего баннеров: {NUM_BANNERS}")
- print(f"Целевых: {target_count}, Нецелевых: {NUM_BANNERS - target_count}")
- print("\n📋 СТРАТЕГИИ:")
- print(" 1️⃣ СТРАТЕГИЯ 1: Только целевые баннеры")
- print(" - На целевых: крутим до получения ЦЕЛЕВОГО 6★")
- print(" - На нецелевых: только бесплатные крутки (магазин, досье)")
- print("\n 2️⃣ СТРАТЕГИЯ 2: Экстренная вербовка на нецелевых")
- print(" - На целевых: крутим до получения ЦЕЛЕВОГО 6★")
- print(" - На нецелевых: крутим ДО ЭКСТРЕННОЙ ВЕРБОВКИ (30 круток) + используем её")
- print("\n 3️⃣ СТРАТЕГИЯ 3: Минимум 60 на КАЖДОМ баннере")
- print(" - На ВСЕХ баннерах: крутим минимум ДО 60 круток (ДАЖЕ ЕСЛИ ПОЛУЧИЛИ ЦЕЛЬ РАНЬШЕ)")
- print(" - На целевых: после 60 продолжаем до получения ЦЕЛЕВОГО 6★ (если еще не получили)")
- print("\n📌 ВАЖНЫЕ ПРАВИЛА:")
- print(" • Экстренная вербовка (10 круток):")
- print(" - Всегда имеет базовый шанс 0.8% (не зависит от soft pity)")
- print(" - НЕ учитывается в счетчике для новых наград")
- print(" - НЕ влияет на pity")
- print(" • Досье и магазин учитываются в счетчике наград")
- print(" • На нецелевых баннерах НЕ останавливаемся при получении любого 6★")
- print("\n🔄 Запуск симуляции (10,000 итераций)...")
- results = simulator.run_multiple_simulations(target_banners, num_simulations=10000)
- simulator.print_statistics(results, target_count)
- simulator.plot_results(results, target_count)
- print("\n" + "=" * 100)
- print("🏆 СРАВНЕНИЕ СТРАТЕГИЙ:")
- print("=" * 100)
- # Собираем данные для сравнения
- comparison = []
- for s in [1, 2, 3]:
- pulls = np.mean(results[s]['total_pulls'])
- free = np.mean(results[s]['total_free'])
- targets = np.mean(results[s]['targets_obtained'])
- events = np.mean(results[s]['total_event_6stars'])
- success_rate = np.mean([1 if t == target_count else 0 for t in results[s]['targets_obtained']])*100
- risk_rate = np.mean([1 if t == 0 else 0 for t in results[s]['targets_obtained']])*100
- comparison.append({
- 'Стратегия': s,
- 'Платные': f"{pulls:.0f}",
- 'Бесплатные': f"{free:.0f}",
- 'Целей': f"{targets:.2f}",
- 'Успех': f"{success_rate:.1f}%",
- 'Риск': f"{risk_rate:.1f}%",
- 'Ивентовых': f"{events:.2f}",
- 'Целей/1000': f"{targets/pulls*1000:.2f}"
- })
- # Выводим таблицу
- print("\n{:<12} {:<10} {:<12} {:<10} {:<10} {:<8} {:<12} {:<12}".format(
- "Стратегия", "Платные", "Бесплатные", "Целей", "Успех", "Риск", "Ивентовых", "Целей/1000"))
- print("-" * 96)
- for row in comparison:
- print("{:<12} {:<10} {:<12} {:<10} {:<10} {:<8} {:<12} {:<12}".format(
- row['Стратегия'], row['Платные'], row['Бесплатные'], row['Целей'],
- row['Успех'], row['Риск'], row['Ивентовых'], row['Целей/1000']))
Advertisement
Add Comment
Please, Sign In to add comment