Spaces:
Runtime error
Runtime error
import pandas as pd | |
import streamlit as st | |
from stqdm import stqdm | |
from tqdm import tqdm | |
class Simulator: | |
def __init__(self, settings: dict, streamlit=False): | |
self.streamlit = streamlit | |
target_rarity_map = { | |
'Any Rarity': [ | |
'sh_special_4', 'special_4', 'non_focus_3', 'focus_5', 'non_focus_4', 'non_focus_5', 'focus_4' | |
], | |
'Any 5β Unit or 4β Special Rate Unit': [ | |
'sh_special_4', 'special_4', 'focus_5', 'non_focus_5' | |
], | |
'Any 5β Unit': [ | |
'focus_5', 'non_focus_5' | |
], | |
'Specific 5β Focus Unit': [ | |
'focus_5' | |
], | |
'Specific 5β Non-Focus Unit': [ | |
'non_focus_5' | |
], | |
'Any 4β Unit or 4β Special Rate Unit or 4β SHSR Unit': [ | |
'sh_special_4', 'special_4', 'non_focus_4', 'focus_4' | |
], | |
'Any 4β Unit': [ | |
'non_focus_4', 'focus_4' | |
], | |
'Specific 4β Focus Unit': [ | |
'focus_4' | |
], | |
'Specific 4β Non-Focus Unit': [ | |
'non_focus_4' | |
], | |
'Any 4β Special Rate Unit or 4β SHSR Unit': [ | |
'sh_special_4', 'special_4' | |
], | |
'Specific 4β Special Rate Unit': [ | |
'special_4' | |
], | |
'Specific 4β SHSR Unit': [ | |
'sh_special_4' | |
], | |
} | |
target_color_map = { | |
'Any Color': ['red', 'blue', 'green', 'colorless'], | |
'Red': ['red'], | |
'Blue': ['blue'], | |
'Green': ['green'], | |
'Colorless': ['colorless'], | |
} | |
banner_selection_map = { | |
"(3%/3%) Normal": ['normal', 'normal_4'], | |
"(4%/2%) Weekly Revival": ['weekly_revival'], | |
"(4%/2%) Weekly Revival 4β SHSR": ['weekly_revival_shsr'], | |
"(5%/3%) Hero Fest": ['hero_fest'], | |
"(4%/2%) Double Special Heroes": ['double_special', 'double_special_4'], | |
"(8%/0%) Legendary / Mythic": ['legendary/mythic'] | |
} | |
banner_rates = pd.DataFrame( | |
[ | |
['normal', 'focus_5', 0.03], | |
['normal', 'non_focus_5', 0.03], | |
['normal', 'special_4', 0.03], | |
['normal', 'non_focus_4', 0.55], | |
['normal', 'non_focus_3', 0.36], | |
['normal_4', 'focus_5', 0.03], | |
['normal_4', 'non_focus_5', 0.03], | |
['normal_4', 'focus_4', 0.03], | |
['normal_4', 'special_4', 0.03], | |
['normal_4', 'non_focus_4', 0.52], | |
['normal_4', 'non_focus_3', 0.36], | |
['weekly_revival', 'focus_5', 0.04], | |
['weekly_revival', 'non_focus_5', 0.02], | |
['weekly_revival', 'special_4', 0.03], | |
['weekly_revival', 'non_focus_4', 0.55], | |
['weekly_revival', 'non_focus_3', 0.36], | |
['weekly_revival_shsr', 'focus_5', 0.04], | |
['weekly_revival_shsr', 'non_focus_5', 0.02], | |
['weekly_revival_shsr', 'sh_special_4', 0.03], | |
['weekly_revival_shsr', 'special_4', 0.03], | |
['weekly_revival_shsr', 'non_focus_4', 0.55], | |
['weekly_revival_shsr', 'non_focus_3', 0.33], | |
['hero_fest', 'focus_5', 0.05], | |
['hero_fest', 'non_focus_5', 0.03], | |
['hero_fest', 'special_4', 0.03], | |
['hero_fest', 'non_focus_4', 0.55], | |
['hero_fest', 'non_focus_3', 0.34], | |
['double_special', 'focus_5', 0.06], | |
['double_special', 'special_4', 0.03], | |
['double_special', 'non_focus_4', 0.57], | |
['double_special', 'non_focus_3', 0.34], | |
['double_special_4', 'focus_5', 0.06], | |
['double_special_4', 'focus_4', 0.03], | |
['double_special_4', 'special_4', 0.03], | |
['double_special_4', 'non_focus_4', 0.54], | |
['double_special_4', 'non_focus_3', 0.34], | |
['legendary/mythic', 'focus_5', 0.08], | |
['legendary/mythic', 'special_4', 0.03], | |
['legendary/mythic', 'non_focus_4', 0.55], | |
['legendary/mythic', 'non_focus_3', 0.34], | |
], | |
columns=['banner_type', 'rarity_pool', 'rate'] | |
) | |
self.pools = settings.get('Pools') | |
self.goals = settings.get('Goals') | |
self.banner_rates = settings.get('Banner Rates') | |
self.goals_required = settings.get('Goals Required') | |
self.orb_limit = settings.get('Orb Limit') | |
self.summon_limit = settings.get('Summon Limit') | |
self.banner_type = settings.get('Banner Type', '(3%/3%) Normal') | |
self.n_simulations = settings.get('Simulations', 1000) | |
self.tickets = settings.get('Tickets', 0) | |
self.sparks = settings.get('Sparks', 0) | |
self.focus_charges_enabled = settings.get('Focus Charges', False) | |
self.color_priority = settings.get('Color Priority', ['red', 'blue', 'green', 'colorless']) | |
if not any([self.goals_required, self.orb_limit, self.summon_limit]): | |
raise ValueError('No End Criteria (Goals Required, Orb Limit, Summon Limit) provided.') | |
self.pools = pd.DataFrame(self.pools) | |
self.goals = pd.DataFrame(self.goals) | |
if self.banner_rates is None: | |
if self.pools.loc['focus_4'].sum() > 0: | |
mapped_banner_type = banner_selection_map[self.banner_type][-1] | |
else: | |
mapped_banner_type = banner_selection_map[self.banner_type][0] | |
self.banner_rates = banner_rates[banner_rates['banner_type'] == mapped_banner_type] | |
else: | |
self.banner_rates = pd.DataFrame(self.banner_rates).reset_index() | |
self.banner_rates.columns = ['rarity_pool', 'rate'] | |
bool_rate_5 = self.banner_rates.rarity_pool.str.contains('5') | |
bool_rate_non_5 = ~bool_rate_5 | |
sum_rate_5 = self.banner_rates.loc[bool_rate_5, 'rate'].sum() | |
sum_rate_non_5 = self.banner_rates.loc[bool_rate_non_5, 'rate'].sum() | |
self.banner_rates['step'] = 0 | |
pre_calc_rates = [self.banner_rates] | |
# feh calculates rate up (rate down for non 5 stars) from base, not from previous step! | |
for i in range(1, 24): | |
inc_rates_df = self.banner_rates.copy(deep=True) | |
inc_rates_df.loc[bool_rate_5, 'rate'] *= 1 + (1 / sum_rate_5) * (0.005 * i) | |
inc_rates_df.loc[bool_rate_non_5, 'rate'] *= 1 - (1 / sum_rate_non_5) * (0.005 * i) | |
inc_rates_df['rate'] = round(inc_rates_df['rate'], 4) | |
inc_rates_df['step'] = i | |
pre_calc_rates.append(inc_rates_df) | |
# max pity rate | |
inc_rates_df = self.banner_rates.copy(deep=True) | |
inc_rates_df.loc[bool_rate_5, 'rate'] /= sum_rate_5 | |
inc_rates_df.loc[bool_rate_non_5, 'rate'] = 0 | |
inc_rates_df['rate'] = round(inc_rates_df['rate'], 4) | |
inc_rates_df['step'] = 24 | |
pre_calc_rates.append(inc_rates_df) | |
self.banner_rates_pro_df = pd.concat(pre_calc_rates) | |
self.pity_step = 0 | |
self.curr_banner_rates_df = self.banner_rates_pro_df[self.banner_rates_pro_df['step'] == self.pity_step] | |
pools_to_use = [rp for rp in self.pools.index if rp in self.banner_rates.rarity_pool.values] | |
self.pools = pd.DataFrame(self.pools.loc[pools_to_use]) | |
self.pools.reset_index(inplace=True, names='rarity_pool') | |
unpivot_unit_pool = pd.melt(self.pools, id_vars=['rarity_pool'], var_name='color', value_name='size') | |
unit_list = [] | |
for row in unpivot_unit_pool.itertuples(): | |
for _ in range(row.size): | |
unit_list_row = [row.color, row.rarity_pool] | |
unit_list.append(unit_list_row) | |
units_df = pd.DataFrame(unit_list, columns=['color', 'rarity_pool']) | |
# joining color priority col to df | |
priority_df = pd.DataFrame(enumerate(self.color_priority, start=1), columns=['color_priority', 'color']) | |
units_df = units_df.join(priority_df.set_index('color'), on='color', validate='m:1') | |
# Goals Setup | |
goals_df = self.goals.copy(deep=True) | |
max_goal_group = goals_df.groupby('goal_group')['target_count'].max().reset_index() | |
goals_df = goals_df.join( | |
max_goal_group.set_index('goal_group'), on='goal_group', rsuffix='_max', validate='m:1' | |
) | |
goals_df['target_count'] = goals_df['target_count_max'] | |
goals_df = goals_df.drop('target_count_max', axis=1) | |
goals_df['target_color'] = goals_df['target_color'] | |
goals_df['current_count'] = 0 | |
# Adding goal_row bool columns to unit_df | |
reserved_unit_index = [] | |
for goal in goals_df.itertuples(): | |
accepted_pools = target_rarity_map[goal.target_rarity] | |
accepted_colors = target_color_map[goal.target_color] | |
gr_col_name = 'goal_row_' + str(goal.Index) | |
goal_is_specific = 'specific' in goal.target_rarity.lower() | |
if goal_is_specific: | |
allowed_units = ~units_df.index.isin(reserved_unit_index) | |
else: | |
allowed_units = True | |
units_df[gr_col_name] = units_df['rarity_pool'].isin(accepted_pools) & units_df['color'].isin( | |
accepted_colors) & allowed_units | |
if units_df[gr_col_name].any() and goal_is_specific: | |
first_true_index = units_df.index[units_df[gr_col_name]].min() | |
reserved_unit_index.append(first_true_index) | |
units_df[gr_col_name] = units_df.index == first_true_index | |
else: | |
if self.streamlit: | |
st.warning(f'{gr_col_name} does not have any available units to target.') | |
st.warning('Target unit may already be reserved by previous goal.') | |
else: | |
print(f'{gr_col_name} does not have any available units to target.') | |
print('Target unit may already be reserved by previous goal.') | |
# Adding goal_group bool columns to unit_df | |
goal_groups_dict = {} | |
for group in set(goals_df['goal_group']): | |
goals_in_group = ['goal_row_' + str(x) for x in list(goals_df[goals_df['goal_group'] == group].index)] | |
goal_groups_dict['goal_group_' + str(group)] = goals_in_group | |
for goal_group in goal_groups_dict: | |
cols = goal_groups_dict[goal_group] | |
units_df[goal_group] = units_df[cols].apply(lambda row: pd.Series(row).any(), axis=1) | |
self.gg_cols = list(goal_groups_dict.keys()) | |
self.gg_cols.sort() | |
slice_cols = [col for col in list(units_df.columns) if 'goal' not in col] + self.gg_cols | |
units_df = units_df[slice_cols] | |
self.base_summon_goals_df = goals_df.copy(deep=True) | |
self.curr_summon_goals_df = self.base_summon_goals_df.copy(deep=True) | |
self.banner_units_df = units_df.copy(deep=True) | |
self.goal_cols = list(self.base_summon_goals_df.columns[5:]) | |
self.colors_to_target = list(set(self.curr_summon_goals_df.target_color)) | |
# small adjustments | |
self.spark_thresholds = [_ * 40 for _ in range(1, self.sparks + 1)] | |
self.sparks_redeemed = 0 | |
self.sparked_indexes = [] | |
self.active_focus_charges = 0 | |
self.apply_focus_charges = False | |
# refs | |
self.circle_df = None | |
self.session_type = 'normal' | |
self.summon_cost = 0 | |
self.n_stones_in_circle = 5 | |
# tracking | |
self.total_orbs_spent = 0 | |
self.total_summons = 0 | |
self.session_count = 0 | |
self.summons_without_any_5 = 0 | |
self.halt_pity_increase = False | |
self.end_criteria_met = False | |
self.summon_log = [] | |
self.prev_summon_log_len = 0 | |
self.orbs_spent_log = [] | |
self.session_count_log = [] | |
self.session_type_log = [] | |
self.session_pity_step_log = [] | |
self.run_num_log = [] | |
self.simulation_log_df = None | |
self.run_simulations() | |
def reset_run(self): | |
self.total_orbs_spent = 0 | |
self.total_summons = 0 | |
self.session_count = 0 | |
self.summons_without_any_5 = 0 | |
self.end_criteria_met = False | |
self.curr_summon_goals_df = self.base_summon_goals_df.copy(deep=True) | |
self.sparks_redeemed = 0 | |
self.sparked_indexes = [] | |
self.active_focus_charges = 0 | |
self.apply_focus_charges = False | |
def run_simulations(self): | |
if self.streamlit: | |
progress_bar = stqdm(range(self.n_simulations)) | |
else: | |
progress_bar = tqdm(range(self.n_simulations)) | |
for n in progress_bar: | |
self.simulate_run() | |
self.log_run(n + 1) | |
self.reset_run() | |
self.simulation_log_df = pd.DataFrame(self.summon_log) | |
self.simulation_log_df.rename(columns={'Index': 'unit_id'}, inplace=True) | |
self.simulation_log_df['unit_id'] += 1 | |
self.simulation_log_df['orbs_spent'] = self.orbs_spent_log | |
self.simulation_log_df['session_count'] = self.session_count_log | |
self.simulation_log_df['session_type'] = self.session_type_log | |
self.simulation_log_df['session_pity_step'] = self.session_pity_step_log | |
self.simulation_log_df['run_num'] = self.run_num_log | |
message = f'{self.n_simulations} Simulations Completed' | |
if self.streamlit: | |
st.success(message) | |
else: | |
print(message) | |
def simulate_run(self): | |
while not self.end_criteria_met: | |
self.setup_session() | |
self.create_circle() | |
self.filter_circle() | |
self.summon_from_circle() | |
def log_run(self, run_num): | |
curr_log_len = len(self.summon_log) - self.prev_summon_log_len | |
self.run_num_log = self.run_num_log + [run_num for _ in range(curr_log_len)] | |
self.prev_summon_log_len = len(self.summon_log) | |
def setup_session(self): | |
self.session_type = 'normal' | |
self.apply_focus_charges = False | |
sparks_remain = self.sparks_redeemed != len(self.spark_thresholds) | |
if sparks_remain and self.total_summons >= self.spark_thresholds[self.sparks_redeemed]: | |
self.sparks_redeemed += 1 | |
self.session_type = 'spark' | |
return | |
if self.focus_charges_enabled and self.active_focus_charges >= 3: | |
self.apply_focus_charges = True | |
self.pity_step = int(self.summons_without_any_5 / 5) | |
self.curr_banner_rates_df = self.banner_rates_pro_df[self.banner_rates_pro_df.step == self.pity_step].copy() | |
def create_circle(self): | |
circle = [] | |
if self.session_type == 'spark': | |
bool_rarity = self.banner_units_df['rarity_pool'] == 'focus_5' | |
spark_circle = self.banner_units_df.loc[bool_rarity] | |
spark_circle = spark_circle[ | |
~spark_circle.index.isin(self.sparked_indexes)] # removes previously sparked units | |
self.circle_df = spark_circle | |
return | |
for i in range(self.n_stones_in_circle): | |
# draws unit rarities | |
drawn_rarity = self.curr_banner_rates_df.sample(weights='rate')['rarity_pool'].iloc[0] | |
if self.apply_focus_charges and drawn_rarity == 'non_focus_5': | |
drawn_rarity = 'focus_5' | |
units_in_rarity = self.banner_units_df[self.banner_units_df['rarity_pool'] == drawn_rarity] | |
# draws unit from drawn rarities | |
drawn_unit = units_in_rarity.sample() | |
circle.append(drawn_unit) | |
self.circle_df = pd.concat(circle) | |
def filter_circle(self): | |
if self.session_type == 'spark': | |
circle = self.circle_df[self.circle_df[self.gg_cols].any(axis=1)].head(1) | |
elif self.goals_required == 'Any Goal Met' or len(self.colors_to_target) == 1: | |
self.colors_to_target = list(self.curr_summon_goals_df['target_color'].str.lower()) | |
circle = self.circle_df[self.circle_df['color'].isin(self.colors_to_target)].sort_values('color_priority') | |
elif self.goals_required == 'All Goals Met': | |
unmet_goals = self.curr_summon_goals_df['current_count'] < self.curr_summon_goals_df['target_count'] | |
self.colors_to_target = list(self.curr_summon_goals_df[unmet_goals]['target_color'].str.lower()) | |
circle = self.circle_df[self.circle_df['color'].isin(self.colors_to_target)].sort_values('color_priority') | |
else: | |
circle = self.circle_df | |
if len(circle) != 0: | |
self.circle_df = circle | |
else: # if nothing returns after filtering, filter for first stone | |
self.circle_df = self.circle_df.sort_values('color_priority').head(1) | |
def summon_from_circle(self): | |
price_index = 0 | |
self.session_count += 1 | |
self.halt_pity_increase = False | |
if self.session_count <= self.tickets + 1: | |
prices = (0, 4, 4, 4, 3) | |
else: | |
prices = (5, 4, 4, 4, 3) | |
for row in self.circle_df.itertuples(index=True): # keep index as true | |
if self.session_type == 'spark': | |
self.sparked_indexes.append(row.Index) | |
self.summon_cost = 0 | |
else: | |
self.summon_cost = prices[price_index] | |
self.eval_end_criteria_limits() | |
if self.end_criteria_met: | |
break | |
self.total_summons += 1 | |
self.orbs_spent_log.append(self.summon_cost) | |
self.total_orbs_spent += self.summon_cost | |
self.summon_log.append(row) | |
self.session_count_log.append(self.session_count) | |
self.session_type_log.append(self.session_type) | |
self.session_pity_step_log.append(self.pity_step) | |
self.update_flags(row) | |
self.update_goals(row) | |
self.eval_end_criteria_goals() | |
if self.end_criteria_met: | |
break | |
price_index += 1 | |
if self.end_criteria_met: | |
return | |
def update_flags(self, row): | |
if row.rarity_pool == 'focus_5': | |
self.halt_pity_increase = True | |
self.summons_without_any_5 = 0 | |
if self.active_focus_charges >= 3: | |
self.active_focus_charges = 0 | |
elif row.rarity_pool == 'non_focus_5': | |
self.summons_without_any_5 = max(0, self.summons_without_any_5 - 20) | |
self.active_focus_charges += 1 | |
else: | |
if not self.halt_pity_increase: | |
self.summons_without_any_5 += 1 | |
def update_goals(self, row): # takes named tuple from circle_df, updates summon goals based on goal group columns | |
# noinspection PyProtectedMember | |
row_dict = row._asdict() | |
for gg in self.gg_cols: | |
if row_dict[gg]: | |
gg_num = gg.split('_')[-1] | |
self.curr_summon_goals_df.loc[self.curr_summon_goals_df['goal_group'] == gg_num, 'current_count'] += 1 | |
def eval_end_criteria_goals(self): | |
if self.goals_required is not None: | |
met_goals = self.curr_summon_goals_df['current_count'] >= self.curr_summon_goals_df['target_count'] | |
if self.goals_required == 'Any Goal Group Met' and any(met_goals): | |
self.end_criteria_met = True | |
elif self.goals_required == 'All Goal Groups Met' and all(met_goals): | |
self.end_criteria_met = True | |
def eval_end_criteria_limits(self): | |
if self.orb_limit != 0: | |
if self.total_orbs_spent + self.summon_cost > self.orb_limit: | |
self.end_criteria_met = True | |
elif self.summon_limit != 0: | |
if self.total_summons + 1 > self.summon_limit: | |
self.end_criteria_met = True | |