Spaces:
Runtime error
Runtime error
import streamlit as st | |
import pandas as pd | |
import fehsim | |
import json | |
from io import BytesIO | |
RARITY_OPTIONS = [ | |
'Any Rarity', | |
'Any 5β Unit or 4β Special Rate Unit', | |
'Any 5β Unit', | |
'Specific 5β Focus Unit', | |
'Specific 5β Non-Focus Unit', | |
'Any 4β Unit or 4β Special Rate Unit or 4β SHSR Unit', | |
'Any 4β Unit', | |
'Specific 4β Focus Unit', | |
'Specific 4β Non-Focus Unit', | |
'Any 4β Special Rate Unit or 4β SHSR Unit', | |
'Specific 4β Special Rate Unit', | |
'Specific 4β SHSR Unit' | |
] | |
COLOR_OPTIONS = [ | |
'Any Color', | |
'Red', | |
'Blue', | |
'Green', | |
'Colorless' | |
] | |
BANNER_OPTIONS = [ | |
'(3%/3%) Normal', | |
'(4%/2%) Weekly Revival', | |
'(4%/2%) Weekly Revival 4β SHSR', | |
'(5%/3%) Hero Fest', | |
'(4%/2%) Double Special Heroes', | |
'(8%/0%) Legendary / Mythic' | |
] | |
END_CRITERIA_OPTIONS = [ | |
'Any Goal Group Met', | |
'All Goal Groups Met', | |
] | |
BANNER_RATES_MAPPING = { | |
"(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'] | |
} | |
POOL_ORDER = [ | |
'focus_5', | |
'non_focus_5', | |
'focus_4', | |
'special_4', | |
'sh_special_4', | |
'non_focus_4', | |
'non_focus_3' | |
] | |
BANNER_RATES = [ | |
['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], | |
] | |
BANNER_RATES_DF = pd.DataFrame(BANNER_RATES, columns=['banner_type', 'rarity_pool', 'rate']) | |
BANNER_RATES_DF['rate'] *= 100 | |
pool_to_alias = { | |
'focus_5': '5β Focus', | |
'focus_4': '4β Focus', | |
'non_focus_5': '5β ', | |
'special_4': '4β SR', | |
'sh_special_4': '4β SHSR', | |
'non_focus_4': '4β ', | |
'non_focus_3': '3β ', | |
} | |
alias_to_pool = {v: k for k, v in pool_to_alias.items()} | |
BANNER_RATES_DF['rarity_pool'] = BANNER_RATES_DF['rarity_pool'].map(pool_to_alias) | |
def core_settings(settings): | |
st.subheader('Simulation Settings', anchor=False) | |
st.write('End Criteria:') | |
tt = 'End a run when goal conditions are met.' | |
if st.toggle('Goals Met', help=tt, key='toggle_goals_met'): | |
goals_required = st.selectbox( | |
'GR', | |
options=END_CRITERIA_OPTIONS, | |
label_visibility='collapsed', | |
key='select_goals_required' | |
) | |
else: | |
goals_required = None | |
tt = 'End a run when Orb Limit is reached or not enough orbs to summon.' | |
if st.toggle('Orb Limit', help=tt, value=True, key='toggle_orb_limit'): | |
orb_limit = st.number_input( | |
'OL', | |
value=3000, | |
step=1, | |
min_value=0, | |
label_visibility='collapsed', | |
key='input_orb_limit' | |
) | |
else: | |
orb_limit = None | |
tt = 'End a run when Summon Limit is reached.' | |
if st.toggle('Summon Limit', help=tt, key='toggle_summon_limit'): | |
summon_limit = st.number_input( | |
'SL', | |
value=15_000, | |
step=1, | |
min_value=0, | |
label_visibility='collapsed', | |
key='input_summon_limit' | |
) | |
else: | |
summon_limit = None | |
if not any([goals_required, orb_limit, summon_limit]): | |
st.warning('Please select at least one End Criteria.') | |
col1, col2 = st.columns([3, 2]) | |
with col2: | |
st.write("") | |
st.write("") | |
focus_charges = st.checkbox('Enable Focus Charges?', key='toggle_focus_charges') | |
with col1: | |
tt = 'Highest Priority -> Lowest Priority' | |
color_priority = st.multiselect( | |
'Color Priority', | |
COLOR_OPTIONS[1:], | |
default=COLOR_OPTIONS[1:], | |
help=tt, | |
key='select_color_priority' | |
) | |
if len(color_priority) != 4: | |
st.warning('Please sort all the colors.') | |
col1, col2 = st.columns(2) | |
with col1: | |
tt = 'Select the banner rates to simulate. Will also determine which pools are used.' | |
banner_type = st.selectbox( | |
'Banner Type:', | |
options=BANNER_OPTIONS, | |
help=tt, | |
key='select_banner_type', | |
) | |
tt = 'Number of simulations to run.' | |
simulations = st.number_input( | |
'Simulations', | |
value=100, | |
step=1, | |
min_value=0, | |
help=tt, | |
key='input_simulations' | |
) | |
tt = 'Number of summoning sessions (i.e. circles) where the first summon is free.' | |
with col2: | |
tickets = st.number_input( | |
'Tickets', | |
value=0, | |
step=1, | |
min_value=0, | |
help=tt, | |
key='input_tickets' | |
) | |
tt = 'Guaranteed 5β Focus Unit (i.e. spark) session after 40 summons.' | |
sparks = st.number_input( | |
'Sparks', | |
value=0, | |
step=1, | |
min_value=0, | |
help=tt, | |
key='input_sparks' | |
) | |
summon_pools = settings['Pools'] | |
focus_5_pool_size = summon_pools.loc['focus_5'][1:].sum() | |
if sparks > focus_5_pool_size: | |
st.warning(f'Sparks exceeding the number of 5β Focus Units.') | |
updated_core_settings = { | |
# End Criteria | |
'Goals Required': goals_required, | |
'Orb Limit': orb_limit, | |
'Summon Limit': summon_limit, | |
# Main Settings | |
'Banner Type': banner_type, | |
'Simulations': simulations, | |
'Tickets': tickets, | |
'Sparks': sparks, | |
'Focus Charges': focus_charges, | |
'Color Priority': color_priority, | |
} | |
return updated_core_settings | |
def goal_settings(settings): | |
st.subheader('Summoning Goals', anchor=False, help='Edit the table below to set your summoning goals') | |
st.caption(f"{bo('Goals')} of the same {bo('Goal Group')} will contribute to a {bo('Shared Target Count')}.") | |
st.caption(f"{bo('Shared Target Count')} will be the max {bo('Target Count')} within the {bo('Goal Group')}.") | |
column_config = { | |
"target_rarity": st.column_config.SelectboxColumn( | |
"Rarity", options=RARITY_OPTIONS, required=True, width='large' | |
), | |
"target_color": st.column_config.SelectboxColumn( | |
"Color", options=COLOR_OPTIONS, required=True, width='small' | |
), | |
"target_count": st.column_config.NumberColumn( | |
"Target Count", min_value=1, step=1, required=True, width='small' | |
), | |
"goal_group": st.column_config.NumberColumn( | |
"Goal Group", min_value=1, step=1, required=True, width='small' | |
), | |
} | |
df = settings['Goals'] | |
return st.data_editor( | |
df, | |
num_rows='dynamic', | |
column_config=column_config, | |
use_container_width=True, | |
hide_index=True, | |
key='data_editor_goals' | |
), df | |
def pool_settings(settings): | |
st.subheader('Summoning Pool', anchor=False, help='Edit the table below to set your summoning pools') | |
column_config = { | |
"red": st.column_config.NumberColumn("Red", min_value=0, step=1, required=True), | |
"blue": st.column_config.NumberColumn("Blue", min_value=0, step=1, required=True), | |
"green": st.column_config.NumberColumn("Green", min_value=0, step=1, required=True), | |
"colorless": st.column_config.NumberColumn("Colorless", min_value=0, step=1, required=True), | |
"rarity_pool": st.column_config.TextColumn("Rarity Pool", disabled=True), | |
} | |
df = settings['Pools'] | |
return st.data_editor( | |
df, | |
column_config=column_config, | |
hide_index=True, | |
key='data_editor_pools' | |
), df | |
def rate_settings(settings): | |
st.subheader('Summoning Rates', anchor=False, help='Edit the table below to set your summoning rates') | |
st.caption(f"Changes to {bo('4β Focus pool')} or {bo('Banner Type')} will reset this table.") | |
column_config = { | |
"rarity_pool": st.column_config.SelectboxColumn( | |
"Rarity Pool", disabled=True | |
), | |
"rate": st.column_config.NumberColumn( | |
"Rate (%)", min_value=0, step=0.01, max_value=100, required=True, format="%.2f" | |
), | |
} | |
summon_pools = settings['Pools'] | |
focus_4_pool_size = summon_pools.loc['focus_4'][1:].sum() | |
if focus_4_pool_size > 0: | |
mapped_banner_type = BANNER_RATES_MAPPING[settings['Banner Type']][-1] | |
if len(BANNER_RATES_MAPPING[settings['Banner Type']]) == 1: | |
st.warning(f'4β Focus Units do not appear in this Banner Type.') | |
else: | |
mapped_banner_type = BANNER_RATES_MAPPING[settings['Banner Type']][0] | |
if mapped_banner_type not in settings['Banner Rates']['banner_type'].values: | |
df = BANNER_RATES_DF[BANNER_RATES_DF.banner_type == mapped_banner_type] | |
else: | |
df = settings['Banner Rates'] | |
return st.data_editor( | |
df, | |
column_config=column_config, | |
hide_index=True, | |
column_order=['rarity_pool', 'rate'], | |
key='data_editor_rates' | |
), df | |
def goal_setting_example(): | |
with st.expander("Goal Group Examples"): | |
st.caption("You want (11) copies of a Red unit present in both the 5β Focus pool and 4β Non-Focus pool.") | |
st.caption("Goal Group 1 will be met once a shared total of (11) units are summoned.") | |
ex_data = [ | |
['Specific 5β Focus Unit', 'Red', 11, 1], | |
['Specific 4β Non-Focus Unit', 'Red', 11, 1] | |
] | |
ex_df = pd.DataFrame(ex_data, columns=['Rarity', 'Color', 'Target Count', 'Goal Group']) | |
st.dataframe(ex_df, hide_index=True) | |
st.divider() | |
st.caption("You want (8) units with a specific skill.") | |
st.caption("(2) Blue units with this skill are present in the 5β Non-Focus pool.") | |
st.caption("(1) Green unit with this skill is present in the 4β Non-Focus pool.") | |
st.caption("Goal Group 2 will be met once a shared total of (8) units are summoned.") | |
ex_data = [ | |
['Specific 5β Non-Focus Unit', 'Blue', 8, 2], | |
['Specific 5β Non-Focus Unit', 'Blue', 8, 2], | |
['Specific 4β Non-Focus Unit', 'Green', 8, 2], | |
] | |
ex_df = pd.DataFrame(ex_data, columns=['Rarity', 'Color', 'Target Count', 'Goal Group']) | |
st.dataframe(ex_df, hide_index=True) | |
def bo(text): # streamlit bold orange markdown | |
return f":orange[__{text}__]" | |
def user_to_sys(settings): | |
sys_settings = {} | |
for k, v in settings.items(): | |
if isinstance(v, pd.DataFrame): | |
val = v.copy(deep=True) | |
else: | |
val = v | |
sys_settings[k] = val | |
sys_goals = sys_settings['Goals'] | |
sys_settings['Goals'] = sys_goals.to_dict() | |
sys_pools = sys_settings['Pools'] | |
sys_pools = sys_pools.drop('rarity_pool', axis=1) | |
sys_settings['Pools'] = sys_pools.to_dict() | |
sys_rates = sys_settings['Banner Rates'] | |
sys_rates = sys_rates.drop('banner_type', axis=1) | |
sys_rates['rarity_pool'] = sys_rates['rarity_pool'].map(alias_to_pool) | |
sys_rates['rate'] = round(sys_rates['rate'] / 100, 4) | |
sys_rates.index = sys_rates['rarity_pool'] | |
sys_rates = sys_rates.drop('rarity_pool', axis=1) | |
sys_settings['Banner Rates'] = sys_rates.to_dict() | |
sys_settings['Color Priority'] = [c.lower() for c in sys_settings['Color Priority']] | |
return sys_settings | |
def sys_to_user(settings): | |
user_settings = {} | |
for k, v in settings.items(): | |
if isinstance(v, dict): | |
val = pd.DataFrame(v) | |
else: | |
val = v | |
user_settings[k] = val | |
user_pools = user_settings['Pools'] | |
user_pools['rarity_pool'] = user_pools.index | |
user_pools['rarity_pool'] = user_pools['rarity_pool'].map(pool_to_alias) | |
col_order = ['rarity_pool', 'red', 'blue', 'green', 'colorless'] | |
user_settings['Pools'] = user_pools[col_order] | |
user_rates = user_settings['Banner Rates'] | |
user_rates = user_rates.reset_index(names='rarity_pool') | |
must_only_have = user_rates.rarity_pool.values | |
df = BANNER_RATES_DF.copy(deep=True) | |
df['rarity_pool'] = df['rarity_pool'].map(alias_to_pool) | |
grouped_df = df.groupby('banner_type')['rarity_pool'] | |
filtered_banner_types = grouped_df.apply(lambda x: set(must_only_have) == set(x)).reset_index() | |
filtered_banner_types = filtered_banner_types[filtered_banner_types['rarity_pool']] | |
compatible_banners = df[df['banner_type'].isin(filtered_banner_types['banner_type'])] | |
user_rates['banner_type'] = compatible_banners.banner_type.values[0] | |
user_rates['rate'] = user_rates['rate'] * 100 | |
col_order = ['banner_type', 'rarity_pool', 'rate'] | |
sorting_order = {v: i for i, v in enumerate(POOL_ORDER)} | |
user_rates['order'] = user_rates['rarity_pool'].map(sorting_order) | |
user_rates = user_rates.sort_values(by='order').drop(columns='order').reset_index(drop=True) | |
user_rates['rarity_pool'] = user_rates['rarity_pool'].map(pool_to_alias) | |
user_settings['Banner Rates'] = user_rates[col_order] | |
user_settings['Color Priority'] = [c.capitalize() for c in user_settings['Color Priority']] | |
return user_settings | |
def debug_compare(setting_1, setting_2): | |
comparison = {} | |
for k, v in setting_1.items(): | |
if isinstance(v, pd.DataFrame): | |
compare = v.equals(setting_2[k]) | |
else: | |
compare = v == setting_2[k] | |
comparison[k] = compare | |
return comparison | |
def settings_app(): | |
st.set_page_config(layout="centered") | |
css = ''' | |
<style> | |
section.main > div {max-width:55rem} | |
</style> | |
''' | |
st.markdown(css, unsafe_allow_html=True) | |
st.subheader("FEH Detailed Summoning Simulator", anchor=False) | |
# initialize session state | |
if 'user_settings' not in st.session_state: | |
default_goals = [['Specific 5β Focus Unit', 'Red', 11, 1]] | |
default_pools = { | |
'focus_5': ['5β Focus', 1, 1, 1, 1], | |
'focus_4': ['4β Focus', 0, 0, 0, 0], | |
'non_focus_5': ['5β ', 26, 27, 19, 19], | |
'special_4': ['4β SR', 62, 42, 36, 28], | |
'sh_special_4': ['4β SHSR', 22, 28, 32, 27], | |
'non_focus_4': ['4β ', 41, 45, 37, 45], | |
'non_focus_3': ['3β ', 41, 45, 37, 45], | |
} | |
pool_cols = ['rarity_pool', 'red', 'blue', 'green', 'colorless'] | |
goal_cols = ['target_rarity', 'target_color', 'target_count', 'goal_group'] | |
mapped_banner_type = BANNER_RATES_MAPPING[BANNER_OPTIONS[0]][0] | |
default_settings = { | |
# Dataframes | |
'Pools': pd.DataFrame.from_dict(default_pools, orient='index', columns=pool_cols), | |
'Goals': pd.DataFrame(default_goals, columns=goal_cols), | |
'Banner Rates': BANNER_RATES_DF[BANNER_RATES_DF.banner_type == mapped_banner_type].copy(deep=True), | |
# End Criteria | |
'Goals Required': 'All Goal Groups Met', | |
'Orb Limit': 3000, | |
'Summon Limit': None, | |
# Main Settings | |
'Banner Type': BANNER_OPTIONS[0], | |
'Simulations': 100, | |
'Tickets': 0, | |
'Sparks': 0, | |
'Focus Charges': False, | |
'Color Priority': ['Red', 'Blue', 'Green', 'Colorless'], | |
} | |
st.session_state.user_settings = default_settings | |
# upload settings from json | |
uploaded_file = st.file_uploader("Upload gui_simulator_settings.json file", type=['json']) | |
if uploaded_file is not None: | |
if st.button("Submit", use_container_width=True): | |
data = json.load(uploaded_file) | |
if 'gui_settings' not in data or 'simulator_settings' not in data: | |
st.error("Invalid gui_simulator_settings.json file") | |
else: | |
st.session_state.user_settings = sys_to_user(data['simulator_settings']) | |
for k, v in data['gui_settings'].items(): | |
st.session_state[k] = v | |
st.divider() | |
current_settings = st.session_state.user_settings | |
prev_banner_type = current_settings['Banner Type'] | |
updated_data = core_settings(current_settings) | |
for k, v in updated_data.items(): | |
st.session_state.user_settings[k] = v | |
if prev_banner_type != current_settings['Banner Type']: | |
st.session_state.flag_update_rates = True | |
else: | |
st.session_state.flag_update_rates = False | |
current_settings = st.session_state.user_settings | |
new_goals, prev_goals = goal_settings(current_settings) | |
goal_setting_example() | |
if st.button("Update Summoning Goals", use_container_width=True): | |
st.session_state.user_settings['Goals'] = new_goals | |
st.experimental_rerun() | |
if not prev_goals.equals(new_goals): | |
st.warning("Unsaved changes") | |
sub_col1, sub_col2 = st.columns([1, 1]) | |
with sub_col1: | |
current_settings = st.session_state.user_settings | |
new_pools, prev_pools = pool_settings(current_settings) | |
if st.button("Update Summoning Pools", use_container_width=True): | |
st.session_state.user_settings['Pools'] = new_pools | |
st.experimental_rerun() | |
if not prev_pools.equals(new_pools): | |
st.warning("Unsaved changes") | |
with sub_col2: | |
current_settings = st.session_state.user_settings | |
new_rates, prev_rates = rate_settings(current_settings) | |
if st.button("Update Summoning Rates", use_container_width=True) or st.session_state.flag_update_rates: | |
st.session_state.user_settings['Banner Rates'] = new_rates | |
st.session_state.flag_update_rates = False | |
st.experimental_rerun() | |
if not prev_rates.equals(new_rates): | |
st.warning("Unsaved changes") | |
sum_rates = sum(new_rates['rate']) | |
if sum_rates != 100: | |
st.warning(f"Rates should sum to 100% | Currently: {sum_rates:.2f}%") | |
st.divider() | |
widget_keys = [ | |
'input_sparks', | |
'select_color_priority', | |
'select_goals_required', | |
'input_summon_limit', | |
'toggle_summon_limit', | |
'toggle_goals_met', | |
'toggle_orb_limit', | |
'input_simulations', | |
'select_banner_type', | |
'input_tickets', | |
'flag_update_rates' | |
] | |
gui_settings = {k: st.session_state.get(k, False) for k in widget_keys} | |
st.session_state.sys_settings = user_to_sys(st.session_state.user_settings) | |
col1, col2 = st.columns([2, 1]) | |
with col1: | |
st.download_button( | |
label=":blue[Download GUI + Simulator Settings]", | |
data=json.dumps({'gui_settings': gui_settings, 'simulator_settings': st.session_state.sys_settings}), | |
file_name="gui_simulator_settings.json", | |
use_container_width=True, | |
help='Settings compatible with the GUI and the simulator.' | |
) | |
with col2: | |
st.download_button( | |
label=":blue[Download Simulator Settings]", | |
data=json.dumps(st.session_state.sys_settings), | |
file_name="simulator_settings.json", | |
use_container_width=True, | |
help='Settings compatible with the simulator.' | |
) | |
if st.button(':orange[Run Simulation]', use_container_width=True): | |
results = fehsim.Simulator(st.session_state.sys_settings, streamlit=True).simulation_log_df | |
st.session_state.simulation_log_df = results | |
buffer = BytesIO() | |
results.to_parquet(buffer, index=False) | |
st.download_button( | |
label=":orange[Download Simulation Data]", | |
data=buffer, | |
file_name='data.parquet', | |
use_container_width=True | |
) | |
if __name__ == "__main__": | |
settings_app() | |