aigurletov commited on
Commit
18cbfa9
·
1 Parent(s): ee7e579
Files changed (4) hide show
  1. README.md +168 -37
  2. app.py +372 -18
  3. config_manager.py +163 -0
  4. mcp_server.py +628 -0
README.md CHANGED
@@ -1,39 +1,170 @@
1
- ---
2
- title: MergeBalanceTools
3
- emoji: 🏢
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.36.2
8
- app_file: app.py
9
- pinned: false
10
- short_description: Набор инструментов для баланса мердж-2 игр.
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
14
-
15
-
16
- Задача сделать тулзу для удобной работы с балансом мердж-2 игры.
17
-
18
- 1) Одна из основных фичей тулзы это Симуляция работы Генератора Заказов на базе конфигов с его правилами и конфигов мердж-цепочек. На выходе мы получаем таблицу с данными по симуляции и небольшой текстовый файл с сводными данными.
19
-
20
- 2) В этой тулзе также можно менять параметры разных мердж конфигов - опять же если нужно протестировать значения. Значит важно чтобы мы могли загружать оригинальные файлы конфигов (.asset) и в интерфейсе удобно автоматически раскладывать их по типу и сразу исключая лишние данные (например ключи переводов и тд) - оставлять только важные параметры влияющие на баланс игры.
21
-
22
- 3) Расчет стоимости предметов мердж цепочки исходя из весов предметов требуемых для производства других уровней предметов цепочки выпадающих из спавнеров.
23
-
24
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- MergeOrderSimulationService - проверенный, рабочий в юнити симулятор генератора заказов который нам нужно переписать под новый инструмент.
27
-
28
- MergeChainCostCalculator - инструмент для расчета стоимости в энергии предметов мердж цепочек. Скрипт пока не работает должным образом. Хорошо если мы его починим для нашего инструмента.
29
-
30
-
31
-
32
- Далее.
33
-
34
- Наша задача взять все эти данные, сделать анализ конфигов для импорта, забрать наработки, сделать удобный интерфейс и произвести стабильный расширяемый при необходимости инструмент для балансировки игровых механик.
35
-
36
-
37
-
38
- Я хочу, чтобы инструмент был сделан реализован на библиотеке и компонентах gradio.app на python.
39
 
 
 
