Spaces:
Runtime error
Runtime error
Commit
·
18cbfa9
1
Parent(s):
ee7e579
mcp
Browse files- README.md +168 -37
- app.py +372 -18
- config_manager.py +163 -0
- mcp_server.py +628 -0
README.md
CHANGED
|
@@ -1,39 +1,170 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 119 |
MergeItemId=reward_info.get('_mergeItemId', ''),
|
| 120 |
Type='Item',
|
| 121 |
-
RewardWeight=
|
| 122 |
-
ReductionFactor=
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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:
|
|
|
|
| 204 |
|
| 205 |
weights = [ruleset.OverrideWeights.get(item.MergeItemId, item.RequirementWeight) for item in selectable_items]
|
| 206 |
-
if sum(weights) == 0:
|
|
|
|
| 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:
|
|
|
|
| 214 |
|
| 215 |
-
total_cost = sum(2**(chain.Items.index(req)) for req in order.Requirements
|
|
|
|
| 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 = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 310 |
-
|
|
|
|
| 311 |
for rg in ruleset.ItemRewards:
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|