Spaces:
Runtime error
Runtime error
timothycho01
commited on
Commit
β’
6cbe716
1
Parent(s):
ae7f4f0
initial commit
Browse files- data/10000_Snipe_R_BGC_Spark_1.parquet +3 -0
- fehsim.py +472 -0
- settings.py +609 -0
data/10000_Snipe_R_BGC_Spark_1.parquet
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:7ecb5ede663e063e3fd5ce514c25eeae89344946bc6e6f355b9932088c3c503b
|
3 |
+
size 23102677
|
fehsim.py
ADDED
@@ -0,0 +1,472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import streamlit as st
|
3 |
+
from stqdm import stqdm
|
4 |
+
from tqdm import tqdm
|
5 |
+
|
6 |
+
|
7 |
+
class Simulator:
|
8 |
+
def __init__(self, settings: dict, streamlit=False):
|
9 |
+
self.streamlit = streamlit
|
10 |
+
target_rarity_map = {
|
11 |
+
'Any Rarity': [
|
12 |
+
'sh_special_4', 'special_4', 'non_focus_3', 'focus_5', 'non_focus_4', 'non_focus_5', 'focus_4'
|
13 |
+
],
|
14 |
+
'Any 5β
Unit or 4β
Special Rate Unit': [
|
15 |
+
'sh_special_4', 'special_4', 'focus_5', 'non_focus_5'
|
16 |
+
],
|
17 |
+
'Any 5β
Unit': [
|
18 |
+
'focus_5', 'non_focus_5'
|
19 |
+
],
|
20 |
+
'Specific 5β
Focus Unit': [
|
21 |
+
'focus_5'
|
22 |
+
],
|
23 |
+
'Specific 5β
Non-Focus Unit': [
|
24 |
+
'non_focus_5'
|
25 |
+
],
|
26 |
+
'Any 4β
Unit or 4β
Special Rate Unit or 4β
SHSR Unit': [
|
27 |
+
'sh_special_4', 'special_4', 'non_focus_4', 'focus_4'
|
28 |
+
],
|
29 |
+
'Any 4β
Unit': [
|
30 |
+
'non_focus_4', 'focus_4'
|
31 |
+
],
|
32 |
+
'Specific 4β
Focus Unit': [
|
33 |
+
'focus_4'
|
34 |
+
],
|
35 |
+
'Specific 4β
Non-Focus Unit': [
|
36 |
+
'non_focus_4'
|
37 |
+
],
|
38 |
+
'Any 4β
Special Rate Unit or 4β
SHSR Unit': [
|
39 |
+
'sh_special_4', 'special_4'
|
40 |
+
],
|
41 |
+
'Specific 4β
Special Rate Unit': [
|
42 |
+
'special_4'
|
43 |
+
],
|
44 |
+
'Specific 4β
SHSR Unit': [
|
45 |
+
'sh_special_4'
|
46 |
+
],
|
47 |
+
}
|
48 |
+
target_color_map = {
|
49 |
+
'Any Color': ['red', 'blue', 'green', 'colorless'],
|
50 |
+
'Red': ['red'],
|
51 |
+
'Blue': ['blue'],
|
52 |
+
'Green': ['green'],
|
53 |
+
'Colorless': ['colorless'],
|
54 |
+
}
|
55 |
+
banner_selection_map = {
|
56 |
+
"(3%/3%) Normal": ['normal', 'normal_4'],
|
57 |
+
"(4%/2%) Weekly Revival": ['weekly_revival'],
|
58 |
+
"(4%/2%) Weekly Revival 4β
SHSR": ['weekly_revival_shsr'],
|
59 |
+
"(5%/3%) Hero Fest": ['hero_fest'],
|
60 |
+
"(4%/2%) Double Special Heroes": ['double_special', 'double_special_4'],
|
61 |
+
"(8%/0%) Legendary / Mythic": ['legendary/mythic']
|
62 |
+
}
|
63 |
+
banner_rates = pd.DataFrame(
|
64 |
+
[
|
65 |
+
['normal', 'focus_5', 0.03],
|
66 |
+
['normal', 'non_focus_5', 0.03],
|
67 |
+
['normal', 'special_4', 0.03],
|
68 |
+
['normal', 'non_focus_4', 0.55],
|
69 |
+
['normal', 'non_focus_3', 0.36],
|
70 |
+
|
71 |
+
['normal_4', 'focus_5', 0.03],
|
72 |
+
['normal_4', 'non_focus_5', 0.03],
|
73 |
+
['normal_4', 'focus_4', 0.03],
|
74 |
+
|
75 |
+
['normal_4', 'special_4', 0.03],
|
76 |
+
['normal_4', 'non_focus_4', 0.52],
|
77 |
+
['normal_4', 'non_focus_3', 0.36],
|
78 |
+
|
79 |
+
['weekly_revival', 'focus_5', 0.04],
|
80 |
+
['weekly_revival', 'non_focus_5', 0.02],
|
81 |
+
['weekly_revival', 'special_4', 0.03],
|
82 |
+
['weekly_revival', 'non_focus_4', 0.55],
|
83 |
+
['weekly_revival', 'non_focus_3', 0.36],
|
84 |
+
|
85 |
+
['weekly_revival_shsr', 'focus_5', 0.04],
|
86 |
+
['weekly_revival_shsr', 'non_focus_5', 0.02],
|
87 |
+
['weekly_revival_shsr', 'sh_special_4', 0.03],
|
88 |
+
['weekly_revival_shsr', 'special_4', 0.03],
|
89 |
+
['weekly_revival_shsr', 'non_focus_4', 0.55],
|
90 |
+
['weekly_revival_shsr', 'non_focus_3', 0.33],
|
91 |
+
|
92 |
+
['hero_fest', 'focus_5', 0.05],
|
93 |
+
['hero_fest', 'non_focus_5', 0.03],
|
94 |
+
['hero_fest', 'special_4', 0.03],
|
95 |
+
['hero_fest', 'non_focus_4', 0.55],
|
96 |
+
['hero_fest', 'non_focus_3', 0.34],
|
97 |
+
|
98 |
+
['double_special', 'focus_5', 0.06],
|
99 |
+
['double_special', 'special_4', 0.03],
|
100 |
+
['double_special', 'non_focus_4', 0.57],
|
101 |
+
['double_special', 'non_focus_3', 0.34],
|
102 |
+
|
103 |
+
['double_special_4', 'focus_5', 0.06],
|
104 |
+
['double_special_4', 'focus_4', 0.03],
|
105 |
+
['double_special_4', 'special_4', 0.03],
|
106 |
+
['double_special_4', 'non_focus_4', 0.54],
|
107 |
+
['double_special_4', 'non_focus_3', 0.34],
|
108 |
+
|
109 |
+
['legendary/mythic', 'focus_5', 0.08],
|
110 |
+
['legendary/mythic', 'special_4', 0.03],
|
111 |
+
['legendary/mythic', 'non_focus_4', 0.55],
|
112 |
+
['legendary/mythic', 'non_focus_3', 0.34],
|
113 |
+
],
|
114 |
+
columns=['banner_type', 'rarity_pool', 'rate']
|
115 |
+
)
|
116 |
+
self.pools = settings.get('Pools')
|
117 |
+
self.goals = settings.get('Goals')
|
118 |
+
self.banner_rates = settings.get('Banner Rates')
|
119 |
+
|
120 |
+
self.goals_required = settings.get('Goals Required')
|
121 |
+
self.orb_limit = settings.get('Orb Limit')
|
122 |
+
self.summon_limit = settings.get('Summon Limit')
|
123 |
+
self.banner_type = settings.get('Banner Type', '(3%/3%) Normal')
|
124 |
+
self.n_simulations = settings.get('Simulations', 1000)
|
125 |
+
self.tickets = settings.get('Tickets', 0)
|
126 |
+
self.sparks = settings.get('Sparks', 0)
|
127 |
+
self.focus_charges_enabled = settings.get('Focus Charges', False)
|
128 |
+
self.color_priority = settings.get('Color Priority', ['red', 'blue', 'green', 'colorless'])
|
129 |
+
|
130 |
+
if not any([self.goals_required, self.orb_limit, self.summon_limit]):
|
131 |
+
raise ValueError('No End Criteria (Goals Required, Orb Limit, Summon Limit) provided.')
|
132 |
+
|
133 |
+
self.pools = pd.DataFrame(self.pools)
|
134 |
+
self.goals = pd.DataFrame(self.goals)
|
135 |
+
|
136 |
+
if self.banner_rates is None:
|
137 |
+
if self.pools.loc['focus_4'].sum() > 0:
|
138 |
+
mapped_banner_type = banner_selection_map[self.banner_type][-1]
|
139 |
+
else:
|
140 |
+
mapped_banner_type = banner_selection_map[self.banner_type][0]
|
141 |
+
self.banner_rates = banner_rates[banner_rates['banner_type'] == mapped_banner_type]
|
142 |
+
else:
|
143 |
+
self.banner_rates = pd.DataFrame(self.banner_rates).reset_index()
|
144 |
+
self.banner_rates.columns = ['rarity_pool', 'rate']
|
145 |
+
|
146 |
+
bool_rate_5 = self.banner_rates.rarity_pool.str.contains('5')
|
147 |
+
bool_rate_non_5 = ~bool_rate_5
|
148 |
+
sum_rate_5 = self.banner_rates.loc[bool_rate_5, 'rate'].sum()
|
149 |
+
sum_rate_non_5 = self.banner_rates.loc[bool_rate_non_5, 'rate'].sum()
|
150 |
+
|
151 |
+
self.banner_rates['step'] = 0
|
152 |
+
pre_calc_rates = [self.banner_rates]
|
153 |
+
|
154 |
+
# feh calculates rate up (rate down for non 5 stars) from base, not from previous step!
|
155 |
+
for i in range(1, 24):
|
156 |
+
inc_rates_df = self.banner_rates.copy(deep=True)
|
157 |
+
inc_rates_df.loc[bool_rate_5, 'rate'] *= 1 + (1 / sum_rate_5) * (0.005 * i)
|
158 |
+
inc_rates_df.loc[bool_rate_non_5, 'rate'] *= 1 - (1 / sum_rate_non_5) * (0.005 * i)
|
159 |
+
inc_rates_df['rate'] = round(inc_rates_df['rate'], 4)
|
160 |
+
inc_rates_df['step'] = i
|
161 |
+
pre_calc_rates.append(inc_rates_df)
|
162 |
+
|
163 |
+
# max pity rate
|
164 |
+
inc_rates_df = self.banner_rates.copy(deep=True)
|
165 |
+
inc_rates_df.loc[bool_rate_5, 'rate'] /= sum_rate_5
|
166 |
+
inc_rates_df.loc[bool_rate_non_5, 'rate'] = 0
|
167 |
+
inc_rates_df['rate'] = round(inc_rates_df['rate'], 4)
|
168 |
+
inc_rates_df['step'] = 24
|
169 |
+
pre_calc_rates.append(inc_rates_df)
|
170 |
+
|
171 |
+
self.banner_rates_pro_df = pd.concat(pre_calc_rates)
|
172 |
+
self.pity_step = 0
|
173 |
+
self.curr_banner_rates_df = self.banner_rates_pro_df[self.banner_rates_pro_df['step'] == self.pity_step]
|
174 |
+
|
175 |
+
pools_to_use = [rp for rp in self.pools.index if rp in self.banner_rates.rarity_pool.values]
|
176 |
+
self.pools = pd.DataFrame(self.pools.loc[pools_to_use])
|
177 |
+
self.pools.reset_index(inplace=True, names='rarity_pool')
|
178 |
+
unpivot_unit_pool = pd.melt(self.pools, id_vars=['rarity_pool'], var_name='color', value_name='size')
|
179 |
+
|
180 |
+
unit_list = []
|
181 |
+
for row in unpivot_unit_pool.itertuples():
|
182 |
+
for _ in range(row.size):
|
183 |
+
unit_list_row = [row.color, row.rarity_pool]
|
184 |
+
unit_list.append(unit_list_row)
|
185 |
+
units_df = pd.DataFrame(unit_list, columns=['color', 'rarity_pool'])
|
186 |
+
|
187 |
+
# joining color priority col to df
|
188 |
+
priority_df = pd.DataFrame(enumerate(self.color_priority, start=1), columns=['color_priority', 'color'])
|
189 |
+
units_df = units_df.join(priority_df.set_index('color'), on='color', validate='m:1')
|
190 |
+
|
191 |
+
# Goals Setup
|
192 |
+
goals_df = self.goals.copy(deep=True)
|
193 |
+
max_goal_group = goals_df.groupby('goal_group')['target_count'].max().reset_index()
|
194 |
+
goals_df = goals_df.join(
|
195 |
+
max_goal_group.set_index('goal_group'), on='goal_group', rsuffix='_max', validate='m:1'
|
196 |
+
)
|
197 |
+
goals_df['target_count'] = goals_df['target_count_max']
|
198 |
+
goals_df = goals_df.drop('target_count_max', axis=1)
|
199 |
+
goals_df['target_color'] = goals_df['target_color']
|
200 |
+
goals_df['current_count'] = 0
|
201 |
+
|
202 |
+
# Adding goal_row bool columns to unit_df
|
203 |
+
reserved_unit_index = []
|
204 |
+
|
205 |
+
for goal in goals_df.itertuples():
|
206 |
+
accepted_pools = target_rarity_map[goal.target_rarity]
|
207 |
+
accepted_colors = target_color_map[goal.target_color]
|
208 |
+
gr_col_name = 'goal_row_' + str(goal.Index)
|
209 |
+
goal_is_specific = 'specific' in goal.target_rarity.lower()
|
210 |
+
|
211 |
+
if goal_is_specific:
|
212 |
+
allowed_units = ~units_df.index.isin(reserved_unit_index)
|
213 |
+
else:
|
214 |
+
allowed_units = True
|
215 |
+
|
216 |
+
units_df[gr_col_name] = units_df['rarity_pool'].isin(accepted_pools) & units_df['color'].isin(
|
217 |
+
accepted_colors) & allowed_units
|
218 |
+
|
219 |
+
if units_df[gr_col_name].any() and goal_is_specific:
|
220 |
+
first_true_index = units_df.index[units_df[gr_col_name]].min()
|
221 |
+
reserved_unit_index.append(first_true_index)
|
222 |
+
units_df[gr_col_name] = units_df.index == first_true_index
|
223 |
+
else:
|
224 |
+
if self.streamlit:
|
225 |
+
st.warning(f'{gr_col_name} does not have any available units to target.')
|
226 |
+
st.warning('Target unit may already be reserved by previous goal.')
|
227 |
+
else:
|
228 |
+
print(f'{gr_col_name} does not have any available units to target.')
|
229 |
+
print('Target unit may already be reserved by previous goal.')
|
230 |
+
|
231 |
+
# Adding goal_group bool columns to unit_df
|
232 |
+
goal_groups_dict = {}
|
233 |
+
for group in set(goals_df['goal_group']):
|
234 |
+
goals_in_group = ['goal_row_' + str(x) for x in list(goals_df[goals_df['goal_group'] == group].index)]
|
235 |
+
goal_groups_dict['goal_group_' + str(group)] = goals_in_group
|
236 |
+
|
237 |
+
for goal_group in goal_groups_dict:
|
238 |
+
cols = goal_groups_dict[goal_group]
|
239 |
+
units_df[goal_group] = units_df[cols].apply(lambda row: pd.Series(row).any(), axis=1)
|
240 |
+
|
241 |
+
self.gg_cols = list(goal_groups_dict.keys())
|
242 |
+
self.gg_cols.sort()
|
243 |
+
slice_cols = [col for col in list(units_df.columns) if 'goal' not in col] + self.gg_cols
|
244 |
+
units_df = units_df[slice_cols]
|
245 |
+
|
246 |
+
self.base_summon_goals_df = goals_df.copy(deep=True)
|
247 |
+
self.curr_summon_goals_df = self.base_summon_goals_df.copy(deep=True)
|
248 |
+
self.banner_units_df = units_df.copy(deep=True)
|
249 |
+
|
250 |
+
self.goal_cols = list(self.base_summon_goals_df.columns[5:])
|
251 |
+
self.colors_to_target = list(set(self.curr_summon_goals_df.target_color))
|
252 |
+
|
253 |
+
# small adjustments
|
254 |
+
self.spark_thresholds = [_ * 40 for _ in range(1, self.sparks + 1)]
|
255 |
+
self.sparks_redeemed = 0
|
256 |
+
self.sparked_indexes = []
|
257 |
+
|
258 |
+
self.active_focus_charges = 0
|
259 |
+
self.apply_focus_charges = False
|
260 |
+
|
261 |
+
# refs
|
262 |
+
self.circle_df = None
|
263 |
+
self.session_type = 'normal'
|
264 |
+
self.summon_cost = 0
|
265 |
+
self.n_stones_in_circle = 5
|
266 |
+
|
267 |
+
# tracking
|
268 |
+
self.total_orbs_spent = 0
|
269 |
+
self.total_summons = 0
|
270 |
+
self.session_count = 0
|
271 |
+
self.summons_without_any_5 = 0
|
272 |
+
self.halt_pity_increase = False
|
273 |
+
self.end_criteria_met = False
|
274 |
+
|
275 |
+
self.summon_log = []
|
276 |
+
self.prev_summon_log_len = 0
|
277 |
+
self.orbs_spent_log = []
|
278 |
+
self.session_count_log = []
|
279 |
+
self.session_type_log = []
|
280 |
+
self.session_pity_step_log = []
|
281 |
+
self.run_num_log = []
|
282 |
+
|
283 |
+
self.simulation_log_df = None
|
284 |
+
|
285 |
+
self.run_simulations()
|
286 |
+
|
287 |
+
def reset_run(self):
|
288 |
+
self.total_orbs_spent = 0
|
289 |
+
self.total_summons = 0
|
290 |
+
self.session_count = 0
|
291 |
+
self.summons_without_any_5 = 0
|
292 |
+
self.end_criteria_met = False
|
293 |
+
|
294 |
+
self.curr_summon_goals_df = self.base_summon_goals_df.copy(deep=True)
|
295 |
+
self.sparks_redeemed = 0
|
296 |
+
self.sparked_indexes = []
|
297 |
+
self.active_focus_charges = 0
|
298 |
+
self.apply_focus_charges = False
|
299 |
+
|
300 |
+
def run_simulations(self):
|
301 |
+
|
302 |
+
if self.streamlit:
|
303 |
+
progress_bar = stqdm(range(self.n_simulations))
|
304 |
+
else:
|
305 |
+
progress_bar = tqdm(range(self.n_simulations))
|
306 |
+
|
307 |
+
for n in progress_bar:
|
308 |
+
self.simulate_run()
|
309 |
+
self.log_run(n + 1)
|
310 |
+
self.reset_run()
|
311 |
+
|
312 |
+
self.simulation_log_df = pd.DataFrame(self.summon_log)
|
313 |
+
self.simulation_log_df.rename(columns={'Index': 'unit_id'}, inplace=True)
|
314 |
+
self.simulation_log_df['unit_id'] += 1
|
315 |
+
self.simulation_log_df['orbs_spent'] = self.orbs_spent_log
|
316 |
+
self.simulation_log_df['session_count'] = self.session_count_log
|
317 |
+
self.simulation_log_df['session_type'] = self.session_type_log
|
318 |
+
self.simulation_log_df['session_pity_step'] = self.session_pity_step_log
|
319 |
+
self.simulation_log_df['run_num'] = self.run_num_log
|
320 |
+
|
321 |
+
message = f'{self.n_simulations} Simulations Completed'
|
322 |
+
if self.streamlit:
|
323 |
+
st.success(message)
|
324 |
+
else:
|
325 |
+
print(message)
|
326 |
+
|
327 |
+
def simulate_run(self):
|
328 |
+
|
329 |
+
while not self.end_criteria_met:
|
330 |
+
self.setup_session()
|
331 |
+
self.create_circle()
|
332 |
+
self.filter_circle()
|
333 |
+
self.summon_from_circle()
|
334 |
+
|
335 |
+
def log_run(self, run_num):
|
336 |
+
curr_log_len = len(self.summon_log) - self.prev_summon_log_len
|
337 |
+
self.run_num_log = self.run_num_log + [run_num for _ in range(curr_log_len)]
|
338 |
+
self.prev_summon_log_len = len(self.summon_log)
|
339 |
+
|
340 |
+
def setup_session(self):
|
341 |
+
self.session_type = 'normal'
|
342 |
+
self.apply_focus_charges = False
|
343 |
+
|
344 |
+
sparks_remain = self.sparks_redeemed != len(self.spark_thresholds)
|
345 |
+
if sparks_remain and self.total_summons >= self.spark_thresholds[self.sparks_redeemed]:
|
346 |
+
self.sparks_redeemed += 1
|
347 |
+
self.session_type = 'spark'
|
348 |
+
return
|
349 |
+
|
350 |
+
if self.focus_charges_enabled and self.active_focus_charges >= 3:
|
351 |
+
self.apply_focus_charges = True
|
352 |
+
|
353 |
+
self.pity_step = int(self.summons_without_any_5 / 5)
|
354 |
+
self.curr_banner_rates_df = self.banner_rates_pro_df[self.banner_rates_pro_df.step == self.pity_step].copy()
|
355 |
+
|
356 |
+
def create_circle(self):
|
357 |
+
circle = []
|
358 |
+
if self.session_type == 'spark':
|
359 |
+
bool_rarity = self.banner_units_df['rarity_pool'] == 'focus_5'
|
360 |
+
spark_circle = self.banner_units_df.loc[bool_rarity]
|
361 |
+
spark_circle = spark_circle[
|
362 |
+
~spark_circle.index.isin(self.sparked_indexes)] # removes previously sparked units
|
363 |
+
self.circle_df = spark_circle
|
364 |
+
return
|
365 |
+
|
366 |
+
for i in range(self.n_stones_in_circle):
|
367 |
+
# draws unit rarities
|
368 |
+
drawn_rarity = self.curr_banner_rates_df.sample(weights='rate')['rarity_pool'].iloc[0]
|
369 |
+
if self.apply_focus_charges and drawn_rarity == 'non_focus_5':
|
370 |
+
drawn_rarity = 'focus_5'
|
371 |
+
units_in_rarity = self.banner_units_df[self.banner_units_df['rarity_pool'] == drawn_rarity]
|
372 |
+
# draws unit from drawn rarities
|
373 |
+
drawn_unit = units_in_rarity.sample()
|
374 |
+
circle.append(drawn_unit)
|
375 |
+
|
376 |
+
self.circle_df = pd.concat(circle)
|
377 |
+
|
378 |
+
def filter_circle(self):
|
379 |
+
|
380 |
+
if self.session_type == 'spark':
|
381 |
+
circle = self.circle_df[self.circle_df[self.gg_cols].any(axis=1)].head(1)
|
382 |
+
elif self.goals_required == 'Any Goal Met' or len(self.colors_to_target) == 1:
|
383 |
+
self.colors_to_target = list(self.curr_summon_goals_df['target_color'].str.lower())
|
384 |
+
circle = self.circle_df[self.circle_df['color'].isin(self.colors_to_target)].sort_values('color_priority')
|
385 |
+
elif self.goals_required == 'All Goals Met':
|
386 |
+
unmet_goals = self.curr_summon_goals_df['current_count'] < self.curr_summon_goals_df['target_count']
|
387 |
+
self.colors_to_target = list(self.curr_summon_goals_df[unmet_goals]['target_color'].str.lower())
|
388 |
+
circle = self.circle_df[self.circle_df['color'].isin(self.colors_to_target)].sort_values('color_priority')
|
389 |
+
else:
|
390 |
+
circle = self.circle_df
|
391 |
+
|
392 |
+
if len(circle) != 0:
|
393 |
+
self.circle_df = circle
|
394 |
+
else: # if nothing returns after filtering, filter for first stone
|
395 |
+
self.circle_df = self.circle_df.sort_values('color_priority').head(1)
|
396 |
+
|
397 |
+
def summon_from_circle(self):
|
398 |
+
price_index = 0
|
399 |
+
self.session_count += 1
|
400 |
+
self.halt_pity_increase = False
|
401 |
+
|
402 |
+
if self.session_count <= self.tickets + 1:
|
403 |
+
prices = (0, 4, 4, 4, 3)
|
404 |
+
else:
|
405 |
+
prices = (5, 4, 4, 4, 3)
|
406 |
+
|
407 |
+
for row in self.circle_df.itertuples(index=True): # keep index as true
|
408 |
+
if self.session_type == 'spark':
|
409 |
+
self.sparked_indexes.append(row.Index)
|
410 |
+
self.summon_cost = 0
|
411 |
+
else:
|
412 |
+
self.summon_cost = prices[price_index]
|
413 |
+
self.eval_end_criteria_limits()
|
414 |
+
if self.end_criteria_met:
|
415 |
+
break
|
416 |
+
self.total_summons += 1
|
417 |
+
|
418 |
+
self.orbs_spent_log.append(self.summon_cost)
|
419 |
+
self.total_orbs_spent += self.summon_cost
|
420 |
+
self.summon_log.append(row)
|
421 |
+
self.session_count_log.append(self.session_count)
|
422 |
+
self.session_type_log.append(self.session_type)
|
423 |
+
self.session_pity_step_log.append(self.pity_step)
|
424 |
+
|
425 |
+
self.update_flags(row)
|
426 |
+
self.update_goals(row)
|
427 |
+
self.eval_end_criteria_goals()
|
428 |
+
|
429 |
+
if self.end_criteria_met:
|
430 |
+
break
|
431 |
+
price_index += 1
|
432 |
+
|
433 |
+
if self.end_criteria_met:
|
434 |
+
return
|
435 |
+
|
436 |
+
def update_flags(self, row):
|
437 |
+
if row.rarity_pool == 'focus_5':
|
438 |
+
self.halt_pity_increase = True
|
439 |
+
self.summons_without_any_5 = 0
|
440 |
+
if self.active_focus_charges >= 3:
|
441 |
+
self.active_focus_charges = 0
|
442 |
+
elif row.rarity_pool == 'non_focus_5':
|
443 |
+
self.summons_without_any_5 = max(0, self.summons_without_any_5 - 20)
|
444 |
+
self.active_focus_charges += 1
|
445 |
+
else:
|
446 |
+
if not self.halt_pity_increase:
|
447 |
+
self.summons_without_any_5 += 1
|
448 |
+
|
449 |
+
def update_goals(self, row): # takes named tuple from circle_df, updates summon goals based on goal group columns
|
450 |
+
# noinspection PyProtectedMember
|
451 |
+
row_dict = row._asdict()
|
452 |
+
for gg in self.gg_cols:
|
453 |
+
if row_dict[gg]:
|
454 |
+
gg_num = gg.split('_')[-1]
|
455 |
+
self.curr_summon_goals_df.loc[self.curr_summon_goals_df['goal_group'] == gg_num, 'current_count'] += 1
|
456 |
+
|
457 |
+
def eval_end_criteria_goals(self):
|
458 |
+
if self.goals_required is not None:
|
459 |
+
met_goals = self.curr_summon_goals_df['current_count'] >= self.curr_summon_goals_df['target_count']
|
460 |
+
if self.goals_required == 'Any Goal Group Met' and any(met_goals):
|
461 |
+
self.end_criteria_met = True
|
462 |
+
elif self.goals_required == 'All Goal Groups Met' and all(met_goals):
|
463 |
+
self.end_criteria_met = True
|
464 |
+
|
465 |
+
def eval_end_criteria_limits(self):
|
466 |
+
if self.orb_limit != 0:
|
467 |
+
if self.total_orbs_spent + self.summon_cost > self.orb_limit:
|
468 |
+
self.end_criteria_met = True
|
469 |
+
elif self.summon_limit != 0:
|
470 |
+
if self.total_summons + 1 > self.summon_limit:
|
471 |
+
self.end_criteria_met = True
|
472 |
+
|
settings.py
ADDED
@@ -0,0 +1,609 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import fehsim
|
4 |
+
import json
|
5 |
+
from io import BytesIO
|
6 |
+
|
7 |
+
RARITY_OPTIONS = [
|
8 |
+
'Any Rarity',
|
9 |
+
'Any 5β
Unit or 4β
Special Rate Unit',
|
10 |
+
'Any 5β
Unit',
|
11 |
+
'Specific 5β
Focus Unit',
|
12 |
+
'Specific 5β
Non-Focus Unit',
|
13 |
+
'Any 4β
Unit or 4β
Special Rate Unit or 4β
SHSR Unit',
|
14 |
+
'Any 4β
Unit',
|
15 |
+
'Specific 4β
Focus Unit',
|
16 |
+
'Specific 4β
Non-Focus Unit',
|
17 |
+
'Any 4β
Special Rate Unit or 4β
SHSR Unit',
|
18 |
+
'Specific 4β
Special Rate Unit',
|
19 |
+
'Specific 4β
SHSR Unit'
|
20 |
+
]
|
21 |
+
COLOR_OPTIONS = [
|
22 |
+
'Any Color',
|
23 |
+
'Red',
|
24 |
+
'Blue',
|
25 |
+
'Green',
|
26 |
+
'Colorless'
|
27 |
+
]
|
28 |
+
BANNER_OPTIONS = [
|
29 |
+
'(3%/3%) Normal',
|
30 |
+
'(4%/2%) Weekly Revival',
|
31 |
+
'(4%/2%) Weekly Revival 4β
SHSR',
|
32 |
+
'(5%/3%) Hero Fest',
|
33 |
+
'(4%/2%) Double Special Heroes',
|
34 |
+
'(8%/0%) Legendary / Mythic'
|
35 |
+
]
|
36 |
+
END_CRITERIA_OPTIONS = [
|
37 |
+
'Any Goal Group Met',
|
38 |
+
'All Goal Groups Met',
|
39 |
+
]
|
40 |
+
BANNER_RATES_MAPPING = {
|
41 |
+
"(3%/3%) Normal": ['normal', 'normal_4'],
|
42 |
+
"(4%/2%) Weekly Revival": ['weekly_revival'],
|
43 |
+
"(4%/2%) Weekly Revival 4β
SHSR": ['weekly_revival_shsr'],
|
44 |
+
"(5%/3%) Hero Fest": ['hero_fest'],
|
45 |
+
"(4%/2%) Double Special Heroes": ['double_special', 'double_special_4'],
|
46 |
+
"(8%/0%) Legendary / Mythic": ['legendary/mythic']
|
47 |
+
}
|
48 |
+
|
49 |
+
POOL_ORDER = [
|
50 |
+
'focus_5',
|
51 |
+
'non_focus_5',
|
52 |
+
'focus_4',
|
53 |
+
'special_4',
|
54 |
+
'sh_special_4',
|
55 |
+
'non_focus_4',
|
56 |
+
'non_focus_3'
|
57 |
+
]
|
58 |
+
|
59 |
+
BANNER_RATES = [
|
60 |
+
['normal', 'focus_5', 0.03],
|
61 |
+
['normal', 'non_focus_5', 0.03],
|
62 |
+
['normal', 'special_4', 0.03],
|
63 |
+
['normal', 'non_focus_4', 0.55],
|
64 |
+
['normal', 'non_focus_3', 0.36],
|
65 |
+
|
66 |
+
['normal_4', 'focus_5', 0.03],
|
67 |
+
['normal_4', 'non_focus_5', 0.03],
|
68 |
+
['normal_4', 'focus_4', 0.03],
|
69 |
+
['normal_4', 'special_4', 0.03],
|
70 |
+
['normal_4', 'non_focus_4', 0.52],
|
71 |
+
['normal_4', 'non_focus_3', 0.36],
|
72 |
+
|
73 |
+
['weekly_revival', 'focus_5', 0.04],
|
74 |
+
['weekly_revival', 'non_focus_5', 0.02],
|
75 |
+
['weekly_revival', 'special_4', 0.03],
|
76 |
+
['weekly_revival', 'non_focus_4', 0.55],
|
77 |
+
['weekly_revival', 'non_focus_3', 0.36],
|
78 |
+
|
79 |
+
['weekly_revival_shsr', 'focus_5', 0.04],
|
80 |
+
['weekly_revival_shsr', 'non_focus_5', 0.02],
|
81 |
+
['weekly_revival_shsr', 'sh_special_4', 0.03],
|
82 |
+
['weekly_revival_shsr', 'special_4', 0.03],
|
83 |
+
['weekly_revival_shsr', 'non_focus_4', 0.55],
|
84 |
+
['weekly_revival_shsr', 'non_focus_3', 0.33],
|
85 |
+
|
86 |
+
['hero_fest', 'focus_5', 0.05],
|
87 |
+
['hero_fest', 'non_focus_5', 0.03],
|
88 |
+
['hero_fest', 'special_4', 0.03],
|
89 |
+
['hero_fest', 'non_focus_4', 0.55],
|
90 |
+
['hero_fest', 'non_focus_3', 0.34],
|
91 |
+
|
92 |
+
['double_special', 'focus_5', 0.06],
|
93 |
+
['double_special', 'special_4', 0.03],
|
94 |
+
['double_special', 'non_focus_4', 0.57],
|
95 |
+
['double_special', 'non_focus_3', 0.34],
|
96 |
+
|
97 |
+
['double_special_4', 'focus_5', 0.06],
|
98 |
+
['double_special_4', 'focus_4', 0.03],
|
99 |
+
['double_special_4', 'special_4', 0.03],
|
100 |
+
['double_special_4', 'non_focus_4', 0.54],
|
101 |
+
['double_special_4', 'non_focus_3', 0.34],
|
102 |
+
|
103 |
+
['legendary/mythic', 'focus_5', 0.08],
|
104 |
+
['legendary/mythic', 'special_4', 0.03],
|
105 |
+
['legendary/mythic', 'non_focus_4', 0.55],
|
106 |
+
['legendary/mythic', 'non_focus_3', 0.34],
|
107 |
+
]
|
108 |
+
BANNER_RATES_DF = pd.DataFrame(BANNER_RATES, columns=['banner_type', 'rarity_pool', 'rate'])
|
109 |
+
BANNER_RATES_DF['rate'] *= 100
|
110 |
+
|
111 |
+
pool_to_alias = {
|
112 |
+
'focus_5': '5β
Focus',
|
113 |
+
'focus_4': '4β
Focus',
|
114 |
+
'non_focus_5': '5β
',
|
115 |
+
'special_4': '4β
SR',
|
116 |
+
'sh_special_4': '4β
SHSR',
|
117 |
+
'non_focus_4': '4β
',
|
118 |
+
'non_focus_3': '3β
',
|
119 |
+
}
|
120 |
+
alias_to_pool = {v: k for k, v in pool_to_alias.items()}
|
121 |
+
BANNER_RATES_DF['rarity_pool'] = BANNER_RATES_DF['rarity_pool'].map(pool_to_alias)
|
122 |
+
|
123 |
+
|
124 |
+
def core_settings(settings):
|
125 |
+
st.subheader('Simulation Settings', anchor=False)
|
126 |
+
|
127 |
+
st.write('End Criteria:')
|
128 |
+
tt = 'End a run when goal conditions are met.'
|
129 |
+
if st.toggle('Goals Met', help=tt, key='toggle_goals_met'):
|
130 |
+
goals_required = st.selectbox(
|
131 |
+
'GR',
|
132 |
+
options=END_CRITERIA_OPTIONS,
|
133 |
+
label_visibility='collapsed',
|
134 |
+
key='select_goals_required'
|
135 |
+
)
|
136 |
+
else:
|
137 |
+
goals_required = None
|
138 |
+
|
139 |
+
tt = 'End a run when Orb Limit is reached or not enough orbs to summon.'
|
140 |
+
if st.toggle('Orb Limit', help=tt, value=True, key='toggle_orb_limit'):
|
141 |
+
orb_limit = st.number_input(
|
142 |
+
'OL',
|
143 |
+
value=3000,
|
144 |
+
step=1,
|
145 |
+
min_value=0,
|
146 |
+
label_visibility='collapsed',
|
147 |
+
key='input_orb_limit'
|
148 |
+
)
|
149 |
+
else:
|
150 |
+
orb_limit = None
|
151 |
+
|
152 |
+
tt = 'End a run when Summon Limit is reached.'
|
153 |
+
if st.toggle('Summon Limit', help=tt, key='toggle_summon_limit'):
|
154 |
+
summon_limit = st.number_input(
|
155 |
+
'SL',
|
156 |
+
value=15_000,
|
157 |
+
step=1,
|
158 |
+
min_value=0,
|
159 |
+
label_visibility='collapsed',
|
160 |
+
key='input_summon_limit'
|
161 |
+
)
|
162 |
+
else:
|
163 |
+
summon_limit = None
|
164 |
+
|
165 |
+
if not any([goals_required, orb_limit, summon_limit]):
|
166 |
+
st.warning('Please select at least one End Criteria.')
|
167 |
+
|
168 |
+
col1, col2 = st.columns([3, 2])
|
169 |
+
|
170 |
+
with col2:
|
171 |
+
st.write("")
|
172 |
+
st.write("")
|
173 |
+
focus_charges = st.checkbox('Enable Focus Charges?', key='toggle_focus_charges')
|
174 |
+
with col1:
|
175 |
+
tt = 'Highest Priority -> Lowest Priority'
|
176 |
+
color_priority = st.multiselect(
|
177 |
+
'Color Priority',
|
178 |
+
COLOR_OPTIONS[1:],
|
179 |
+
default=COLOR_OPTIONS[1:],
|
180 |
+
help=tt,
|
181 |
+
key='select_color_priority'
|
182 |
+
)
|
183 |
+
if len(color_priority) != 4:
|
184 |
+
st.warning('Please sort all the colors.')
|
185 |
+
|
186 |
+
col1, col2 = st.columns(2)
|
187 |
+
|
188 |
+
with col1:
|
189 |
+
tt = 'Select the banner rates to simulate. Will also determine which pools are used.'
|
190 |
+
banner_type = st.selectbox(
|
191 |
+
'Banner Type:',
|
192 |
+
options=BANNER_OPTIONS,
|
193 |
+
help=tt,
|
194 |
+
key='select_banner_type',
|
195 |
+
)
|
196 |
+
tt = 'Number of simulations to run.'
|
197 |
+
simulations = st.number_input(
|
198 |
+
'Simulations',
|
199 |
+
value=100,
|
200 |
+
step=1,
|
201 |
+
min_value=0,
|
202 |
+
help=tt,
|
203 |
+
key='input_simulations'
|
204 |
+
)
|
205 |
+
tt = 'Number of summoning sessions (i.e. circles) where the first summon is free.'
|
206 |
+
|
207 |
+
with col2:
|
208 |
+
tickets = st.number_input(
|
209 |
+
'Tickets',
|
210 |
+
value=0,
|
211 |
+
step=1,
|
212 |
+
min_value=0,
|
213 |
+
help=tt,
|
214 |
+
key='input_tickets'
|
215 |
+
)
|
216 |
+
tt = 'Guaranteed 5β
Focus Unit (i.e. spark) session after 40 summons.'
|
217 |
+
sparks = st.number_input(
|
218 |
+
'Sparks',
|
219 |
+
value=0,
|
220 |
+
step=1,
|
221 |
+
min_value=0,
|
222 |
+
help=tt,
|
223 |
+
key='input_sparks'
|
224 |
+
)
|
225 |
+
|
226 |
+
summon_pools = settings['Pools']
|
227 |
+
focus_5_pool_size = summon_pools.loc['focus_5'][1:].sum()
|
228 |
+
if sparks > focus_5_pool_size:
|
229 |
+
st.warning(f'Sparks exceeding the number of 5β
Focus Units.')
|
230 |
+
|
231 |
+
updated_core_settings = {
|
232 |
+
# End Criteria
|
233 |
+
'Goals Required': goals_required,
|
234 |
+
'Orb Limit': orb_limit,
|
235 |
+
'Summon Limit': summon_limit,
|
236 |
+
# Main Settings
|
237 |
+
'Banner Type': banner_type,
|
238 |
+
'Simulations': simulations,
|
239 |
+
'Tickets': tickets,
|
240 |
+
'Sparks': sparks,
|
241 |
+
'Focus Charges': focus_charges,
|
242 |
+
'Color Priority': color_priority,
|
243 |
+
}
|
244 |
+
|
245 |
+
return updated_core_settings
|
246 |
+
|
247 |
+
|
248 |
+
def goal_settings(settings):
|
249 |
+
st.subheader('Summoning Goals', anchor=False, help='Edit the table below to set your summoning goals')
|
250 |
+
st.caption(f"{bo('Goals')} of the same {bo('Goal Group')} will contribute to a {bo('Shared Target Count')}.")
|
251 |
+
st.caption(f"{bo('Shared Target Count')} will be the max {bo('Target Count')} within the {bo('Goal Group')}.")
|
252 |
+
column_config = {
|
253 |
+
"target_rarity": st.column_config.SelectboxColumn(
|
254 |
+
"Rarity", options=RARITY_OPTIONS, required=True, width='large'
|
255 |
+
),
|
256 |
+
"target_color": st.column_config.SelectboxColumn(
|
257 |
+
"Color", options=COLOR_OPTIONS, required=True, width='small'
|
258 |
+
),
|
259 |
+
"target_count": st.column_config.NumberColumn(
|
260 |
+
"Target Count", min_value=1, step=1, required=True, width='small'
|
261 |
+
),
|
262 |
+
"goal_group": st.column_config.NumberColumn(
|
263 |
+
"Goal Group", min_value=1, step=1, required=True, width='small'
|
264 |
+
),
|
265 |
+
}
|
266 |
+
df = settings['Goals']
|
267 |
+
return st.data_editor(
|
268 |
+
df,
|
269 |
+
num_rows='dynamic',
|
270 |
+
column_config=column_config,
|
271 |
+
use_container_width=True,
|
272 |
+
hide_index=True,
|
273 |
+
key='data_editor_goals'
|
274 |
+
), df
|
275 |
+
|
276 |
+
|
277 |
+
def pool_settings(settings):
|
278 |
+
st.subheader('Summoning Pool', anchor=False, help='Edit the table below to set your summoning pools')
|
279 |
+
column_config = {
|
280 |
+
"red": st.column_config.NumberColumn("Red", min_value=0, step=1, required=True),
|
281 |
+
"blue": st.column_config.NumberColumn("Blue", min_value=0, step=1, required=True),
|
282 |
+
"green": st.column_config.NumberColumn("Green", min_value=0, step=1, required=True),
|
283 |
+
"colorless": st.column_config.NumberColumn("Colorless", min_value=0, step=1, required=True),
|
284 |
+
"rarity_pool": st.column_config.TextColumn("Rarity Pool", disabled=True),
|
285 |
+
}
|
286 |
+
df = settings['Pools']
|
287 |
+
return st.data_editor(
|
288 |
+
df,
|
289 |
+
column_config=column_config,
|
290 |
+
hide_index=True,
|
291 |
+
key='data_editor_pools'
|
292 |
+
), df
|
293 |
+
|
294 |
+
|
295 |
+
def rate_settings(settings):
|
296 |
+
st.subheader('Summoning Rates', anchor=False, help='Edit the table below to set your summoning rates')
|
297 |
+
st.caption(f"Changes to {bo('4β
Focus pool')} or {bo('Banner Type')} will reset this table.")
|
298 |
+
column_config = {
|
299 |
+
"rarity_pool": st.column_config.SelectboxColumn(
|
300 |
+
"Rarity Pool", disabled=True
|
301 |
+
),
|
302 |
+
"rate": st.column_config.NumberColumn(
|
303 |
+
"Rate (%)", min_value=0, step=0.01, max_value=100, required=True, format="%.2f"
|
304 |
+
),
|
305 |
+
}
|
306 |
+
summon_pools = settings['Pools']
|
307 |
+
focus_4_pool_size = summon_pools.loc['focus_4'][1:].sum()
|
308 |
+
if focus_4_pool_size > 0:
|
309 |
+
mapped_banner_type = BANNER_RATES_MAPPING[settings['Banner Type']][-1]
|
310 |
+
if len(BANNER_RATES_MAPPING[settings['Banner Type']]) == 1:
|
311 |
+
st.warning(f'4β
Focus Units do not appear in this Banner Type.')
|
312 |
+
else:
|
313 |
+
mapped_banner_type = BANNER_RATES_MAPPING[settings['Banner Type']][0]
|
314 |
+
|
315 |
+
if mapped_banner_type not in settings['Banner Rates']['banner_type'].values:
|
316 |
+
df = BANNER_RATES_DF[BANNER_RATES_DF.banner_type == mapped_banner_type]
|
317 |
+
else:
|
318 |
+
df = settings['Banner Rates']
|
319 |
+
|
320 |
+
return st.data_editor(
|
321 |
+
df,
|
322 |
+
column_config=column_config,
|
323 |
+
hide_index=True,
|
324 |
+
column_order=['rarity_pool', 'rate'],
|
325 |
+
key='data_editor_rates'
|
326 |
+
), df
|
327 |
+
|
328 |
+
|
329 |
+
def goal_setting_example():
|
330 |
+
with st.expander("Goal Group Examples"):
|
331 |
+
st.caption("You want (11) copies of a Red unit present in both the 5β
Focus pool and 4β
Non-Focus pool.")
|
332 |
+
st.caption("Goal Group 1 will be met once a shared total of (11) units are summoned.")
|
333 |
+
ex_data = [
|
334 |
+
['Specific 5β
Focus Unit', 'Red', 11, 1],
|
335 |
+
['Specific 4β
Non-Focus Unit', 'Red', 11, 1]
|
336 |
+
]
|
337 |
+
ex_df = pd.DataFrame(ex_data, columns=['Rarity', 'Color', 'Target Count', 'Goal Group'])
|
338 |
+
st.dataframe(ex_df, hide_index=True)
|
339 |
+
st.divider()
|
340 |
+
st.caption("You want (8) units with a specific skill.")
|
341 |
+
st.caption("(2) Blue units with this skill are present in the 5β
Non-Focus pool.")
|
342 |
+
st.caption("(1) Green unit with this skill is present in the 4β
Non-Focus pool.")
|
343 |
+
st.caption("Goal Group 2 will be met once a shared total of (8) units are summoned.")
|
344 |
+
ex_data = [
|
345 |
+
['Specific 5β
Non-Focus Unit', 'Blue', 8, 2],
|
346 |
+
['Specific 5β
Non-Focus Unit', 'Blue', 8, 2],
|
347 |
+
['Specific 4β
Non-Focus Unit', 'Green', 8, 2],
|
348 |
+
]
|
349 |
+
ex_df = pd.DataFrame(ex_data, columns=['Rarity', 'Color', 'Target Count', 'Goal Group'])
|
350 |
+
st.dataframe(ex_df, hide_index=True)
|
351 |
+
|
352 |
+
|
353 |
+
def bo(text): # streamlit bold orange markdown
|
354 |
+
return f":orange[__{text}__]"
|
355 |
+
|
356 |
+
|
357 |
+
def user_to_sys(settings):
|
358 |
+
sys_settings = {}
|
359 |
+
|
360 |
+
for k, v in settings.items():
|
361 |
+
if isinstance(v, pd.DataFrame):
|
362 |
+
val = v.copy(deep=True)
|
363 |
+
else:
|
364 |
+
val = v
|
365 |
+
sys_settings[k] = val
|
366 |
+
|
367 |
+
sys_goals = sys_settings['Goals']
|
368 |
+
sys_settings['Goals'] = sys_goals.to_dict()
|
369 |
+
|
370 |
+
sys_pools = sys_settings['Pools']
|
371 |
+
sys_pools = sys_pools.drop('rarity_pool', axis=1)
|
372 |
+
sys_settings['Pools'] = sys_pools.to_dict()
|
373 |
+
|
374 |
+
sys_rates = sys_settings['Banner Rates']
|
375 |
+
sys_rates = sys_rates.drop('banner_type', axis=1)
|
376 |
+
sys_rates['rarity_pool'] = sys_rates['rarity_pool'].map(alias_to_pool)
|
377 |
+
sys_rates['rate'] = round(sys_rates['rate'] / 100, 4)
|
378 |
+
sys_rates.index = sys_rates['rarity_pool']
|
379 |
+
sys_rates = sys_rates.drop('rarity_pool', axis=1)
|
380 |
+
sys_settings['Banner Rates'] = sys_rates.to_dict()
|
381 |
+
|
382 |
+
sys_settings['Color Priority'] = [c.lower() for c in sys_settings['Color Priority']]
|
383 |
+
|
384 |
+
return sys_settings
|
385 |
+
|
386 |
+
|
387 |
+
def sys_to_user(settings):
|
388 |
+
user_settings = {}
|
389 |
+
|
390 |
+
for k, v in settings.items():
|
391 |
+
if isinstance(v, dict):
|
392 |
+
val = pd.DataFrame(v)
|
393 |
+
else:
|
394 |
+
val = v
|
395 |
+
user_settings[k] = val
|
396 |
+
|
397 |
+
user_pools = user_settings['Pools']
|
398 |
+
user_pools['rarity_pool'] = user_pools.index
|
399 |
+
user_pools['rarity_pool'] = user_pools['rarity_pool'].map(pool_to_alias)
|
400 |
+
col_order = ['rarity_pool', 'red', 'blue', 'green', 'colorless']
|
401 |
+
user_settings['Pools'] = user_pools[col_order]
|
402 |
+
|
403 |
+
user_rates = user_settings['Banner Rates']
|
404 |
+
user_rates = user_rates.reset_index(names='rarity_pool')
|
405 |
+
must_only_have = user_rates.rarity_pool.values
|
406 |
+
df = BANNER_RATES_DF.copy(deep=True)
|
407 |
+
df['rarity_pool'] = df['rarity_pool'].map(alias_to_pool)
|
408 |
+
grouped_df = df.groupby('banner_type')['rarity_pool']
|
409 |
+
filtered_banner_types = grouped_df.apply(lambda x: set(must_only_have) == set(x)).reset_index()
|
410 |
+
filtered_banner_types = filtered_banner_types[filtered_banner_types['rarity_pool']]
|
411 |
+
compatible_banners = df[df['banner_type'].isin(filtered_banner_types['banner_type'])]
|
412 |
+
user_rates['banner_type'] = compatible_banners.banner_type.values[0]
|
413 |
+
user_rates['rate'] = user_rates['rate'] * 100
|
414 |
+
col_order = ['banner_type', 'rarity_pool', 'rate']
|
415 |
+
sorting_order = {v: i for i, v in enumerate(POOL_ORDER)}
|
416 |
+
user_rates['order'] = user_rates['rarity_pool'].map(sorting_order)
|
417 |
+
user_rates = user_rates.sort_values(by='order').drop(columns='order').reset_index(drop=True)
|
418 |
+
user_rates['rarity_pool'] = user_rates['rarity_pool'].map(pool_to_alias)
|
419 |
+
|
420 |
+
user_settings['Banner Rates'] = user_rates[col_order]
|
421 |
+
user_settings['Color Priority'] = [c.capitalize() for c in user_settings['Color Priority']]
|
422 |
+
|
423 |
+
return user_settings
|
424 |
+
|
425 |
+
|
426 |
+
def debug_compare(setting_1, setting_2):
|
427 |
+
comparison = {}
|
428 |
+
for k, v in setting_1.items():
|
429 |
+
if isinstance(v, pd.DataFrame):
|
430 |
+
compare = v.equals(setting_2[k])
|
431 |
+
else:
|
432 |
+
compare = v == setting_2[k]
|
433 |
+
comparison[k] = compare
|
434 |
+
return comparison
|
435 |
+
|
436 |
+
|
437 |
+
def settings_app():
|
438 |
+
st.set_page_config(layout="centered")
|
439 |
+
css = '''
|
440 |
+
<style>
|
441 |
+
section.main > div {max-width:55rem}
|
442 |
+
</style>
|
443 |
+
'''
|
444 |
+
st.markdown(css, unsafe_allow_html=True)
|
445 |
+
st.subheader("FEH Detailed Summoning Simulator", anchor=False)
|
446 |
+
|
447 |
+
# initialize session state
|
448 |
+
if 'user_settings' not in st.session_state:
|
449 |
+
default_goals = [['Specific 5β
Focus Unit', 'Red', 11, 1]]
|
450 |
+
default_pools = {
|
451 |
+
'focus_5': ['5β
Focus', 1, 1, 1, 1],
|
452 |
+
'focus_4': ['4β
Focus', 0, 0, 0, 0],
|
453 |
+
'non_focus_5': ['5β
', 26, 27, 19, 19],
|
454 |
+
'special_4': ['4β
SR', 62, 42, 36, 28],
|
455 |
+
'sh_special_4': ['4β
SHSR', 22, 28, 32, 27],
|
456 |
+
'non_focus_4': ['4β
', 41, 45, 37, 45],
|
457 |
+
'non_focus_3': ['3β
', 41, 45, 37, 45],
|
458 |
+
}
|
459 |
+
pool_cols = ['rarity_pool', 'red', 'blue', 'green', 'colorless']
|
460 |
+
goal_cols = ['target_rarity', 'target_color', 'target_count', 'goal_group']
|
461 |
+
mapped_banner_type = BANNER_RATES_MAPPING[BANNER_OPTIONS[0]][0]
|
462 |
+
default_settings = {
|
463 |
+
# Dataframes
|
464 |
+
'Pools': pd.DataFrame.from_dict(default_pools, orient='index', columns=pool_cols),
|
465 |
+
'Goals': pd.DataFrame(default_goals, columns=goal_cols),
|
466 |
+
'Banner Rates': BANNER_RATES_DF[BANNER_RATES_DF.banner_type == mapped_banner_type].copy(deep=True),
|
467 |
+
# End Criteria
|
468 |
+
'Goals Required': 'All Goal Groups Met',
|
469 |
+
'Orb Limit': 3000,
|
470 |
+
'Summon Limit': None,
|
471 |
+
# Main Settings
|
472 |
+
'Banner Type': BANNER_OPTIONS[0],
|
473 |
+
'Simulations': 100,
|
474 |
+
'Tickets': 0,
|
475 |
+
'Sparks': 0,
|
476 |
+
'Focus Charges': False,
|
477 |
+
'Color Priority': ['Red', 'Blue', 'Green', 'Colorless'],
|
478 |
+
}
|
479 |
+
st.session_state.user_settings = default_settings
|
480 |
+
|
481 |
+
# upload settings from json
|
482 |
+
uploaded_file = st.file_uploader("Upload gui_simulator_settings.json file", type=['json'])
|
483 |
+
if uploaded_file is not None:
|
484 |
+
if st.button("Submit", use_container_width=True):
|
485 |
+
data = json.load(uploaded_file)
|
486 |
+
if 'gui_settings' not in data or 'simulator_settings' not in data:
|
487 |
+
st.error("Invalid gui_simulator_settings.json file")
|
488 |
+
else:
|
489 |
+
st.session_state.user_settings = sys_to_user(data['simulator_settings'])
|
490 |
+
for k, v in data['gui_settings'].items():
|
491 |
+
st.session_state[k] = v
|
492 |
+
|
493 |
+
st.divider()
|
494 |
+
|
495 |
+
current_settings = st.session_state.user_settings
|
496 |
+
prev_banner_type = current_settings['Banner Type']
|
497 |
+
|
498 |
+
updated_data = core_settings(current_settings)
|
499 |
+
for k, v in updated_data.items():
|
500 |
+
st.session_state.user_settings[k] = v
|
501 |
+
|
502 |
+
if prev_banner_type != current_settings['Banner Type']:
|
503 |
+
st.session_state.flag_update_rates = True
|
504 |
+
else:
|
505 |
+
st.session_state.flag_update_rates = False
|
506 |
+
|
507 |
+
current_settings = st.session_state.user_settings
|
508 |
+
new_goals, prev_goals = goal_settings(current_settings)
|
509 |
+
|
510 |
+
goal_setting_example()
|
511 |
+
|
512 |
+
if st.button("Update Summoning Goals", use_container_width=True):
|
513 |
+
st.session_state.user_settings['Goals'] = new_goals
|
514 |
+
st.experimental_rerun()
|
515 |
+
|
516 |
+
if not prev_goals.equals(new_goals):
|
517 |
+
st.warning("Unsaved changes")
|
518 |
+
|
519 |
+
sub_col1, sub_col2 = st.columns([1, 1])
|
520 |
+
|
521 |
+
with sub_col1:
|
522 |
+
current_settings = st.session_state.user_settings
|
523 |
+
new_pools, prev_pools = pool_settings(current_settings)
|
524 |
+
|
525 |
+
if st.button("Update Summoning Pools", use_container_width=True):
|
526 |
+
st.session_state.user_settings['Pools'] = new_pools
|
527 |
+
st.experimental_rerun()
|
528 |
+
|
529 |
+
if not prev_pools.equals(new_pools):
|
530 |
+
st.warning("Unsaved changes")
|
531 |
+
|
532 |
+
with sub_col2:
|
533 |
+
current_settings = st.session_state.user_settings
|
534 |
+
new_rates, prev_rates = rate_settings(current_settings)
|
535 |
+
|
536 |
+
if st.button("Update Summoning Rates", use_container_width=True) or st.session_state.flag_update_rates:
|
537 |
+
st.session_state.user_settings['Banner Rates'] = new_rates
|
538 |
+
st.session_state.flag_update_rates = False
|
539 |
+
st.experimental_rerun()
|
540 |
+
|
541 |
+
if not prev_rates.equals(new_rates):
|
542 |
+
st.warning("Unsaved changes")
|
543 |
+
|
544 |
+
sum_rates = sum(new_rates['rate'])
|
545 |
+
if sum_rates != 100:
|
546 |
+
st.warning(f"Rates should sum to 100% | Currently: {sum_rates:.2f}%")
|
547 |
+
|
548 |
+
st.divider()
|
549 |
+
widget_keys = [
|
550 |
+
'input_sparks',
|
551 |
+
'select_color_priority',
|
552 |
+
'select_goals_required',
|
553 |
+
'input_summon_limit',
|
554 |
+
'toggle_summon_limit',
|
555 |
+
'toggle_goals_met',
|
556 |
+
'toggle_orb_limit',
|
557 |
+
'input_simulations',
|
558 |
+
'select_banner_type',
|
559 |
+
'input_tickets',
|
560 |
+
'flag_update_rates'
|
561 |
+
]
|
562 |
+
gui_settings = {k: st.session_state.get(k, False) for k in widget_keys}
|
563 |
+
st.session_state.sys_settings = user_to_sys(st.session_state.user_settings)
|
564 |
+
|
565 |
+
col1, col2 = st.columns([2, 1])
|
566 |
+
with col1:
|
567 |
+
st.download_button(
|
568 |
+
label=":blue[Download GUI + Simulator Settings]",
|
569 |
+
data=json.dumps({'gui_settings': gui_settings, 'simulator_settings': st.session_state.sys_settings}),
|
570 |
+
file_name="gui_simulator_settings.json",
|
571 |
+
use_container_width=True,
|
572 |
+
help='Settings compatible with the GUI and the simulator.'
|
573 |
+
)
|
574 |
+
|
575 |
+
with col2:
|
576 |
+
st.download_button(
|
577 |
+
label=":blue[Download Simulator Settings]",
|
578 |
+
data=json.dumps(st.session_state.sys_settings),
|
579 |
+
file_name="simulator_settings.json",
|
580 |
+
use_container_width=True,
|
581 |
+
help='Settings compatible with the simulator.'
|
582 |
+
)
|
583 |
+
|
584 |
+
if st.button(':orange[Run Simulation]', use_container_width=True):
|
585 |
+
results = fehsim.Simulator(st.session_state.sys_settings, streamlit=True).simulation_log_df
|
586 |
+
st.session_state.simulation_log_df = results
|
587 |
+
buffer = BytesIO()
|
588 |
+
results.to_parquet(buffer, index=False)
|
589 |
+
st.download_button(
|
590 |
+
label=":orange[Download Simulation Data]",
|
591 |
+
data=buffer,
|
592 |
+
file_name='data.parquet',
|
593 |
+
use_container_width=True
|
594 |
+
)
|
595 |
+
|
596 |
+
with st.expander('debug_compare', expanded=True):
|
597 |
+
st.divider()
|
598 |
+
st.json(debug_compare(sys_to_user(st.session_state.sys_settings), st.session_state.user_settings))
|
599 |
+
|
600 |
+
# with st.expander('debug'):
|
601 |
+
# st.json(st.session_state.user_settings)
|
602 |
+
# st.json(sys_to_user(st.session_state.sys_settings))
|
603 |
+
# st.divider()
|
604 |
+
# st.json(st.session_state.sys_settings)
|
605 |
+
|
606 |
+
|
607 |
+
if __name__ == "__main__":
|
608 |
+
settings_app()
|
609 |
+
|