timothycho01 commited on
Commit
6cbe716
β€’
1 Parent(s): ae7f4f0

initial commit

Browse files
Files changed (3) hide show
  1. data/10000_Snipe_R_BGC_Spark_1.parquet +3 -0
  2. fehsim.py +472 -0
  3. 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
+