aigurletov commited on
Commit
ee7e579
·
1 Parent(s): ab3618c
Files changed (1) hide show
  1. app.py +341 -223
app.py CHANGED
@@ -5,266 +5,384 @@ import io
5
  import random
6
  import os
7
  from collections import deque
 
 
 
8
 
9
- # HELPER FUNCTIONS TO PARSE .asset FILES
10
- def parse_asset_file(file_content):
11
- """Parses a YAML-like .asset file from a string, skipping the initial %YAML and %TAG lines."""
12
- if file_content is None:
13
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  try:
15
- # Find the first '---' which indicates the start of the YAML document
16
- yaml_start = file_content.find('---')
17
- if yaml_start == -1:
18
- raise ValueError("Not a valid Unity .asset file (missing '---')")
19
-
20
- # Clean up the content for standard YAML parsing
21
- yaml_content = file_content[yaml_start:]
22
- # Remove the MonoBehaviour header which is not valid YAML
23
- lines = yaml_content.split('\n')
24
- cleaned_lines = [line for line in lines if not line.strip().startswith('MonoBehaviour:')]
25
- cleaned_yaml = '\n'.join(cleaned_lines)
26
-
27
- # The file seems to have a custom serialization format within 'serializationData'.
28
- # For now, we will parse the main structure.
29
- # A more robust solution might need a dedicated parser for Unity's format.
30
-
31
- # Let's try to parse the main body first
32
- data = yaml.safe_load(io.StringIO(cleaned_yaml))
33
-
34
- # A simplified approach to extract key-value pairs for now
35
- # This will need to be much more robust based on the actual file structure.
36
- config_data = {}
37
- for line in file_content.split('\n'):
38
- if ':' in line:
39
- parts = line.split(':', 1)
40
- key = parts[0].strip()
41
- value = parts[1].strip()
42
- if key.startswith('_'):
43
- config_data[key] = value
44
-
45
- return config_data, data
46
  except Exception as e:
47
- print(f"Error parsing file: {e}")
48
- return None, None
49
-
50
- # --- SIMULATION LOGIC ---
51
- def run_simulation_logic(ruleset_file, chain_files, iteration_count, initial_energy):
52
- """
53
- This function will contain the Python port of the C# MergeOrderSimulationService.
54
- For now, it returns placeholder data.
55
- """
56
- # Placeholder for parsed ruleset
57
- ruleset_id = "ruleset_placeholder"
58
-
59
- # Placeholder for selected chain IDs
60
- selected_chain_ids = [f"chain_{i}" for i in range(len(chain_files))]
61
-
62
- # --- This is where the core simulation logic will go ---
63
- # We will replicate the steps from MergeOrderSimulationService.cs:
64
- # 1. Get all available items from selected chains.
65
- # 2. Apply MergeBoardItemFinder simulation.
66
- # 3. Filter by unlock level.
67
- # 4. Filter recently used items.
68
- # 5. Generate order with proper difficulty.
69
- # 6. Update history.
70
- # 7. Calculate costs and rewards.
71
- # 8. Store data for CSV.
72
 
73
- # For now, let's generate some dummy data to show in the UI
74
- sim_data = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  current_energy = initial_energy
76
- difficulty_history = deque(maxlen=5) # Assuming max history of 5 from ruleset
77
 
78
  for i in range(iteration_count):
79
- req1 = f"merge_item_wood_branch_{random.randint(1, 3)}"
80
- req2 = f"merge_item_stones_stones_{random.randint(1, 2)}" if random.random() > 0.5 else ""
81
- energy_cost = random.randint(5, 20)
82
- current_energy -= energy_cost
83
- total_difficulty = random.randint(500, 2000)
84
- difficulty_history.append(total_difficulty)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- sim_data.append({
87
- "Order": i + 1,
88
- "MEnergy_Price": energy_cost,
89
- "MEnergy_Amount": current_energy,
90
- "Requirement_1": req1,
91
- "Weight_1": random.randint(50, 150),
92
- "ChainId_1": "orders_merge_wood",
93
- "Level_1": int(req1.split('_')[-1]),
94
- "Requirement_2": req2,
95
- "Weight_2": random.randint(75, 125) if req2 else 0,
96
- "ChainId_2": "merge_stones" if req2 else "",
97
- "Level_2": int(req2.split('_')[-1]) if req2 else 0,
98
- "Total_Difficulty": total_difficulty,
99
- "Adjusted_Difficulty": total_difficulty + sum(list(difficulty_history)[:-1]),
100
- })
101
-
102
- df = pd.DataFrame(sim_data)
103
-
104
- # Placeholder for statistics report
105
- stats_report = f"""
106
- SIMULATION STATISTICS
107
- =====================
108
- Total Orders Generated: {iteration_count}
109
- Average Difficulty: {df['Total_Difficulty'].mean():.2f}
110
-
111
- Item Frequency (Top 5):
112
- {df['Requirement_1'].value_counts().head(5).to_string()}
113
- """
114
-
115
- return df, stats_report, "simulation_results.csv", "simulation_stats.txt"
116
 
