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