detailed_fehsim / app.py
timothycho01's picture
removed leftover debugging
1e37cfb
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()