117
- def run_simulation_interface(ruleset_file, chain_files, iteration_count, initial_energy):
118
- if not ruleset_file or not chain_files:
119
- raise gr.Error("Пожалуйста, загрузите файл правил и хотя бы один файл цепочки.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- df, stats_report, df_path, report_path = run_simulation_logic(
122
- ruleset_file, chain_files, iteration_count, initial_energy
123
- )
 
 
 
 
 
 
 
124
 
125
- # Convert DataFrame to a downloadable CSV file
126
- csv_buffer = io.StringIO()
127
- df.to_csv(csv_buffer, index=False)
128
- csv_bytes = csv_buffer.getvalue().encode('utf-8')
129
-
130
- # FIX: Removed the 'name' keyword argument from gr.update for the File component
131
- # as it causes a TypeError in recent Gradio versions.
132
- return df, stats_report, gr.update(value=csv_bytes, visible=True), gr.update(value=stats_report.encode('utf-8'), visible=True)
133
-
134
- # --- COST CALCULATION LOGIC ---
135
- def run_cost_calculation_logic(chain_files, item_files):
136
- """
137
- This function will contain the Python port of the C# MergeChainCostCalculator.
138
- For now, it returns placeholder data.
139
- """
140
- # --- This is where the core cost calculation logic will go ---
141
- # We will replicate the steps from MergeChainCostCalculator.cs:
142
- # 1. Load and parse all merge chain and item configs.
143
- # 2. For each chain, find its associated producer (spawner).
144
- # 3. For each item in the chain, calculate the expected number of attempts to get it.
145
- # 4. Calculate the final energy cost (expected attempts * spawn cost).
146
- # 5. Handle dynamic weights (IncWeight).
147
 
148
- # Dummy data for now
149
- cost_data = []
150
- for i, chain_file in enumerate(chain_files):
151
- chain_id = f"chain_{i+1}"
152
- for level in range(1, 7):
153
- cost_data.append({
154
- "Chain ID": chain_id,
155
- "Item ID": f"{chain_id}_item_{level}",
156
- "Level": level,
157
- "Producer ID": f"spawner_{i+1}",
158
- "Spawn Cost": 5,
159
- "Weight": 100 - (level * 10),
160
- "Inc Weight": 0,
161
- "Expected Attempts": 1.0 + (level * 0.5),
162
- "Energy Cost": (1.0 + (level * 0.5)) * 5,
163
- "Warning": ""
164
- })
165
-
166
- df = pd.DataFrame(cost_data)
167
- return df
168
-
169
- def run_cost_calculation_interface(chain_files, item_files):
170
- if not chain_files or not item_files:
171
- raise gr.Error("Пожалуйста, загрузите файлы цепочек и предметов.")
172
 
173
- df = run_cost_calculation_logic(chain_files, item_files)
174
- return df
 
 
 
 
 
 
 
175
 
176
- # --- CONFIG EDITOR LOGIC ---
177
- def view_config_file(file):
178
- if file is None:
179
- return "Загрузите файл для просмотра.", ""
180
 