1
+ # 🎮 Merge Balance Tools - Симулятор генерации заказов
2
+
3
+ Профессиональный инструмент для балансировки экономики merge-игр с поддержкой автоматизации через MCP протокол.
4
+
5
+ ## ✨ Основные возможности
6
+
7
+ - 🔄 **Симуляция генерации заказов** с настраиваемыми параметрами сложности
8
+ - 📊 **Детальная аналитика** игрового баланса через статистические отчеты
9
+ - 💾 **Система конфигураций** для сохранения и повторного использования настроек
10
+ - 🤖 **MCP API интеграция** для работы с ИИ агентами и автоматизации
11
+ - 📁 **Импорт Unity Asset файлов** для загрузки существующих конфигураций
12
+ - 📈 **Экспорт результатов** в CSV формате для дальнейшего анализа
13
+
14
+ ## 🚀 Быстрый старт
15
+
16
+ ### Требования
17
+ - Python 3.8+
18
+ - pip
19
+
20
+ ### Установка
21
+ ```bash
22
+ git clone <repository_url>
23
+ cd MergeBalanceTools
24
+ pip install gradio pandas pyyaml
25
+ ```
26
+
27
+ ### Запуск
28
+ ```bash
29
+ python app.py
30
+ ```
31
+ Откройте браузер по адресу `http://localhost:7860`
32
+
33
+ ## 📋 Использование
34
+
35
+ ### Базовый workflow
36
+ 1. **Загрузите файлы конфигурации** (Unity .asset файлы) или создайте конфигурацию вручную
37
+ 2. **Настройте параметры** генератора заказов в соответствующих секциях
38
+ 3. **Запустите симуляцию** с нужным количеством итераций
39
+ 4. **Проанализируйте результаты** в таблице данных и сводном отчете
40
+ 5. **Сохраните конфигурацию** для повторного использования
41
+
42
+ ### Ключевые параметры
43
+ - **Макс. заказов в истории**: Влияет на разнообразие генерируемых требований
44
+ - **Инкремент сложности**: Скорость роста сложности заказов
45
+ - **Шанс награды-энергии**: Баланс между энергетическими и предметными наградами
46
+ - **Веса требований**: Распределение вероятностей для заказов с 1-2 требованиями
47
+
48
+ ## 🏗️ Архитектура проекта
49
+
50
+ ```
51
+ MergeBalanceTools/
52
+ ├── app.py # Основное Gradio приложение
53
+ ├── config_manager.py # Система управления конфигурациями
54
+ ├── mcp_server.py # MCP сервер для ИИ агентов
55
+ ├── configs/ # Автоматически создаваемая папка для конфигураций
56
+ ├── Main_merge_item_chains/ # Примеры файлов цепочек Unity
57
+ ├── MergeItems/ # Примеры merge-предметов
58
+ └── README.md # Документация проекта
59
+ ```
60
+
61
+ ## 🤖 MCP API для ИИ агентов
62
+
63
+ Проект предоставляет полнофункциональный MCP (Model Context Protocol) сервер для интеграции с ИИ агентами:
64
+
65
+ ### Основные функции API
66
+ - `mcp_save_simulator_config` - Сохранение конфигураций
67
+ - `mcp_load_simulator_config` - Загрузка конфигураций
68
+ - `mcp_run_simulation` - Запуск симуляций
69
+ - `mcp_get_simulation_results` - Получение результатов
70
+ - `mcp_analyze_simulation_results` - Анализ данных
71
+
72
+ ### Пример использования
73
+ ```json
74
+ {
75
+ "function": "mcp_run_simulation",
76
+ "params": {
77
+ "config_name_or_data": "test_balance_v1",
78
+ "iteration_count": 200,
79
+ "initial_energy": 10000,
80
+ "return_detailed_results": true
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## 📊 Форматы данных
86
+
87
+ ### Структура цепочки merge-предметов
88
+ ```json
89
+ {
90
+ "ChainId": "flowers_chain",
91
+ "MergeItemId": "flower_seed_1",
92
+ "RequirementWeight": 100,
93
+ "RewardDifficulty": 15
94
+ }
95
+ ```
96
+
97
+ ### Конфигурация наград
98
+ ```json
99
+ {
100
+ "DifficultyScore": 500,
101
+ "Amount": 3,
102
+ "MergeItemId": "energy_boost",
103
+ "RewardWeight": 80,
104
+ "ReductionFactor": 5
105
+ }
106
+ ```
107
+
108
+ ## 🔧 Расширенные возможности
109
+
110
+ ### Импорт Unity Asset файлов
111
+ Система поддерживает прямой импорт файлов конфигурации из Unity:
112
+ - Файлы цепочек merge-предметов (`.asset`)
113
+ - Ruleset файлы с правилами генерации (`.asset`)
114
+ - Settings файлы с общими настройками (`.asset`)
115
+
116
+ ### Система конфигураций
117
+ - Автоматическое сохранение в JSON формате
118
+ - Версионность конфигураций с временными метками
119
+ - Валидация данных при загрузке
120
+ - Удобный интерфейс управления
121
+
122
+ ### Экспорт и анализ
123
+ - Экспорт результатов симуляции в CSV
124
+ - Генерация текстовых отчетов со статистикой
125
+ - Поддержка различных форматов анализа через MCP API
126
+
127
+ ## 🛠️ Техническая информация
128
+
129
+ ### Зависимости
130
+ - `gradio` - Веб-интерфейс приложения
131
+ - `pandas` - Обработка и анализ данных
132
+ - `pyyaml` - Парсинг Unity Asset файлов
133
+ - `dataclasses` - Типизированные структуры данных
134
+
135
+ ### Совместимость
136
+ - Python 3.8+
137
+ - Кроссплатформенность (Windows, macOS, Linux)
138
+ - Веб-браузер с поддержкой JavaScript
139
+
140
+ ## 📚 Документация
141
+
142
+ Полная документация доступна в интерфейсе приложения во вкладке "MCP API для ИИ агентов" или в отдельном файле документации.
143
+
144
+ ### Основные разделы документации:
145
+ - Быстрый старт и установка
146
+ - Детальное описание интерфейса
147
+ - Примеры использования
148
+ - API Reference для MCP функций
149
+ - Устранение типичных проблем
150
+
151
+ ## 🤝 Поддержка и развитие
152
+
153
+ ### Типичные проблемы
154
+ - **Ошибки парсинга .asset файлов**: Проверьте кодировку UTF-8 и структуру YAML
155
+ - **Преждевременное завершение симуляции**: Увеличьте начальную энергию или снизьте стоимость предметов
156
+ - **MCP функции недоступны**: Убедитесь в наличии всех файлов проекта
157
+
158
+ ### Roadmap
159
+ - [ ] Поддержка дополнительных форматов экспорта
160
+ - [ ] Расширенная аналитика с визуализацией
161
+ - [ ] Интеграция с системами CI/CD для автоматического тестирования баланса
162
+ - [ ] Поддержка распределенных симуляций
163
+
164
+ ## 📄 Лицензия
165
+
166
+ Проект разработан для внутреннего использования в целях балансировки игр.
167
 
168
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ **Разработано для профессионального использования в геймдеве** 🎯
app.py CHANGED
@@ -80,7 +80,14 @@ def robust_asset_parser(file_content: str) -> dict:
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 {}
@@ -102,24 +109,48 @@ def parse_rewards_from_data(reward_list_data, reward_type) -> List[GenerationRew
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))
@@ -127,13 +158,21 @@ def parse_rewards_from_data(reward_list_data, reward_type) -> List[GenerationRew
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'),
@@ -170,12 +209,14 @@ def get_requirement_count(ruleset, settings):
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:
@@ -200,19 +241,23 @@ def run_simulation_logic(chains, ruleset, settings, iteration_count, initial_ene
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
 
@@ -224,7 +269,13 @@ def run_simulation_logic(chains, ruleset, settings, iteration_count, initial_ene
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
@@ -237,6 +288,301 @@ def run_simulation_logic(chains, ruleset, settings, iteration_count, initial_ene
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',
@@ -302,15 +648,23 @@ def update_ui_from_files(chain_files, ruleset_file, settings_file):
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'])
 
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
+ parsed_data = yaml.safe_load(yaml_string) or {}
84
+ # FIX: Clean binary data fields
85
+ for key, value in parsed_data.items():
86
+ if isinstance(value, dict):
87
+ for sub_key, sub_value in value.items():
88
+ if isinstance(sub_value, str) and len(sub_value) == 16 and all(c in '0123456789abcdef' for c in sub_value.lower()):
89
+ parsed_data[key][sub_key] = "70,30" # Default fallback for binary weight data
90
+ return parsed_data
91
  except Exception as e:
92
  print(f"Asset parsing error: {e}")
93
  return {}
 
109
  if not isinstance(reward_list_data, list): return rewards_with_difficulty
110
 
111
  for reward_group in reward_list_data:
112
+ # FIX: Safe conversion for DifficultyScore
113
+ try:
114
+ difficulty = int(reward_group.get('DifficultyScore', 0))
115
+ except (ValueError, TypeError):
116
+ difficulty = 0
117
+
118
  rewards = []
119
 
120
  if reward_type == "Energy":
121
  if 'Reward' in reward_group and reward_group['Reward']:
122
  reward_info = reward_group['Reward']
123
+ # FIX: Safe conversion for amount
124
+ try:
125
+ amount = int(reward_info.get('_amount', 0))
126
+ except (ValueError, TypeError):
127
+ amount = 0
128
+ rewards.append(GenerationReward(Amount=amount, Type='Energy'))
129
  elif reward_type == "Item":
130
  if 'Rewards' in reward_group and isinstance(reward_group['Rewards'], list):
131
  for weighted_reward in reward_group['Rewards']:
132
  if 'Reward' in weighted_reward and weighted_reward['Reward']:
133
  reward_info = weighted_reward['Reward']
134
+ # FIX: Safe conversions for all numeric fields
135
+ try:
136
+ amount = int(reward_info.get('_amount', 1))
137
+ except (ValueError, TypeError):
138
+ amount = 1
139
+ try:
140
+ reward_weight = int(weighted_reward.get('RewardWeight', 100))
141
+ except (ValueError, TypeError):
142
+ reward_weight = 100
143
+ try:
144
+ reduction_factor = int(weighted_reward.get('ReductionFactor', 0))
145
+ except (ValueError, TypeError):
146
+ reduction_factor = 0
147
+
148
  rewards.append(GenerationReward(
149
+ Amount=amount,
150
  MergeItemId=reward_info.get('_mergeItemId', ''),
151
  Type='Item',
152
+ RewardWeight=reward_weight,
153
+ ReductionFactor=reduction_factor
154
  ))
155
  if rewards:
156
  rewards_with_difficulty.append(GenerationRewardWithDifficulty(DifficultyScore=difficulty, Rewards=rewards))
 
158
  return rewards_with_difficulty
159
 
160
  def load_ruleset_config(file) -> MergeGeneratorRuleset:
161
+ # DEBUG_LOG: Asset parsing initiation
162
  print("--- Loading ruleset config ---")
163
  with open(file.name, 'r', encoding='utf-8') as f: content = f.read()
164
  data = robust_asset_parser(content)
165
  print(f"Parsed raw data from {file.name}")
166
 
167
+ # ERROR_FIX: Binary weight data handling + hex validation
168
  raw_weights = data.get('_overrideMaxOrdersWithWeight', {}).get('_requirementOrderWeights', "70,30")
169
+ if isinstance(raw_weights, str) and ',' in raw_weights:
170
+ try:
171
+ weights_list = [int(w.strip()) for w in raw_weights.split(',')]
172
+ except ValueError:
173
+ weights_list = [70, 30] # FALLBACK_DEFAULT
174
+ else:
175
+ weights_list = [70, 30] # FALLBACK_BINARY_DATA
176
 
177
  ruleset = MergeGeneratorRuleset(
178
  Id=data.get('_id'),
 
209
 
210
  def generate_rewards(order, ruleset):
211
  if random.randint(1, 100) <= ruleset.OverallChanceToDropExpeditionEnergy:
212
+ # FIX: Handle NaN values in DifficultyScore
213
+ suitable_reward_groups = [rg for rg in ruleset.EnergyRewards if pd.notna(rg.DifficultyScore) and order.TotalDifficulty >= int(rg.DifficultyScore)]
214
  if suitable_reward_groups:
215
  chosen_group = max(suitable_reward_groups, key=lambda rg: int(rg.DifficultyScore))
216
  if chosen_group.Rewards: order.Rewards.append(chosen_group.Rewards[0])
217
  else:
218
+ # FIX: Handle NaN values in DifficultyScore
219
+ suitable_reward_groups = [rg for rg in ruleset.ItemRewards if pd.notna(rg.DifficultyScore) and order.TotalDifficulty >= int(rg.DifficultyScore)]
220
  if suitable_reward_groups:
221
  chosen_group = max(suitable_reward_groups, key=lambda rg: int(rg.DifficultyScore))
222
  if chosen_group.Rewards:
 
241
 
242
  for _ in range(req_count):
243
  selectable_items = [item for item in final_items if item not in used_items_in_order]
244
+ if not selectable_items:
245
+ break
246
 
247
  weights = [ruleset.OverrideWeights.get(item.MergeItemId, item.RequirementWeight) for item in selectable_items]
248
+ if sum(weights) == 0:
249
+ continue
250
 
251
  selected_item = random.choices(selectable_items, weights=weights, k=1)[0]
252
  order.Requirements.append(selected_item)
253
  order.TotalDifficulty += selected_item.RewardDifficulty
254
  used_items_in_order.append(selected_item)
255
 
256
+ if not order.Requirements:
257
+ continue
258
 
259
+ total_cost = sum(2**(chain.Items.index(req)) for req in order.Requirements
260
+ if (chain := next((c for c in chains if req in c.Items), None)))
261
  order.MergeEnergyPrice = total_cost
262
  current_energy -= total_cost
263
 
 
269
  if chain_of_item and (current_level := chain_of_item.Items.index(req) + 1) == chain_unlock_levels.get(chain_of_item.Id, 1):
270
  chain_unlock_levels[chain_of_item.Id] += 1
271
 
272
+ row = {
273
+ "Order": i + 1,
274
+ "Total_Difficulty": order.TotalDifficulty,
275
+ "MergeEnergyPrice": order.MergeEnergyPrice,
276
+ "MEnergy_Amount": current_energy
277
+ }
278
+
279
  for j, req in enumerate(order.Requirements):
280
  chain_of_item = next((c for c in chains if req in c.Items), None)
281
  row[f'Requirement_{j+1}'] = req.MergeItemId
 
288
  row['MergeItemReward'] = next((r.MergeItemId for r in order.Rewards if r.Type == 'Item'), "")
289
  simulation_results.append(row)
290
 
291
+ df = pd.DataFrame(simulation_results).fillna(0)
292
+ full_column_list = [
293
+ 'Order', 'MergeEnergyPrice', 'MEnergy_Amount', 'Total_Difficulty', 'ExpeditionEnergyReward', 'MergeItemReward',
294
+ 'Requirement_1', 'Weight_1', 'ChainId_1', 'Level_1', 'RewardDifficulty_1',
295
+ 'Requirement_2', 'Weight_2', 'ChainId_2', 'Level_2', 'RewardDifficulty_2'
296
+ ]
297
+
298
+ for col in full_column_list:
299
+ if col not in df.columns:
300
+ df[col] = 0
301
+
302
+ stats_report = f"SIMULATION_RESULT: ORDERS={len(df)} AVG_DIFFICULTY={df['Total_Difficulty'].mean():.2f} FINAL_ENERGY={current_energy}"
303
+ return df[full_column_list], stats_report
304
+
305
+ def run_simulation_interface(
306
+ chain_df, energy_rewards_df, item_rewards_df,
307
+ max_hist, inc_diff, energy_chance, req_weights_str,
308
+ red_factor, inc_factor, iteration_count, initial_energy
309
+ ):
310
+ """
311
+ INTERFACE_WRAPPER: SIMULATION_EXECUTION
312
+ """
313
+ if chain_df is None or chain_df.empty:
314
+ raise gr.Error("CHAIN_DATA: EMPTY → LOAD_REQUIRED")
315
+
316
+ chains = [MergeChain(Id=chain_id, Items=[
317
+ MergeChainItemData(row['MergeItemId'], int(row['RequirementWeight']), int(row['RewardDifficulty']))
318
+ for _, row in group.iterrows()
319
+ ]) for chain_id, group in chain_df.groupby('ChainId')]
320
+
321
+ settings = GeneratorSettings(
322
+ ReductionFactor=red_factor,
323
+ IncreaseFactor=inc_factor,
324
+ DefaultRequirementWeights=RequirementWeight(Weights=[int(w.strip()) for w in req_weights_str.split(',')])
325
+ )
326
+
327
+ ruleset = MergeGeneratorRuleset(
328
+ MaxHistoryOrders=max_hist,
329
+ IncrementDifficulty=inc_diff,
330
+ OverallChanceToDropExpeditionEnergy=energy_chance
331
+ )
332
+
333
+ if energy_rewards_df is not None and not energy_rewards_df.empty:
334
+ df_copy = energy_rewards_df.dropna().copy()
335
+ df_copy['DifficultyScore'] = pd.to_numeric(df_copy['DifficultyScore'])
336
+ df_copy['Amount'] = pd.to_numeric(df_copy['Amount'])
337
+ ruleset.EnergyRewards = [GenerationRewardWithDifficulty(r['DifficultyScore'], [GenerationReward(Amount=r['Amount'], Type='Energy')])
338
+ for i, r in df_copy.iterrows()]
339
+
340
+ if item_rewards_df is not None and not item_rewards_df.empty:
341
+ df_copy = item_rewards_df.dropna().copy()
342
+ df_copy['DifficultyScore'] = pd.to_numeric(df_copy['DifficultyScore'])
343
+ df_copy['Amount'] = pd.to_numeric(df_copy['Amount'])
344
+ df_copy['RewardWeight'] = pd.to_numeric(df_copy['RewardWeight'])
345
+ df_copy['ReductionFactor'] = pd.to_numeric(df_copy['ReductionFactor'])
346
+
347
+ for score, group in df_copy.groupby('DifficultyScore'):
348
+ rewards = [GenerationReward(
349
+ Amount=r['Amount'],
350
+ MergeItemId=r['MergeItemId'],
351
+ Type='Item',
352
+ RewardWeight=r['RewardWeight'],
353
+ ReductionFactor=r['ReductionFactor']
354
+ ) for i, r in group.iterrows()]
355
+ ruleset.ItemRewards.append(GenerationRewardWithDifficulty(int(score), rewards))
356
+
357
+ df, stats_report = run_simulation_logic(chains, ruleset, settings, iteration_count, initial_energy)
358
+
359
+ with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.csv', encoding='utf-8', newline='') as tmp_csv:
360
+ df.to_csv(tmp_csv.name, index=False)
361
+ csv_path = tmp_csv.name
362
+
363
+ with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.txt', encoding='utf-8') as tmp_txt:
364
+ tmp_txt.write(stats_report)
365
+ report_path = tmp_txt.name
366
+
367
+ return df, stats_report, gr.update(value=csv_path, visible=True), gr.update(value=report_path, visible=True)
368
+
369
+ # ===============================================================
370
+ # 5. UI_UPDATE_FUNCTIONS
371
+ # ===============================================================
372
+ def update_ui_from_files(chain_files, ruleset_file, settings_file):
373
+ """
374
+ ERROR_FIX: ENHANCED_FILE_PROCESSING + REWARD_EXTRACTION
375
+ """
376
+ chain_data = []
377
+ if chain_files:
378
+ for file in chain_files:
379
+ chain = load_chain_config(file)
380
+ for item in chain.Items:
381
+ chain_data.append([chain.Id, item.MergeItemId, item.RequirementWeight, item.RewardDifficulty])
382
+
383
+ chain_df = pd.DataFrame(chain_data, columns=['ChainId', 'MergeItemId', 'RequirementWeight', 'RewardDifficulty'])
384
+
385
+ max_hist, inc_diff, energy_chance, req_weights = 5, 2, 90, "70,30"
386
+ energy_df_data, item_df_data = [], []
387
+
388
+ if ruleset_file:
389
+ ruleset = load_ruleset_config(ruleset_file)
390
+ max_hist, inc_diff, energy_chance = ruleset.MaxHistoryOrders, ruleset.IncrementDifficulty, ruleset.OverallChanceToDropExpeditionEnergy
391
+ req_weights = ",".join(map(str, ruleset.OverrideMaxOrdersWithWeight.Weights))
392
+
393
+ # ERROR_FIX: REWARD_DATA_EXTRACTION_WITH_NaN_VALIDATION
394
+ for rg in ruleset.EnergyRewards:
395
+ if pd.notna(rg.DifficultyScore):
396
+ for r in rg.Rewards:
397
+ energy_df_data.append([rg.DifficultyScore, r.Amount])
398
+
399
+ for rg in ruleset.ItemRewards:
400
+ if pd.notna(rg.DifficultyScore):
401
+ for r in rg.Rewards:
402
+ item_df_data.append([rg.DifficultyScore, r.Amount, r.MergeItemId, r.RewardWeight, r.ReductionFactor])
403
+
404
+ print(f"REWARD_EXTRACTION: ENERGY={len(energy_df_data)} ITEM={len(item_df_data)}")
405
+
406
+ energy_df = pd.DataFrame(energy_df_data, columns=['DifficultyScore', 'Amount'])
407
+ item_df = pd.DataFrame(item_df_data, columns=['DifficultyScore', 'Amount', 'MergeItemId', 'RewardWeight', 'ReductionFactor'])
408
+
409
+ red_factor, inc_factor = 3, 5
410
+ if settings_file:
411
+ settings = load_settings_config(settings_file)
412
+ red_factor, inc_factor = settings.ReductionFactor, settings.IncreaseFactor
413
+ if not ruleset_file:
414
+ req_weights = ",".join(map(str, settings.DefaultRequirementWeights.Weights))
415
+
416
+ return chain_df, max_hist, inc_diff, energy_chance, req_weights, red_factor, inc_factor, energy_df, item_df
417
+
418
+ # ===============================================================
419
+ # 6. CONFIG_MANAGEMENT_FUNCTIONS
420
+ # ===============================================================
421
+ def save_current_config(
422
+ config_name, config_description,
423
+ chain_df, energy_rewards_df, item_rewards_df,
424
+ max_history, increment_diff, energy_chance, req_weights,
425
+ reduction_factor, increase_factor, iteration_count, initial_energy
426
+ ):
427
+ """
428
+ CONFIG_SAVE: PERSISTENCE_INTERFACE
429
+ """
430
+ if not CONFIG_MANAGER_AVAILABLE:
431
+ return "CONFIG_MANAGER: UNAVAILABLE", gr.update()
432
+
433
+ try:
434
+ if not config_name or not config_name.strip():
435
+ return "CONFIG_NAME: EMPTY → INPUT_REQUIRED", gr.update()
436
+
437
+ if not config_description or not config_description.strip():
438
+ config_description = f"AUTO_DESCRIPTION: {config_name}"
439
+
440
+ config_manager = ConfigManager()
441
+ config = create_config_from_ui_state(
442
+ config_name.strip(),
443
+ config_description.strip(),
444
+ chain_df, energy_rewards_df, item_rewards_df,
445
+ max_history, increment_diff, energy_chance, req_weights,
446
+ reduction_factor, increase_factor, iteration_count, initial_energy
447
+ )
448
+
449
+ filepath = config_manager.save_config(config)
450
+ configs = config_manager.list_configs()
451
+ config_choices = [f"{c['name']} - {c['description'][:50]}..." if len(c['description']) > 50 else f"{c['name']} - {c['description']}" for c in configs]
452
+
453
+ return f"CONFIG_SAVED: {config_name} → {filepath}", gr.update(choices=config_choices, value=None)
454
+
455
+ except Exception as e:
456
+ return f"CONFIG_SAVE: ERROR → {str(e)}", gr.update()
457
+
458
+ def load_selected_config(selected_config):
459
+ """
460
+ CONFIG_LOAD: RESTORATION_INTERFACE
461
+ """
462
+ if not CONFIG_MANAGER_AVAILABLE:
463
+ return [None] * 11 + ["CONFIG_MANAGER: UNAVAILABLE"]
464
+
465
+ try:
466
+ if not selected_config:
467
+ return [None] * 11 + ["CONFIG_SELECTION: EMPTY"]
468
+
469
+ config_name = selected_config.split(" - ")[0]
470
+ config_manager = ConfigManager()
471
+ configs = config_manager.list_configs()
472
+ matching_config = next((c for c in configs if c['name'] == config_name), None)
473
+
474
+ if not matching_config:
475
+ return [None] * 11 + [f"CONFIG_NOT_FOUND: {config_name}"]
476
+
477
+ config = config_manager.load_config(matching_config['filepath'])
478
+ result = apply_config_to_ui(config)
479
+
480
+ return list(result) + [f"CONFIG_LOADED: {config.name}"]
481
+
482
+ except Exception as e:
483
+ return [None] * 11 + [f"CONFIG_LOAD: ERROR → {str(e)}"]
484
+
485
+ def delete_selected_config(selected_config):
486
+ """
487
+ CONFIG_DELETE: REMOVAL_INTERFACE
488
+ """
489
+ if not CONFIG_MANAGER_AVAILABLE:
490
+ return "CONFIG_MANAGER: UNAVAILABLE", gr.update()
491
+
492
+ try:
493
+ if not selected_config:
494
+ return "CONFIG_SELECTION: EMPTY", gr.update()
495
+
496
+ config_name = selected_config.split(" - ")[0]
497
+ config_manager = ConfigManager()
498
+ configs = config_manager.list_configs()
499
+ matching_config = next((c for c in configs if c['name'] == config_name), None)
500
+
501
+ if not matching_config:
502
+ return f"CONFIG_NOT_FOUND: {config_name}", gr.update()
503
+
504
+ success = config_manager.delete_config(matching_config['filepath'])
505
+
506
+ if success:
507
+ configs = config_manager.list_configs()
508
+ config_choices = [f"{c['name']} - {c['description'][:50]}..." if len(c['description']) > 50 else f"{c['name']} - {c['description']}" for c in configs]
509
+ return f"CONFIG_DELETED: {config_name}", gr.update(choices=config_choices, value=None)
510
+ else:
511
+ return f"CONFIG_DELETE: FAILED → {config_name}", gr.update()
512
+
513
+ except Exception as e:
514
+ return f"CONFIG_DELETE: ERROR → {str(e)}", gr.update()
515
+
516
+ def refresh_config_list():
517
+ """
518
+ CONFIG_LIST: REFRESH_INTERFACE
519
+ """
520
+ if not CONFIG_MANAGER_AVAILABLE:
521
+ return gr.update()
522
+
523
+ try:
524
+ config_manager = ConfigManager()
525
+ configs = config_manager.list_configs()
526
+ config_choices = [f"{c['name']} - {c['description'][:50]}..." if len(c['description']) > 50 else f"{c['name']} - {c['description']}" for c in configs]
527
+ return gr.update(choices=config_choices, value=None)
528
+ except Exception as e:
529
+ print(f"CONFIG_REFRESH: ERROR → {e}")
530
+ return gr.update()
531
+
532
+ # ===============================================================
533
+ # 7. MCP_API_FUNCTIONS
534
+ # ===============================================================
535
+ def mcp_api_call(function_name: str, **kwargs):
536
+ """
537
+ MCP_INTERFACE: UNIVERSAL_FUNCTION_DISPATCHER
538
+ """
539
+ if not MCP_SERVER_AVAILABLE:
540
+ return {"success": False, "error": "MCP_SERVER: UNAVAILABLE"}
541
+
542
+ try:
543
+ if function_name not in MCP_FUNCTIONS:
544
+ return {
545
+ "success": False,
546
+ "error": f"MCP_FUNCTION: NOT_FOUND → {function_name}",
547
+ "available_functions": list(MCP_FUNCTIONS.keys())
548
+ }
549
+
550
+ # MCP_FUNCTION: EXECUTE
551
+ result = MCP_FUNCTIONS[function_name](**kwargs)
552
+ return result
553
+
554
+ except Exception as e:
555
+ return {
556
+ "success": False,
557
+ "error": str(e),
558
+ "message": f"MCP_CALL: ERROR → {function_name}: {str(e)}"
559
+ }
560
+
561
+ def handle_mcp_call(function_name, params_json):
562
+ """
563
+ MCP_TEST_INTERFACE: JSON_PARAMETER_HANDLER
564
+ """
565
+ try:
566
+ if not function_name:
567
+ return {"error": "MCP_FUNCTION: SELECTION_REQUIRED"}
568
+
569
+ # JSON_PARAMS: PARSE
570
+ if params_json and params_json.strip():
571
+ try:
572
+ params = json.loads(params_json)
573
+ except json.JSONDecodeError as e:
574
+ return {"error": f"JSON_PARSE: INVALID → {str(e)}"}
575
+ else:
576
+ params = {}
577
+
578
+ # MCP_FUNCTION: CALL
579
+ result = mcp_api_call(function_name, **params)
580
+ return result
581
+
582
+ except Exception as e:
583
+ return {"error": f"MCP_HANDLER: ERROR → {str(e)}"}
584
+ # This section was duplicated and should be removed as it already exists in the run_simulation_logic function
585
+
586
  df = pd.DataFrame(simulation_results).fillna(0)
587
  full_column_list = ['Order', 'MergeEnergyPrice', 'MEnergy_Amount', 'Total_Difficulty', 'ExpeditionEnergyReward', 'MergeItemReward',
588
  'Requirement_1', 'Weight_1', 'ChainId_1', 'Level_1', 'RewardDifficulty_1',
 
648
  energy_df_data, item_df_data = [], []
649
 
650
  if ruleset_file:
651
+ # ERROR_FIX: Enhanced ruleset parsing with validation
652
  ruleset = load_ruleset_config(ruleset_file)
653
  max_hist, inc_diff, energy_chance = ruleset.MaxHistoryOrders, ruleset.IncrementDifficulty, ruleset.OverallChanceToDropExpeditionEnergy
654
  req_weights = ",".join(map(str, ruleset.OverrideMaxOrdersWithWeight.Weights))
655
+
656
+ # ERROR_FIX: Extract rewards data with NaN validation
657
  for rg in ruleset.EnergyRewards:
658
+ if pd.notna(rg.DifficultyScore):
659
+ for r in rg.Rewards:
660
+ energy_df_data.append([rg.DifficultyScore, r.Amount])
661
  for rg in ruleset.ItemRewards:
662
+ if pd.notna(rg.DifficultyScore):
663
+ for r in rg.Rewards:
664
+ item_df_data.append([rg.DifficultyScore, r.Amount, r.MergeItemId, r.RewardWeight, r.ReductionFactor])
665
+
666
+ # DEBUG_LOG: Reward extraction statistics
667
+ print(f"LOADED: {len(energy_df_data)} energy_rewards, {len(item_df_data)} item_rewards")
668
 
669
  energy_df = pd.DataFrame(energy_df_data, columns=['DifficultyScore', 'Amount'])
670
  item_df = pd.DataFrame(item_df_data, columns=['DifficultyScore', 'Amount', 'MergeItemId', 'RewardWeight', 'ReductionFactor'])
config_manager.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from datetime import datetime
4
+ from typing import Dict, List, Any, Optional
5
+ from dataclasses import asdict, dataclass
6
+ import pandas as pd
7
+
8
+ # ===============================================================
9
+ # CONFIG MANAGEMENT SYSTEM
10
+ # ===============================================================
11
+
12
+ @dataclass
13
+ class SimulatorConfig:
14
+ """Конфигурация симулятора для сохранения и загрузки"""
15
+ name: str
16
+ description: str
17
+ created_at: str
18
+
19
+ # Настройки генератора
20
+ max_history_orders: int = 5
21
+ increment_difficulty: int = 2
22
+ energy_chance: int = 90
23
+ requirement_weights: str = "70,30"
24
+ reduction_factor: int = 3
25
+ increase_factor: int = 5
26
+
27
+ # Симуляция
28
+ iteration_count: int = 100
29
+ initial_energy: int = 10000
30
+
31
+ # Данные цепочек (JSON сериализованные)
32
+ chain_data: List[Dict[str, Any]] = None
33
+ energy_rewards_data: List[Dict[str, Any]] = None
34
+ item_rewards_data: List[Dict[str, Any]] = None
35
+
36
+ def to_dict(self) -> Dict[str, Any]:
37
+ return asdict(self)
38
+
39
+ @classmethod
40
+ def from_dict(cls, data: Dict[str, Any]) -> 'SimulatorConfig':
41
+ return cls(**data)
42
+
43
+ class ConfigManager:
44
+ """Менеджер конфигураций симулятора"""
45
+
46
+ def __init__(self, config_dir: str = "configs"):
47
+ self.config_dir = config_dir
48
+ self.ensure_config_dir()
49
+
50
+ def ensure_config_dir(self):
51
+ """Создает директорию для конфигураций если её нет"""
52
+ if not os.path.exists(self.config_dir):
53
+ os.makedirs(self.config_dir)
54
+
55
+ def save_config(self, config: SimulatorConfig) -> str:
56
+ """Сохраняет конфигурацию в файл"""
57
+ filename = f"{config.name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
58
+ filepath = os.path.join(self.config_dir, filename)
59
+
60
+ with open(filepath, 'w', encoding='utf-8') as f:
61
+ json.dump(config.to_dict(), f, indent=2, ensure_ascii=False)
62
+
63
+ return filepath
64
+
65
+ def load_config(self, filepath: str) -> SimulatorConfig:
66
+ """Загружает конфигурацию из файла"""
67
+ with open(filepath, 'r', encoding='utf-8') as f:
68
+ data = json.load(f)
69
+
70
+ return SimulatorConfig.from_dict(data)
71
+
72
+ def list_configs(self) -> List[Dict[str, str]]:
73
+ """Возвращает список доступных конфигураций"""
74
+ configs = []
75
+
76
+ if not os.path.exists(self.config_dir):
77
+ return configs
78
+
79
+ for filename in os.listdir(self.config_dir):
80
+ if filename.endswith('.json'):
81
+ filepath = os.path.join(self.config_dir, filename)
82
+ try:
83
+ config = self.load_config(filepath)
84
+ configs.append({
85
+ 'name': config.name,
86
+ 'description': config.description,
87
+ 'created_at': config.created_at,
88
+ 'filepath': filepath
89
+ })
90
+ except Exception as e:
91
+ print(f"Error loading config {filename}: {e}")
92
+
93
+ return sorted(configs, key=lambda x: x['created_at'], reverse=True)
94
+
95
+ def delete_config(self, filepath: str) -> bool:
96
+ """Удаляет конфигурацию"""
97
+ try:
98
+ os.remove(filepath)
99
+ return True
100
+ except Exception as e:
101
+ print(f"Error deleting config: {e}")
102
+ return False
103
+
104
+ def create_config_from_ui_state(
105
+ name: str,
106
+ description: str,
107
+ chain_df,
108
+ energy_rewards_df,
109
+ item_rewards_df,
110
+ max_history: int,
111
+ increment_diff: int,
112
+ energy_chance: int,
113
+ req_weights: str,
114
+ reduction_factor: int,
115
+ increase_factor: int,
116
+ iteration_count: int,
117
+ initial_energy: int
118
+ ) -> SimulatorConfig:
119
+ """Создает конфигурацию из состояния UI"""
120
+
121
+ # Преобразуем DataFrames в JSON-сериализуемые структуры
122
+ chain_data = chain_df.to_dict('records') if chain_df is not None and not chain_df.empty else []
123
+ energy_rewards_data = energy_rewards_df.to_dict('records') if energy_rewards_df is not None and not energy_rewards_df.empty else []
124
+ item_rewards_data = item_rewards_df.to_dict('records') if item_rewards_df is not None and not item_rewards_df.empty else []
125
+
126
+ return SimulatorConfig(
127
+ name=name,
128
+ description=description,
129
+ created_at=datetime.now().isoformat(),
130
+ max_history_orders=max_history,
131
+ increment_difficulty=increment_diff,
132
+ energy_chance=energy_chance,
133
+ requirement_weights=req_weights,
134
+ reduction_factor=reduction_factor,
135
+ increase_factor=increase_factor,
136
+ iteration_count=iteration_count,
137
+ initial_energy=initial_energy,
138
+ chain_data=chain_data,
139
+ energy_rewards_data=energy_rewards_data,
140
+ item_rewards_data=item_rewards_data
141
+ )
142
+
143
+ def apply_config_to_ui(config: SimulatorConfig):
144
+ """Применяет конфигурацию к состоянию UI"""
145
+
146
+ # Восстанавливаем DataFrames
147
+ chain_df = pd.DataFrame(config.chain_data) if config.chain_data else pd.DataFrame(columns=['ChainId', 'MergeItemId', 'RequirementWeight', 'RewardDifficulty'])
148
+ energy_rewards_df = pd.DataFrame(config.energy_rewards_data) if config.energy_rewards_data else pd.DataFrame(columns=['DifficultyScore', 'Amount'])
149
+ item_rewards_df = pd.DataFrame(config.item_rewards_data) if config.item_rewards_data else pd.DataFrame(columns=['DifficultyScore', 'Amount', 'MergeItemId', 'RewardWeight', 'ReductionFactor'])
150
+
151
+ return (
152
+ chain_df,
153
+ config.max_history_orders,
154
+ config.increment_difficulty,
155
+ config.energy_chance,
156
+ config.requirement_weights,
157
+ config.reduction_factor,
158
+ config.increase_factor,
159
+ energy_rewards_df,
160
+ item_rewards_df,
161
+ config.iteration_count,
162
+ config.initial_energy
163
+ )
mcp_server.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Server для интеграции симулятора заказов с ИИ агентами
3
+ Предоставляет инструменты для автоматизированной работы с симулятором
4
+ """
5
+
6
+ import json
7
+ import pandas as pd
8
+ from typing import Dict, List, Any, Optional, Union
9
+ from dataclasses import asdict
10
+ import tempfile
11
+ import os
12
+
13
+ from config_manager import ConfigManager, SimulatorConfig, create_config_from_ui_state, apply_config_to_ui
14
+
15
+ class MergeSimulatorMCPServer:
16
+ """MCP сервер для симулятора генерации заказов"""
17
+
18
+ def __init__(self):
19
+ self.config_manager = ConfigManager()
20
+ self.current_simulation_results = None
21
+ self.current_stats_report = ""
22
+
23
+ # ===============================================================
24
+ # CONFIG MANAGEMENT TOOLS
25
+ # ===============================================================
26
+
27
+ def mcp_save_simulator_config(
28
+ self,
29
+ name: str,
30
+ description: str,
31
+ chain_data: List[Dict[str, Any]] = None,
32
+ energy_rewards_data: List[Dict[str, Any]] = None,
33
+ item_rewards_data: List[Dict[str, Any]] = None,
34
+ max_history_orders: int = 5,
35
+ increment_difficulty: int = 2,
36
+ energy_chance: int = 90,
37
+ requirement_weights: str = "70,30",
38
+ reduction_factor: int = 3,
39
+ increase_factor: int = 5,
40
+ iteration_count: int = 100,
41
+ initial_energy: int = 10000
42
+ ) -> Dict[str, Any]:
43
+ """
44
+ Сохраняет конфигурацию симулятора для последующего использования
45
+
46
+ Args:
47
+ name: Название конфигурации
48
+ description: Описание конфигурации
49
+ chain_data: Данные цепочек merge-предметов
50
+ energy_rewards_data: Данные энергетических наград
51
+ item_rewards_data: Данные предметных наград
52
+ max_history_orders: Максимальное количество заказов в истории
53
+ increment_difficulty: Инкремент сложности
54
+ energy_chance: Шанс выпадения энергетической награды (%)
55
+ requirement_weights: Веса требований (строка, например "70,30")
56
+ reduction_factor: Фактор уменьшения
57
+ increase_factor: Фактор увеличения
58
+ iteration_count: Количество итераций симуляции
59
+ initial_energy: Начальное количество энергии
60
+
61
+ Returns:
62
+ Словарь с результатом операции и путем к сохраненному файлу
63
+ """
64
+ try:
65
+ config = SimulatorConfig(
66
+ name=name,
67
+ description=description,
68
+ created_at=pd.Timestamp.now().isoformat(),
69
+ max_history_orders=max_history_orders,
70
+ increment_difficulty=increment_difficulty,
71
+ energy_chance=energy_chance,
72
+ requirement_weights=requirement_weights,
73
+ reduction_factor=reduction_factor,
74
+ increase_factor=increase_factor,
75
+ iteration_count=iteration_count,
76
+ initial_energy=initial_energy,
77
+ chain_data=chain_data or [],
78
+ energy_rewards_data=energy_rewards_data or [],
79
+ item_rewards_data=item_rewards_data or []
80
+ )
81
+
82
+ filepath = self.config_manager.save_config(config)
83
+
84
+ return {
85
+ "success": True,
86
+ "message": f"Конфигурация '{name}' успешно сохранена",
87
+ "filepath": filepath,
88
+ "config_name": name
89
+ }
90
+ except Exception as e:
91
+ return {
92
+ "success": False,
93
+ "error": str(e),
94
+ "message": f"Ошибка при сохранении конфигурации: {str(e)}"
95
+ }
96
+
97
+ def mcp_load_simulator_config(self, config_name_or_path: str) -> Dict[str, Any]:
98
+ """
99
+ Загружает конфигурацию симулятора
100
+
101
+ Args:
102
+ config_name_or_path: Название конфигурации или путь к файлу
103
+
104
+ Returns:
105
+ Словарь с данными конфигурации
106
+ """
107
+ try:
108
+ # Если передан путь к файлу
109
+ if config_name_or_path.endswith('.json') and os.path.exists(config_name_or_path):
110
+ config = self.config_manager.load_config(config_name_or_path)
111
+ else:
112
+ # Ищем конфигурацию п�� имени
113
+ configs = self.config_manager.list_configs()
114
+ matching_config = next((c for c in configs if c['name'] == config_name_or_path), None)
115
+
116
+ if not matching_config:
117
+ return {
118
+ "success": False,
119
+ "error": f"Конфигурация '{config_name_or_path}' не найдена",
120
+ "available_configs": [c['name'] for c in configs]
121
+ }
122
+
123
+ config = self.config_manager.load_config(matching_config['filepath'])
124
+
125
+ return {
126
+ "success": True,
127
+ "config": config.to_dict(),
128
+ "message": f"Конфигурация '{config.name}' успешно загружена"
129
+ }
130
+ except Exception as e:
131
+ return {
132
+ "success": False,
133
+ "error": str(e),
134
+ "message": f"Ошибка при загрузке конфигурации: {str(e)}"
135
+ }
136
+
137
+ def mcp_list_simulator_configs(self) -> Dict[str, Any]:
138
+ """
139
+ Возвращает список доступных конфигураций симулятора
140
+
141
+ Returns:
142
+ Словарь со списком конфигураций
143
+ """
144
+ try:
145
+ configs = self.config_manager.list_configs()
146
+ return {
147
+ "success": True,
148
+ "configs": configs,
149
+ "count": len(configs),
150
+ "message": f"Найдено {len(configs)} конфигураций"
151
+ }
152
+ except Exception as e:
153
+ return {
154
+ "success": False,
155
+ "error": str(e),
156
+ "message": f"Ошибка при получении списка конфигураций: {str(e)}"
157
+ }
158
+
159
+ def mcp_delete_simulator_config(self, config_name_or_path: str) -> Dict[str, Any]:
160
+ """
161
+ Удаляет конфигурацию симулятора
162
+
163
+ Args:
164
+ config_name_or_path: Название конфигурации или путь к файлу
165
+
166
+ Returns:
167
+ Словарь с результатом операции
168
+ """
169
+ try:
170
+ # Если передан путь к файлу
171
+ if config_name_or_path.endswith('.json') and os.path.exists(config_name_or_path):
172
+ filepath = config_name_or_path
173
+ else:
174
+ # Ищем конфигурацию по имени
175
+ configs = self.config_manager.list_configs()
176
+ matching_config = next((c for c in configs if c['name'] == config_name_or_path), None)
177
+
178
+ if not matching_config:
179
+ return {
180
+ "success": False,
181
+ "error": f"Конфигурация '{config_name_or_path}' не найдена"
182
+ }
183
+
184
+ filepath = matching_config['filepath']
185
+
186
+ success = self.config_manager.delete_config(filepath)
187
+
188
+ if success:
189
+ return {
190
+ "success": True,
191
+ "message": f"Конфигурация успешно удалена"
192
+ }
193
+ else:
194
+ return {
195
+ "success": False,
196
+ "error": "Не удалось удалить конфигурацию"
197
+ }
198
+ except Exception as e:
199
+ return {
200
+ "success": False,
201
+ "error": str(e),
202
+ "message": f"Ошибка при удалении конфигурации: {str(e)}"
203
+ }
204
+
205
+ # ===============================================================
206
+ # SIMULATION TOOLS
207
+ # ===============================================================
208
+
209
+ def mcp_run_simulation(
210
+ self,
211
+ config_name_or_data: Union[str, Dict[str, Any]] = None,
212
+ chain_data: List[Dict[str, Any]] = None,
213
+ energy_rewards_data: List[Dict[str, Any]] = None,
214
+ item_rewards_data: List[Dict[str, Any]] = None,
215
+ max_history_orders: int = None,
216
+ increment_difficulty: int = None,
217
+ energy_chance: int = None,
218
+ requirement_weights: str = None,
219
+ reduction_factor: int = None,
220
+ increase_factor: int = None,
221
+ iteration_count: int = None,
222
+ initial_energy: int = None,
223
+ return_detailed_results: bool = True
224
+ ) -> Dict[str, Any]:
225
+ """
226
+ Запускает симуляцию генерации заказов
227
+
228
+ Args:
229
+ config_name_or_data: Название конфигурации или данные конфигурации
230
+ chain_data: Данные цепочек (переопределяет данные из конфигур��ции)
231
+ energy_rewards_data: Данные энергетических наград
232
+ item_rewards_data: Данные предметных наград
233
+ max_history_orders: Максимальное количество заказов в истории
234
+ increment_difficulty: Инкремент сложности
235
+ energy_chance: Шанс выпадения энергетической награды (%)
236
+ requirement_weights: Веса требований
237
+ reduction_factor: Фактор уменьшения
238
+ increase_factor: Фактор увеличения
239
+ iteration_count: Количество итераций симуляции
240
+ initial_energy: Начальное количество энергии
241
+ return_detailed_results: Возвращать ли детальные результаты
242
+
243
+ Returns:
244
+ Словарь с результатами симуляции
245
+ """
246
+ try:
247
+ # Импортируем функцию симуляции
248
+ from app import run_simulation_interface
249
+
250
+ # Загружаем конфигурацию если указано имя
251
+ if isinstance(config_name_or_data, str):
252
+ config_result = self.mcp_load_simulator_config(config_name_or_data)
253
+ if not config_result["success"]:
254
+ return config_result
255
+ config_data = config_result["config"]
256
+ elif isinstance(config_name_or_data, dict):
257
+ config_data = config_name_or_data
258
+ else:
259
+ config_data = {}
260
+
261
+ # Применяем параметры (переданные параметры имеют приоритет над конфигурацией)
262
+ params = {
263
+ 'max_history': max_history_orders or config_data.get('max_history_orders', 5),
264
+ 'increment_diff': increment_difficulty or config_data.get('increment_difficulty', 2),
265
+ 'energy_chance': energy_chance or config_data.get('energy_chance', 90),
266
+ 'req_weights': requirement_weights or config_data.get('requirement_weights', "70,30"),
267
+ 'reduction_factor': reduction_factor or config_data.get('reduction_factor', 3),
268
+ 'increase_factor': increase_factor or config_data.get('increase_factor', 5),
269
+ 'iteration_count': iteration_count or config_data.get('iteration_count', 100),
270
+ 'initial_energy': initial_energy or config_data.get('initial_energy', 10000)
271
+ }
272
+
273
+ # Подготавливаем данные
274
+ chain_df = pd.DataFrame(chain_data or config_data.get('chain_data', []))
275
+ energy_rewards_df = pd.DataFrame(energy_rewards_data or config_data.get('energy_rewards_data', []))
276
+ item_rewards_df = pd.DataFrame(item_rewards_data or config_data.get('item_rewards_data', []))
277
+
278
+ # Запускаем симуляцию
279
+ results_df, stats_report, _, _ = run_simulation_interface(
280
+ chain_df, energy_rewards_df, item_rewards_df,
281
+ params['max_history'], params['increment_diff'], params['energy_chance'],
282
+ params['req_weights'], params['reduction_factor'], params['increase_factor'],
283
+ params['iteration_count'], params['initial_energy']
284
+ )
285
+
286
+ # Сохраняем результаты
287
+ self.current_simulation_results = results_df
288
+ self.current_stats_report = stats_report
289
+
290
+ response = {
291
+ "success": True,
292
+ "message": "Симуляция завершена успешно",
293
+ "stats_report": stats_report,
294
+ "total_orders": len(results_df),
295
+ "simulation_parameters": params
296
+ }
297
+
298
+ if return_detailed_results:
299
+ response["detailed_results"] = results_df.to_dict('records')
300
+
301
+ return response
302
+
303
+ except Exception as e:
304
+ return {
305
+ "success": False,
306
+ "error": str(e),
307
+ "message": f"Ошибка при выполнении симуляции: {str(e)}"
308
+ }
309
+
310
+ def mcp_get_simulation_results(self, format: str = "summary") -> Dict[str, Any]:
311
+ """
312
+ Получает результаты последней симуляции
313
+
314
+ Args:
315
+ format: Формат результатов ("summary", "detailed", "csv")
316
+
317
+ Returns:
318
+ Словарь с результатами симуляции
319
+ """
320
+ try:
321
+ if self.current_simulation_results is None:
322
+ return {
323
+ "success": False,
324
+ "error": "Нет доступн��х результатов симуляции",
325
+ "message": "Сначала запустите симуляцию"
326
+ }
327
+
328
+ if format == "summary":
329
+ return {
330
+ "success": True,
331
+ "stats_report": self.current_stats_report,
332
+ "total_orders": len(self.current_simulation_results),
333
+ "avg_difficulty": self.current_simulation_results['Total_Difficulty'].mean(),
334
+ "final_energy": self.current_simulation_results['MEnergy_Amount'].iloc[-1] if len(self.current_simulation_results) > 0 else 0
335
+ }
336
+ elif format == "detailed":
337
+ return {
338
+ "success": True,
339
+ "stats_report": self.current_stats_report,
340
+ "detailed_results": self.current_simulation_results.to_dict('records')
341
+ }
342
+ elif format == "csv":
343
+ # Создаем временный CSV файл
344
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv', encoding='utf-8') as tmp_file:
345
+ self.current_simulation_results.to_csv(tmp_file.name, index=False)
346
+ csv_path = tmp_file.name
347
+
348
+ return {
349
+ "success": True,
350
+ "csv_file_path": csv_path,
351
+ "stats_report": self.current_stats_report
352
+ }
353
+ else:
354
+ return {
355
+ "success": False,
356
+ "error": f"Неизвестный формат: {format}",
357
+ "available_formats": ["summary", "detailed", "csv"]
358
+ }
359
+
360
+ except Exception as e:
361
+ return {
362
+ "success": False,
363
+ "error": str(e),
364
+ "message": f"Ошибка при получении результатов: {str(e)}"
365
+ }
366
+
367
+ def mcp_analyze_simulation_results(self, analysis_type: str = "basic") -> Dict[str, Any]:
368
+ """
369
+ Анализирует результаты симуляции
370
+
371
+ Args:
372
+ analysis_type: Тип анализа ("basic", "detailed", "chains", "rewards")
373
+
374
+ Returns:
375
+ Словарь с результатами анализа
376
+ """
377
+ try:
378
+ if self.current_simulation_results is None:
379
+ return {
380
+ "success": False,
381
+ "error": "Нет доступных результатов симуляции",
382
+ "message": "Сначала запустите симуляцию"
383
+ }
384
+
385
+ df = self.current_simulation_results
386
+
387
+ if analysis_type == "basic":
388
+ return {
389
+ "success": True,
390
+ "analysis": {
391
+ "total_orders": len(df),
392
+ "avg_difficulty": float(df['Total_Difficulty'].mean()),
393
+ "min_difficulty": int(df['Total_Difficulty'].min()),
394
+ "max_difficulty": int(df['Total_Difficulty'].max()),
395
+ "avg_energy_cost": float(df['MergeEnergyPrice'].mean()),
396
+ "total_energy_spent": int(df['MergeEnergyPrice'].sum()),
397
+ "final_energy": int(df['MEnergy_Amount'].iloc[-1]) if len(df) > 0 else 0,
398
+ "energy_rewards_count": int((df['ExpeditionEnergyReward'] > 0).sum()),
399
+ "item_rewards_count": int((df['MergeItemReward'] != "").sum())
400
+ }
401
+ }
402
+ elif analysis_type == "chains":
403
+ # Анализ по цепочкам
404
+ chain_analysis = {}
405
+ for col in df.columns:
406
+ if col.startswith('ChainId_'):
407
+ chain_counts = df[col].value_counts()
408
+ chain_analysis.update(chain_counts.to_dict())
409
+
410
+ return {
411
+ "success": True,
412
+ "chain_usage": chain_analysis,
413
+ "most_used_chain": max(chain_analysis.items(), key=lambda x: x[1]) if chain_analysis else None
414
+ }
415
+ elif analysis_type == "rewards":
416
+ # Анализ наград
417
+ energy_rewards = df[df['ExpeditionEnergyReward'] > 0]
418
+ item_rewards = df[df['MergeItemReward'] != ""]
419
+
420
+ return {
421
+ "success": True,
422
+ "rewards_analysis": {
423
+ "energy_rewards": {
424
+ "count": len(energy_rewards),
425
+ "total_amount": int(energy_rewards['ExpeditionEnergyReward'].sum()),
426
+ "avg_amount": float(energy_rewards['ExpeditionEnergyReward'].mean()) if len(energy_rewards) > 0 else 0
427
+ },
428
+ "item_rewards": {
429
+ "count": len(item_rewards),
430
+ "unique_items": item_rewards['MergeItemReward'].nunique(),
431
+ "most_common_item": item_rewards['MergeItemReward'].mode().iloc[0] if len(item_rewards) > 0 else None
432
+ }
433
+ }
434
+ }
435
+ else:
436
+ return {
437
+ "success": False,
438
+ "error": f"Неизвестный тип анализа: {analysis_type}",
439
+ "available_types": ["basic", "chains", "rewards"]
440
+ }
441
+
442
+ except Exception as e:
443
+ return {
444
+ "success": False,
445
+ "error": str(e),
446
+ "message": f"Ошибка при анализе результатов: {str(e)}"
447
+ }
448
+
449
+ # ===============================================================
450
+ # UTILITY TOOLS
451
+ # ===============================================================
452
+
453
+ def mcp_create_chain_data_template(self) -> Dict[str, Any]:
454
+ """
455
+ Создает шаблон данных для цепочек merge-предметов
456
+
457
+ Returns:
458
+ Словарь с шаблоном данных цепочек
459
+ """
460
+ template = [
461
+ {
462
+ "ChainId": "example_chain",
463
+ "MergeItemId": "item_level_1",
464
+ "RequirementWeight": 100,
465
+ "RewardDifficulty": 10
466
+ },
467
+ {
468
+ "ChainId": "example_chain",
469
+ "MergeItemId": "item_level_2",
470
+ "RequirementWeight": 80,
471
+ "RewardDifficulty": 25
472
+ },
473
+ {
474
+ "ChainId": "example_chain",
475
+ "MergeItemId": "item_level_3",
476
+ "RequirementWeight": 60,
477
+ "RewardDifficulty": 50
478
+ }
479
+ ]
480
+
481
+ return {
482
+ "success": True,
483
+ "template": template,
484
+ "description": "Шаблон данных цепочек. ChainId - идентификатор цепочки, MergeItemId - идентификатор предмета, RequirementWeight - вес для генерации требований, RewardDifficulty - сложность для расчета наград"
485
+ }
486
+
487
+ def mcp_create_rewards_data_template(self) -> Dict[str, Any]:
488
+ """
489
+ Создает шаблоны данных для наград
490
+
491
+ Returns:
492
+ Словарь с шаблонами данных наград
493
+ """
494
+ energy_template = [
495
+ {"DifficultyScore": 100, "Amount": 1},
496
+ {"DifficultyScore": 500, "Amount": 3},
497
+ {"DifficultyScore": 1000, "Amount": 5}
498
+ ]
499
+
500
+ item_template = [
501
+ {
502
+ "DifficultyScore": 200,
503
+ "Amount": 1,
504
+ "MergeItemId": "energy_1",
505
+ "RewardWeight": 80,
506
+ "ReductionFactor": 5
507
+ },
508
+ {
509
+ "DifficultyScore": 500,
510
+ "Amount": 1,
511
+ "MergeItemId": "coins_1",
512
+ "RewardWeight": 20,
513
+ "ReductionFactor": 0
514
+ }
515
+ ]
516
+
517
+ return {
518
+ "success": True,
519
+ "energy_rewards_template": energy_template,
520
+ "item_rewards_template": item_template,
521
+ "description": "Шаблоны данных наград. DifficultyScore - порог сложности, Amount - количество награды, MergeItemId - ID предмета для предметных наград, RewardWeight - вес награды, ReductionFactor - фактор уменьшения"
522
+ }
523
+
524
+ def mcp_validate_config_data(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
525
+ """
526
+ Валидирует данные конфигурации симулятора
527
+
528
+ Args:
529
+ config_data: Данные конфигурации для валидации
530
+
531
+ Returns:
532
+ Словарь с результатами валидации
533
+ """
534
+ try:
535
+ errors = []
536
+ warnings = []
537
+
538
+ # Проверяем обязательные поля
539
+ required_fields = ['name', 'description']
540
+ for field in required_fields:
541
+ if not config_data.get(field):
542
+ errors.append(f"Отсутствует обязательное поле: {field}")
543
+
544
+ # Проверяем числовые параметры
545
+ numeric_params = {
546
+ 'max_history_orders': (1, 20),
547
+ 'increment_difficulty': (0, 10),
548
+ 'energy_chance': (0, 100),
549
+ 'reduction_factor': (0, 100),
550
+ 'increase_factor': (0, 100),
551
+ 'iteration_count': (1, 10000),
552
+ 'initial_energy': (1, 1000000)
553
+ }
554
+
555
+ for param, (min_val, max_val) in numeric_params.items():
556
+ value = config_data.get(param)
557
+ if value is not None:
558
+ if not isinstance(value, (int, float)) or value < min_val or value > max_val:
559
+ errors.append(f"Параметр {param} должен быть числом от {min_val} до {max_val}")
560
+
561
+ # Проверяем данные цепочек
562
+ chain_data = config_data.get('chain_data', [])
563
+ if chain_data:
564
+ for i, chain_item in enumerate(chain_data):
565
+ if not chain_item.get('ChainId'):
566
+ errors.append(f"Цепочка {i+1}: отсутствует ChainId")
567
+ if not chain_item.get('MergeItemId'):
568
+ errors.append(f"Цепочка {i+1}: отсутствует MergeItemId")
569
+ if not isinstance(chain_item.get('RequirementWeight', 0), (int, float)):
570
+ errors.append(f"Цепочка {i+1}: RequirementWeight должен быть числом")
571
+ if not isinstance(chain_item.get('RewardDifficulty', 0), (int, float)):
572
+ errors.append(f"Цепочка {i+1}: RewardDifficulty должен быть числом")
573
+ else:
574
+ warnings.append("Нет данных о цепочках - симуляция может не работать корректно")
575
+
576
+ # Проверяем веса требований
577
+ req_weights = config_data.get('requirement_weights', "70,30")
578
+ try:
579
+ weights = [int(w.strip()) for w in req_weights.split(',')]
580
+ if len(weights) != 2:
581
+ errors.append("Веса требований должны содержать ровно 2 значения")
582
+ elif any(w < 0 for w in weights):
583
+ errors.append("Веса требований должны быть положительными числами")
584
+ except:
585
+ errors.append("Неверный формат весов требований (ожидается 'число,число')")
586
+
587
+ is_valid = len(errors) == 0
588
+
589
+ return {
590
+ "success": True,
591
+ "is_valid": is_valid,
592
+ "errors": errors,
593
+ "warnings": warnings,
594
+ "message": "Конфигурация валидна" if is_valid else f"Найдено {len(errors)} ошибок"
595
+ }
596
+
597
+ except Exception as e:
598
+ return {
599
+ "success": False,
600
+ "error": str(e),
601
+ "message": f"Ошибка при валидации: {str(e)}"
602
+ }
603
+
604
+ # ===============================================================
605
+ # MCP SERVER INSTANCE
606
+ # ===============================================================
607
+
608
+ # Глобальный экземпляр MCP сервера
609
+ mcp_server = MergeSimulatorMCPServer()
610
+
611
+ # Экспорт функций для использования в других модулях
612
+ def get_mcp_server() -> MergeSimulatorMCPServer:
613
+ """Возвращает экземпляр MCP сервера"""
614
+ return mcp_server
615
+
616
+ # Список всех доступных MCP функций
617
+ MCP_FUNCTIONS = {
618
+ "mcp_save_simulator_config": mcp_server.mcp_save_simulator_config,
619
+ "mcp_load_simulator_config": mcp_server.mcp_load_simulator_config,
620
+ "mcp_list_simulator_configs": mcp_server.mcp_list_simulator_configs,
621
+ "mcp_delete_simulator_config": mcp_server.mcp_delete_simulator_config,
622
+ "mcp_run_simulation": mcp_server.mcp_run_simulation,
623
+ "mcp_get_simulation_results": mcp_server.mcp_get_simulation_results,
624
+ "mcp_analyze_simulation_results": mcp_server.mcp_analyze_simulation_results,
625
+ "mcp_create_chain_data_template": mcp_server.mcp_create_chain_data_template,
626
+ "mcp_create_rewards_data_template": mcp_server.mcp_create_rewards_data_template,
627
+ "mcp_validate_config_data": mcp_server.mcp_validate_config_data
628
+ }