181
- # The 'file' object from gr.File is a string path to a temporary file
182
- with open(file, 'r', encoding='utf-8') as f:
183
- raw_content = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- return raw_content, os.path.basename(file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- # --- GRADIO UI ---
 
 
188
  with gr.Blocks(theme=gr.themes.Soft(), title="Инструмент Балансировки Merge-2") as demo:
189
  gr.Markdown("# Инструмент для Балансировки Игр Merge-2")
190
-
191
  with gr.Tabs():
192
- # --- SIMULATION TAB ---
193
  with gr.TabItem("Симуляция генератора заказов"):
194
- gr.Markdown("## Симуляция генератора заказов\nЗагрузите конфиги и запустите симуляцию для получения данных по генерации заказов.")
195
  with gr.Row():
196
- with gr.Column(scale=1):
197
- sim_ruleset_upload = gr.File(label="Загрузить Ruleset (.asset)")
198
- sim_chains_upload = gr.File(label="Загрузить файлы цепочек (.asset)", file_count="multiple")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  sim_iterations = gr.Slider(10, 1000, value=100, step=10, label="Количество итераций")
200
- sim_initial_energy = gr.Number(value=10000, label="Начальное количество энергии")
201
  sim_run_button = gr.Button("Запустить симуляцию", variant="primary")
 
202
  with gr.Column(scale=3):
203
  gr.Markdown("### Результаты симуляции")
204
- sim_results_df = gr.DataFrame(label="Данные по заказам")
205
  gr.Markdown("### Сводный отчет")
206
  sim_stats_report = gr.Textbox(label="Статистика", lines=10)
207
  with gr.Row():
208
  sim_download_csv = gr.File(label="Скачать CSV", visible=False, interactive=False)
209
  sim_download_report = gr.File(label="Скачать отчет", visible=False, interactive=False)
210
 
211
- # --- COST CALCULATION TAB ---
212
- with gr.TabItem("Расчет стоимости цепочек"):
213
- gr.Markdown("## Расчет стоимости предметов\nЗагрузите конфиги цепочек и спавнеров для расчета стоимости производства предметов в энергии.")
214
- with gr.Row():
215
- with gr.Column(scale=1):
216
- cost_chains_upload = gr.File(label="Загрузить файлы цепочек (.asset)", file_count="multiple")
217
- cost_items_upload = gr.File(label="Загрузить файлы спавнеров/предметов (.asset)", file_count="multiple")
218
- cost_run_button = gr.Button("Рассчитать стоимость", variant="primary")
219
- with gr.Column(scale=3):
220
- gr.Markdown("### Рассчитанная стоимость")
221
- cost_results_df = gr.DataFrame(label="Стоимость предметов в энергии")
222
-
223
- # --- CONFIG EDITOR TAB ---
224
- with gr.TabItem("Редактор конфигов"):
225
- gr.Markdown("## Редактор Конфигурационных Файлов\nЗагрузите файл для пр��смотра и редактирования его параметров.")
226
- with gr.Row():
227
- with gr.Column(scale=1):
228
- config_upload = gr.File(label="Загрузить файл .asset для редактирования")
229
- config_file_name = gr.Label(label="Выбранный файл")
230
- with gr.Column(scale=2):
231
- # For now, just a textbox. Could be replaced with dynamic fields.
232
- config_content_viewer = gr.Textbox(label="Содержимое файла", lines=30, interactive=True)
233
- config_save_button = gr.Button("Сохранить изменения (не реализовано)", interactive=False)
234
-
235
  # --- Event Handlers ---
 
 
 
 
 
 
236
  sim_run_button.click(
237
  fn=run_simulation_interface,
238
- inputs=[sim_ruleset_upload, sim_chains_upload, sim_iterations, sim_initial_energy],
239
  outputs=[sim_results_df, sim_stats_report, sim_download_csv, sim_download_report]
240
  )
241
-
242
- cost_run_button.click(
243
- fn=run_cost_calculation_interface,
244
- inputs=[cost_chains_upload, cost_items_upload],
245
- outputs=[cost_results_df]
246
- )
247
-
248
- def get_file_path_and_content(file_obj):
249
- if not file_obj:
250
- return "", ""
251
- with open(file_obj.name, 'r', encoding='utf-8') as f:
252
- content = f.read()
253
- return file_obj.name, content
254
-
255
- config_upload.upload(
256
- fn=lambda file_obj: os.path.basename(file_obj.name) if file_obj else "",
257
- inputs=config_upload,
258
- outputs=config_file_name
259
- )
260
-
261
- # FIX: The input 'file' is a temporary file object. We need to open and read it.
262
- config_upload.upload(
263
- fn=lambda file_obj: open(file_obj.name, 'r', encoding='utf-8').read() if file_obj else "",
264
- inputs=config_upload,
265
- outputs=config_content_viewer
266
- )
267
-
268
 
269
  if __name__ == "__main__":
270
  demo.launch()
 
5
  import random
6
  import os
7
  from collections import deque
8
+ import tempfile
9
+ from dataclasses import dataclass, field
10
+ from typing import List, Dict, Any, Optional
11
 
12
+ # ===============================================================
13
+ # 1. DATA CLASSES (полная структура)
14
+ # ===============================================================
15
+ @dataclass
16
+ class MergeChainItemData:
17
+ MergeItemId: str
18
+ RequirementWeight: int
19
+ RewardDifficulty: int
20
+
21
+ @dataclass
22
+ class MergeChain:
23
+ Id: str
24
+ Items: List[MergeChainItemData] = field(default_factory=list)
25
+
26
+ @dataclass
27
+ class GenerationReward:
28
+ Amount: int
29
+ MergeItemId: str = ""
30
+ Type: str = "Energy"
31
+ RewardWeight: int = 100
32
+ ReductionFactor: int = 0
33
+
34
+ @dataclass
35
+ class GenerationRewardWithDifficulty:
36
+ DifficultyScore: int
37
+ Rewards: List[GenerationReward] = field(default_factory=list)
38
+
39
+ @dataclass
40
+ class RequirementWeight:
41
+ Weights: List[int] = field(default_factory=lambda: [70, 30])
42
+
43
+ @dataclass
44
+ class GeneratorSettings:
45
+ Id: str = "DefaultSettings"
46
+ DefaultRequirementWeights: RequirementWeight = field(default_factory=RequirementWeight)
47
+ MaxActiveOrders: int = 4
48
+ ReductionFactor: int = 3
49
+ IncreaseFactor: int = 5
50
+
51
+ @dataclass
52
+ class MergeGeneratorRuleset:
53
+ Id: str = "DefaultRuleset"
54
+ MaxHistoryOrders: int = 5
55
+ IncrementDifficulty: int = 2
56
+ OverallChanceToDropExpeditionEnergy: int = 90
57
+ OverrideWeights: Dict[str, int] = field(default_factory=dict)
58
+ EnergyRewards: List[GenerationRewardWithDifficulty] = field(default_factory=list)
59
+ ItemRewards: List[GenerationRewardWithDifficulty] = field(default_factory=list)
60
+ OverrideMaxRequirementOrders: bool = False
61
+ OverrideMaxOrdersWithWeight: RequirementWeight = field(default_factory=RequirementWeight)
62
+
63
+ @dataclass
64
+ class SimulatedOrder:
65
+ Requirements: list = field(default_factory=list)
66
+ Rewards: list = field(default_factory=list)
67
+ TotalDifficulty: int = 0
68
+ MergeEnergyPrice: int = 0
69
+
70
+ # ===============================================================
71
+ # 2. ADVANCED PARSERS
72
+ # ===============================================================
73
+ def robust_asset_parser(file_content: str) -> dict:
74
  try:
75
+ lines = file_content.splitlines()
76
+ mono_behaviour_line_index = next((i for i, line in enumerate(lines) if "MonoBehaviour:" in line), -1)
77
+ if mono_behaviour_line_index == -1: return {}
78
+ data_lines = lines[mono_behaviour_line_index + 1:]
79
+ if not data_lines: return {}
80
+ indentation = len(data_lines[0]) - len(data_lines[0].lstrip(' '))
81
+ dedented_lines = [(line[indentation:] if line.strip() else "") for line in data_lines]
82
+ yaml_string = "\n".join(dedented_lines)
83
+ return yaml.safe_load(yaml_string) or {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  except Exception as e:
85
+ print(f"Asset parsing error: {e}")
86
+ return {}
87
+
88
+ def load_chain_config(file) -> MergeChain:
89
+ with open(file.name, 'r', encoding='utf-8') as f: content = f.read()
90
+ data = robust_asset_parser(content)
91
+ chain = MergeChain(Id=data.get('_id'))
92
+ for item_dict in data.get('_mergeChainItemsData', []):
93
+ chain.Items.append(MergeChainItemData(
94
+ MergeItemId=item_dict.get('MergeItemId'),
95
+ RequirementWeight=int(item_dict.get('RequirementWeight', 0)),
96
+ RewardDifficulty=int(item_dict.get('RewardDifficulty', 0))
97
+ ))
98
+ return chain
99
+
100
+ def parse_rewards_from_data(reward_list_data, reward_type) -> List[GenerationRewardWithDifficulty]:
101
+ rewards_with_difficulty = []
102
+ if not isinstance(reward_list_data, list): return rewards_with_difficulty
 
 
 
 
 
 
 
103
 
104
+ for reward_group in reward_list_data:
105
+ difficulty = int(reward_group.get('DifficultyScore', 0))
106
+ rewards = []
107
+
108
+ if reward_type == "Energy":
109
+ if 'Reward' in reward_group and reward_group['Reward']:
110
+ reward_info = reward_group['Reward']
111
+ rewards.append(GenerationReward(Amount=int(reward_info.get('_amount', 0)), Type='Energy'))
112
+ elif reward_type == "Item":
113
+ if 'Rewards' in reward_group and isinstance(reward_group['Rewards'], list):
114
+ for weighted_reward in reward_group['Rewards']:
115
+ if 'Reward' in weighted_reward and weighted_reward['Reward']:
116
+ reward_info = weighted_reward['Reward']
117
+ rewards.append(GenerationReward(
118
+ Amount=int(reward_info.get('_amount', 1)),
119
+ MergeItemId=reward_info.get('_mergeItemId', ''),
120
+ Type='Item',
121
+ RewardWeight=int(weighted_reward.get('RewardWeight', 100)),
122
+ ReductionFactor=int(weighted_reward.get('ReductionFactor', 0))
123
+ ))
124
+ if rewards:
125
+ rewards_with_difficulty.append(GenerationRewardWithDifficulty(DifficultyScore=difficulty, Rewards=rewards))
126
+
127
+ return rewards_with_difficulty
128
+
129
+ def load_ruleset_config(file) -> MergeGeneratorRuleset:
130
+ print("--- Loading ruleset config ---")
131
+ with open(file.name, 'r', encoding='utf-8') as f: content = f.read()
132
+ data = robust_asset_parser(content)
133
+ print(f"Parsed raw data from {file.name}")
134
+
135
+ raw_weights = data.get('_overrideMaxOrdersWithWeight', {}).get('_requirementOrderWeights', "70,30")
136
+ weights_list = [int(w.strip()) for w in str(raw_weights).split(',')] if isinstance(raw_weights, str) else [70, 30]
137
+
138
+ ruleset = MergeGeneratorRuleset(
139
+ Id=data.get('_id'),
140
+ MaxHistoryOrders=int(data.get('_maxHistoryOrders', 5)),
141
+ IncrementDifficulty=int(data.get('_incrementDifficulty', 2)),
142
+ OverallChanceToDropExpeditionEnergy=int(data.get('_overallChanceToDropExpeditionEnergy', 90)),
143
+ OverrideMaxRequirementOrders=bool(int(data.get('_overrideMaxRequirementOrders', 0))),
144
+ OverrideMaxOrdersWithWeight=RequirementWeight(Weights=weights_list)
145
+ )
146
+ ruleset.OverrideWeights = {ow.get('_mergeItemId'): int(ow.get('_weight', 0)) for ow in data.get('_overrideWeights', [])}
147
+ ruleset.EnergyRewards = parse_rewards_from_data(data.get('_energyRewards', []), "Energy")
148
+ ruleset.ItemRewards = parse_rewards_from_data(data.get('_itemRewards', []), "Item")
149
+ print(f"Loaded {len(ruleset.EnergyRewards)} energy reward groups and {len(ruleset.ItemRewards)} item reward groups.")
150
+ return ruleset
151
+
152
+ def load_settings_config(file) -> GeneratorSettings:
153
+ with open(file.name, 'r', encoding='utf-8') as f: content = f.read()
154
+ data = robust_asset_parser(content)
155
+ return GeneratorSettings(
156
+ Id=data.get('m_Name', 'MergeGeneratorSettings'),
157
+ MaxActiveOrders=int(data.get('_maxActiveOrders', 4)),
158
+ ReductionFactor=int(data.get('_reductionFactor', 3)),
159
+ IncreaseFactor=int(data.get('_increaseFactor', 5))
160
+ )
161
+
162
+ # ===============================================================
163
+ # 3. SIMULATION LOGIC
164
+ # ===============================================================
165
+ def get_requirement_count(ruleset, settings):
166
+ weights_source = settings.DefaultRequirementWeights if not ruleset.OverrideMaxRequirementOrders else ruleset.OverrideMaxOrdersWithWeight
167
+ weights = weights_source.Weights
168
+ if len(weights) < 2: weights.extend([0] * (2 - len(weights)))
169
+ return random.choices([1, 2], weights=weights[:2], k=1)[0]
170
+
171
+ def generate_rewards(order, ruleset):
172
+ if random.randint(1, 100) <= ruleset.OverallChanceToDropExpeditionEnergy:
173
+ suitable_reward_groups = [rg for rg in ruleset.EnergyRewards if order.TotalDifficulty >= int(rg.DifficultyScore)]
174
+ if suitable_reward_groups:
175
+ chosen_group = max(suitable_reward_groups, key=lambda rg: int(rg.DifficultyScore))
176
+ if chosen_group.Rewards: order.Rewards.append(chosen_group.Rewards[0])
177
+ else:
178
+ suitable_reward_groups = [rg for rg in ruleset.ItemRewards if order.TotalDifficulty >= int(rg.DifficultyScore)]
179
+ if suitable_reward_groups:
180
+ chosen_group = max(suitable_reward_groups, key=lambda rg: int(rg.DifficultyScore))
181
+ if chosen_group.Rewards:
182
+ rewards, weights = chosen_group.Rewards, [r.RewardWeight for r in chosen_group.Rewards]
183
+ if sum(weights) > 0: order.Rewards.append(random.choices(rewards, weights=weights, k=1)[0])
184
+
185
+ def run_simulation_logic(chains, ruleset, settings, iteration_count, initial_energy):
186
+ order_history = deque(maxlen=ruleset.MaxHistoryOrders)
187
+ chain_unlock_levels = {chain.Id: 1 for chain in chains}
188
+ simulation_results = []
189
  current_energy = initial_energy
 
190
 
191
  for i in range(iteration_count):
192
+ all_available_items = [item for chain in chains for level, item in enumerate(chain.Items, 1) if item.RequirementWeight > 0 and level <= chain_unlock_levels.get(chain.Id, 1)]
193
+ recently_used_ids = {req.MergeItemId for order in order_history for req in order.Requirements}
194
+ final_items = [item for item in all_available_items if item.MergeItemId not in recently_used_ids] or all_available_items
195
+ if not final_items: continue
196
+
197
+ req_count = get_requirement_count(ruleset, settings)
198
+ order = SimulatedOrder()
199
+ used_items_in_order = []
200
+
201
+ for _ in range(req_count):
202
+ selectable_items = [item for item in final_items if item not in used_items_in_order]
203
+ if not selectable_items: break
204
+
205
+ weights = [ruleset.OverrideWeights.get(item.MergeItemId, item.RequirementWeight) for item in selectable_items]
206
+ if sum(weights) == 0: continue
207
+
208
+ selected_item = random.choices(selectable_items, weights=weights, k=1)[0]
209
+ order.Requirements.append(selected_item)
210
+ order.TotalDifficulty += selected_item.RewardDifficulty
211
+ used_items_in_order.append(selected_item)
212
 
213
+ if not order.Requirements: continue
214
+
215
+ total_cost = sum(2**(chain.Items.index(req)) for req in order.Requirements if (chain := next((c for c in chains if req in c.Items), None)))
216
+ order.MergeEnergyPrice = total_cost
217
+ current_energy -= total_cost
218
+
219
+ generate_rewards(order, ruleset)
220
+ order_history.append(order)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ for req in order.Requirements:
223
+ chain_of_item = next((c for c in chains if req in c.Items), None)
224
+ if chain_of_item and (current_level := chain_of_item.Items.index(req) + 1) == chain_unlock_levels.get(chain_of_item.Id, 1):
225
+ chain_unlock_levels[chain_of_item.Id] += 1
226
+
227
+ row = {"Order": i + 1, "Total_Difficulty": order.TotalDifficulty, "MergeEnergyPrice": order.MergeEnergyPrice, "MEnergy_Amount": current_energy}
228
+ for j, req in enumerate(order.Requirements):
229
+ chain_of_item = next((c for c in chains if req in c.Items), None)
230
+ row[f'Requirement_{j+1}'] = req.MergeItemId
231
+ row[f'Weight_{j+1}'] = ruleset.OverrideWeights.get(req.MergeItemId, req.RequirementWeight)
232
+ row[f'ChainId_{j+1}'] = chain_of_item.Id if chain_of_item else "N/A"
233
+ row[f'Level_{j+1}'] = chain_of_item.Items.index(req) + 1 if chain_of_item else 0
234
+ row[f'RewardDifficulty_{j+1}'] = req.RewardDifficulty
235
+
236
+ row['ExpeditionEnergyReward'] = next((r.Amount for r in order.Rewards if r.Type == 'Energy'), 0)
237
+ row['MergeItemReward'] = next((r.MergeItemId for r in order.Rewards if r.Type == 'Item'), "")
238
+ simulation_results.append(row)
239
+
240
+ df = pd.DataFrame(simulation_results).fillna(0)
241
+ full_column_list = ['Order', 'MergeEnergyPrice', 'MEnergy_Amount', 'Total_Difficulty', 'ExpeditionEnergyReward', 'MergeItemReward',
242
+ 'Requirement_1', 'Weight_1', 'ChainId_1', 'Level_1', 'RewardDifficulty_1',
243
+ 'Requirement_2', 'Weight_2', 'ChainId_2', 'Level_2', 'RewardDifficulty_2']
244
+ for col in full_column_list:
245
+ if col not in df.columns: df[col] = 0
246
 
247
+ stats_report = f"Total Orders: {len(df)}\nAvg Difficulty: {df['Total_Difficulty'].mean():.2f}\nFinal Energy: {current_energy}"
248
+ return df[full_column_list], stats_report
249
+
250
+ def run_simulation_interface(
251
+ chain_df, energy_rewards_df, item_rewards_df,
252
+ max_hist, inc_diff, energy_chance, req_weights_str,
253
+ red_factor, inc_factor,
254
+ iteration_count, initial_energy
255
+ ):
256
+ if chain_df is None or chain_df.empty: raise gr.Error("Нет данных о цепочках. Загрузите файлы или заполните таблицу.")
257
 
258
+ chains = [MergeChain(Id=chain_id, Items=[MergeChainItemData(row['MergeItemId'], int(row['RequirementWeight']), int(row['RewardDifficulty'])) for _, row in group.iterrows()]) for chain_id, group in chain_df.groupby('ChainId')]
259
+ settings = GeneratorSettings(ReductionFactor=red_factor, IncreaseFactor=inc_factor, DefaultRequirementWeights=RequirementWeight(Weights=[int(w.strip()) for w in req_weights_str.split(',')]))
260
+ ruleset = MergeGeneratorRuleset(MaxHistoryOrders=max_hist, IncrementDifficulty=inc_diff, OverallChanceToDropExpeditionEnergy=energy_chance)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
+ if energy_rewards_df is not None and not energy_rewards_df.empty:
263
+ df_copy = energy_rewards_df.dropna().copy()
264
+ df_copy['DifficultyScore'] = pd.to_numeric(df_copy['DifficultyScore'])
265
+ df_copy['Amount'] = pd.to_numeric(df_copy['Amount'])
266
+ ruleset.EnergyRewards = [GenerationRewardWithDifficulty(r['DifficultyScore'], [GenerationReward(Amount=r['Amount'], Type='Energy')]) for i, r in df_copy.iterrows()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ if item_rewards_df is not None and not item_rewards_df.empty:
269
+ df_copy = item_rewards_df.dropna().copy()
270
+ df_copy['DifficultyScore'] = pd.to_numeric(df_copy['DifficultyScore'])
271
+ df_copy['Amount'] = pd.to_numeric(df_copy['Amount'])
272
+ df_copy['RewardWeight'] = pd.to_numeric(df_copy['RewardWeight'])
273
+ df_copy['ReductionFactor'] = pd.to_numeric(df_copy['ReductionFactor'])
274
+ for score, group in df_copy.groupby('DifficultyScore'):
275
+ rewards = [GenerationReward(Amount=r['Amount'], MergeItemId=r['MergeItemId'], Type='Item', RewardWeight=r['RewardWeight'], ReductionFactor=r['ReductionFactor']) for i, r in group.iterrows()]
276
+ ruleset.ItemRewards.append(GenerationRewardWithDifficulty(int(score), rewards))
277
 
278
+ df, stats_report = run_simulation_logic(chains, ruleset, settings, iteration_count, initial_energy)
 
 
 
279
 
280
+ with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.csv', encoding='utf-8', newline='') as tmp_csv:
281
+ df.to_csv(tmp_csv.name, index=False)
282
+ csv_path = tmp_csv.name
283
+ with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.txt', encoding='utf-8') as tmp_txt:
284
+ tmp_txt.write(stats_report)
285
+ report_path = tmp_txt.name
286
+
287
+ return df, stats_report, gr.update(value=csv_path, visible=True), gr.update(value=report_path, visible=True)
288
+
289
+ # ===============================================================
290
+ # 4. UI UPDATE FUNCTIONS
291
+ # ===============================================================
292
+ def update_ui_from_files(chain_files, ruleset_file, settings_file):
293
+ chain_data = []
294
+ if chain_files:
295
+ for file in chain_files:
296
+ chain = load_chain_config(file)
297
+ for item in chain.Items:
298
+ chain_data.append([chain.Id, item.MergeItemId, item.RequirementWeight, item.RewardDifficulty])
299
+ chain_df = pd.DataFrame(chain_data, columns=['ChainId', 'MergeItemId', 'RequirementWeight', 'RewardDifficulty'])
300
+
301
+ max_hist, inc_diff, energy_chance, req_weights = 5, 2, 90, "70,30"
302
+ energy_df_data, item_df_data = [], []
303
 
304
+ if ruleset_file:
305
+ ruleset = load_ruleset_config(ruleset_file)
306
+ max_hist, inc_diff, energy_chance = ruleset.MaxHistoryOrders, ruleset.IncrementDifficulty, ruleset.OverallChanceToDropExpeditionEnergy
307
+ req_weights = ",".join(map(str, ruleset.OverrideMaxOrdersWithWeight.Weights))
308
+ for rg in ruleset.EnergyRewards:
309
+ for r in rg.Rewards:
310
+ energy_df_data.append([rg.DifficultyScore, r.Amount])
311
+ for rg in ruleset.ItemRewards:
312
+ for r in rg.Rewards:
313
+ item_df_data.append([rg.DifficultyScore, r.Amount, r.MergeItemId, r.RewardWeight, r.ReductionFactor])
314
+
315
+ energy_df = pd.DataFrame(energy_df_data, columns=['DifficultyScore', 'Amount'])
316
+ item_df = pd.DataFrame(item_df_data, columns=['DifficultyScore', 'Amount', 'MergeItemId', 'RewardWeight', 'ReductionFactor'])
317
+
318
+ red_factor, inc_factor = 3, 5
319
+ if settings_file:
320
+ settings = load_settings_config(settings_file)
321
+ red_factor, inc_factor = settings.ReductionFactor, settings.IncreaseFactor
322
+ if not ruleset_file:
323
+ req_weights = ",".join(map(str, settings.DefaultRequirementWeights.Weights))
324
+
325
+ return chain_df, max_hist, inc_diff, energy_chance, req_weights, red_factor, inc_factor, energy_df, item_df
326
 
327
+ # ===============================================================
328
+ # 5. GRADIO UI
329
+ # ===============================================================
330
  with gr.Blocks(theme=gr.themes.Soft(), title="Инструмент Балансировки Merge-2") as demo:
331
  gr.Markdown("# Инструмент для Балансировки Игр Merge-2")
 
332
  with gr.Tabs():
 
333
  with gr.TabItem("Симуляция генератора заказов"):
 
334
  with gr.Row():
335
+ with gr.Column(scale=2):
336
+ gr.Markdown("### 1. Загрузите файлы (опционально)")
337
+ chain_files_upload = gr.File(label="Файлы цепочек (.asset)", file_count="multiple")
338
+ ruleset_upload = gr.File(label="Ruleset (.asset)")
339
+ settings_upload = gr.File(label="Settings (.asset)")
340
+
341
+ with gr.Accordion("Редактор Мердж-цепочек", open=False):
342
+ chain_editor_df = gr.DataFrame(headers=['ChainId', 'MergeItemId', 'RequirementWeight', 'RewardDifficulty'], datatype=['str', 'str', 'number', 'number'], label="Состав цепочек", interactive=True, row_count=(10, "dynamic"))
343
+
344
+ with gr.Accordion("Настройки генератора", open=True):
345
+ with gr.Row():
346
+ max_history_input = gr.Slider(1, 20, value=5, step=1, label="Макс. заказов в истории")
347
+ increment_diff_input = gr.Slider(0, 10, value=2, step=1, label="Инкремент сложности")
348
+ with gr.Row():
349
+ energy_chance_input = gr.Slider(0, 100, value=90, step=5, label="Шанс награды-энергии (%)")
350
+ req_weights_input = gr.Textbox(label="Веса требований (1, 2)", value="70, 30")
351
+ with gr.Row():
352
+ reduction_factor_input = gr.Number(label="Reduction Factor", value=3)
353
+ increase_factor_input = gr.Number(label="Increase Factor", value=5)
354
+
355
+ with gr.Accordion("Редактор Наград", open=False):
356
+ with gr.Row():
357
+ energy_rewards_df = gr.DataFrame(headers=['DifficultyScore', 'Amount'], datatype=['number', 'number'], label="Энергетические награды", interactive=True, col_count=(2, "fixed"), row_count=(5, "dynamic"))
358
+ item_rewards_df = gr.DataFrame(headers=['DifficultyScore', 'Amount', 'MergeItemId', 'RewardWeight', 'ReductionFactor'], datatype=['number', 'number', 'str', 'number', 'number'], label="Предметные награды", interactive=True, col_count=(5, "fixed"), row_count=(5, "dynamic"))
359
+
360
+ gr.Markdown("### 2. Запустите симуляцию")
361
  sim_iterations = gr.Slider(10, 1000, value=100, step=10, label="Количество итераций")
362
+ sim_initial_energy_input = gr.Number(value=10000, label="Начальное количество энергии")
363
  sim_run_button = gr.Button("Запустить симуляцию", variant="primary")
364
+
365
  with gr.Column(scale=3):
366
  gr.Markdown("### Результаты симуляции")
367
+ sim_results_df = gr.DataFrame(label="Данные по заказам", wrap=True)
368
  gr.Markdown("### Сводный отчет")
369
  sim_stats_report = gr.Textbox(label="Статистика", lines=10)
370
  with gr.Row():
371
  sim_download_csv = gr.File(label="Скачать CSV", visible=False, interactive=False)
372
  sim_download_report = gr.File(label="Скачать отчет", visible=False, interactive=False)
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  # --- Event Handlers ---
375
+ files_to_update_ui = [chain_files_upload, ruleset_upload, settings_upload]
376
+ ui_outputs_to_update = [chain_editor_df, max_history_input, increment_diff_input, energy_chance_input, req_weights_input, reduction_factor_input, increase_factor_input, energy_rewards_df, item_rewards_df]
377
+
378
+ for file_input in files_to_update_ui:
379
+ file_input.upload(update_ui_from_files, files_to_update_ui, ui_outputs_to_update)
380
+
381
  sim_run_button.click(
382
  fn=run_simulation_interface,
383
+ inputs=[chain_editor_df, energy_rewards_df, item_rewards_df, max_history_input, increment_diff_input, energy_chance_input, req_weights_input, reduction_factor_input, increase_factor_input, sim_iterations, sim_initial_energy_input],
384
  outputs=[sim_results_df, sim_stats_report, sim_download_csv, sim_download_report]
385
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
  if __name__ == "__main__":
388
  demo.launch()